import React, { useState, useEffect, useCallback } from 'react'; import { Line } from 'react-chartjs-2'; import { Chart as ChartJS } from 'chart.js'; import annotationPlugin from 'chartjs-plugin-annotation'; import { Button } from './ui/button.js'; import authFetch from '../utils/authFetch.js'; import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js'; import AISuggestedMilestones from './AISuggestedMilestones.js'; import ScenarioEditModal from './ScenarioEditModal.js'; // Register the annotation plugin globally ChartJS.register(annotationPlugin); export default function ScenarioContainer({ scenario, financialProfile, onRemove, onClone, onEdit }) { /************************************************************* * 1) Scenario Dropdown *************************************************************/ const [allScenarios, setAllScenarios] = useState([]); const [localScenario, setLocalScenario] = useState(scenario || null); useEffect(() => { async function loadScenarios() { try { const res = await authFetch('/api/premium/career-profile/all'); if (!res.ok) { throw new Error(`Failed fetching scenario list: ${res.status}`); } const data = await res.json(); setAllScenarios(data.careerPaths || []); } catch (err) { console.error('Error loading allScenarios for dropdown:', err); } } loadScenarios(); }, []); useEffect(() => { setLocalScenario(scenario || null); }, [scenario]); function handleScenarioSelect(e) { const chosenId = e.target.value; const found = allScenarios.find((s) => s.id === chosenId); setLocalScenario(found || null); } /************************************************************* * 2) College Profile + Milestones *************************************************************/ const [collegeProfile, setCollegeProfile] = useState(null); const [milestones, setMilestones] = useState([]); const [impactsByMilestone, setImpactsByMilestone] = useState({}); const [showEditModal, setShowEditModal] = useState(false); const [editingScenarioData, setEditingScenarioData] = useState({ scenario: null, collegeProfile: null }); // load the college profile useEffect(() => { if (!localScenario?.id) { setCollegeProfile(null); return; } async function loadCollegeProfile() { try { const url = `/api/premium/college-profile?careerPathId=${localScenario.id}`; const res = await authFetch(url); if (res.ok) { const data = await res.json(); setCollegeProfile(Array.isArray(data) ? data[0] || {} : data); } else { setCollegeProfile({}); } } catch (err) { console.error('Error loading collegeProfile:', err); setCollegeProfile({}); } } loadCollegeProfile(); }, [localScenario]); // load milestones (and each milestone's impacts) const fetchMilestones = useCallback(async () => { if (!localScenario?.id) { setMilestones([]); setImpactsByMilestone({}); return; } try { const res = await authFetch( `/api/premium/milestones?careerPathId=${localScenario.id}` ); if (!res.ok) { console.error('Failed fetching milestones. Status:', res.status); return; } const data = await res.json(); const mils = data.milestones || []; setMilestones(mils); // For each milestone => fetch impacts const impactsData = {}; for (const m of mils) { const iRes = await authFetch( `/api/premium/milestone-impacts?milestone_id=${m.id}` ); if (iRes.ok) { const iData = await iRes.json(); impactsData[m.id] = iData.impacts || []; } else { impactsData[m.id] = []; } } setImpactsByMilestone(impactsData); } catch (err) { console.error('Error fetching milestones:', err); } }, [localScenario?.id]); useEffect(() => { fetchMilestones(); }, [fetchMilestones]); /************************************************************* * 3) Run Simulation *************************************************************/ const [projectionData, setProjectionData] = useState([]); const [loanPaidOffMonth, setLoanPaidOffMonth] = useState(null); const [simulationYearsInput, setSimulationYearsInput] = useState('20'); useEffect(() => { if (!financialProfile || !localScenario?.id || !collegeProfile) return; // gather all milestoneImpacts let allImpacts = []; Object.keys(impactsByMilestone).forEach((mId) => { allImpacts = allImpacts.concat(impactsByMilestone[mId]); }); const simYears = parseInt(simulationYearsInput, 10) || 20; // Merge scenario + user financial + college + milestone 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 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 || 'monthly', annualFinancialAid: collegeProfile.annual_financial_aid || 0, calculatedTuition: collegeProfile.tuition || 0, extraPayment: collegeProfile.extra_payment || 0, inCollege: collegeProfile.college_enrollment_status === 'currently_enrolled' || collegeProfile.college_enrollment_status === 'prospective_student', gradDate: collegeProfile.expected_graduation || null, programType: collegeProfile.program_type || null, creditHoursPerYear: collegeProfile.credit_hours_per_year || 0, hoursCompleted: collegeProfile.hours_completed || 0, programLength: collegeProfile.program_length || 0, expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary || 0, // scenario horizon startDate: localScenario.start_date || new Date().toISOString(), simulationYears: simYears, milestoneImpacts: allImpacts }; const { projectionData, loanPaidOffMonth } = simulateFinancialProjection(mergedProfile); let cumulative = mergedProfile.emergencySavings || 0; const finalData = projectionData.map((monthRow) => { cumulative += monthRow.netSavings || 0; return { ...monthRow, cumulativeNetSavings: cumulative }; }); setProjectionData(finalData); setLoanPaidOffMonth(loanPaidOffMonth); }, [ financialProfile, localScenario, collegeProfile, impactsByMilestone, simulationYearsInput ]); function handleSimulationYearsChange(e) { setSimulationYearsInput(e.target.value); } function handleSimulationYearsBlur() { if (!simulationYearsInput.trim()) { setSimulationYearsInput('20'); } } /************************************************************* * 4) Chart + Annotations *************************************************************/ const chartLabels = projectionData.map((p) => p.month); const netSavingsData = projectionData.map((p) => p.cumulativeNetSavings || 0); const retData = projectionData.map((p) => p.retirementSavings || 0); const loanData = projectionData.map((p) => p.loanBalance || 0); const milestoneAnnotations = milestones .map((m) => { if (!m.date) return null; const d = new Date(m.date); if (isNaN(d)) return null; const year = d.getUTCFullYear(); const month = String(d.getUTCMonth() + 1).padStart(2, '0'); const short = `${year}-${month}`; if (!chartLabels.includes(short)) return null; return { type: 'line', xMin: short, xMax: short, borderColor: 'orange', borderWidth: 2, label: { display: true, content: m.title || 'Milestone', color: 'orange', position: 'end' }, milestoneObj: m, onClick: () => handleEditMilestone(m) }; }) .filter(Boolean); const chartData = { labels: chartLabels, datasets: [ { label: 'Net Savings', data: netSavingsData, borderColor: 'blue', fill: false }, { label: 'Retirement', data: retData, borderColor: 'green', fill: false }, { label: 'Loan', data: loanData, borderColor: 'red', fill: false } ] }; const chartOptions = { responsive: true, scales: { x: { type: 'category' }, y: { title: { display: true, text: 'Amount ($)' } } }, plugins: { annotation: { annotations: milestoneAnnotations }, tooltip: { callbacks: { label: (context) => `${context.dataset.label}: ${context.formattedValue}` } } } }; /************************************************************* * 5) MILESTONE CRUD *************************************************************/ const [showForm, setShowForm] = useState(false); const [editingMilestone, setEditingMilestone] = useState(null); const [newMilestone, setNewMilestone] = useState({ title: '', description: '', date: '', progress: 0, newSalary: '', impacts: [], isUniversal: 0 }); const [impactsToDelete, setImpactsToDelete] = useState([]); // tasks const [showTaskForm, setShowTaskForm] = useState(null); const [editingTask, setEditingTask] = useState({ id: null, title: '', description: '', due_date: '' }); // copy wizard const [copyWizardMilestone, setCopyWizardMilestone] = useState(null); function handleNewMilestone() { setEditingMilestone(null); setNewMilestone({ title: '', description: '', date: '', progress: 0, newSalary: '', impacts: [], isUniversal: 0 }); setImpactsToDelete([]); setShowForm(true); } async function handleEditMilestone(m) { if (!localScenario?.id) return; setEditingMilestone(m); setImpactsToDelete([]); try { const impRes = await authFetch( `/api/premium/milestone-impacts?milestone_id=${m.id}` ); if (impRes.ok) { const data = await impRes.json(); const fetchedImpacts = data.impacts || []; setNewMilestone({ title: m.title || '', description: m.description || '', date: m.date || '', progress: m.progress || 0, newSalary: m.new_salary || '', impacts: fetchedImpacts.map((imp) => ({ id: imp.id, impact_type: imp.impact_type || 'ONE_TIME', direction: imp.direction || 'subtract', amount: imp.amount || 0, start_date: imp.start_date || '', end_date: imp.end_date || '' })), isUniversal: m.is_universal ? 1 : 0 }); setShowForm(true); } } catch (err) { console.error('Error loading milestone impacts:', err); } } function addNewImpact() { setNewMilestone((prev) => ({ ...prev, impacts: [ ...prev.impacts, { impact_type: 'ONE_TIME', direction: 'subtract', amount: 0, start_date: '', end_date: '' } ] })); } function removeImpact(idx) { setNewMilestone((prev) => { const copy = [...prev.impacts]; const removed = copy[idx]; if (removed && removed.id) { setImpactsToDelete((old) => [...old, removed.id]); } copy.splice(idx, 1); return { ...prev, impacts: copy }; }); } function updateImpact(idx, field, value) { setNewMilestone((prev) => { const copy = [...prev.impacts]; copy[idx] = { ...copy[idx], [field]: value }; return { ...prev, impacts: copy }; }); } async function saveMilestone() { if (!localScenario?.id) return; const url = editingMilestone ? `/api/premium/milestones/${editingMilestone.id}` : `/api/premium/milestone`; const method = editingMilestone ? 'PUT' : 'POST'; const payload = { milestone_type: 'Financial', title: newMilestone.title, description: newMilestone.description, date: newMilestone.date, career_path_id: localScenario.id, progress: newMilestone.progress, status: newMilestone.progress >= 100 ? 'completed' : 'planned', new_salary: newMilestone.newSalary ? parseFloat(newMilestone.newSalary) : null, is_universal: newMilestone.isUniversal || 0 }; try { const res = await authFetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!res.ok) { const errData = await res.json(); alert(errData.error || 'Error saving milestone'); return; } const savedMilestone = await res.json(); // handle impacts for (const id of impactsToDelete) { await authFetch(`/api/premium/milestone-impacts/${id}`, { method: 'DELETE' }); } for (let i = 0; i < newMilestone.impacts.length; i++) { const imp = newMilestone.impacts[i]; const impPayload = { milestone_id: savedMilestone.id, impact_type: imp.impact_type, direction: imp.direction, amount: parseFloat(imp.amount) || 0, start_date: imp.start_date || null, end_date: imp.end_date || null }; if (imp.id) { await authFetch(`/api/premium/milestone-impacts/${imp.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(impPayload) }); } else { await authFetch('/api/premium/milestone-impacts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(impPayload) }); } } // re-fetch await fetchMilestones(); // reset form setShowForm(false); setEditingMilestone(null); setNewMilestone({ title: '', description: '', date: '', progress: 0, newSalary: '', impacts: [], isUniversal: 0 }); setImpactsToDelete([]); } catch (err) { console.error('Error saving milestone:', err); alert('Failed to save milestone'); } } async function handleDeleteMilestone(m) { if (m.is_universal === 1) { const userChoice = window.confirm( 'Universal milestone. OK => remove from ALL scenarios, or Cancel => just remove from this scenario.' ); if (userChoice) { try { await authFetch(`/api/premium/milestones/${m.id}/all`, { method: 'DELETE' }); } catch (err) { console.error('Error removing universal milestone from all:', err); } } else { await deleteSingleMilestone(m); } } else { await deleteSingleMilestone(m); } await fetchMilestones(); } async function deleteSingleMilestone(m) { try { await authFetch(`/api/premium/milestones/${m.id}`, { method: 'DELETE' }); } catch (err) { console.error('Error removing milestone:', err); } } /************************************************************* * 6) TASK CRUD *************************************************************/ // handle both new and existing tasks function handleAddTask(milestoneId) { setShowTaskForm(milestoneId); setEditingTask({ id: null, title: '', description: '', due_date: '' }); } function handleEditTask(milestoneId, task) { setShowTaskForm(milestoneId); setEditingTask({ id: task.id, title: task.title, description: task.description || '', due_date: task.due_date || '' }); } async function saveTask(milestoneId) { if (!editingTask.title.trim()) { alert('Task needs a title'); return; } const payload = { milestone_id: milestoneId, title: editingTask.title, description: editingTask.description, due_date: editingTask.due_date }; // If we have editingTask.id => PUT, else => POST try { let url = '/api/premium/tasks'; let method = 'POST'; if (editingTask.id) { url = `/api/premium/tasks/${editingTask.id}`; method = 'PUT'; } const res = await authFetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!res.ok) { alert('Failed to save task'); return; } await fetchMilestones(); setShowTaskForm(null); setEditingTask({ id: null, title: '', description: '', due_date: '' }); } catch (err) { console.error('Error saving task:', err); } } async function deleteTask(taskId) { if (!taskId) return; try { const res = await authFetch(`/api/premium/tasks/${taskId}`, { method: 'DELETE' }); if (!res.ok) { alert('Failed to delete task'); return; } await fetchMilestones(); } catch (err) { console.error('Error deleting task:', err); } } // scenario-level editing function handleEditScenario() { if (!localScenario) return; setEditingScenarioData({ scenario: localScenario, collegeProfile }); setShowEditModal(true); } function handleDeleteScenario() { if (localScenario) onRemove(localScenario.id); } function handleCloneScenario() { if (localScenario) onClone(localScenario); } /************************************************************* * 7) COPY WIZARD *************************************************************/ function CopyMilestoneWizard({ milestone, scenarios, onClose }) { const [selectedScenarios, setSelectedScenarios] = useState([]); if (!milestone) return null; function toggleScenario(id) { setSelectedScenarios((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id] ); } async function handleCopy() { try { const res = await authFetch('/api/premium/milestone/copy', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ milestoneId: milestone.id, scenarioIds: selectedScenarios }) }); if (!res.ok) throw new Error('Failed to copy milestone'); onClose(); window.location.reload(); } catch (err) { console.error('Error copying milestone:', err); } } return (

Copy Milestone to Other Scenarios

Milestone: {milestone.title}

{scenarios.map((s) => (
))}
); } /************************************************************* * 8) RENDER *************************************************************/ return (
{/* scenario dropdown */} {localScenario && ( <>

{localScenario.scenario_title || localScenario.career_name}

Status: {localScenario.status}
Start: {localScenario.start_date}
End: {localScenario.projected_end_date}

{/* The line chart */} {projectionData.length > 0 && (
Loan Paid Off: {loanPaidOffMonth || 'N/A'}
Final Retirement:{' '} {Math.round( projectionData[projectionData.length - 1].retirementSavings )}
)}
{/* The milestone form */} {showForm && (

{editingMilestone ? 'Edit Milestone' : 'New Milestone'}

setNewMilestone({ ...newMilestone, title: e.target.value }) } /> setNewMilestone({ ...newMilestone, description: e.target.value }) } /> setNewMilestone({ ...newMilestone, date: e.target.value }) } /> setNewMilestone((prev) => ({ ...prev, progress: parseInt(e.target.value || '0', 10) })) } /> {/* Impacts sub-form */}
Financial Impacts
{newMilestone.impacts.map((imp, idx) => (
{imp.id && (

ID: {imp.id}

)}
updateImpact(idx, 'amount', e.target.value) } />
updateImpact(idx, 'start_date', e.target.value) } />
{imp.impact_type === 'MONTHLY' && (
updateImpact(idx, 'end_date', e.target.value) } />
)}
))}
)} {/* Render existing milestones */} {milestones.map((m) => { const tasks = m.tasks || []; return (
{m.title}
{m.description &&

{m.description}

}

Date: {m.date} — Progress: {m.progress}%

{/* tasks list */} {tasks.length > 0 && (
    {tasks.map((t) => (
  • {t.title} {t.description ? ` - ${t.description}` : ''} {t.due_date ? ` (Due: ${t.due_date})` : ''}{' '}
  • ))}
)} {/* The "Add/Edit Task" form if showTaskForm === this milestone */} {showTaskForm === m.id && (
{editingTask.id ? 'Edit Task' : 'New Task'}
setEditingTask({ ...editingTask, title: e.target.value }) } /> setEditingTask({ ...editingTask, description: e.target.value }) } /> setEditingTask({ ...editingTask, due_date: e.target.value }) } />
)}
); })} {/* Scenario edit modal */} setShowEditModal(false)} scenario={editingScenarioData.scenario} collegeProfile={editingScenarioData.collegeProfile} /> {/* Copy wizard */} {copyWizardMilestone && ( setCopyWizardMilestone(null)} /> )} )}
); }