Added scenario length user input field and drafted scenario multi-view and scenario container components.
This commit is contained in:
parent
3c4247f3b8
commit
eed3767172
@ -58,6 +58,8 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
const [projectionData, setProjectionData] = useState([]);
|
const [projectionData, setProjectionData] = useState([]);
|
||||||
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
|
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
|
||||||
|
|
||||||
|
const [simulationYearsInput, setSimulationYearsInput] = useState("20");
|
||||||
|
|
||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
|
|
||||||
// Possibly loaded from location.state
|
// Possibly loaded from location.state
|
||||||
@ -66,6 +68,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
loanPayoffMonth: initialLoanPayoffMonth = null
|
loanPayoffMonth: initialLoanPayoffMonth = null
|
||||||
} = location.state || {};
|
} = location.state || {};
|
||||||
|
|
||||||
|
const simulationYears = parseInt(simulationYearsInput, 10) || 20;
|
||||||
// -------------------------
|
// -------------------------
|
||||||
// 1. Fetch career paths + financialProfile on mount
|
// 1. Fetch career paths + financialProfile on mount
|
||||||
// -------------------------
|
// -------------------------
|
||||||
@ -198,7 +201,9 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary,
|
expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary,
|
||||||
|
|
||||||
// The key: impacts
|
// The key: impacts
|
||||||
milestoneImpacts: allImpacts
|
milestoneImpacts: allImpacts,
|
||||||
|
|
||||||
|
simulationYears,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 5) Run the simulation
|
// 5) Run the simulation
|
||||||
@ -218,7 +223,19 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
console.error('Error fetching initial milestones/impacts or simulating:', err);
|
console.error('Error fetching initial milestones/impacts or simulating:', err);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [financialProfile, collegeProfile, selectedCareer, careerPathId]);
|
}, [financialProfile, collegeProfile, simulationYears, selectedCareer, careerPathId]);
|
||||||
|
|
||||||
|
const handleSimulationYearsChange = (e) => {
|
||||||
|
setSimulationYearsInput(e.target.value); // let user type partial/blank
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSimulationYearsBlur = () => {
|
||||||
|
// Optionally, onBlur you can “normalize” the value
|
||||||
|
// (e.g. if they left it blank, revert to "20").
|
||||||
|
if (simulationYearsInput.trim() === "") {
|
||||||
|
setSimulationYearsInput("20");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// -------------------------------------------------
|
// -------------------------------------------------
|
||||||
// 4. reSimulate() => re-fetch everything (financial, college, milestones & impacts),
|
// 4. reSimulate() => re-fetch everything (financial, college, milestones & impacts),
|
||||||
@ -453,6 +470,16 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>Simulation Length (years): </label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={simulationYearsInput}
|
||||||
|
onChange={handleSimulationYearsChange}
|
||||||
|
onBlur={handleSimulationYearsBlur}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<CareerSearch
|
<CareerSearch
|
||||||
onSelectCareer={(careerName) => setPendingCareerForModal(careerName)}
|
onSelectCareer={(careerName) => setPendingCareerForModal(careerName)}
|
||||||
setPendingCareerForModal={setPendingCareerForModal}
|
setPendingCareerForModal={setPendingCareerForModal}
|
||||||
|
162
src/components/MultiScenarioView.js
Normal file
162
src/components/MultiScenarioView.js
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
// src/components/MultiScenarioView.js
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import authFetch from '../utils/authFetch.js';
|
||||||
|
import ScenarioContainer from './ScenarioContainer.js';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
export default function MultiScenarioView() {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [financialProfile, setFinancialProfile] = useState(null);
|
||||||
|
const [scenarios, setScenarios] = useState([]); // each scenario corresponds to a row in career_paths
|
||||||
|
|
||||||
|
// For error reporting
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
// 1) On mount, fetch the user’s single financial profile + all career_paths.
|
||||||
|
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();
|
||||||
|
|
||||||
|
// B) fetch all career_paths (scenarios)
|
||||||
|
let scenRes = await authFetch('/api/premium/career-profile/all');
|
||||||
|
if (!scenRes.ok) throw new Error(`Scenarios error: ${scenRes.status}`);
|
||||||
|
let 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');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 2) “Add Scenario” => create a brand new row in career_paths
|
||||||
|
async function handleAddScenario() {
|
||||||
|
try {
|
||||||
|
// You might prompt user for a scenario name, or just default
|
||||||
|
const body = {
|
||||||
|
career_name: 'New Scenario ' + new Date().toLocaleDateString(),
|
||||||
|
status: 'planned',
|
||||||
|
start_date: new Date().toISOString(),
|
||||||
|
college_enrollment_status: 'not_enrolled',
|
||||||
|
currently_working: 'no'
|
||||||
|
};
|
||||||
|
const res = await authFetch('/api/premium/career-profile', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`Add scenario error: ${res.status}`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
// re-fetch scenarios or just push a new scenario object
|
||||||
|
const newRow = {
|
||||||
|
id: data.career_path_id,
|
||||||
|
user_id: null, // we can skip if not needed
|
||||||
|
career_name: body.career_name,
|
||||||
|
status: body.status,
|
||||||
|
start_date: body.start_date,
|
||||||
|
projected_end_date: null,
|
||||||
|
college_enrollment_status: body.college_enrollment_status,
|
||||||
|
currently_working: body.currently_working
|
||||||
|
};
|
||||||
|
setScenarios(prev => [...prev, newRow]);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed adding scenario:', err);
|
||||||
|
alert('Could not add scenario');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) “Clone” => POST a new row in career_paths with copied fields
|
||||||
|
async function handleCloneScenario(sourceScenario) {
|
||||||
|
try {
|
||||||
|
// A simple approach: just create a new row with the same fields
|
||||||
|
// Then copy the existing scenario fields
|
||||||
|
const body = {
|
||||||
|
career_name: sourceScenario.career_name + ' (Copy)',
|
||||||
|
status: sourceScenario.status,
|
||||||
|
start_date: sourceScenario.start_date,
|
||||||
|
projected_end_date: sourceScenario.projected_end_date,
|
||||||
|
college_enrollment_status: sourceScenario.college_enrollment_status,
|
||||||
|
currently_working: sourceScenario.currently_working
|
||||||
|
};
|
||||||
|
const res = await authFetch('/api/premium/career-profile', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`Clone scenario error: ${res.status}`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
const newScenarioId = data.career_path_id;
|
||||||
|
|
||||||
|
// Optionally, also clone the scenario’s milestones, if you want them duplicated:
|
||||||
|
// (You’d fetch all existing milestones for sourceScenario, then re-insert them for newScenario.)
|
||||||
|
// This example just leaves that out for brevity.
|
||||||
|
|
||||||
|
// Add it to local state
|
||||||
|
const newRow = {
|
||||||
|
id: newScenarioId,
|
||||||
|
career_name: body.career_name,
|
||||||
|
status: body.status,
|
||||||
|
start_date: body.start_date,
|
||||||
|
projected_end_date: body.projected_end_date,
|
||||||
|
college_enrollment_status: body.college_enrollment_status,
|
||||||
|
currently_working: body.currently_working
|
||||||
|
};
|
||||||
|
setScenarios(prev => [...prev, newRow]);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed cloning scenario:', err);
|
||||||
|
alert('Could not clone scenario');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) “Remove” => (If you want a delete scenario)
|
||||||
|
async function handleRemoveScenario(scenarioId) {
|
||||||
|
try {
|
||||||
|
// If you have a real DELETE endpoint for career_paths, use it:
|
||||||
|
// For now, we’ll just remove from the local UI:
|
||||||
|
setScenarios(prev => prev.filter(s => s.id !== scenarioId));
|
||||||
|
// Optionally, implement an API call:
|
||||||
|
// await authFetch(`/api/premium/career-profile/${scenarioId}`, { method: 'DELETE' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed removing scenario:', err);
|
||||||
|
alert('Could not remove scenario');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) return <p>Loading scenarios...</p>;
|
||||||
|
if (error) return <p className="text-red-600">Error: {error}</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="multi-scenario-view" style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem' }}>
|
||||||
|
{scenarios.map((scen) => (
|
||||||
|
<ScenarioContainer
|
||||||
|
key={scen.id}
|
||||||
|
scenario={scen}
|
||||||
|
financialProfile={financialProfile} // shared for all scenarios
|
||||||
|
onClone={() => handleCloneScenario(scen)}
|
||||||
|
onRemove={() => handleRemoveScenario(scen.id)}
|
||||||
|
// Optionally refresh the scenario if user changes it
|
||||||
|
onScenarioUpdated={(updated) => {
|
||||||
|
setScenarios(prev => prev.map(s => s.id === scen.id ? { ...s, ...updated } : s));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div style={{ alignSelf: 'flex-start' }}>
|
||||||
|
<button onClick={handleAddScenario}>+ Add Scenario</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -266,7 +266,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerPathId })
|
|||||||
}
|
}
|
||||||
|
|
||||||
setAutoTuition(Math.round(estimate));
|
setAutoTuition(Math.round(estimate));
|
||||||
// We do NOT auto-update parent's data. We'll do that in handleSubmit or if you prefer, you can store it in parent's data anyway.
|
|
||||||
}, [
|
}, [
|
||||||
icTuitionData, selected_school, program_type,
|
icTuitionData, selected_school, program_type,
|
||||||
credit_hours_per_year, is_in_state, is_in_district, schoolData
|
credit_hours_per_year, is_in_state, is_in_district, schoolData
|
||||||
|
231
src/components/ScenarioContainer.js
Normal file
231
src/components/ScenarioContainer.js
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
// src/components/ScenarioContainer.js
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Line } from 'react-chartjs-2';
|
||||||
|
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
|
||||||
|
|
||||||
|
// Reuse your existing:
|
||||||
|
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
|
||||||
|
onClone,
|
||||||
|
onRemove,
|
||||||
|
onScenarioUpdated // callback to parent to store updated scenario data
|
||||||
|
}) {
|
||||||
|
const [collegeProfile, setCollegeProfile] = useState(null);
|
||||||
|
const [milestones, setMilestones] = useState([]);
|
||||||
|
const [universalMilestones, setUniversalMilestones] = useState([]);
|
||||||
|
|
||||||
|
const [projectionData, setProjectionData] = useState([]);
|
||||||
|
const [loanPaidOffMonth, setLoanPaidOffMonth] = useState(null);
|
||||||
|
|
||||||
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
|
|
||||||
|
// 1) Fetch the college profile for this scenario
|
||||||
|
useEffect(() => {
|
||||||
|
if (!scenario?.id) return;
|
||||||
|
async function loadCollegeProfile() {
|
||||||
|
try {
|
||||||
|
const res = await authFetch(`/api/premium/college-profile?careerPathId=${scenario.id}`);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadCollegeProfile();
|
||||||
|
}, [scenario]);
|
||||||
|
|
||||||
|
// 2) Fetch scenario’s milestones (where is_universal=0) + universal (is_universal=1)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!scenario?.id) return;
|
||||||
|
async function loadMilestones() {
|
||||||
|
try {
|
||||||
|
const [scenRes, uniRes] = await Promise.all([
|
||||||
|
authFetch(`/api/premium/milestones?careerPathId=${scenario.id}`),
|
||||||
|
// for universal: we do an extra call with no careerPathId.
|
||||||
|
// But your current code always requires a careerPathId. So you might
|
||||||
|
// create a new endpoint /api/premium/milestones?is_universal=1 or something.
|
||||||
|
// We'll assume you have it:
|
||||||
|
authFetch(`/api/premium/milestones?careerPathId=universal`)
|
||||||
|
]);
|
||||||
|
|
||||||
|
let scenarioData = scenRes.ok ? (await scenRes.json()) : { milestones: [] };
|
||||||
|
let universalData = uniRes.ok ? (await uniRes.json()) : { milestones: [] };
|
||||||
|
|
||||||
|
setMilestones(scenarioData.milestones || []);
|
||||||
|
setUniversalMilestones(universalData.milestones || []);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load milestones:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadMilestones();
|
||||||
|
}, [scenario]);
|
||||||
|
|
||||||
|
// 3) Whenever we have financialProfile + collegeProfile + milestones, run the simulation
|
||||||
|
useEffect(() => {
|
||||||
|
if (!financialProfile || !collegeProfile) return;
|
||||||
|
|
||||||
|
// Merge them into the userProfile object for the simulator:
|
||||||
|
const mergedProfile = {
|
||||||
|
// Financial fields
|
||||||
|
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)
|
||||||
|
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,
|
||||||
|
|
||||||
|
// We assume user’s baseline “inCollege” from the DB:
|
||||||
|
inCollege:
|
||||||
|
collegeProfile.college_enrollment_status === 'currently_enrolled' ||
|
||||||
|
collegeProfile.college_enrollment_status === 'prospective_student',
|
||||||
|
|
||||||
|
// If you store expected_salary in collegeProfile
|
||||||
|
expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary,
|
||||||
|
|
||||||
|
// Flatten the scenario + universal milestones’ impacts
|
||||||
|
milestoneImpacts: buildAllImpacts([...milestones, ...universalMilestones])
|
||||||
|
};
|
||||||
|
|
||||||
|
const { projectionData, loanPaidOffMonth } = simulateFinancialProjection(mergedProfile);
|
||||||
|
setProjectionData(projectionData);
|
||||||
|
setLoanPaidOffMonth(loanPaidOffMonth);
|
||||||
|
}, [financialProfile, collegeProfile, milestones, universalMilestones]);
|
||||||
|
|
||||||
|
// Helper: Flatten all milestone impacts into one array for the simulator
|
||||||
|
function buildAllImpacts(allMilestones) {
|
||||||
|
let impacts = [];
|
||||||
|
for (let m of allMilestones) {
|
||||||
|
// Possibly fetch m.impacts if you store them directly on the milestone
|
||||||
|
// or if you fetch them separately.
|
||||||
|
// If your code stores them as `m.impacts = [ { direction, amount, ... } ]`
|
||||||
|
if (m.impacts) {
|
||||||
|
impacts.push(...m.impacts);
|
||||||
|
}
|
||||||
|
// If you also want a milestone that sets a new salary, handle that logic too
|
||||||
|
// E.g., { impact_type: 'SALARY_CHANGE', start_date: m.date, newSalary: m.new_salary }
|
||||||
|
}
|
||||||
|
return impacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) We’ll display a single line chart with Net Savings (or cumulativeNetSavings)
|
||||||
|
const labels = projectionData.map((p) => p.month);
|
||||||
|
const netSavingsData = projectionData.map((p) => p.cumulativeNetSavings || 0);
|
||||||
|
|
||||||
|
// Grab final row for some KPIs
|
||||||
|
const finalRow = projectionData[projectionData.length - 1] || {};
|
||||||
|
const finalRet = finalRow.retirementSavings?.toFixed(0) || '0';
|
||||||
|
const finalEmerg = finalRow.emergencySavings?.toFixed(0) || '0';
|
||||||
|
|
||||||
|
// 5) Handle “Edit” scenario -> open your existing `ScenarioEditModal.js`
|
||||||
|
// But that modal currently references setFinancialProfile, setCollegeProfile directly,
|
||||||
|
// so you may want a specialized version that changes only this scenario’s row.
|
||||||
|
// For simplicity, we’ll just show how to open it:
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: '420px', border: '1px solid #ccc', padding: '1rem' }}>
|
||||||
|
<h3>{scenario.career_name || 'Untitled Scenario'}</h3>
|
||||||
|
<p>Status: {scenario.status}</p>
|
||||||
|
|
||||||
|
<Line
|
||||||
|
data={{
|
||||||
|
labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Net Savings',
|
||||||
|
data: netSavingsData,
|
||||||
|
borderColor: 'blue',
|
||||||
|
fill: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
options={{ responsive: true }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '0.5rem' }}>
|
||||||
|
<strong>Loan Paid Off:</strong> {loanPaidOffMonth || 'N/A'} <br />
|
||||||
|
<strong>Final Retirement:</strong> ${finalRet} <br />
|
||||||
|
<strong>Final Emergency:</strong> ${finalEmerg}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* The timeline for this scenario. We pass careerPathId */}
|
||||||
|
<MilestoneTimeline
|
||||||
|
careerPathId={scenario.id}
|
||||||
|
authFetch={authFetch}
|
||||||
|
activeView="Financial" // or a state that toggles Career vs. Financial
|
||||||
|
setActiveView={() => {}}
|
||||||
|
onMilestoneUpdated={() => {
|
||||||
|
// re-fetch or something
|
||||||
|
// We'll just force a re-fetch of scenario’s milestones
|
||||||
|
// or re-run the entire load effect
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Show AI suggestions if you like */}
|
||||||
|
<AISuggestedMilestones
|
||||||
|
career={scenario.career_name}
|
||||||
|
careerPathId={scenario.id}
|
||||||
|
authFetch={authFetch}
|
||||||
|
activeView="Financial"
|
||||||
|
projectionData={projectionData}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '0.5rem' }}>
|
||||||
|
<button onClick={() => setEditOpen(true)}>Edit</button>
|
||||||
|
<button onClick={onClone} style={{ marginLeft: '0.5rem' }}>Clone</button>
|
||||||
|
<button onClick={onRemove} style={{ marginLeft: '0.5rem', color: 'red' }}>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reuse your existing ScenarioEditModal that expects
|
||||||
|
setFinancialProfile, setCollegeProfile, etc.
|
||||||
|
However, you might want a specialized "ScenarioEditModal" that updates
|
||||||
|
the DB fields for *this* scenario. For now, we just show how to open. */}
|
||||||
|
<ScenarioEditModal
|
||||||
|
show={editOpen}
|
||||||
|
onClose={() => setEditOpen(false)}
|
||||||
|
financialProfile={financialProfile}
|
||||||
|
setFinancialProfile={() => {
|
||||||
|
// If you truly want scenario-specific financial data,
|
||||||
|
// you’d do a more advanced approach.
|
||||||
|
// For now, do nothing or re-fetch from server.
|
||||||
|
}}
|
||||||
|
collegeProfile={collegeProfile}
|
||||||
|
setCollegeProfile={(updated) => {
|
||||||
|
setCollegeProfile((prev) => ({ ...prev, ...updated }));
|
||||||
|
}}
|
||||||
|
apiURL="/api"
|
||||||
|
authFetch={authFetch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -135,7 +135,11 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
stateCode = 'GA',
|
stateCode = 'GA',
|
||||||
|
|
||||||
// Financial milestone impacts
|
// Financial milestone impacts
|
||||||
milestoneImpacts = []
|
milestoneImpacts = [],
|
||||||
|
|
||||||
|
// Simulation duration
|
||||||
|
simulationYears = 20
|
||||||
|
|
||||||
} = userProfile;
|
} = userProfile;
|
||||||
|
|
||||||
/***************************************************
|
/***************************************************
|
||||||
@ -204,7 +208,7 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
/***************************************************
|
/***************************************************
|
||||||
* 6) SETUP FOR THE SIMULATION LOOP
|
* 6) SETUP FOR THE SIMULATION LOOP
|
||||||
***************************************************/
|
***************************************************/
|
||||||
const maxMonths = 240; // 20 years
|
const maxMonths = simulationYears*12; // 20 years
|
||||||
let loanBalance = Math.max(studentLoanAmount, 0);
|
let loanBalance = Math.max(studentLoanAmount, 0);
|
||||||
let loanPaidOffMonth = null;
|
let loanPaidOffMonth = null;
|
||||||
|
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user