1244 lines
45 KiB
JavaScript
1244 lines
45 KiB
JavaScript
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 (
|
||
<div
|
||
className={`
|
||
fixed inset-0 z-50 flex items-start justify-center
|
||
bg-black bg-opacity-60 overflow-hidden pt-8
|
||
`}
|
||
>
|
||
<div
|
||
className={`
|
||
relative bg-white rounded-lg p-6 w-full max-w-4xl
|
||
max-h-[85vh] overflow-y-auto
|
||
`}
|
||
>
|
||
<h2 className="text-2xl font-semibold mb-4">
|
||
Edit Scenario: {scenario?.scenario_title || scenario?.career_name || '(untitled)'}
|
||
</h2>
|
||
|
||
{/* SECTION: Scenario & Career */}
|
||
<h3 className="text-xl font-medium mb-2">Scenario & Career</h3>
|
||
|
||
{/* Scenario Title */}
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Scenario Title
|
||
</label>
|
||
<input
|
||
type="text"
|
||
name="scenario_title"
|
||
value={formData.scenario_title}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full focus:outline-none focus:border-blue-500"
|
||
/>
|
||
</div>
|
||
|
||
{/* Career Search */}
|
||
<div className="mb-4 relative">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Career Search
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={careerSearchInput}
|
||
onChange={handleCareerInputChange}
|
||
className="border border-gray-300 rounded p-2 w-full focus:outline-none focus:border-blue-500"
|
||
/>
|
||
{careerMatches.length > 0 && (
|
||
<ul
|
||
ref={careerDropdownRef}
|
||
className="
|
||
absolute top-full left-0 w-full border border-gray-200 bg-white z-10
|
||
max-h-48 overflow-auto mt-1
|
||
"
|
||
>
|
||
{careerMatches.map((c, idx) => (
|
||
<li
|
||
key={idx}
|
||
className="p-2 cursor-pointer hover:bg-gray-100"
|
||
onClick={() => handleSelectCareer(c)}
|
||
>
|
||
{c}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
<p className="text-sm text-gray-600 mt-1">
|
||
<em>Current Career:</em> {formData.career_name || '(none)'}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Status */}
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Status
|
||
</label>
|
||
<select
|
||
name="status"
|
||
value={formData.status}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
>
|
||
<option value="planned">Planned</option>
|
||
<option value="current">Current</option>
|
||
<option value="completed">Completed</option>
|
||
<option value="exploring">Exploring</option>
|
||
</select>
|
||
</div>
|
||
|
||
{/* Dates */}
|
||
<div className="mb-4 grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Start Date
|
||
</label>
|
||
<input
|
||
type="date"
|
||
name="start_date"
|
||
value={formData.start_date || ''}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Retirement date */}
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Planned Retirement Date
|
||
</label>
|
||
<input
|
||
type="date"
|
||
name="retirement_start_date"
|
||
value={formData.retirement_start_date}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
|
||
{/* Desired retirement income (monthly) */}
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Desired Retirement Income (monthly $)
|
||
</label>
|
||
<input
|
||
type="number"
|
||
name="desired_retirement_income_monthly"
|
||
value={formData.desired_retirement_income_monthly}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
{formData.desired_retirement_income_monthly && (
|
||
<p className="text-xs text-gray-500">
|
||
≈ $
|
||
{(formData.desired_retirement_income_monthly*12)
|
||
.toLocaleString()} per year
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* College Enrollment Status */}
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
College Enrollment Status
|
||
</label>
|
||
<select
|
||
name="college_enrollment_status"
|
||
value={formData.college_enrollment_status}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
>
|
||
<option value="not_enrolled">Not Enrolled</option>
|
||
<option value="currently_enrolled">Currently Enrolled</option>
|
||
<option value="prospective_student">Prospective</option>
|
||
</select>
|
||
</div>
|
||
|
||
{/* Currently Working */}
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Currently Working?
|
||
</label>
|
||
<select
|
||
name="currently_working"
|
||
value={formData.currently_working}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
>
|
||
<option value="yes">Yes</option>
|
||
<option value="no">No</option>
|
||
</select>
|
||
</div>
|
||
|
||
{/* SECTION: Scenario Financial Overrides */}
|
||
<h3 className="text-xl font-medium mt-6 mb-3">Scenario Financial Overrides</h3>
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 mb-6">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Monthly Expenses
|
||
</label>
|
||
<input
|
||
type="number"
|
||
name="planned_monthly_expenses"
|
||
value={formData.planned_monthly_expenses}
|
||
placeholder={financialProfile?.monthly_expenses ?? ''}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Monthly Debt Payments
|
||
</label>
|
||
<input
|
||
type="number"
|
||
name="planned_monthly_debt_payments"
|
||
value={formData.planned_monthly_debt_payments}
|
||
placeholder={financialProfile?.monthly_debt_payments ?? ''}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Retirement Contrib
|
||
</label>
|
||
<input
|
||
type="number"
|
||
name="planned_monthly_retirement_contribution"
|
||
value={formData.planned_monthly_retirement_contribution}
|
||
placeholder={financialProfile?.retirement_contribution ?? ''}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Emergency Contrib
|
||
</label>
|
||
<input
|
||
type="number"
|
||
name="planned_monthly_emergency_contribution"
|
||
value={formData.planned_monthly_emergency_contribution}
|
||
placeholder={financialProfile?.emergency_contribution ?? ''}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Surplus % to Emergency
|
||
</label>
|
||
<input
|
||
type="number"
|
||
name="planned_surplus_emergency_pct"
|
||
value={formData.planned_surplus_emergency_pct}
|
||
placeholder={financialProfile?.extra_cash_emergency_pct ?? ''}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Surplus % to Retirement
|
||
</label>
|
||
<input
|
||
type="number"
|
||
name="planned_surplus_retirement_pct"
|
||
value={formData.planned_surplus_retirement_pct}
|
||
placeholder={financialProfile?.extra_cash_retirement_pct ?? ''}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Additional Income
|
||
</label>
|
||
<input
|
||
type="number"
|
||
name="planned_additional_income"
|
||
value={formData.planned_additional_income}
|
||
placeholder={financialProfile?.additional_income ?? ''}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ───────── COLLEGE PROFILE heading + Add plan ───────── */}
|
||
|
||
<div className="flex items-center justify-between mb-2">
|
||
{/* left cluster (title + info badge) */}
|
||
<div className="flex items-center space-x-2">
|
||
<h3 className="text-xl font-medium">College Profile</h3>
|
||
<InfoTooltip message="Get this info from the “Educational Programs” tab in Preparing & Upskilling for Your Career." />
|
||
</div>
|
||
|
||
{/* right cluster (toggle button) */}
|
||
<Button
|
||
variant="secondary"
|
||
size="sm"
|
||
onClick={() => {
|
||
if (
|
||
!showCollegeForm &&
|
||
formData.college_enrollment_status === 'not_enrolled'
|
||
) {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
college_enrollment_status: 'prospective_student'
|
||
}));
|
||
}
|
||
setShowCollegeForm(prev => !prev);
|
||
}}
|
||
>
|
||
{showCollegeForm ? 'Hide' : 'Add plan'}
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Collapse / expand the full set of inputs */}
|
||
{showCollegeForm ? (
|
||
<>
|
||
{/* District / State / Online check-boxes ------------------ */}
|
||
<div className="flex items-center space-x-4 mb-4">
|
||
{[
|
||
{ name: 'is_in_district', label: 'In District' },
|
||
{ name: 'is_in_state', label: 'In State' },
|
||
{ name: 'is_online', label: 'Fully Online' }
|
||
].map(({ name, label }) => (
|
||
<label key={name} className="inline-flex items-center">
|
||
<input
|
||
type="checkbox"
|
||
name={name}
|
||
checked={!!formData[name]}
|
||
onChange={handleFormChange}
|
||
className="mr-1"
|
||
/>
|
||
{label}
|
||
</label>
|
||
))}
|
||
</div>
|
||
|
||
{/* Loan Deferral */}
|
||
<div className="mb-4">
|
||
<label className="inline-flex items-center">
|
||
<input
|
||
type="checkbox"
|
||
name="loan_deferral_until_graduation"
|
||
checked={!!formData.loan_deferral_until_graduation}
|
||
onChange={handleFormChange}
|
||
className="mr-1"
|
||
/>
|
||
Defer Loan Payments until Graduation?
|
||
</label>
|
||
</div>
|
||
|
||
{/* School */}
|
||
<div className="mb-4 relative">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
School
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.selected_school}
|
||
onChange={handleSchoolChange}
|
||
className="border border-gray-300 rounded p-2 w-full focus:outline-none focus:border-blue-500"
|
||
/>
|
||
{schoolSuggestions.length > 0 && (
|
||
<ul
|
||
className="
|
||
absolute top-full left-0 w-full
|
||
border border-gray-200 bg-white z-10
|
||
max-h-48 overflow-auto mt-1
|
||
"
|
||
>
|
||
{schoolSuggestions.map((sch, i) => (
|
||
<li
|
||
key={i}
|
||
className="p-2 cursor-pointer hover:bg-gray-100"
|
||
onClick={() => handleSchoolSelect(sch)}
|
||
>
|
||
{sch}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</div>
|
||
|
||
{/* Program */}
|
||
<div className="mb-4 relative">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Program
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.selected_program}
|
||
onChange={handleProgramChange}
|
||
className="border border-gray-300 rounded p-2 w-full focus:outline-none focus:border-blue-500"
|
||
/>
|
||
{programSuggestions.length > 0 && (
|
||
<ul
|
||
className="
|
||
absolute top-full left-0 w-full
|
||
border border-gray-200 bg-white z-10
|
||
max-h-48 overflow-auto mt-1
|
||
"
|
||
>
|
||
{programSuggestions.map((prog, idx) => (
|
||
<li
|
||
key={idx}
|
||
className="p-2 cursor-pointer hover:bg-gray-100"
|
||
onClick={() => handleProgramSelect(prog)}
|
||
>
|
||
{prog}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</div>
|
||
|
||
{/* Program Type */}
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Program Type
|
||
</label>
|
||
<select
|
||
name="program_type"
|
||
value={formData.program_type}
|
||
onChange={handleProgramTypeSelect}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
>
|
||
<option value="">(none)</option>
|
||
{availableProgramTypes.map((pt, i) => (
|
||
<option key={i} value={pt}>
|
||
{pt}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
{/* Academic Calendar */}
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Academic Calendar
|
||
</label>
|
||
<select
|
||
name="academic_calendar"
|
||
value={formData.academic_calendar}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
>
|
||
<option value="monthly">Monthly</option>
|
||
<option value="semester">Semester</option>
|
||
<option value="quarter">Quarter</option>
|
||
<option value="trimester">Trimester</option>
|
||
</select>
|
||
</div>
|
||
|
||
{/* Credit Hours per Year */}
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Credit Hours per Year
|
||
</label>
|
||
<input
|
||
type="number"
|
||
name="credit_hours_per_year"
|
||
value={formData.credit_hours_per_year}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
|
||
{/* Yearly Tuition */}
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Yearly Tuition (auto or override)
|
||
</label>
|
||
<input
|
||
type="number"
|
||
value={displayedTuition}
|
||
onChange={handleManualTuitionChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
|
||
{/* Annual Financial Aid */}
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Annual Financial Aid
|
||
</label>
|
||
<input
|
||
type="number"
|
||
name="annual_financial_aid"
|
||
value={formData.annual_financial_aid}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
|
||
{/* Existing College Debt */}
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Existing College Debt
|
||
</label>
|
||
<input
|
||
type="number"
|
||
name="existing_college_debt"
|
||
value={formData.existing_college_debt}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
|
||
{/* Currently Enrolled Only Fields */}
|
||
{formData.college_enrollment_status === 'currently_enrolled' && (
|
||
<>
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Tuition Paid
|
||
</label>
|
||
<input
|
||
type="number"
|
||
name="tuition_paid"
|
||
value={formData.tuition_paid}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Hours Completed
|
||
</label>
|
||
<input
|
||
type="number"
|
||
name="hours_completed"
|
||
value={formData.hours_completed}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Program Length (auto or override)
|
||
</label>
|
||
<input
|
||
type="number"
|
||
value={displayedProgLength}
|
||
onChange={handleManualProgLengthChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* Enrollment Date */}
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Enrollment Date
|
||
</label>
|
||
<input
|
||
type="date"
|
||
name="enrollment_date"
|
||
value={formData.enrollment_date || ''}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
|
||
{/* Expected Graduation */}
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Expected Graduation
|
||
</label>
|
||
<input
|
||
type="date"
|
||
name="expected_graduation"
|
||
value={formData.expected_graduation}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
|
||
{/* Interest Rate */}
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Interest Rate (%)
|
||
</label>
|
||
<input
|
||
type="number"
|
||
name="interest_rate"
|
||
value={formData.interest_rate}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
|
||
{/* Loan Term */}
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Loan Term (years)
|
||
</label>
|
||
<input
|
||
type="number"
|
||
name="loan_term"
|
||
value={formData.loan_term}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
|
||
{/* Extra Payment */}
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Extra Payment (monthly)
|
||
</label>
|
||
<input
|
||
type="number"
|
||
name="extra_payment"
|
||
value={formData.extra_payment}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
|
||
{/* Expected Salary */}
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Expected Salary After Graduation
|
||
</label>
|
||
<input
|
||
type="number"
|
||
name="expected_salary"
|
||
value={formData.expected_salary}
|
||
onChange={handleFormChange}
|
||
className="border border-gray-300 rounded p-2 w-full"
|
||
/>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<p className="text-sm text-gray-500 italic mb-4">
|
||
Not currently enrolled or prospective. Add a plan to enter this info.
|
||
</p>
|
||
)}
|
||
|
||
{/* ACTIONS */}
|
||
<div className="mt-6 flex justify-end space-x-2">
|
||
<Button variant="secondary" onClick={() => onClose(null, null)}>
|
||
Cancel
|
||
</Button>
|
||
<Button variant="primary" onClick={handleSave}>
|
||
Save
|
||
</Button>
|
||
{!formData.retirement_start_date && (
|
||
<p className="mt-1 text-xs text-red-500">
|
||
Pick a Planned Retirement Date to run the simulation.
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Show a preview if we have simulation data */}
|
||
{projectionData.length > 0 && (
|
||
<div className="mt-6 border border-gray-200 p-4 rounded">
|
||
<h4 className="text-lg font-semibold mb-2">
|
||
Simulation Preview (first 5 months):
|
||
</h4>
|
||
<pre className="bg-gray-50 p-2 rounded overflow-auto max-h-52 text-sm">
|
||
{JSON.stringify(projectionData.slice(0, 5), null, 2)}
|
||
</pre>
|
||
{loanPayoffMonth && (
|
||
<p className="mt-2 text-sm">Loan Payoff Month: {loanPayoffMonth}</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|