217 lines
7.7 KiB
JavaScript
217 lines
7.7 KiB
JavaScript
// src/components/ScenarioContainer.js
|
||
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
|
||
onClone,
|
||
onRemove,
|
||
onScenarioUpdated
|
||
}) {
|
||
const [localScenario, setLocalScenario] = useState(scenario);
|
||
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);
|
||
|
||
// Re-sync if parent updates scenario
|
||
useEffect(() => {
|
||
setLocalScenario(scenario);
|
||
}, [scenario]);
|
||
|
||
// 1) Fetch the college profile for this scenario
|
||
useEffect(() => {
|
||
if (!localScenario?.id) return;
|
||
async function loadCollegeProfile() {
|
||
try {
|
||
const res = await authFetch(
|
||
`/api/premium/college-profile?careerPathId=${localScenario.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();
|
||
}, [localScenario]);
|
||
|
||
// 2) Fetch scenario’s milestones (and universal)
|
||
useEffect(() => {
|
||
if (!localScenario?.id) return;
|
||
async function loadMilestones() {
|
||
try {
|
||
const [scenRes, uniRes] = await Promise.all([
|
||
authFetch(`/api/premium/milestones?careerPathId=${localScenario.id}`),
|
||
authFetch(`/api/premium/milestones?careerPathId=universal`) // if you have that route
|
||
]);
|
||
|
||
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();
|
||
}, [localScenario]);
|
||
|
||
// 3) Merge real snapshot + scenario overrides => run simulation
|
||
useEffect(() => {
|
||
if (!financialProfile || !collegeProfile) return;
|
||
|
||
// Merge the scenario's planned overrides if not null,
|
||
// else fallback to the real snapshot in financialProfile
|
||
const mergedProfile = {
|
||
currentSalary: financialProfile.current_salary || 0,
|
||
monthlyExpenses:
|
||
localScenario.planned_monthly_expenses ?? financialProfile.monthly_expenses ?? 0,
|
||
monthlyDebtPayments:
|
||
localScenario.planned_monthly_debt_payments ?? financialProfile.monthly_debt_payments ?? 0,
|
||
retirementSavings: financialProfile.retirement_savings ?? 0,
|
||
emergencySavings: financialProfile.emergency_fund ?? 0,
|
||
monthlyRetirementContribution:
|
||
localScenario.planned_monthly_retirement_contribution ??
|
||
financialProfile.retirement_contribution ??
|
||
0,
|
||
monthlyEmergencyContribution:
|
||
localScenario.planned_monthly_emergency_contribution ??
|
||
financialProfile.emergency_contribution ??
|
||
0,
|
||
surplusEmergencyAllocation:
|
||
localScenario.planned_surplus_emergency_pct ??
|
||
financialProfile.extra_cash_emergency_pct ??
|
||
50,
|
||
surplusRetirementAllocation:
|
||
localScenario.planned_surplus_retirement_pct ??
|
||
financialProfile.extra_cash_retirement_pct ??
|
||
50,
|
||
additionalIncome:
|
||
localScenario.planned_additional_income ?? financialProfile.additional_income ?? 0,
|
||
|
||
// College fields
|
||
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,
|
||
|
||
// Flatten scenario + universal milestoneImpacts
|
||
milestoneImpacts: buildAllImpacts([...milestones, ...universalMilestones])
|
||
};
|
||
|
||
const { projectionData, loanPaidOffMonth } =
|
||
simulateFinancialProjection(mergedProfile);
|
||
setProjectionData(projectionData);
|
||
setLoanPaidOffMonth(loanPaidOffMonth);
|
||
}, [financialProfile, collegeProfile, localScenario, milestones, universalMilestones]);
|
||
|
||
function buildAllImpacts(allMilestones) {
|
||
let impacts = [];
|
||
for (let m of allMilestones) {
|
||
if (m.impacts) {
|
||
impacts.push(...m.impacts);
|
||
}
|
||
// If new_salary logic is relevant, handle it here
|
||
}
|
||
return impacts;
|
||
}
|
||
|
||
// Edit => open modal
|
||
return (
|
||
<div style={{ width: '420px', border: '1px solid #ccc', padding: '1rem' }}>
|
||
<h3>{localScenario.career_name || 'Untitled Scenario'}</h3>
|
||
<p>Status: {localScenario.status}</p>
|
||
|
||
<Line
|
||
data={{
|
||
labels: projectionData.map((p) => p.month),
|
||
datasets: [
|
||
{
|
||
label: 'Net Savings',
|
||
data: projectionData.map((p) => p.cumulativeNetSavings || 0),
|
||
borderColor: 'blue',
|
||
fill: false
|
||
}
|
||
]
|
||
}}
|
||
options={{ responsive: true }}
|
||
/>
|
||
|
||
<div style={{ marginTop: '0.5rem' }}>
|
||
<strong>Loan Paid Off:</strong> {loanPaidOffMonth || 'N/A'} <br />
|
||
<strong>Retirement (final):</strong> ${
|
||
projectionData[projectionData.length - 1]?.retirementSavings?.toFixed(0) || 0
|
||
}
|
||
</div>
|
||
|
||
<MilestoneTimeline
|
||
careerPathId={localScenario.id}
|
||
authFetch={authFetch}
|
||
activeView="Financial"
|
||
setActiveView={() => {}}
|
||
onMilestoneUpdated={() => {
|
||
// re-fetch or something
|
||
}}
|
||
/>
|
||
|
||
<AISuggestedMilestones
|
||
career={localScenario.career_name}
|
||
careerPathId={localScenario.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>
|
||
|
||
{/* Updated ScenarioEditModal that references localScenario + setLocalScenario */}
|
||
<ScenarioEditModal
|
||
show={editOpen}
|
||
onClose={() => setEditOpen(false)}
|
||
scenario={localScenario}
|
||
setScenario={setLocalScenario}
|
||
apiURL="/api"
|
||
/>
|
||
</div>
|
||
);
|
||
}
|