From d48f33572a6cd484d4fd43c2e1d9f9a31deb8d25 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 25 Apr 2025 16:18:05 +0000 Subject: [PATCH] Made modal float with scroll. --- backend/server3.js | 101 ++- src/components/MultiScenarioView.css | 18 + src/components/MultiScenarioView.js | 117 ++- src/components/ScenarioContainer.js | 197 +++-- src/components/ScenarioEditModal.js | 1088 ++++++++++++++++++++++---- user_profile.db | Bin 106496 -> 106496 bytes 6 files changed, 1234 insertions(+), 287 deletions(-) create mode 100644 src/components/MultiScenarioView.css diff --git a/backend/server3.js b/backend/server3.js index 29caa9f..a319b50 100644 --- a/backend/server3.js +++ b/backend/server3.js @@ -118,6 +118,7 @@ app.get('/api/premium/career-profile/:careerPathId', authenticatePremiumUser, as // server3.js app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res) => { const { + scenario_title, career_name, status, start_date, @@ -125,7 +126,6 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res college_enrollment_status, currently_working, - // NEW planned columns planned_monthly_expenses, planned_monthly_debt_payments, planned_monthly_retirement_contribution, @@ -149,6 +149,7 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res INSERT INTO career_paths ( id, user_id, + scenario_title, career_name, status, start_date, @@ -190,6 +191,7 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res `, [ newCareerPathId, req.userId, + scenario_title || null, career_name, status || 'planned', start_date || now, @@ -229,6 +231,103 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res } }); +// server3.js (or your premium server file) + +// Delete a career path (scenario) by ID +app.delete('/api/premium/career-profile/:careerPathId', authenticatePremiumUser, async (req, res) => { + const { careerPathId } = req.params; + + try { + // 1) Confirm that this career_path belongs to the user + const existing = await db.get( + ` + SELECT id + FROM career_paths + WHERE id = ? + AND user_id = ? + `, + [careerPathId, req.userId] + ); + + if (!existing) { + return res.status(404).json({ error: 'Career path not found or not yours.' }); + } + + // 2) Optionally delete the college_profile for this scenario + // (If you always keep 1-to-1 relationship: careerPathId => college_profile) + await db.run( + ` + DELETE FROM college_profiles + WHERE user_id = ? + AND career_path_id = ? + `, + [req.userId, careerPathId] + ); + + // 3) Optionally delete scenario’s milestones + // (and any associated tasks, impacts, etc.) + // If you store tasks in tasks table, and impacts in milestone_impacts table: + + // First find scenario milestones + const scenarioMilestones = await db.all( + ` + SELECT id + FROM milestones + WHERE user_id = ? + AND career_path_id = ? + `, + [req.userId, careerPathId] + ); + const milestoneIds = scenarioMilestones.map((m) => m.id); + + if (milestoneIds.length > 0) { + // Delete tasks for these milestones + const placeholders = milestoneIds.map(() => '?').join(','); + await db.run( + ` + DELETE FROM tasks + WHERE milestone_id IN (${placeholders}) + `, + milestoneIds + ); + + // Delete impacts for these milestones + await db.run( + ` + DELETE FROM milestone_impacts + WHERE milestone_id IN (${placeholders}) + `, + milestoneIds + ); + + // Finally delete the milestones themselves + await db.run( + ` + DELETE FROM milestones + WHERE id IN (${placeholders}) + `, + milestoneIds + ); + } + + // 4) Finally delete the career_path row + await db.run( + ` + DELETE FROM career_paths + WHERE user_id = ? + AND id = ? + `, + [req.userId, careerPathId] + ); + + res.json({ message: 'Career path and related data successfully deleted.' }); + } catch (error) { + console.error('Error deleting career path:', error); + res.status(500).json({ error: 'Failed to delete career path.' }); + } +}); + + /* ------------------------------------------------------------------ Milestone ENDPOINTS ------------------------------------------------------------------ */ diff --git a/src/components/MultiScenarioView.css b/src/components/MultiScenarioView.css new file mode 100644 index 0000000..3de975f --- /dev/null +++ b/src/components/MultiScenarioView.css @@ -0,0 +1,18 @@ +.modal-backdrop { + position: fixed; + top:0; left:0; + width:100vw; height:100vh; + background: rgba(0,0,0,0.5); + z-index: 9999; + } + .modal-container { + position: absolute; + top: 50%; left: 50%; + transform: translate(-50%,-50%); + background: #fff; + width: 600px; + max-height: 80vh; + overflow-y: auto; + padding: 1rem; + } + diff --git a/src/components/MultiScenarioView.js b/src/components/MultiScenarioView.js index 5200dd4..01a5767 100644 --- a/src/components/MultiScenarioView.js +++ b/src/components/MultiScenarioView.js @@ -2,33 +2,43 @@ import React, { useEffect, useState } from 'react'; import authFetch from '../utils/authFetch.js'; import ScenarioContainer from './ScenarioContainer.js'; +import ScenarioEditModal from './ScenarioEditModal.js'; // The floating modal export default function MultiScenarioView() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + + // The user’s single overall financial profile const [financialProfile, setFinancialProfile] = useState(null); - const [scenarios, setScenarios] = useState([]); // each scenario is a row in career_paths + + // All scenario rows (from career_paths) + const [scenarios, setScenarios] = useState([]); + + // The scenario we’re currently editing in a top-level modal: + const [editingScenario, setEditingScenario] = useState(null); + // The collegeProfile we load for that scenario (passed to edit modal) + const [editingCollegeProfile, setEditingCollegeProfile] = useState(null); useEffect(() => { async function loadData() { setLoading(true); setError(null); try { - // A) fetch financial profile - let finRes = await authFetch('/api/premium/financial-profile'); - if (!finRes.ok) throw new Error(`FIN profile error: ${finRes.status}`); - let finData = await finRes.json(); + // 1) Fetch the user’s overall financial profile + const finRes = await authFetch('/api/premium/financial-profile'); + if (!finRes.ok) throw new Error(`FinancialProfile error: ${finRes.status}`); + const finData = await finRes.json(); - // B) fetch all career_paths (scenarios) - let scenRes = await authFetch('/api/premium/career-profile/all'); + // 2) Fetch all scenarios (career_paths) + const scenRes = await authFetch('/api/premium/career-profile/all'); if (!scenRes.ok) throw new Error(`Scenarios error: ${scenRes.status}`); - let scenData = await scenRes.json(); + const scenData = await scenRes.json(); setFinancialProfile(finData); setScenarios(scenData.careerPaths || []); } catch (err) { - console.error('Error loading premium data:', err); - setError(err.message || 'Failed to load data'); + console.error('Error loading data in MultiScenarioView:', err); + setError(err.message || 'Failed to load scenarios/financial'); } finally { setLoading(false); } @@ -36,7 +46,9 @@ export default function MultiScenarioView() { loadData(); }, []); - // “Add Scenario” => create a brand new row in career_paths + // --------------------------- + // Add a new scenario + // --------------------------- async function handleAddScenario() { try { const body = { @@ -54,9 +66,9 @@ export default function MultiScenarioView() { if (!res.ok) throw new Error(`Add scenario error: ${res.status}`); const data = await res.json(); + // Insert the new row into local state const newRow = { id: data.career_path_id, - user_id: null, career_name: body.career_name, status: body.status, start_date: body.start_date, @@ -67,11 +79,13 @@ export default function MultiScenarioView() { setScenarios((prev) => [...prev, newRow]); } catch (err) { console.error('Failed adding scenario:', err); - alert('Could not add scenario'); + alert(err.message || 'Could not add scenario'); } } - // “Clone” => create a new row in career_paths with copied fields + // --------------------------- + // Clone scenario + // --------------------------- async function handleCloneScenario(sourceScenario) { try { const body = { @@ -90,8 +104,9 @@ export default function MultiScenarioView() { if (!res.ok) throw new Error(`Clone scenario error: ${res.status}`); const data = await res.json(); + const newScenarioId = data.career_path_id; const newRow = { - id: data.career_path_id, + id: newScenarioId, career_name: body.career_name, status: body.status, start_date: body.start_date, @@ -102,23 +117,70 @@ export default function MultiScenarioView() { setScenarios((prev) => [...prev, newRow]); } catch (err) { console.error('Failed cloning scenario:', err); - alert('Could not clone scenario'); + alert(err.message || 'Could not clone scenario'); } } - // “Remove” => possibly remove from DB or just local + // --------------------------- + // Delete scenario + // --------------------------- async function handleRemoveScenario(scenarioId) { + // confirm + const confirmDel = window.confirm( + 'Delete this scenario (and associated collegeProfile/milestones)?' + ); + if (!confirmDel) return; + try { + const res = await authFetch(`/api/premium/career-profile/${scenarioId}`, { + method: 'DELETE' + }); + if (!res.ok) throw new Error(`Delete scenario error: ${res.status}`); + // remove from local setScenarios((prev) => prev.filter((s) => s.id !== scenarioId)); - // Optionally do an API call: DELETE /api/premium/career-profile/:id } catch (err) { - console.error('Failed removing scenario:', err); - alert('Could not remove scenario'); + console.error('Delete scenario error:', err); + alert(err.message || 'Could not delete scenario'); } } + // --------------------------- + // User clicks "Edit" in ScenarioContainer => we fetch that scenario’s collegeProfile + // set editingScenario / editingCollegeProfile => modal + // --------------------------- + async function handleEditScenario(scenarioObj) { + if (!scenarioObj?.id) return; + try { + // fetch the collegeProfile + const colResp = await authFetch(`/api/premium/college-profile?careerPathId=${scenarioObj.id}`); + let colData = {}; + if (colResp.ok) { + colData = await colResp.json(); + } + setEditingScenario(scenarioObj); + setEditingCollegeProfile(colData); + } catch (err) { + console.error('Error loading collegeProfile for editing:', err); + setEditingScenario(scenarioObj); + setEditingCollegeProfile({}); + } + } + + // Called by on close => we optionally update local scenario + function handleModalClose(updatedScenario, updatedCollege) { + if (updatedScenario) { + setScenarios(prev => + prev.map((s) => (s.id === updatedScenario.id ? { ...s, ...updatedScenario } : s)) + ); + } + // We might not store the updatedCollege in local state unless we want to re-simulate immediately + // For now, do nothing or re-fetch if needed + setEditingScenario(null); + setEditingCollegeProfile(null); + } + if (loading) return

Loading scenarios...

; - if (error) return

Error: {error}

; + if (error) return

Error: {error}

; return (
@@ -126,15 +188,26 @@ export default function MultiScenarioView() { handleCloneScenario(scen)} onRemove={() => handleRemoveScenario(scen.id)} + onEdit={() => handleEditScenario(scen)} // new callback /> ))}
+ + {/* The floating modal at the bottom => only if editingScenario != null */} + {editingScenario && ( + + )}
); } diff --git a/src/components/ScenarioContainer.js b/src/components/ScenarioContainer.js index 9e8f938..ac75a62 100644 --- a/src/components/ScenarioContainer.js +++ b/src/components/ScenarioContainer.js @@ -2,160 +2,151 @@ import React, { useState, useEffect } from 'react'; import { Line } from 'react-chartjs-2'; import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js'; -import ScenarioEditModal from './ScenarioEditModal.js'; import MilestoneTimeline from './MilestoneTimeline.js'; import AISuggestedMilestones from './AISuggestedMilestones.js'; import authFetch from '../utils/authFetch.js'; export default function ScenarioContainer({ - scenario, // from career_paths row - financialProfile, // single row, shared across user + scenario, + financialProfile, + onRemove, onClone, - onRemove + onEdit // <-- new callback to open the floating modal }) { - const [localScenario, setLocalScenario] = useState(scenario); const [collegeProfile, setCollegeProfile] = useState(null); - const [projectionData, setProjectionData] = useState([]); const [loanPaidOffMonth, setLoanPaidOffMonth] = useState(null); - const [editOpen, setEditOpen] = useState(false); + // An input for sim length + const [simulationYearsInput, setSimulationYearsInput] = useState('20'); - // Re-sync if parent updates scenario useEffect(() => { - setLocalScenario(scenario); - }, [scenario]); - - // 1) Fetch the college profile for this scenario - useEffect(() => { - if (!localScenario?.id) return; + if (!scenario?.id) { + setCollegeProfile(null); + return; + } async function loadCollegeProfile() { try { - const res = await authFetch(`/api/premium/college-profile?careerPathId=${localScenario.id}`); + const url = `/api/premium/college-profile?careerPathId=${scenario.id}`; + const res = await authFetch(url); if (res.ok) { const data = await res.json(); setCollegeProfile(data); } else { - console.warn('No college profile found or error:', res.status); setCollegeProfile({}); } } catch (err) { - console.error('Failed fetching college profile:', err); + console.error('Error loading collegeProfile:', err); } } loadCollegeProfile(); - }, [localScenario]); + }, [scenario]); - // 2) Whenever we have financialProfile + collegeProfile => run the simulation useEffect(() => { - if (!financialProfile || !collegeProfile) return; + if (!financialProfile || !collegeProfile || !scenario?.id) return; - // Merge them into the userProfile object for the simulator: + // Merge the user’s base financial profile + scenario overwrites + college profile const mergedProfile = { currentSalary: financialProfile.current_salary || 0, - monthlyExpenses: financialProfile.monthly_expenses || 0, - monthlyDebtPayments: financialProfile.monthly_debt_payments || 0, - retirementSavings: financialProfile.retirement_savings || 0, - emergencySavings: financialProfile.emergency_fund || 0, - monthlyRetirementContribution: financialProfile.retirement_contribution || 0, - monthlyEmergencyContribution: financialProfile.emergency_contribution || 0, - surplusEmergencyAllocation: financialProfile.extra_cash_emergency_pct || 50, - surplusRetirementAllocation: financialProfile.extra_cash_retirement_pct || 50, - - // College fields (scenario-based) + monthlyExpenses: + scenario.planned_monthly_expenses ?? financialProfile.monthly_expenses ?? 0, + monthlyDebtPayments: + scenario.planned_monthly_debt_payments ?? financialProfile.monthly_debt_payments ?? 0, + // ... studentLoanAmount: collegeProfile.existing_college_debt || 0, interestRate: collegeProfile.interest_rate || 5, loanTerm: collegeProfile.loan_term || 10, - loanDeferralUntilGraduation: !!collegeProfile.loan_deferral_until_graduation, - academicCalendar: collegeProfile.academic_calendar || 'semester', - annualFinancialAid: collegeProfile.annual_financial_aid || 0, - calculatedTuition: collegeProfile.tuition || 0, - extraPayment: collegeProfile.extra_payment || 0, - gradDate: collegeProfile.expected_graduation || null, - programType: collegeProfile.program_type || '', - creditHoursPerYear: collegeProfile.credit_hours_per_year || 0, - hoursCompleted: collegeProfile.hours_completed || 0, - programLength: collegeProfile.program_length || 0, - inCollege: - collegeProfile.college_enrollment_status === 'currently_enrolled' || - collegeProfile.college_enrollment_status === 'prospective_student', - expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary || 0, - - // milestoneImpacts is fetched & merged in MilestoneTimeline, not here + // ... + simulationYears: parseInt(simulationYearsInput, 10) || 20, milestoneImpacts: [] }; - // 3) run the simulation const { projectionData, loanPaidOffMonth } = simulateFinancialProjection(mergedProfile); setProjectionData(projectionData); setLoanPaidOffMonth(loanPaidOffMonth); - }, [financialProfile, collegeProfile]); + }, [financialProfile, collegeProfile, scenario, simulationYearsInput]); + + function handleDeleteScenario() { + // let the parent actually do the DB deletion + onRemove(scenario.id); + } + + function handleSimulationYearsBlur() { + if (simulationYearsInput.trim() === '') { + setSimulationYearsInput('20'); + } + } + + // chart data + const labels = projectionData.map(p => p.month); + const netSavData = projectionData.map(p => p.cumulativeNetSavings || 0); + const retData = projectionData.map(p => p.retirementSavings || 0); + const loanData = projectionData.map(p => p.loanBalance || 0); + + const chartData = { + labels, + datasets: [ + { label: 'Net Savings', data: netSavData, borderColor: 'blue', fill: false }, + { label: 'Retirement', data: retData, borderColor: 'green', fill: false }, + { label: 'Loan', data: loanData, borderColor: 'red', fill: false } + ] + }; return (
-

{localScenario.career_name || 'Untitled Scenario'}

-

Status: {localScenario.status}

+

{scenario.scenario_title || scenario.career_name || 'Untitled Scenario'}

- p.month), - datasets: [ - { - label: 'Net Savings', - data: projectionData.map((p) => p.cumulativeNetSavings || 0), - borderColor: 'blue', - fill: false - } - ] - }} - options={{ responsive: true }} - /> - -
- Loan Paid Off: {loanPaidOffMonth || 'N/A'}
- Final Retirement:{' '} - {projectionData[projectionData.length - 1]?.retirementSavings?.toFixed(0) || 0} +
+ + setSimulationYearsInput(e.target.value)} + onBlur={handleSimulationYearsBlur} + />
- {/* The timeline that fetches scenario/universal milestones for display */} - {}} - onMilestoneUpdated={() => { - // might do scenario changes if you want - }} - /> - - +
- - - + Loan Paid Off: {loanPaidOffMonth || 'N/A'} + {projectionData.length > 0 && ( + <> +
+ Final Retirement: {projectionData[projectionData.length - 1].retirementSavings.toFixed(0)} + + )}
- {/* If you do scenario-level editing for planned fields, show scenario edit modal */} - {editOpen && ( - setEditOpen(false)} - scenario={localScenario} - setScenario={setLocalScenario} - apiURL="/api" + {scenario?.id && ( + {}} + onMilestoneUpdated={() => {}} /> )} + {scenario?.id && ( + + )} + +
+ + + +
); } diff --git a/src/components/ScenarioEditModal.js b/src/components/ScenarioEditModal.js index be03aeb..f6317ac 100644 --- a/src/components/ScenarioEditModal.js +++ b/src/components/ScenarioEditModal.js @@ -1,205 +1,971 @@ // src/components/ScenarioEditModal.js -import React, { useState, useEffect } from 'react'; + +import React, { useState, useEffect, useRef } from 'react'; import authFetch from '../utils/authFetch.js'; -const ScenarioEditModal = ({ +// Paths to your JSON/CSV data. Adjust if needed: +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, // <== We'll need the scenario object here - setScenario, // callback to update the scenario in parent - apiURL -}) => { + onClose, // onClose(updatedScenario, updatedCollege) + scenario, + collegeProfile +}) { const [formData, setFormData] = useState({}); + // CIP & IPEDS data + const [schoolData, setSchoolData] = useState([]); + const [icTuitionData, setIcTuitionData] = useState([]); + + // suggestions + const [schoolSuggestions, setSchoolSuggestions] = useState([]); + const [programSuggestions, setProgramSuggestions] = useState([]); + const [availableProgramTypes, setAvailableProgramTypes] = useState([]); + + // 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'); + + // career auto-suggest + const [allCareers, setAllCareers] = useState([]); + const [careerSearchInput, setCareerSearchInput] = useState(''); + const [careerMatches, setCareerMatches] = useState([]); + const careerDropdownRef = useRef(null); + + // ---------- Load CIP/iPEDS/career data once ---------- useEffect(() => { - if (!show || !scenario) return; + async function loadCIP() { + 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 to load CIP data:', err); + } + } + async function loadIPEDS() { + 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 to load iPEDS data:', err); + } + } + async function loadCareers() { + try { + const res = await fetch(CAREER_CLUSTERS_URL); + const data = await res.json(); + const titles = new Set(); + for (const cluster of Object.keys(data)) { + for (const subdiv of Object.keys(data[cluster])) { + const arr = data[cluster][subdiv]; + arr.forEach((obj) => { + if (obj.title) titles.add(obj.title); + }); + } + } + setAllCareers([...titles]); + } catch (err) { + console.error('Failed to load career_clusters:', err); + } + } + + loadCIP(); + loadIPEDS(); + loadCareers(); + }, []); + + // ---------- career auto-suggest logic ---------- + useEffect(() => { + if (!careerSearchInput) { + setCareerMatches([]); + return; + } + const lower = careerSearchInput.toLowerCase(); + const partials = allCareers + .filter((title) => title.toLowerCase().includes(lower)) + .slice(0, 15); + setCareerMatches(partials); + }, [careerSearchInput, allCareers]); + + 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([]); + } + + // ---------- Show => populate formData from scenario & college ---------- + useEffect(() => { + if (!show) return; + if (!scenario) return; + + const s = scenario || {}; + const c = collegeProfile || {}; setFormData({ - careerName: scenario.career_name || '', - status: scenario.status || 'planned', - startDate: scenario.start_date || '', - projectedEndDate: scenario.projected_end_date || '', - // existing fields - // newly added columns: - plannedMonthlyExpenses: scenario.planned_monthly_expenses ?? '', - plannedMonthlyDebt: scenario.planned_monthly_debt_payments ?? '', - plannedMonthlyRetirement: scenario.planned_monthly_retirement_contribution ?? '', - plannedMonthlyEmergency: scenario.planned_monthly_emergency_contribution ?? '', - plannedSurplusEmergencyPct: scenario.planned_surplus_emergency_pct ?? '', - plannedSurplusRetirementPct: scenario.planned_surplus_retirement_pct ?? '', - plannedAdditionalIncome: scenario.planned_additional_income ?? '', - // ... + // Scenario + 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 + selected_school: c.selected_school || '', + selected_program: c.selected_program || '', + program_type: c.program_type || '', + academic_calendar: c.academic_calendar || 'semester', + is_in_state: !!c.is_in_state, + is_in_district: !!c.is_in_district, + is_online: !!c.is_online, + 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: c.tuition ?? 0, + 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 ?? '' }); - }, [show, scenario]); - const handleChange = (e) => { - const { name, value } = e.target; - setFormData(prev => ({ ...prev, [name]: value })); - }; + // set up manual vs auto + setManualTuition(''); + setAutoTuition(0); + setManualProgLength(''); + setAutoProgLength('0.00'); - const handleSave = async () => { - if (!scenario) return; + // career input + setCareerSearchInput(s.career_name || ''); + }, [show, scenario, collegeProfile]); + + // ---------- handle form changes ---------- + function handleFormChange(e) { + const { name, type, checked, value } = e.target; + let val = value; + if (type === 'checkbox') { + val = checked; + } + setFormData((prev) => ({ ...prev, [name]: val })); + } + + // ---------- school / program changes ---------- + function handleSchoolChange(e) { + const val = e.target.value; + setFormData((prev) => ({ + ...prev, + selected_school: val, + selected_program: '', + program_type: '', + credit_hours_required: '' + })); + if (!val) { + setSchoolSuggestions([]); + return; + } + const filtered = schoolData.filter((s) => + s.INSTNM.toLowerCase().includes(val.toLowerCase()) + ); + const uniqueSchools = [...new Set(filtered.map((s) => s.INSTNM))]; + setSchoolSuggestions(uniqueSchools.slice(0, 10)); + setProgramSuggestions([]); + setAvailableProgramTypes([]); + } + function handleSchoolSelect(schoolName) { + setFormData((prev) => ({ + ...prev, + selected_school: schoolName, + selected_program: '', + program_type: '', + credit_hours_required: '' + })); + setSchoolSuggestions([]); + setProgramSuggestions([]); + setAvailableProgramTypes([]); + } + + 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 uniquePrograms = [...new Set(filtered.map((r) => r.CIPDESC))]; + setProgramSuggestions(uniquePrograms.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'); + } + + // ---------- manual tuition & program length ---------- + function handleManualTuitionChange(e) { + setManualTuition(e.target.value); + } + function handleManualProgLengthChange(e) { + setManualProgLength(e.target.value); + } + + // ---------- auto-calc tuition ---------- + useEffect(() => { + const { + selected_school, + program_type, + credit_hours_per_year, + is_in_state, + is_in_district + } = formData; + if (!icTuitionData.length) return; + if (!selected_school || !program_type || !credit_hours_per_year) return; + + // find + const found = schoolData.find( + (s) => s.INSTNM.toLowerCase() === selected_school.toLowerCase() + ); + if (!found?.UNITID) return; + const match = icTuitionData.find((row) => row.UNITID === found.UNITID); + if (!match) return; + + const isGradOrProf = [ + "Master's Degree", + "Doctoral Degree", + "Graduate/Professional Certificate", + "First Professional Degree" + ].includes(program_type); + + let partTimeRate = 0; + let fullTimeTuition = 0; + if (isGradOrProf) { + if (is_in_district) { + partTimeRate = parseFloat(match.HRCHG5 || 0); + fullTimeTuition = parseFloat(match.TUITION5 || 0); + } else if (is_in_state) { + partTimeRate = parseFloat(match.HRCHG6 || 0); + fullTimeTuition = parseFloat(match.TUITION6 || 0); + } else { + partTimeRate = parseFloat(match.HRCHG7 || 0); + fullTimeTuition = parseFloat(match.TUITION7 || 0); + } + } else { + // undergrad + if (is_in_district) { + partTimeRate = parseFloat(match.HRCHG1 || 0); + fullTimeTuition = parseFloat(match.TUITION1 || 0); + } else if (is_in_state) { + partTimeRate = parseFloat(match.HRCHG2 || 0); + fullTimeTuition = parseFloat(match.TUITION2 || 0); + } else { + partTimeRate = parseFloat(match.HRCHG3 || 0); + fullTimeTuition = parseFloat(match.TUITION3 || 0); + } + } + const chpy = parseFloat(credit_hours_per_year) || 0; + let estimate = 0; + if (chpy < 24 && partTimeRate) { + estimate = partTimeRate * chpy; + } else { + estimate = fullTimeTuition; + } + setAutoTuition(Math.round(estimate)); + }, [ + icTuitionData, + formData.selected_school, + formData.program_type, + formData.credit_hours_per_year, + formData.is_in_district, + formData.is_in_state, + schoolData + ]); + + // ---------- auto-calc program length ---------- + useEffect(() => { + const { program_type, hours_completed, credit_hours_per_year, credit_hours_required } = + formData; + if (!program_type) return; + if (!hours_completed || !credit_hours_per_year) return; + + let required = 0; + switch (program_type) { + case "Associate's Degree": + required = 60; + break; + case "Bachelor's Degree": + required = 120; + break; + case "Master's Degree": + required = 60; + break; + case "Doctoral Degree": + required = 120; + break; + case "First Professional Degree": + required = 180; + break; + case "Graduate/Professional Certificate": + required = parseInt(credit_hours_required, 10) || 0; + break; + default: + required = parseInt(credit_hours_required, 10) || 0; + break; + } + const remain = required - (parseInt(hours_completed, 10) || 0); + const yrs = remain / (parseFloat(credit_hours_per_year) || 1); + setAutoProgLength(yrs.toFixed(2)); + }, [ + formData.program_type, + formData.hours_completed, + formData.credit_hours_per_year, + formData.credit_hours_required + ]); + + // ---------- handleSave => PUT scenario & college => onClose(...) + async function handleSave() { try { - // We'll call POST /api/premium/career-profile or a separate PUT. - // Because the code is "upsert," we can do the same POST - // and rely on ON CONFLICT. - const payload = { - career_name: formData.careerName, - status: formData.status, - start_date: formData.startDate, - projected_end_date: formData.projectedEndDate, + // chosen tuition + const chosenTuition = + manualTuition.trim() === '' ? autoTuition : parseFloat(manualTuition); + const chosenProgLen = + manualProgLength.trim() === '' ? autoProgLength : manualProgLength; - planned_monthly_expenses: formData.plannedMonthlyExpenses === '' - ? null - : parseFloat(formData.plannedMonthlyExpenses), - planned_monthly_debt_payments: formData.plannedMonthlyDebt === '' - ? null - : parseFloat(formData.plannedMonthlyDebt), - planned_monthly_retirement_contribution: formData.plannedMonthlyRetirement === '' - ? null - : parseFloat(formData.plannedMonthlyRetirement), - planned_monthly_emergency_contribution: formData.plannedMonthlyEmergency === '' - ? null - : parseFloat(formData.plannedMonthlyEmergency), - planned_surplus_emergency_pct: formData.plannedSurplusEmergencyPct === '' - ? null - : parseFloat(formData.plannedSurplusEmergencyPct), - planned_surplus_retirement_pct: formData.plannedSurplusRetirementPct === '' - ? null - : parseFloat(formData.plannedSurplusRetirementPct), - planned_additional_income: formData.plannedAdditionalIncome === '' - ? null - : parseFloat(formData.plannedAdditionalIncome), + // scenario payload + const scenarioPayload = { + scenario_title: formData.scenario_title || '', + career_name: formData.career_name || '', + status: formData.status || 'planned', + start_date: formData.start_date || null, + projected_end_date: formData.projected_end_date || null, + college_enrollment_status: formData.college_enrollment_status || 'not_enrolled', + currently_working: formData.currently_working || 'no', + + planned_monthly_expenses: + formData.planned_monthly_expenses === '' ? null : Number(formData.planned_monthly_expenses), + planned_monthly_debt_payments: + formData.planned_monthly_debt_payments === '' ? null : Number(formData.planned_monthly_debt_payments), + planned_monthly_retirement_contribution: + formData.planned_monthly_retirement_contribution === '' ? null : Number(formData.planned_monthly_retirement_contribution), + planned_monthly_emergency_contribution: + formData.planned_monthly_emergency_contribution === '' ? null : Number(formData.planned_monthly_emergency_contribution), + planned_surplus_emergency_pct: + formData.planned_surplus_emergency_pct === '' ? null : Number(formData.planned_surplus_emergency_pct), + planned_surplus_retirement_pct: + formData.planned_surplus_retirement_pct === '' ? null : Number(formData.planned_surplus_retirement_pct), + planned_additional_income: + formData.planned_additional_income === '' ? null : Number(formData.planned_additional_income) }; - const res = await authFetch(`${apiURL}/premium/career-profile`, { - method: 'POST', + // 1) Put scenario + const scenRes = await authFetch(`/api/premium/career-profile/${scenario.id}`, { + method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) + body: JSON.stringify(scenarioPayload) }); - if (!res.ok) throw new Error(`HTTP ${res.status} - failed to update scenario`); + if (!scenRes.ok) { + const eText = await scenRes.text(); + throw new Error('Scenario update failed: ' + eText); + } + const updatedScenario = await scenRes.json(); // updated scenario row - // If successful, we can optionally fetch the updated row or just - // update local scenario: - const data = await res.json(); - console.log('Scenario upserted:', data); + // 2) Put college + const colId = collegeProfile?.id; // or handle no ID + const collegePayload = { + selected_school: formData.selected_school || null, + selected_program: formData.selected_program || null, + program_type: formData.program_type || null, + academic_calendar: formData.academic_calendar || 'semester', + is_in_state: formData.is_in_state ? 1 : 0, + is_in_district: formData.is_in_district ? 1 : 0, + is_online: formData.is_online ? 1 : 0, + college_enrollment_status: formData.college_enrollment_status_db || 'not_enrolled', + annual_financial_aid: + formData.annual_financial_aid === '' ? 0 : Number(formData.annual_financial_aid), + existing_college_debt: + formData.existing_college_debt === '' ? 0 : Number(formData.existing_college_debt), + tuition: chosenTuition, + tuition_paid: + formData.tuition_paid === '' ? 0 : Number(formData.tuition_paid), + loan_deferral_until_graduation: + formData.loan_deferral_until_graduation ? 1 : 0, + loan_term: + formData.loan_term === '' ? 10 : Number(formData.loan_term), + interest_rate: + formData.interest_rate === '' ? 5 : Number(formData.interest_rate), + extra_payment: + formData.extra_payment === '' ? 0 : Number(formData.extra_payment), + credit_hours_per_year: + formData.credit_hours_per_year === '' ? 0 : Number(formData.credit_hours_per_year), + hours_completed: + formData.hours_completed === '' ? 0 : Number(formData.hours_completed), + program_length: chosenProgLen, + credit_hours_required: + formData.credit_hours_required === '' ? 0 : Number(formData.credit_hours_required), + expected_graduation: formData.expected_graduation || null, + expected_salary: + formData.expected_salary === '' ? 0 : Number(formData.expected_salary) + }; - // Optionally call setScenario if you want to reflect changes in UI - setScenario(prev => ({ - ...prev, - career_name: formData.careerName, - status: formData.status, - start_date: formData.startDate, - projected_end_date: formData.projectedEndDate, - planned_monthly_expenses: payload.planned_monthly_expenses, - planned_monthly_debt_payments: payload.planned_monthly_debt_payments, - planned_monthly_retirement_contribution: payload.planned_monthly_retirement_contribution, - planned_monthly_emergency_contribution: payload.planned_monthly_emergency_contribution, - planned_surplus_emergency_pct: payload.planned_surplus_emergency_pct, - planned_surplus_retirement_pct: payload.planned_surplus_retirement_pct, - planned_additional_income: payload.planned_additional_income - })); + const colRes = await authFetch(`/api/premium/college-profile/${colId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(collegePayload) + }); + if (!colRes.ok) { + const colText = await colRes.text(); + throw new Error('College update failed: ' + colText); + } + const updatedCollege = await colRes.json(); - onClose(); + onClose(updatedScenario, updatedCollege); } catch (err) { - console.error('Error saving scenario changes:', err); - alert('Failed to save scenario. See console for details.'); + console.error('Error in handleSave:', err); + alert(err.message || 'Failed to save scenario changes'); } - }; + } if (!show) return null; + // displayed tuition/programLength + const displayedTuition = + manualTuition.trim() === '' ? autoTuition : manualTuition; + const displayedProgLen = + manualProgLength.trim() === '' ? autoProgLength : manualProgLength; + return ( -
-
-

Edit Scenario

+
+
+

+ Edit Scenario: {scenario?.scenario_title || scenario?.career_name} +

- - + {/* ============ SCENARIO (CAREER) SECTION ============ */} +

Scenario (Career Paths)

+
+
+ + - - + {careerMatches.length > 0 && ( +
    + {careerMatches.map((c, idx) => ( +
  • handleSelectCareer(c)} + > + {c} +
  • + ))} +
+ )} +

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

+ + + + + + + + + + + + + + + +
+ + {/* ============ SCENARIO (FINANCIAL) SECTION ============ */} +

Scenario Overwrites (financial)

+
+
- - - - +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
- {/* A few new fields for “planned_” columns: */} - - + {/* ============ COLLEGE SECTION ============ */} +

College Profile

+
+ {(formData.college_enrollment_status === 'currently_enrolled' || + formData.college_enrollment_status === 'prospective_student') ? ( +
+
+ + + +
- - + - - + + + {schoolSuggestions.length > 0 && ( +
    + {schoolSuggestions.map((sch, idx) => ( +
  • handleSchoolSelect(sch)} + > + {sch} +
  • + ))} +
+ )} - - + + + {programSuggestions.length > 0 && ( +
    + {programSuggestions.map((prog, i) => ( +
  • handleProgramSelect(prog)} + > + {prog} +
  • + ))} +
+ )} - - + + - - + {['Graduate/Professional Certificate','Doctoral Degree','First Professional Degree'] + .includes(formData.program_type) && ( + <> + + + + )} - - + + -
-
+ ) : ( +

+ Not currently enrolled or prospective. Minimal college fields only. +

+ )} + +
+ - +
); -}; - -export default ScenarioEditModal; +} diff --git a/user_profile.db b/user_profile.db index bfe03ff2ec821577dd6289fe1b26bae0e126775b..b9c2af5b9e60b3a1e5d2b38d3731e7d420f401c2 100644 GIT binary patch delta 501 zcmZoTz}9epZGyC58Uq7^6cBR*F*6XyPt-AHOxu{SFrJ%lF9R$0KL+l9{2`lp5;pR& z0Hvo-KA1OIv{92uD3r-rT9Ls}*wz*(3<8r+=e5fjnS>Y`Ss7SZ85`-Dnwm#(0VSCE zwleT-<)6s+WMkuUJ}y0Wrcj_LBc~EjhYB}yqc#UqsC}cMKI8O{F^p=PMe08k2(j{; zFz~PEkLAC?Z^GXOf*Tvp@NItdPlZt=iGi=3&yV*H&u^X+Tzgq~ITte~ZIxp5VB~7m z5N8)xRb}iF-rmT_n9U@jP@J5amspgUA77GLl9Q?s;u;aM{WUYAqz@M}&pHO4b-X+H z9XB>E;Mu+|hB1hViNSd~Q#_-+YoiUbOeolyvK$;7h8#>x9Gr}d9E?D$$ZTw!Xkwa{ zY><|uYnp0kplf1gZlG&nVQHkBmY8IjVq%zNY+{iL@!s@#ag0*V!X4@YT&&_EEJg-K zmbwNex<=-VYc~KnreKbV0xZn12!TS7mEUVSdjjKMeJ*DHHU?mrwDGUs*jT}@slgn| ONQ`5cr>~#S7zhAvUVIb) delta 395 zcmZoTz}9epZGyC5A_D`16cBR*F*6XyP1G@FOx&2TFrJ$~iGiJK5d-%>zP+1y5;pR& zG%7K1Pd=D8dGf7%J`JGSd7(_E(u#_P!p6pyX(@?@<`zl1=4r-_jEvl18K4ql+sPm5 zR3;nbh z8pXu-gn@q|-;<4vH~6?3wK4?;oDuJSVvJvhZ>)W-Q+-#puB}ae^pUtFSn`xT-2+ zXXf@CM#gNW?QfVFC44xTc-ArStmECWv2hX4_H{9gK}<}HOw*a-8SO!qHi`;&2zM}C zn%)B9r?<%A5)6%5U+j18;|j6rT^=jYqbp1}B5pM!~iJp<5f>o*G; MEasoSem-L$08{^Iv;Y7A