Fixed tax implementation in simulator, milestone impacts.
This commit is contained in:
parent
253dbee9fe
commit
8d1dcf26b9
@ -3,7 +3,7 @@ import React, { useEffect, useState, useCallback } from 'react';
|
|||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
|
|
||||||
const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView }) => {
|
const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView, onMilestoneUpdated }) => {
|
||||||
const [milestones, setMilestones] = useState({ Career: [], Financial: [] });
|
const [milestones, setMilestones] = useState({ Career: [], Financial: [] });
|
||||||
|
|
||||||
// The "new or edit" milestone form state
|
// The "new or edit" milestone form state
|
||||||
@ -148,6 +148,7 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
|||||||
alert(errorData.error || 'Error saving milestone');
|
alert(errorData.error || 'Error saving milestone');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (onMilestoneUpdated) onMilestoneUpdated();
|
||||||
|
|
||||||
const savedMilestone = await res.json();
|
const savedMilestone = await res.json();
|
||||||
console.log('Milestone saved/updated:', savedMilestone);
|
console.log('Milestone saved/updated:', savedMilestone);
|
||||||
|
@ -1,51 +1,74 @@
|
|||||||
// src/components/MilestoneTracker.js
|
// src/components/MilestoneTracker.js
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { Line } from 'react-chartjs-2';
|
import { Line } from 'react-chartjs-2';
|
||||||
import { Chart as ChartJS, LineElement, CategoryScale, LinearScale, PointElement, Tooltip, Legend } from 'chart.js';
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
LineElement,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
Tooltip,
|
||||||
|
Legend
|
||||||
|
} from 'chart.js';
|
||||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||||
import { Filler } from 'chart.js';
|
import { Filler } from 'chart.js';
|
||||||
|
|
||||||
import authFetch from '../utils/authFetch.js';
|
import authFetch from '../utils/authFetch.js';
|
||||||
import CareerSelectDropdown from './CareerSelectDropdown.js';
|
import CareerSelectDropdown from './CareerSelectDropdown.js';
|
||||||
import CareerSearch from './CareerSearch.js';
|
import CareerSearch from './CareerSearch.js';
|
||||||
import MilestoneTimeline from './MilestoneTimeline.js';
|
import MilestoneTimeline from './MilestoneTimeline.js';
|
||||||
import AISuggestedMilestones from './AISuggestedMilestones.js';
|
import AISuggestedMilestones from './AISuggestedMilestones.js';
|
||||||
|
import ScenarioEditModal from './ScenarioEditModal.js';
|
||||||
import './MilestoneTracker.css';
|
import './MilestoneTracker.css';
|
||||||
import './MilestoneTimeline.css';
|
import './MilestoneTimeline.css';
|
||||||
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
|
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
|
||||||
import ScenarioEditModal from './ScenarioEditModal.js';
|
|
||||||
|
|
||||||
ChartJS.register(LineElement, CategoryScale, LinearScale, Filler, PointElement, Tooltip, Legend, annotationPlugin);
|
ChartJS.register(
|
||||||
|
LineElement,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
Filler,
|
||||||
|
PointElement,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
annotationPlugin
|
||||||
|
);
|
||||||
|
|
||||||
const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const apiURL = process.env.REACT_APP_API_URL;
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// State
|
||||||
|
// -------------------------
|
||||||
const [selectedCareer, setSelectedCareer] = useState(initialCareer || null);
|
const [selectedCareer, setSelectedCareer] = useState(initialCareer || null);
|
||||||
const [careerPathId, setCareerPathId] = useState(null);
|
const [careerPathId, setCareerPathId] = useState(null);
|
||||||
const [existingCareerPaths, setExistingCareerPaths] = useState([]);
|
const [existingCareerPaths, setExistingCareerPaths] = useState([]);
|
||||||
const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
|
const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
|
||||||
const [activeView, setActiveView] = useState("Career");
|
const [activeView, setActiveView] = useState("Career");
|
||||||
|
|
||||||
// Store each profile separately
|
|
||||||
const [financialProfile, setFinancialProfile] = useState(null);
|
const [financialProfile, setFinancialProfile] = useState(null);
|
||||||
const [collegeProfile, setCollegeProfile] = useState(null);
|
const [collegeProfile, setCollegeProfile] = useState(null);
|
||||||
|
|
||||||
// For the chart
|
|
||||||
const [projectionData, setProjectionData] = useState([]);
|
const [projectionData, setProjectionData] = useState([]);
|
||||||
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
|
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
|
||||||
|
|
||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
|
|
||||||
const apiURL = process.env.REACT_APP_API_URL;
|
|
||||||
|
|
||||||
// Possibly loaded from location.state
|
// Possibly loaded from location.state
|
||||||
const { projectionData: initialProjectionData = [], loanPayoffMonth: initialLoanPayoffMonth = null } = location.state || {};
|
const {
|
||||||
|
projectionData: initialProjectionData = [],
|
||||||
|
loanPayoffMonth: initialLoanPayoffMonth = null
|
||||||
|
} = location.state || {};
|
||||||
|
|
||||||
// ----------------------------
|
// -------------------------
|
||||||
// 1. Fetch career paths + financialProfile
|
// 1. Fetch career paths + financialProfile on mount
|
||||||
// ----------------------------
|
// -------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchCareerPaths = async () => {
|
const fetchCareerPaths = async () => {
|
||||||
const res = await authFetch(`${apiURL}/premium/career-profile/all`);
|
const res = await authFetch(`${apiURL}/premium/career-profile/all`);
|
||||||
@ -58,6 +81,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
setSelectedCareer(fromPopout);
|
setSelectedCareer(fromPopout);
|
||||||
setCareerPathId(fromPopout.career_path_id);
|
setCareerPathId(fromPopout.career_path_id);
|
||||||
} else if (!selectedCareer) {
|
} else if (!selectedCareer) {
|
||||||
|
// Try to fetch the latest
|
||||||
const latest = await authFetch(`${apiURL}/premium/career-profile/latest`);
|
const latest = await authFetch(`${apiURL}/premium/career-profile/latest`);
|
||||||
if (latest && latest.ok) {
|
if (latest && latest.ok) {
|
||||||
const latestData = await latest.json();
|
const latestData = await latest.json();
|
||||||
@ -81,9 +105,9 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
fetchFinancialProfile();
|
fetchFinancialProfile();
|
||||||
}, [apiURL, location.state, selectedCareer]);
|
}, [apiURL, location.state, selectedCareer]);
|
||||||
|
|
||||||
// ----------------------------
|
// -------------------------
|
||||||
// 2. Fetch the college profile for the selected careerPathId
|
// 2. Fetch the college profile for the selected careerPathId
|
||||||
// ----------------------------
|
// -------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!careerPathId) {
|
if (!careerPathId) {
|
||||||
setCollegeProfile(null);
|
setCollegeProfile(null);
|
||||||
@ -91,38 +115,56 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fetchCollegeProfile = async () => {
|
const fetchCollegeProfile = async () => {
|
||||||
// If you have a route like GET /api/premium/college-profile?careerPathId=XYZ
|
|
||||||
const res = await authFetch(`${apiURL}/premium/college-profile?careerPathId=${careerPathId}`);
|
const res = await authFetch(`${apiURL}/premium/college-profile?careerPathId=${careerPathId}`);
|
||||||
if (!res || !res.ok) {
|
if (!res || !res.ok) {
|
||||||
setCollegeProfile(null);
|
setCollegeProfile(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setCollegeProfile(data); // could be an object or empty {}
|
setCollegeProfile(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchCollegeProfile();
|
fetchCollegeProfile();
|
||||||
}, [careerPathId, apiURL]);
|
}, [careerPathId, apiURL]);
|
||||||
|
|
||||||
// ----------------------------
|
// -------------------------
|
||||||
// 3. Merge data + simulate once both profiles + selectedCareer are loaded
|
// 3. Initial simulation when profiles + career loaded
|
||||||
// ----------------------------
|
// (But this does NOT update after milestone changes yet)
|
||||||
|
// -------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!financialProfile || !collegeProfile || !selectedCareer) return;
|
if (!financialProfile || !collegeProfile || !selectedCareer || !careerPathId) return;
|
||||||
console.log("About to build mergedProfile");
|
|
||||||
console.log("collegeProfile from DB/fetch = ", collegeProfile);
|
|
||||||
console.log(
|
|
||||||
"college_enrollment_status check:",
|
|
||||||
"[" + collegeProfile.college_enrollment_status + "]",
|
|
||||||
"length=", collegeProfile.college_enrollment_status?.length
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
"Comparison => ",
|
|
||||||
collegeProfile.college_enrollment_status === 'currently_enrolled'
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// 1) Fetch the raw milestones for this careerPath
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const milRes = await authFetch(`${apiURL}/premium/milestones?careerPathId=${careerPathId}`);
|
||||||
|
if (!milRes.ok) {
|
||||||
|
console.error('Failed to fetch initial milestones');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const milestonesData = await milRes.json();
|
||||||
|
const allMilestones = milestonesData.milestones || [];
|
||||||
|
|
||||||
// Merge financial + college data
|
// 2) For each milestone, fetch impacts
|
||||||
|
const impactPromises = allMilestones.map((m) =>
|
||||||
|
authFetch(`${apiURL}/premium/milestone-impacts?milestone_id=${m.id}`)
|
||||||
|
.then((r) => (r.ok ? r.json() : null))
|
||||||
|
.then((data) => data?.impacts || [])
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Failed fetching impacts for milestone', m.id, err);
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const impactsForEach = await Promise.all(impactPromises);
|
||||||
|
const milestonesWithImpacts = allMilestones.map((m, i) => ({
|
||||||
|
...m,
|
||||||
|
impacts: impactsForEach[i] || [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 3) Flatten them
|
||||||
|
const allImpacts = milestonesWithImpacts.flatMap((m) => m.impacts || []);
|
||||||
|
|
||||||
|
// 4) Build the mergedProfile (like you already do)
|
||||||
const mergedProfile = {
|
const mergedProfile = {
|
||||||
// From financialProfile
|
// From financialProfile
|
||||||
currentSalary: financialProfile.current_salary || 0,
|
currentSalary: financialProfile.current_salary || 0,
|
||||||
@ -144,56 +186,163 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
annualFinancialAid: collegeProfile.annual_financial_aid || 0,
|
annualFinancialAid: collegeProfile.annual_financial_aid || 0,
|
||||||
calculatedTuition: collegeProfile.tuition || 0,
|
calculatedTuition: collegeProfile.tuition || 0,
|
||||||
extraPayment: collegeProfile.extra_payment || 0,
|
extraPayment: collegeProfile.extra_payment || 0,
|
||||||
partTimeIncome: 0, // or collegeProfile.part_time_income if you store it
|
inCollege:
|
||||||
|
collegeProfile.college_enrollment_status === 'currently_enrolled' ||
|
||||||
|
collegeProfile.college_enrollment_status === 'prospective_student',
|
||||||
gradDate: collegeProfile.expected_graduation || null,
|
gradDate: collegeProfile.expected_graduation || null,
|
||||||
programType: collegeProfile.program_type,
|
programType: collegeProfile.program_type,
|
||||||
creditHoursPerYear: collegeProfile.credit_hours_per_year || 0,
|
creditHoursPerYear: collegeProfile.credit_hours_per_year || 0,
|
||||||
hoursCompleted: collegeProfile.hours_completed || 0,
|
hoursCompleted: collegeProfile.hours_completed || 0,
|
||||||
programLength: collegeProfile.program_length || 0,
|
programLength: collegeProfile.program_length || 0,
|
||||||
|
|
||||||
// Are they in college?
|
|
||||||
inCollege: (collegeProfile.college_enrollment_status === 'currently_enrolled' ||
|
|
||||||
collegeProfile.college_enrollment_status === 'prospective_student'),
|
|
||||||
// If they've graduated or not in college, false
|
|
||||||
startDate: new Date().toISOString(),
|
startDate: new Date().toISOString(),
|
||||||
// Future logic could set expectedSalary if there's a difference
|
|
||||||
expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary,
|
expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary,
|
||||||
|
|
||||||
|
// The key: impacts
|
||||||
|
milestoneImpacts: allImpacts
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = simulateFinancialProjection(mergedProfile);
|
// 5) Run the simulation
|
||||||
console.log("mergedProfile for simulation:", mergedProfile);
|
const { projectionData: initialProjData, loanPaidOffMonth: payoff } =
|
||||||
|
simulateFinancialProjection(mergedProfile);
|
||||||
|
|
||||||
const { projectionData, loanPaidOffMonth } = result;
|
|
||||||
|
|
||||||
// If you want to accumulate net savings:
|
|
||||||
let cumulativeSavings = mergedProfile.emergencySavings || 0;
|
let cumulativeSavings = mergedProfile.emergencySavings || 0;
|
||||||
const cumulativeProjectionData = projectionData.map(month => {
|
const finalData = initialProjData.map((month) => {
|
||||||
cumulativeSavings += (month.netSavings || 0);
|
cumulativeSavings += (month.netSavings || 0);
|
||||||
return { ...month, cumulativeNetSavings: cumulativeSavings };
|
return { ...month, cumulativeNetSavings: cumulativeSavings };
|
||||||
});
|
});
|
||||||
|
|
||||||
if (cumulativeProjectionData.length > 0) {
|
setProjectionData(finalData);
|
||||||
setProjectionData(cumulativeProjectionData);
|
setLoanPayoffMonth(payoff);
|
||||||
setLoanPayoffMonth(loanPaidOffMonth);
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching initial milestones/impacts or simulating:', err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [financialProfile, collegeProfile, selectedCareer, careerPathId]);
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// 4. reSimulate() => re-fetch everything (financial, college, milestones & impacts),
|
||||||
|
// re-run the simulation. This is triggered AFTER user updates a milestone in MilestoneTimeline.
|
||||||
|
// -------------------------------------------------
|
||||||
|
const reSimulate = async () => {
|
||||||
|
if (!careerPathId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1) Fetch financial + college + raw milestones
|
||||||
|
const [finResp, colResp, milResp] = await Promise.all([
|
||||||
|
authFetch(`${apiURL}/premium/financial-profile`),
|
||||||
|
authFetch(`${apiURL}/premium/college-profile?careerPathId=${careerPathId}`),
|
||||||
|
authFetch(`${apiURL}/premium/milestones?careerPathId=${careerPathId}`)
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!finResp.ok || !colResp.ok || !milResp.ok) {
|
||||||
|
console.error('One reSimulate fetch failed:', finResp.status, colResp.status, milResp.status);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('mergedProfile for simulation:', mergedProfile);
|
const [updatedFinancial, updatedCollege, milestonesData] = await Promise.all([
|
||||||
|
finResp.json(),
|
||||||
|
colResp.json(),
|
||||||
|
milResp.json()
|
||||||
|
]);
|
||||||
|
|
||||||
}, [financialProfile, collegeProfile, selectedCareer]);
|
// 2) For each milestone, fetch its impacts separately (if not already included)
|
||||||
|
const allMilestones = milestonesData.milestones || [];
|
||||||
|
const impactsPromises = allMilestones.map(m =>
|
||||||
|
authFetch(`${apiURL}/premium/milestone-impacts?milestone_id=${m.id}`)
|
||||||
|
.then(r => r.ok ? r.json() : null)
|
||||||
|
.then(data => data?.impacts || [])
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Failed fetching impacts for milestone', m.id, err);
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const impactsForEach = await Promise.all(impactsPromises);
|
||||||
|
// Merge them onto the milestone array if desired
|
||||||
|
const milestonesWithImpacts = allMilestones.map((m, i) => ({
|
||||||
|
...m,
|
||||||
|
impacts: impactsForEach[i] || []
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Flatten or gather all impacts if your simulation function needs them
|
||||||
|
const allImpacts = milestonesWithImpacts.flatMap(m => m.impacts || []);
|
||||||
|
|
||||||
|
// 3) Build mergedProfile
|
||||||
|
const mergedProfile = {
|
||||||
|
// From updatedFinancial
|
||||||
|
currentSalary: updatedFinancial.current_salary || 0,
|
||||||
|
monthlyExpenses: updatedFinancial.monthly_expenses || 0,
|
||||||
|
monthlyDebtPayments: updatedFinancial.monthly_debt_payments || 0,
|
||||||
|
retirementSavings: updatedFinancial.retirement_savings || 0,
|
||||||
|
emergencySavings: updatedFinancial.emergency_fund || 0,
|
||||||
|
monthlyRetirementContribution: updatedFinancial.retirement_contribution || 0,
|
||||||
|
monthlyEmergencyContribution: updatedFinancial.emergency_contribution || 0,
|
||||||
|
surplusEmergencyAllocation: updatedFinancial.extra_cash_emergency_pct || 50,
|
||||||
|
surplusRetirementAllocation: updatedFinancial.extra_cash_retirement_pct || 50,
|
||||||
|
|
||||||
|
// From updatedCollege
|
||||||
|
studentLoanAmount: updatedCollege.existing_college_debt || 0,
|
||||||
|
interestRate: updatedCollege.interest_rate || 5,
|
||||||
|
loanTerm: updatedCollege.loan_term || 10,
|
||||||
|
loanDeferralUntilGraduation: !!updatedCollege.loan_deferral_until_graduation,
|
||||||
|
academicCalendar: updatedCollege.academic_calendar || 'monthly',
|
||||||
|
annualFinancialAid: updatedCollege.annual_financial_aid || 0,
|
||||||
|
calculatedTuition: updatedCollege.tuition || 0,
|
||||||
|
extraPayment: updatedCollege.extra_payment || 0,
|
||||||
|
inCollege:
|
||||||
|
updatedCollege.college_enrollment_status === 'currently_enrolled' ||
|
||||||
|
updatedCollege.college_enrollment_status === 'prospective_student',
|
||||||
|
gradDate: updatedCollege.expected_graduation || null,
|
||||||
|
programType: updatedCollege.program_type,
|
||||||
|
creditHoursPerYear: updatedCollege.credit_hours_per_year || 0,
|
||||||
|
hoursCompleted: updatedCollege.hours_completed || 0,
|
||||||
|
programLength: updatedCollege.program_length || 0,
|
||||||
|
startDate: new Date().toISOString(),
|
||||||
|
expectedSalary: updatedCollege.expected_salary || updatedFinancial.current_salary,
|
||||||
|
|
||||||
|
// The key: pass the impacts to the simulation if needed
|
||||||
|
milestoneImpacts: allImpacts
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4) Re-run simulation
|
||||||
|
const { projectionData: newProjData, loanPaidOffMonth: payoff } =
|
||||||
|
simulateFinancialProjection(mergedProfile);
|
||||||
|
|
||||||
|
// 5) If you track cumulative net savings:
|
||||||
|
let cumulativeSavings = mergedProfile.emergencySavings || 0;
|
||||||
|
const finalData = newProjData.map(month => {
|
||||||
|
cumulativeSavings += (month.netSavings || 0);
|
||||||
|
return { ...month, cumulativeNetSavings: cumulativeSavings };
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6) Update states => triggers chart refresh
|
||||||
|
setProjectionData(finalData);
|
||||||
|
setLoanPayoffMonth(payoff);
|
||||||
|
|
||||||
|
// Optionally store the new profiles in state if you like
|
||||||
|
setFinancialProfile(updatedFinancial);
|
||||||
|
setCollegeProfile(updatedCollege);
|
||||||
|
|
||||||
|
console.log('Re-simulated after Milestone update!', {
|
||||||
|
mergedProfile,
|
||||||
|
milestonesWithImpacts
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in reSimulate:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 4. The rest of your code is unchanged, e.g. handleConfirmCareerSelection, etc.
|
|
||||||
// ...
|
// ...
|
||||||
|
// The rest of your component logic
|
||||||
|
// ...
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
'First 5 items of projectionData:',
|
'First 5 items of projectionData:',
|
||||||
Array.isArray(projectionData) ? projectionData.slice(0, 5) : 'projectionData not yet available'
|
Array.isArray(projectionData) ? projectionData.slice(0, 5) : 'projectionData not yet available'
|
||||||
);
|
);
|
||||||
|
|
||||||
// ...
|
|
||||||
// The remainder of your component: timeline, chart, AISuggestedMilestones, etc.
|
|
||||||
// ...
|
|
||||||
return (
|
return (
|
||||||
<div className="milestone-tracker">
|
<div className="milestone-tracker">
|
||||||
<CareerSelectDropdown
|
<CareerSelectDropdown
|
||||||
@ -207,11 +356,13 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
authFetch={authFetch}
|
authFetch={authFetch}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Pass reSimulate as onMilestoneUpdated: */}
|
||||||
<MilestoneTimeline
|
<MilestoneTimeline
|
||||||
careerPathId={careerPathId}
|
careerPathId={careerPathId}
|
||||||
authFetch={authFetch}
|
authFetch={authFetch}
|
||||||
activeView={activeView}
|
activeView={activeView}
|
||||||
setActiveView={setActiveView}
|
setActiveView={setActiveView}
|
||||||
|
onMilestoneUpdated={reSimulate}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AISuggestedMilestones
|
<AISuggestedMilestones
|
||||||
@ -227,19 +378,19 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
<h3 className="text-lg font-semibold mb-2">Financial Projection</h3>
|
<h3 className="text-lg font-semibold mb-2">Financial Projection</h3>
|
||||||
<Line
|
<Line
|
||||||
data={{
|
data={{
|
||||||
labels: projectionData.map(p => p.month),
|
labels: projectionData.map((p) => p.month),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Total Savings',
|
label: 'Total Savings',
|
||||||
data: projectionData.map(p => p.cumulativeNetSavings),
|
data: projectionData.map((p) => p.cumulativeNetSavings),
|
||||||
borderColor: 'rgba(54, 162, 235, 1)',
|
borderColor: 'rgba(54, 162, 235, 1)',
|
||||||
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
fill: true,
|
fill: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Loan Balance',
|
label: 'Loan Balance',
|
||||||
data: projectionData.map(p => p.loanBalance),
|
data: projectionData.map((p) => p.loanBalance),
|
||||||
borderColor: 'rgba(255, 99, 132, 1)',
|
borderColor: 'rgba(255, 99, 132, 1)',
|
||||||
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
@ -251,7 +402,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Retirement Savings',
|
label: 'Retirement Savings',
|
||||||
data: projectionData.map(p => p.retirementSavings),
|
data: projectionData.map((p) => p.retirementSavings),
|
||||||
borderColor: 'rgba(75, 192, 192, 1)',
|
borderColor: 'rgba(75, 192, 192, 1)',
|
||||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
@ -308,8 +459,6 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
authFetch={authFetch}
|
authFetch={authFetch}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
{/* SCENARIO EDIT MODAL */}
|
|
||||||
<ScenarioEditModal
|
<ScenarioEditModal
|
||||||
show={showEditModal}
|
show={showEditModal}
|
||||||
onClose={() => setShowEditModal(false)}
|
onClose={() => setShowEditModal(false)}
|
||||||
@ -322,9 +471,11 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{pendingCareerForModal && (
|
{pendingCareerForModal && (
|
||||||
<button onClick={() => {
|
<button
|
||||||
|
onClick={() => {
|
||||||
// handleConfirmCareerSelection logic
|
// handleConfirmCareerSelection logic
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
Confirm Career Change to {pendingCareerForModal}
|
Confirm Career Change to {pendingCareerForModal}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
@ -1,67 +1,23 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
/**
|
/***************************************************
|
||||||
* Single-filer federal tax calculation (2023).
|
* HELPER: Approx State Tax Rates
|
||||||
* Includes standard deduction ($13,850).
|
***************************************************/
|
||||||
*/
|
|
||||||
const APPROX_STATE_TAX_RATES = {
|
const APPROX_STATE_TAX_RATES = {
|
||||||
AL: 0.05,
|
AL: 0.05, AK: 0.00, AZ: 0.025, AR: 0.05, CA: 0.07, CO: 0.045, CT: 0.055, DE: 0.05,
|
||||||
AK: 0.00,
|
FL: 0.00, GA: 0.05, HI: 0.06, ID: 0.058, IL: 0.05, IN: 0.035, IA: 0.05, KS: 0.05,
|
||||||
AZ: 0.025,
|
KY: 0.05, LA: 0.04, ME: 0.055, MD: 0.05, MA: 0.05, MI: 0.0425, MN: 0.06, MS: 0.04,
|
||||||
AR: 0.05,
|
MO: 0.05, MT: 0.05, NE: 0.05, NV: 0.00, NH: 0.00, NJ: 0.057, NM: 0.045, NY: 0.06,
|
||||||
CA: 0.07,
|
NC: 0.0475, ND: 0.02, OH: 0.04, OK: 0.045, OR: 0.07, PA: 0.03, RI: 0.045, SC: 0.04,
|
||||||
CO: 0.045,
|
SD: 0.00, TN: 0.00, TX: 0.00, UT: 0.045, VT: 0.055, VA: 0.05, WA: 0.00, WV: 0.05,
|
||||||
CT: 0.055,
|
WI: 0.05, WY: 0.00, DC: 0.05
|
||||||
DE: 0.05,
|
|
||||||
FL: 0.00,
|
|
||||||
GA: 0.05,
|
|
||||||
HI: 0.06,
|
|
||||||
ID: 0.058,
|
|
||||||
IL: 0.05,
|
|
||||||
IN: 0.035,
|
|
||||||
IA: 0.05,
|
|
||||||
KS: 0.05,
|
|
||||||
KY: 0.05,
|
|
||||||
LA: 0.04,
|
|
||||||
ME: 0.055,
|
|
||||||
MD: 0.05,
|
|
||||||
MA: 0.05,
|
|
||||||
MI: 0.0425,
|
|
||||||
MN: 0.06,
|
|
||||||
MS: 0.04,
|
|
||||||
MO: 0.05,
|
|
||||||
MT: 0.05,
|
|
||||||
NE: 0.05,
|
|
||||||
NV: 0.00,
|
|
||||||
NH: 0.00, // ignoring interest/dividend nuance
|
|
||||||
NJ: 0.057,
|
|
||||||
NM: 0.045,
|
|
||||||
NY: 0.06,
|
|
||||||
NC: 0.0475,
|
|
||||||
ND: 0.02,
|
|
||||||
OH: 0.04,
|
|
||||||
OK: 0.045,
|
|
||||||
OR: 0.07,
|
|
||||||
PA: 0.03,
|
|
||||||
RI: 0.045,
|
|
||||||
SC: 0.04,
|
|
||||||
SD: 0.00,
|
|
||||||
TN: 0.00,
|
|
||||||
TX: 0.00,
|
|
||||||
UT: 0.045,
|
|
||||||
VT: 0.055,
|
|
||||||
VA: 0.05,
|
|
||||||
WA: 0.00,
|
|
||||||
WV: 0.05,
|
|
||||||
WI: 0.05,
|
|
||||||
WY: 0.00,
|
|
||||||
DC: 0.05
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function calculateAnnualFederalTaxSingle(annualIncome) {
|
/***************************************************
|
||||||
|
* HELPER: Federal Tax Brackets
|
||||||
|
***************************************************/
|
||||||
const STANDARD_DEDUCTION_SINGLE = 13850;
|
const STANDARD_DEDUCTION_SINGLE = 13850;
|
||||||
const taxableIncome = Math.max(0, annualIncome - STANDARD_DEDUCTION_SINGLE);
|
function calculateAnnualFederalTaxSingle(annualTaxable) {
|
||||||
|
|
||||||
const brackets = [
|
const brackets = [
|
||||||
{ limit: 11000, rate: 0.10 },
|
{ limit: 11000, rate: 0.10 },
|
||||||
{ limit: 44725, rate: 0.12 },
|
{ limit: 44725, rate: 0.12 },
|
||||||
@ -74,11 +30,10 @@ function calculateAnnualFederalTaxSingle(annualIncome) {
|
|||||||
|
|
||||||
let tax = 0;
|
let tax = 0;
|
||||||
let lastLimit = 0;
|
let lastLimit = 0;
|
||||||
|
|
||||||
for (let i = 0; i < brackets.length; i++) {
|
for (let i = 0; i < brackets.length; i++) {
|
||||||
const { limit, rate } = brackets[i];
|
const { limit, rate } = brackets[i];
|
||||||
if (taxableIncome <= limit) {
|
if (annualTaxable <= limit) {
|
||||||
tax += (taxableIncome - lastLimit) * rate;
|
tax += (annualTaxable - lastLimit) * rate;
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
tax += (limit - lastLimit) * rate;
|
tax += (limit - lastLimit) * rate;
|
||||||
@ -88,14 +43,34 @@ function calculateAnnualFederalTaxSingle(annualIncome) {
|
|||||||
return tax;
|
return tax;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateAnnualStateTax(annualIncome, stateCode) {
|
/***************************************************
|
||||||
const rate = APPROX_STATE_TAX_RATES[stateCode] ?? 0.05;
|
* HELPER: Monthly Federal Tax (no YTD)
|
||||||
return annualIncome * rate;
|
* We just treat (monthlyGross * 12) - standardDed
|
||||||
|
* -> bracket -> / 12
|
||||||
|
***************************************************/
|
||||||
|
function calculateMonthlyFedTaxNoYTD(monthlyGross) {
|
||||||
|
const annualGross = monthlyGross * 12;
|
||||||
|
let annualTaxable = annualGross - STANDARD_DEDUCTION_SINGLE;
|
||||||
|
if (annualTaxable < 0) annualTaxable = 0;
|
||||||
|
|
||||||
|
const annualTax = calculateAnnualFederalTaxSingle(annualTaxable);
|
||||||
|
return annualTax / 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/***************************************************
|
||||||
|
* HELPER: Monthly State Tax (no YTD)
|
||||||
|
* Uses GA (5%) by default if user doesn't override
|
||||||
|
***************************************************/
|
||||||
|
function calculateMonthlyStateTaxNoYTD(monthlyGross, stateCode = 'GA') {
|
||||||
|
const rate = APPROX_STATE_TAX_RATES[stateCode] ?? 0.05;
|
||||||
|
return monthlyGross * rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************************************
|
||||||
|
* HELPER: Loan Payment (if not deferring)
|
||||||
|
***************************************************/
|
||||||
function calculateLoanPayment(principal, annualRate, years) {
|
function calculateLoanPayment(principal, annualRate, years) {
|
||||||
if (principal <= 0) return 0;
|
if (principal <= 0) return 0;
|
||||||
|
|
||||||
const monthlyRate = annualRate / 100 / 12;
|
const monthlyRate = annualRate / 100 / 12;
|
||||||
const numPayments = years * 12;
|
const numPayments = years * 12;
|
||||||
|
|
||||||
@ -108,35 +83,28 @@ function calculateLoanPayment(principal, annualRate, years) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/***************************************************
|
||||||
* Main projection function with bracket-based FEDERAL + optional STATE tax logic.
|
* MAIN SIMULATION FUNCTION
|
||||||
*
|
***************************************************/
|
||||||
* milestoneImpacts: [
|
|
||||||
* {
|
|
||||||
* impact_type: 'ONE_TIME' | 'MONTHLY',
|
|
||||||
* direction: 'add' | 'subtract',
|
|
||||||
* amount: number,
|
|
||||||
* start_date: 'YYYY-MM-DD',
|
|
||||||
* end_date?: 'YYYY-MM-DD' | null
|
|
||||||
* }, ...
|
|
||||||
* ]
|
|
||||||
*/
|
|
||||||
export function simulateFinancialProjection(userProfile) {
|
export function simulateFinancialProjection(userProfile) {
|
||||||
|
/***************************************************
|
||||||
|
* 1) DESTRUCTURE USER PROFILE
|
||||||
|
***************************************************/
|
||||||
const {
|
const {
|
||||||
// Income & expenses
|
// Basic incomes
|
||||||
currentSalary = 0,
|
currentSalary = 0,
|
||||||
monthlyExpenses = 0,
|
monthlyExpenses = 0,
|
||||||
monthlyDebtPayments = 0,
|
monthlyDebtPayments = 0,
|
||||||
partTimeIncome = 0,
|
partTimeIncome = 0,
|
||||||
extraPayment = 0,
|
extraPayment = 0,
|
||||||
|
|
||||||
// Loan info
|
// Student loan config
|
||||||
studentLoanAmount = 0,
|
studentLoanAmount = 0,
|
||||||
interestRate = 5, // %
|
interestRate = 5,
|
||||||
loanTerm = 10, // years
|
loanTerm = 10,
|
||||||
loanDeferralUntilGraduation = false,
|
loanDeferralUntilGraduation = false,
|
||||||
|
|
||||||
// College & tuition
|
// College config
|
||||||
inCollege = false,
|
inCollege = false,
|
||||||
programType,
|
programType,
|
||||||
hoursCompleted = 0,
|
hoursCompleted = 0,
|
||||||
@ -147,40 +115,37 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
academicCalendar = 'monthly',
|
academicCalendar = 'monthly',
|
||||||
annualFinancialAid = 0,
|
annualFinancialAid = 0,
|
||||||
|
|
||||||
// Salary after graduation
|
// Post-college salary
|
||||||
expectedSalary = 0,
|
expectedSalary = 0,
|
||||||
|
|
||||||
// Savings
|
// Savings & monthly contributions
|
||||||
emergencySavings = 0,
|
emergencySavings = 0,
|
||||||
retirementSavings = 0,
|
retirementSavings = 0,
|
||||||
|
|
||||||
// Monthly contributions
|
|
||||||
monthlyRetirementContribution = 0,
|
monthlyRetirementContribution = 0,
|
||||||
monthlyEmergencyContribution = 0,
|
monthlyEmergencyContribution = 0,
|
||||||
|
|
||||||
// Surplus allocation
|
// Surplus distribution
|
||||||
surplusEmergencyAllocation = 50,
|
surplusEmergencyAllocation = 50,
|
||||||
surplusRetirementAllocation = 50,
|
surplusRetirementAllocation = 50,
|
||||||
|
|
||||||
// Potential override
|
// Program length override
|
||||||
programLength,
|
programLength,
|
||||||
|
|
||||||
// State code
|
// State code for taxes (default to GA if not provided)
|
||||||
stateCode = 'TX',
|
stateCode = 'GA',
|
||||||
|
|
||||||
// Milestone impacts (with dates, add/subtract logic)
|
// Financial milestone impacts
|
||||||
milestoneImpacts = []
|
milestoneImpacts = []
|
||||||
} = userProfile;
|
} = userProfile;
|
||||||
|
|
||||||
// scenario start date
|
/***************************************************
|
||||||
const scenarioStart = startDate ? new Date(startDate) : new Date();
|
* 2) CLAMP THE SCENARIO START TO MONTH-BEGIN
|
||||||
|
***************************************************/
|
||||||
|
const scenarioStartClamped = moment(startDate || new Date()).startOf('month');
|
||||||
|
|
||||||
// 1. Monthly loan payment if not deferring
|
/***************************************************
|
||||||
let monthlyLoanPayment = loanDeferralUntilGraduation
|
* 3) DETERMINE PROGRAM LENGTH (credit hours)
|
||||||
? 0
|
***************************************************/
|
||||||
: calculateLoanPayment(studentLoanAmount, interestRate, loanTerm);
|
|
||||||
|
|
||||||
// 2. Determine credit hours
|
|
||||||
let requiredCreditHours = 120;
|
let requiredCreditHours = 120;
|
||||||
switch (programType) {
|
switch (programType) {
|
||||||
case "Associate's Degree":
|
case "Associate's Degree":
|
||||||
@ -192,17 +157,20 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
case "Doctoral Degree":
|
case "Doctoral Degree":
|
||||||
requiredCreditHours = 60;
|
requiredCreditHours = 60;
|
||||||
break;
|
break;
|
||||||
// otherwise Bachelor's
|
// else Bachelor's = 120
|
||||||
}
|
}
|
||||||
const remainingCreditHours = Math.max(0, requiredCreditHours - hoursCompleted);
|
const remainingCreditHours = Math.max(0, requiredCreditHours - hoursCompleted);
|
||||||
const dynamicProgramLength = Math.ceil(remainingCreditHours / creditHoursPerYear);
|
const dynamicProgramLength = Math.ceil(
|
||||||
|
remainingCreditHours / (creditHoursPerYear || 30)
|
||||||
|
);
|
||||||
const finalProgramLength = programLength || dynamicProgramLength;
|
const finalProgramLength = programLength || dynamicProgramLength;
|
||||||
|
|
||||||
// 3. Net annual tuition
|
/***************************************************
|
||||||
const netAnnualTuition = Math.max(0, calculatedTuition - annualFinancialAid);
|
* 4) TUITION CALC: lumps, deferral, etc.
|
||||||
|
***************************************************/
|
||||||
|
const netAnnualTuition = Math.max(0, (calculatedTuition || 0) - (annualFinancialAid || 0));
|
||||||
const totalTuitionCost = netAnnualTuition * finalProgramLength;
|
const totalTuitionCost = netAnnualTuition * finalProgramLength;
|
||||||
|
|
||||||
// 4. lumps
|
|
||||||
let lumpsPerYear, lumpsSchedule;
|
let lumpsPerYear, lumpsSchedule;
|
||||||
switch (academicCalendar) {
|
switch (academicCalendar) {
|
||||||
case 'semester':
|
case 'semester':
|
||||||
@ -226,99 +194,118 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
const totalAcademicMonths = finalProgramLength * 12;
|
const totalAcademicMonths = finalProgramLength * 12;
|
||||||
const lumpAmount = totalTuitionCost / (lumpsPerYear * finalProgramLength);
|
const lumpAmount = totalTuitionCost / (lumpsPerYear * finalProgramLength);
|
||||||
|
|
||||||
// 5. Simulation loop
|
/***************************************************
|
||||||
const maxMonths = 240; // 20 years
|
* 5) LOAN PAYMENT (if not deferring)
|
||||||
let date = new Date(scenarioStart);
|
***************************************************/
|
||||||
|
let monthlyLoanPayment = loanDeferralUntilGraduation
|
||||||
|
? 0
|
||||||
|
: calculateLoanPayment(studentLoanAmount, interestRate, loanTerm);
|
||||||
|
|
||||||
|
/***************************************************
|
||||||
|
* 6) SETUP FOR THE SIMULATION LOOP
|
||||||
|
***************************************************/
|
||||||
|
const maxMonths = 240; // 20 years
|
||||||
let loanBalance = Math.max(studentLoanAmount, 0);
|
let loanBalance = Math.max(studentLoanAmount, 0);
|
||||||
let loanPaidOffMonth = null;
|
let loanPaidOffMonth = null;
|
||||||
|
|
||||||
let currentEmergencySavings = emergencySavings;
|
let currentEmergencySavings = emergencySavings;
|
||||||
let currentRetirementSavings = retirementSavings;
|
let currentRetirementSavings = retirementSavings;
|
||||||
|
|
||||||
let projectionData = [];
|
let projectionData = [];
|
||||||
|
|
||||||
|
// Keep track of YTD gross & tax for reference
|
||||||
|
let fedYTDgross = 0;
|
||||||
|
let fedYTDtax = 0;
|
||||||
|
let stateYTDgross = 0;
|
||||||
|
let stateYTDtax = 0;
|
||||||
|
|
||||||
let wasInDeferral = inCollege && loanDeferralUntilGraduation;
|
let wasInDeferral = inCollege && loanDeferralUntilGraduation;
|
||||||
const graduationDateObj = gradDate ? new Date(gradDate) : null;
|
const graduationDateObj = gradDate ? moment(gradDate).startOf('month') : null;
|
||||||
|
|
||||||
// For YTD taxes
|
console.log('simulateFinancialProjection - monthly tax approach');
|
||||||
const taxStateByYear = {};
|
console.log('scenarioStartClamped:', scenarioStartClamped.format('YYYY-MM-DD'));
|
||||||
|
|
||||||
for (let month = 0; month < maxMonths; month++) {
|
/***************************************************
|
||||||
date.setMonth(date.getMonth() + 1);
|
* 7) THE MONTHLY LOOP
|
||||||
const currentYear = date.getFullYear();
|
***************************************************/
|
||||||
|
for (let monthIndex = 0; monthIndex < maxMonths; monthIndex++) {
|
||||||
|
// date for this iteration
|
||||||
|
const currentSimDate = scenarioStartClamped.clone().add(monthIndex, 'months');
|
||||||
|
|
||||||
// elapsed months since scenario start
|
// check if loan is fully paid
|
||||||
const elapsedMonths = moment(date).diff(moment(scenarioStart), 'months');
|
|
||||||
|
|
||||||
// if loan paid
|
|
||||||
if (loanBalance <= 0 && !loanPaidOffMonth) {
|
if (loanBalance <= 0 && !loanPaidOffMonth) {
|
||||||
loanPaidOffMonth = `${currentYear}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
loanPaidOffMonth = currentSimDate.format('YYYY-MM');
|
||||||
}
|
}
|
||||||
|
|
||||||
// are we in college?
|
// Are we still in college?
|
||||||
let stillInCollege = false;
|
let stillInCollege = false;
|
||||||
if (inCollege) {
|
if (inCollege) {
|
||||||
if (graduationDateObj) {
|
if (graduationDateObj) {
|
||||||
stillInCollege = date < graduationDateObj;
|
stillInCollege = currentSimDate.isBefore(graduationDateObj, 'month');
|
||||||
} else {
|
} else {
|
||||||
stillInCollege = (elapsedMonths < totalAcademicMonths);
|
stillInCollege = (monthIndex < totalAcademicMonths);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. tuition lumps
|
/************************************************
|
||||||
|
* 7.1 TUITION lumps
|
||||||
|
************************************************/
|
||||||
let tuitionCostThisMonth = 0;
|
let tuitionCostThisMonth = 0;
|
||||||
if (stillInCollege && lumpsPerYear > 0) {
|
if (stillInCollege && lumpsPerYear > 0) {
|
||||||
const academicYearIndex = Math.floor(elapsedMonths / 12);
|
const academicYearIndex = Math.floor(monthIndex / 12);
|
||||||
const monthInYear = elapsedMonths % 12;
|
const monthInYear = monthIndex % 12;
|
||||||
if (lumpsSchedule.includes(monthInYear) && academicYearIndex < finalProgramLength) {
|
if (lumpsSchedule.includes(monthInYear) && academicYearIndex < finalProgramLength) {
|
||||||
tuitionCostThisMonth = lumpAmount;
|
tuitionCostThisMonth = lumpAmount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Exiting college?
|
// If deferring tuition => add to loan, no direct expense
|
||||||
const nowExitingCollege = wasInDeferral && !stillInCollege;
|
if (stillInCollege && loanDeferralUntilGraduation && tuitionCostThisMonth > 0) {
|
||||||
|
|
||||||
// 8. deferral lumps
|
|
||||||
if (stillInCollege && loanDeferralUntilGraduation) {
|
|
||||||
if (tuitionCostThisMonth > 0) {
|
|
||||||
loanBalance += tuitionCostThisMonth;
|
loanBalance += tuitionCostThisMonth;
|
||||||
tuitionCostThisMonth = 0;
|
tuitionCostThisMonth = 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 9. Base monthly income
|
/************************************************
|
||||||
let grossMonthlyIncome = 0;
|
* 7.2 BASE MONTHLY INCOME
|
||||||
|
************************************************/
|
||||||
|
let baseMonthlyIncome = 0;
|
||||||
if (!stillInCollege) {
|
if (!stillInCollege) {
|
||||||
grossMonthlyIncome = (expectedSalary > 0 ? expectedSalary : currentSalary) / 12;
|
// user is out of college => expected or current
|
||||||
|
baseMonthlyIncome = (expectedSalary || currentSalary) / 12;
|
||||||
} else {
|
} else {
|
||||||
grossMonthlyIncome = (currentSalary / 12) + (partTimeIncome / 12);
|
// in college => might have partTimeIncome + current
|
||||||
|
baseMonthlyIncome = (currentSalary / 12) + (partTimeIncome / 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track extra subtracting impacts in a separate variable
|
/************************************************
|
||||||
|
* 7.3 MILESTONE IMPACTS
|
||||||
|
************************************************/
|
||||||
let extraImpactsThisMonth = 0;
|
let extraImpactsThisMonth = 0;
|
||||||
|
|
||||||
// 9b. Apply milestone impacts
|
|
||||||
milestoneImpacts.forEach((impact) => {
|
milestoneImpacts.forEach((impact) => {
|
||||||
const startOffset = impact.start_date
|
const startDateClamped = moment(impact.start_date).startOf('month');
|
||||||
? moment(impact.start_date).diff(moment(scenarioStart), 'months')
|
let startOffset = startDateClamped.diff(scenarioStartClamped, 'months');
|
||||||
: 0;
|
if (startOffset < 0) startOffset = 0;
|
||||||
|
|
||||||
let endOffset = Infinity;
|
let endOffset = Infinity;
|
||||||
if (impact.end_date && impact.end_date.trim() !== '') {
|
if (impact.end_date && impact.end_date.trim() !== '') {
|
||||||
endOffset = moment(impact.end_date).diff(moment(scenarioStart), 'months');
|
const endDateClamped = moment(impact.end_date).startOf('month');
|
||||||
|
endOffset = endDateClamped.diff(scenarioStartClamped, 'months');
|
||||||
|
if (endOffset < 0) endOffset = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (impact.impact_type === 'ONE_TIME') {
|
if (impact.impact_type === 'ONE_TIME') {
|
||||||
if (elapsedMonths === startOffset) {
|
if (monthIndex === startOffset) {
|
||||||
if (impact.direction === 'add') {
|
if (impact.direction === 'add') {
|
||||||
grossMonthlyIncome += impact.amount;
|
baseMonthlyIncome += impact.amount;
|
||||||
} else {
|
} else {
|
||||||
extraImpactsThisMonth += impact.amount;
|
extraImpactsThisMonth += impact.amount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 'MONTHLY'
|
// 'MONTHLY'
|
||||||
if (elapsedMonths >= startOffset && elapsedMonths <= endOffset) {
|
if (monthIndex >= startOffset && monthIndex <= endOffset) {
|
||||||
if (impact.direction === 'add') {
|
if (impact.direction === 'add') {
|
||||||
grossMonthlyIncome += impact.amount;
|
baseMonthlyIncome += impact.amount;
|
||||||
} else {
|
} else {
|
||||||
extraImpactsThisMonth += impact.amount;
|
extraImpactsThisMonth += impact.amount;
|
||||||
}
|
}
|
||||||
@ -326,70 +313,51 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 10. Taxes
|
/************************************************
|
||||||
if (!taxStateByYear[currentYear]) {
|
* 7.4 CALCULATE TAXES (No YTD approach)
|
||||||
taxStateByYear[currentYear] = {
|
************************************************/
|
||||||
federalYtdGross: 0,
|
const monthlyFederalTax = calculateMonthlyFedTaxNoYTD(baseMonthlyIncome);
|
||||||
federalYtdTaxSoFar: 0,
|
const monthlyStateTax = calculateMonthlyStateTaxNoYTD(baseMonthlyIncome, stateCode);
|
||||||
stateYtdGross: 0,
|
const combinedTax = monthlyFederalTax + monthlyStateTax;
|
||||||
stateYtdTaxSoFar: 0
|
|
||||||
};
|
// net after tax
|
||||||
|
const netMonthlyIncome = baseMonthlyIncome - combinedTax;
|
||||||
|
|
||||||
|
// increment YTD gross & tax for reference
|
||||||
|
fedYTDgross += baseMonthlyIncome;
|
||||||
|
fedYTDtax += monthlyFederalTax;
|
||||||
|
stateYTDgross += baseMonthlyIncome;
|
||||||
|
stateYTDtax += monthlyStateTax;
|
||||||
|
|
||||||
|
/************************************************
|
||||||
|
* 7.5 LOAN + EXPENSES
|
||||||
|
************************************************/
|
||||||
|
const nowExitingCollege = wasInDeferral && !stillInCollege;
|
||||||
|
if (nowExitingCollege) {
|
||||||
|
monthlyLoanPayment = calculateLoanPayment(loanBalance, interestRate, loanTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
// accumulate YTD gross
|
|
||||||
taxStateByYear[currentYear].federalYtdGross += grossMonthlyIncome;
|
|
||||||
taxStateByYear[currentYear].stateYtdGross += grossMonthlyIncome;
|
|
||||||
|
|
||||||
// fed tax
|
|
||||||
const newFedTaxTotal = calculateAnnualFederalTaxSingle(
|
|
||||||
taxStateByYear[currentYear].federalYtdGross
|
|
||||||
);
|
|
||||||
const monthlyFederalTax = newFedTaxTotal - taxStateByYear[currentYear].federalYtdTaxSoFar;
|
|
||||||
taxStateByYear[currentYear].federalYtdTaxSoFar = newFedTaxTotal;
|
|
||||||
|
|
||||||
// state tax
|
|
||||||
const newStateTaxTotal = calculateAnnualStateTax(
|
|
||||||
taxStateByYear[currentYear].stateYtdGross,
|
|
||||||
stateCode
|
|
||||||
);
|
|
||||||
const monthlyStateTax = newStateTaxTotal - taxStateByYear[currentYear].stateYtdTaxSoFar;
|
|
||||||
taxStateByYear[currentYear].stateYtdTaxSoFar = newStateTaxTotal;
|
|
||||||
|
|
||||||
const combinedTax = monthlyFederalTax + monthlyStateTax;
|
|
||||||
const netMonthlyIncome = grossMonthlyIncome - combinedTax;
|
|
||||||
|
|
||||||
// 11. Expenses & loan
|
|
||||||
let thisMonthLoanPayment = 0;
|
|
||||||
// now include tuition lumps + any 'subtract' impacts
|
|
||||||
let totalMonthlyExpenses = monthlyExpenses + monthlyDebtPayments + tuitionCostThisMonth + extraImpactsThisMonth;
|
let totalMonthlyExpenses = monthlyExpenses + monthlyDebtPayments + tuitionCostThisMonth + extraImpactsThisMonth;
|
||||||
|
|
||||||
// re-amortize after deferral ends
|
|
||||||
if (nowExitingCollege) {
|
|
||||||
monthlyLoanPayment = calculateLoanPayment(loanBalance, interestRate, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if deferring
|
|
||||||
if (stillInCollege && loanDeferralUntilGraduation) {
|
if (stillInCollege && loanDeferralUntilGraduation) {
|
||||||
|
// accumulate interest
|
||||||
const interestForMonth = loanBalance * (interestRate / 100 / 12);
|
const interestForMonth = loanBalance * (interestRate / 100 / 12);
|
||||||
loanBalance += interestForMonth;
|
loanBalance += interestForMonth;
|
||||||
} else {
|
} else {
|
||||||
|
// pay principal
|
||||||
if (loanBalance > 0) {
|
if (loanBalance > 0) {
|
||||||
const interestForMonth = loanBalance * (interestRate / 100 / 12);
|
const interestForMonth = loanBalance * (interestRate / 100 / 12);
|
||||||
const principalForMonth = Math.min(
|
const principalForMonth = Math.min(
|
||||||
loanBalance,
|
loanBalance,
|
||||||
(monthlyLoanPayment + extraPayment) - interestForMonth
|
(monthlyLoanPayment + extraPayment) - interestForMonth
|
||||||
);
|
);
|
||||||
loanBalance -= principalForMonth;
|
loanBalance = Math.max(loanBalance - principalForMonth, 0);
|
||||||
loanBalance = Math.max(loanBalance, 0);
|
|
||||||
|
|
||||||
thisMonthLoanPayment = monthlyLoanPayment + extraPayment;
|
totalMonthlyExpenses += (monthlyLoanPayment + extraPayment);
|
||||||
totalMonthlyExpenses += thisMonthLoanPayment;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// leftover after mandatory expenses
|
|
||||||
let leftover = netMonthlyIncome - totalMonthlyExpenses;
|
let leftover = netMonthlyIncome - totalMonthlyExpenses;
|
||||||
if (leftover < 0) leftover = 0;
|
|
||||||
|
|
||||||
// baseline contributions
|
// baseline contributions
|
||||||
const baselineContributions = monthlyRetirementContribution + monthlyEmergencyContribution;
|
const baselineContributions = monthlyRetirementContribution + monthlyEmergencyContribution;
|
||||||
@ -402,59 +370,72 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
leftover -= baselineContributions;
|
leftover -= baselineContributions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// shortfall check
|
const actualExpensesPaid = totalMonthlyExpenses + effectiveRetirementContribution + effectiveEmergencyContribution;
|
||||||
const totalWantedContributions = effectiveRetirementContribution + effectiveEmergencyContribution;
|
|
||||||
const actualExpensesPaid = totalMonthlyExpenses + totalWantedContributions;
|
|
||||||
let shortfall = actualExpensesPaid - netMonthlyIncome;
|
let shortfall = actualExpensesPaid - netMonthlyIncome;
|
||||||
|
|
||||||
|
// cover shortfall with emergency
|
||||||
if (shortfall > 0) {
|
if (shortfall > 0) {
|
||||||
const canCover = Math.min(shortfall, currentEmergencySavings);
|
const canCover = Math.min(shortfall, currentEmergencySavings);
|
||||||
currentEmergencySavings -= canCover;
|
currentEmergencySavings -= canCover;
|
||||||
shortfall -= canCover;
|
shortfall -= canCover;
|
||||||
if (shortfall > 0) {
|
// leftover -= shortfall; // if you want negative leftover
|
||||||
// bankrupt scenario, end
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 13. Surplus
|
// Surplus => leftover
|
||||||
if (leftover > 0) {
|
if (leftover > 0) {
|
||||||
const totalPct = surplusEmergencyAllocation + surplusRetirementAllocation;
|
const totalPct = surplusEmergencyAllocation + surplusRetirementAllocation;
|
||||||
const emergencyPortion = leftover * (surplusEmergencyAllocation / totalPct);
|
const emergPortion = leftover * (surplusEmergencyAllocation / totalPct);
|
||||||
const retirementPortion = leftover * (surplusRetirementAllocation / totalPct);
|
const retPortion = leftover * (surplusRetirementAllocation / totalPct);
|
||||||
|
|
||||||
currentEmergencySavings += emergencyPortion;
|
currentEmergencySavings += emergPortion;
|
||||||
currentRetirementSavings += retirementPortion;
|
currentRetirementSavings += retPortion;
|
||||||
}
|
}
|
||||||
|
|
||||||
// netSavings for display
|
// net savings
|
||||||
const finalExpensesPaid = totalMonthlyExpenses + totalWantedContributions;
|
const netSavings = netMonthlyIncome - actualExpensesPaid;
|
||||||
const netSavings = netMonthlyIncome - finalExpensesPaid;
|
|
||||||
|
|
||||||
projectionData.push({
|
projectionData.push({
|
||||||
month: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`,
|
month: currentSimDate.format('YYYY-MM'),
|
||||||
grossMonthlyIncome: Math.round(grossMonthlyIncome * 100) / 100,
|
grossMonthlyIncome: +baseMonthlyIncome.toFixed(2),
|
||||||
monthlyFederalTax: Math.round(monthlyFederalTax * 100) / 100,
|
monthlyFederalTax: +monthlyFederalTax.toFixed(2),
|
||||||
monthlyStateTax: Math.round(monthlyStateTax * 100) / 100,
|
monthlyStateTax: +monthlyStateTax.toFixed(2),
|
||||||
combinedTax: Math.round(combinedTax * 100) / 100,
|
combinedTax: +combinedTax.toFixed(2),
|
||||||
netMonthlyIncome: Math.round(netMonthlyIncome * 100) / 100,
|
netMonthlyIncome: +netMonthlyIncome.toFixed(2),
|
||||||
totalExpenses: Math.round(finalExpensesPaid * 100) / 100,
|
|
||||||
effectiveRetirementContribution: Math.round(effectiveRetirementContribution * 100) / 100,
|
totalExpenses: +actualExpensesPaid.toFixed(2),
|
||||||
effectiveEmergencyContribution: Math.round(effectiveEmergencyContribution * 100) / 100,
|
effectiveRetirementContribution: +effectiveRetirementContribution.toFixed(2),
|
||||||
netSavings: Math.round(netSavings * 100) / 100,
|
effectiveEmergencyContribution: +effectiveEmergencyContribution.toFixed(2),
|
||||||
emergencySavings: Math.round(currentEmergencySavings * 100) / 100,
|
|
||||||
retirementSavings: Math.round(currentRetirementSavings * 100) / 100,
|
netSavings: +netSavings.toFixed(2),
|
||||||
loanBalance: Math.round(loanBalance * 100) / 100,
|
emergencySavings: +currentEmergencySavings.toFixed(2),
|
||||||
loanPaymentThisMonth: Math.round(thisMonthLoanPayment * 100) / 100
|
retirementSavings: +currentRetirementSavings.toFixed(2),
|
||||||
|
loanBalance: +loanBalance.toFixed(2),
|
||||||
|
|
||||||
|
// actual loan payment
|
||||||
|
loanPaymentThisMonth: +(monthlyLoanPayment + extraPayment).toFixed(2),
|
||||||
|
|
||||||
|
// YTD references
|
||||||
|
fedYTDgross: +fedYTDgross.toFixed(2),
|
||||||
|
fedYTDtax: +fedYTDtax.toFixed(2),
|
||||||
|
stateYTDgross: +stateYTDgross.toFixed(2),
|
||||||
|
stateYTDtax: +stateYTDtax.toFixed(2),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// update deferral
|
||||||
wasInDeferral = stillInCollege && loanDeferralUntilGraduation;
|
wasInDeferral = stillInCollege && loanDeferralUntilGraduation;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
projectionData,
|
projectionData,
|
||||||
loanPaidOffMonth,
|
loanPaidOffMonth,
|
||||||
finalEmergencySavings: Math.round(currentEmergencySavings * 100) / 100,
|
finalEmergencySavings: +currentEmergencySavings.toFixed(2),
|
||||||
finalRetirementSavings: Math.round(currentRetirementSavings * 100) / 100,
|
finalRetirementSavings: +currentRetirementSavings.toFixed(2),
|
||||||
finalLoanBalance: Math.round(loanBalance * 100) / 100
|
finalLoanBalance: +loanBalance.toFixed(2),
|
||||||
|
|
||||||
|
// Final YTD totals
|
||||||
|
fedYTDgross: +fedYTDgross.toFixed(2),
|
||||||
|
fedYTDtax: +fedYTDtax.toFixed(2),
|
||||||
|
stateYTDgross: +stateYTDgross.toFixed(2),
|
||||||
|
stateYTDtax: +stateYTDtax.toFixed(2),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user