Fixed multiscenarioView to simulate properly.

This commit is contained in:
Josh 2025-04-29 15:04:43 +00:00
parent 16667918bc
commit 402e760672
11 changed files with 2229 additions and 888 deletions

View File

@ -115,7 +115,7 @@ app.get('/api/premium/career-profile/:careerPathId', authenticatePremiumUser, as
});
// POST a new career profile
// server3.js
app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res) => {
const {
scenario_title,
@ -143,8 +143,7 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
const newCareerPathId = uuidv4();
const now = new Date().toISOString();
// Insert or update row in career_paths. We rely on ON CONFLICT(user_id, career_name).
// If you want a different conflict target, change accordingly.
// Upsert via ON CONFLICT(user_id, career_name)
await db.run(`
INSERT INTO career_paths (
id,
@ -156,7 +155,6 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
projected_end_date,
college_enrollment_status,
currently_working,
planned_monthly_expenses,
planned_monthly_debt_payments,
planned_monthly_retirement_contribution,
@ -164,13 +162,16 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
planned_surplus_emergency_pct,
planned_surplus_retirement_pct,
planned_additional_income,
created_at,
updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?,
?, ?)
VALUES (
?, ?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?,
?, ?
)
ON CONFLICT(user_id, career_name)
DO UPDATE SET
status = excluded.status,
@ -189,19 +190,19 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
updated_at = ?
`, [
newCareerPathId,
req.userId,
scenario_title || null,
career_name,
status || 'planned',
start_date || now,
projected_end_date || null,
college_enrollment_status || null,
currently_working || null,
// 18 items for the INSERT columns
newCareerPathId, // id
req.userId, // user_id
scenario_title || null, // scenario_title
career_name, // career_name
status || 'planned', // status
start_date || now, // start_date
projected_end_date || null, // projected_end_date
college_enrollment_status || null, // college_enrollment_status
currently_working || null, // currently_working
// new planned columns
planned_monthly_expenses ?? null,
planned_monthly_debt_payments ?? null,
planned_monthly_expenses ?? null, // planned_monthly_expenses
planned_monthly_debt_payments ?? null, // planned_monthly_debt_payments
planned_monthly_retirement_contribution ?? null,
planned_monthly_emergency_contribution ?? null,
planned_surplus_emergency_pct ?? null,
@ -210,10 +211,12 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
now, // created_at
now, // updated_at
now // updated_at on conflict
// Then 1 more param for "updated_at = ?" in the conflict update
now
]);
// Optionally fetch the row's ID (or entire row) after upsert:
// Optionally fetch the row's ID or entire row after upsert
const result = await db.get(`
SELECT id
FROM career_paths
@ -231,7 +234,6 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
}
});
// server3.js (or your premium server file)
// Delete a career path (scenario) by ID
app.delete('/api/premium/career-profile/:careerPathId', authenticatePremiumUser, async (req, res) => {

View File

@ -0,0 +1,45 @@
// src/components/MilestoneCopyWizard.js
import React, { useState, useEffect } from 'react';
export default function MilestoneCopyWizard({ milestone, authFetch, onClose }) {
const [scenarios, setScenarios] = useState([]);
const [selectedScenarios, setSelectedScenarios] = useState([]);
useEffect(() => {
// fetch /api/premium/career-profile/all => setScenarios
}, [authFetch]);
function toggleScenario(id) {
setSelectedScenarios((prev) =>
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
);
}
async function handleCopy() {
// POST => /api/premium/milestone/copy
// with { milestoneId: milestone.id, scenarioIds: selectedScenarios }
// Then onClose(true)
}
if (!milestone) return null;
return (
<div className="copy-wizard-backdrop">
<div className="copy-wizard-content">
<h3>Copy: {milestone.title}</h3>
{scenarios.map((s) => (
<label key={s.id}>
<input
type="checkbox"
checked={selectedScenarios.includes(s.id)}
onChange={() => toggleScenario(s.id)}
/>
{s.career_name}
</label>
))}
<br />
<button onClick={() => onClose(false)}>Cancel</button>
<button onClick={() => handleCopy()}>Copy</button>
</div>
</div>
);
}

View File

@ -1,8 +1,10 @@
// src/components/MultiScenarioView.js
import React, { useEffect, useState } from 'react';
import authFetch from '../utils/authFetch.js';
import ScenarioContainer from './ScenarioContainer.js';
import ScenarioEditModal from './ScenarioEditModal.js'; // The floating modal
// This component loads the user's global financial profile
// plus a list of all scenarios, and renders one <ScenarioContainer>
// for each. It also has the "Add Scenario" and "Clone/Remove" logic.
export default function MultiScenarioView() {
const [loading, setLoading] = useState(false);
@ -11,44 +13,38 @@ export default function MultiScenarioView() {
// The users single overall financial profile
const [financialProfile, setFinancialProfile] = useState(null);
// All scenario rows (from career_paths)
// The list of scenario "headers" (career_paths)
const [scenarios, setScenarios] = useState([]);
// The scenario were currently editing in a top-level modal:
const [editingScenario, setEditingScenario] = useState(null);
// The collegeProfile we load for that scenario (passed to edit modal)
const [editingCollegeProfile, setEditingCollegeProfile] = useState(null);
useEffect(() => {
async function loadData() {
setLoading(true);
setError(null);
try {
// 1) Fetch the users overall financial profile
const finRes = await authFetch('/api/premium/financial-profile');
if (!finRes.ok) throw new Error(`FinancialProfile error: ${finRes.status}`);
const finData = await finRes.json();
// 2) Fetch all scenarios (career_paths)
const scenRes = await authFetch('/api/premium/career-profile/all');
if (!scenRes.ok) throw new Error(`Scenarios error: ${scenRes.status}`);
const scenData = await scenRes.json();
setFinancialProfile(finData);
setScenarios(scenData.careerPaths || []);
} catch (err) {
console.error('Error loading data in MultiScenarioView:', err);
setError(err.message || 'Failed to load scenarios/financial');
} finally {
setLoading(false);
}
}
loadData();
loadScenariosAndFinancial();
}, []);
// ---------------------------
// Add a new scenario
// ---------------------------
async function loadScenariosAndFinancial() {
setLoading(true);
setError(null);
try {
// 1) fetch users global financialProfile
const finRes = await authFetch('/api/premium/financial-profile');
if (!finRes.ok) throw new Error(`FinancialProfile error: ${finRes.status}`);
const finData = await finRes.json();
// 2) fetch scenario list
const scenRes = await authFetch('/api/premium/career-profile/all');
if (!scenRes.ok) throw new Error(`Scenarios error: ${scenRes.status}`);
const scenData = await scenRes.json();
setFinancialProfile(finData);
setScenarios(scenData.careerPaths || []);
} catch (err) {
console.error('MultiScenarioView load error:', err);
setError(err.message || 'Failed to load multi-scenarios');
} finally {
setLoading(false);
}
}
// Add a new scenario => then reload
async function handleAddScenario() {
try {
const body = {
@ -64,37 +60,30 @@ export default function MultiScenarioView() {
body: JSON.stringify(body)
});
if (!res.ok) throw new Error(`Add scenario error: ${res.status}`);
const data = await res.json();
// Insert the new row into local state
const newRow = {
id: data.career_path_id,
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]);
await loadScenariosAndFinancial();
} catch (err) {
console.error('Failed adding scenario:', err);
alert(err.message || 'Could not add scenario');
alert(err.message);
}
}
// ---------------------------
// Clone scenario
// ---------------------------
async function handleCloneScenario(sourceScenario) {
// Clone a scenario => then reload
async function handleCloneScenario(s) {
try {
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
scenario_title: s.scenario_title ? s.scenario_title + ' (Copy)' : null,
career_name: s.career_name ? s.career_name + ' (Copy)' : 'Untitled (Copy)',
status: s.status,
start_date: s.start_date,
projected_end_date: s.projected_end_date,
college_enrollment_status: s.college_enrollment_status,
currently_working: s.currently_working,
planned_monthly_expenses: s.planned_monthly_expenses,
planned_monthly_debt_payments: s.planned_monthly_debt_payments,
planned_monthly_retirement_contribution: s.planned_monthly_retirement_contribution,
planned_monthly_emergency_contribution: s.planned_monthly_emergency_contribution,
planned_surplus_emergency_pct: s.planned_surplus_emergency_pct,
planned_surplus_retirement_pct: s.planned_surplus_retirement_pct,
planned_additional_income: s.planned_additional_income
};
const res = await authFetch('/api/premium/career-profile', {
method: 'POST',
@ -102,112 +91,51 @@ export default function MultiScenarioView() {
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;
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]);
await loadScenariosAndFinancial();
} catch (err) {
console.error('Failed cloning scenario:', err);
alert(err.message || 'Could not clone scenario');
alert(err.message);
}
}
// ---------------------------
// Delete scenario
// ---------------------------
async function handleRemoveScenario(scenarioId) {
// confirm
const confirmDel = window.confirm(
'Delete this scenario (and associated collegeProfile/milestones)?'
);
// Remove => reload
async function handleRemoveScenario(id) {
const confirmDel = window.confirm('Delete this scenario?');
if (!confirmDel) return;
try {
const res = await authFetch(`/api/premium/career-profile/${scenarioId}`, {
const res = await authFetch(`/api/premium/career-profile/${id}`, {
method: 'DELETE'
});
if (!res.ok) throw new Error(`Delete scenario error: ${res.status}`);
// remove from local
setScenarios((prev) => prev.filter((s) => s.id !== scenarioId));
await loadScenariosAndFinancial();
} catch (err) {
console.error('Delete scenario error:', err);
alert(err.message || 'Could not delete scenario');
alert(err.message);
}
}
// ---------------------------
// User clicks "Edit" in ScenarioContainer => we fetch that scenarios collegeProfile
// set editingScenario / editingCollegeProfile => modal
// ---------------------------
async function handleEditScenario(scenarioObj) {
if (!scenarioObj?.id) return;
try {
// fetch the collegeProfile
const colResp = await authFetch(`/api/premium/college-profile?careerPathId=${scenarioObj.id}`);
let colData = {};
if (colResp.ok) {
colData = await colResp.json();
}
setEditingScenario(scenarioObj);
setEditingCollegeProfile(colData);
} catch (err) {
console.error('Error loading collegeProfile for editing:', err);
setEditingScenario(scenarioObj);
setEditingCollegeProfile({});
}
}
// Called by <ScenarioEditModal> on close => we optionally update local scenario
function handleModalClose(updatedScenario, updatedCollege) {
if (updatedScenario) {
setScenarios(prev =>
prev.map((s) => (s.id === updatedScenario.id ? { ...s, ...updatedScenario } : s))
);
}
// We might not store the updatedCollege in local state unless we want to re-simulate immediately
// For now, do nothing or re-fetch if needed
setEditingScenario(null);
setEditingCollegeProfile(null);
}
// If user wants to "edit" a scenario, we'll pass it down to the container's "onEdit"
// or you can open a modal at this level. For now, we rely on the ScenarioContainer
// "onEdit" prop if needed.
if (loading) return <p>Loading scenarios...</p>;
if (error) return <p style={{ color: 'red' }}>Error: {error}</p>;
if (error) return <p style={{ color:'red' }}>{error}</p>;
return (
<div className="multi-scenario-view" style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem' }}>
{scenarios.map((scen) => (
<div style={{ display:'flex', flexWrap:'wrap', gap:'1rem' }}>
{scenarios.map(sc => (
<ScenarioContainer
key={scen.id}
scenario={scen}
key={sc.id}
scenario={sc} // pass the scenario row
financialProfile={financialProfile}
onClone={() => handleCloneScenario(scen)}
onRemove={() => handleRemoveScenario(scen.id)}
onEdit={() => handleEditScenario(scen)} // new callback
onClone={(s) => handleCloneScenario(s)}
onRemove={(id) => handleRemoveScenario(id)}
onEdit={(sc) => {
// Example: open an edit modal or navigate to a scenario editor.
console.log('Edit scenario clicked:', sc);
}}
/>
))}
<div style={{ alignSelf: 'flex-start' }}>
<button onClick={handleAddScenario}>+ Add Scenario</button>
</div>
{/* The floating modal at the bottom => only if editingScenario != null */}
{editingScenario && (
<ScenarioEditModal
show={true}
scenario={editingScenario}
collegeProfile={editingCollegeProfile}
onClose={handleModalClose}
/>
)}
<button onClick={handleAddScenario}>+ Add Scenario</button>
</div>
);
}

View File

@ -1,6 +1,6 @@
import React from 'react';
const FinancialOnboarding = ({ nextStep, prevStep, data, setData }) => {
const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = false }) => {
const {
currently_working = '',
current_salary = 0,
@ -12,7 +12,15 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData }) => {
emergency_fund = 0,
emergency_contribution = 0,
extra_cash_emergency_pct = "",
extra_cash_retirement_pct = ""
extra_cash_retirement_pct = "",
planned_monthly_expenses = '',
planned_monthly_debt_payments = '',
planned_monthly_retirement_contribution = '',
planned_monthly_emergency_contribution = '',
planned_surplus_emergency_pct = '',
planned_surplus_retirement_pct = '',
planned_additional_income = ''
} = data;
const handleChange = (e) => {
@ -134,6 +142,70 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData }) => {
onChange={handleChange}
/>
{/* Only show the planned overrides if isEditMode is true */}
{isEditMode && (
<>
<hr />
<h2>Planned Scenario Overrides</h2>
<p>These fields let you override your real finances for this scenario.</p>
<label>Planned Monthly Expenses</label>
<input
type="number"
name="planned_monthly_expenses"
value={planned_monthly_expenses}
onChange={handleChange}
/>
<label>Planned Monthly Debt Payments</label>
<input
type="number"
name="planned_monthly_debt_payments"
value={planned_monthly_debt_payments}
onChange={handleChange}
/>
<label>Planned Monthly Retirement Contribution</label>
<input
type="number"
name="planned_monthly_retirement_contribution"
value={planned_monthly_retirement_contribution}
onChange={handleChange}
/>
<label>Planned Monthly Emergency Contribution</label>
<input
type="number"
name="planned_monthly_emergency_contribution"
value={planned_monthly_emergency_contribution}
onChange={handleChange}
/>
<label>Planned Surplus % to Emergency</label>
<input
type="number"
name="planned_surplus_emergency_pct"
value={planned_surplus_emergency_pct}
onChange={handleChange}
/>
<label>Planned Surplus % to Retirement</label>
<input
type="number"
name="planned_surplus_retirement_pct"
value={planned_surplus_retirement_pct}
onChange={handleChange}
/>
<label>Planned Additional Annual Income</label>
<input
type="number"
name="planned_additional_income"
value={planned_additional_income}
onChange={handleChange}
/>
</>
)}
<button onClick={prevStep}> Previous: Career</button>
<button onClick={nextStep}>Next: College </button>
</div>

View File

@ -26,23 +26,30 @@ const OnboardingContainer = () => {
// Now we do the final “all done” submission when the user finishes the last step
const handleFinalSubmit = async () => {
try {
// 1) POST career-profile
// Build a scenarioPayload that includes the optional planned_* fields.
// We parseFloat them to avoid sending strings, and default to 0 if empty.
const scenarioPayload = {
...careerData,
planned_monthly_expenses: parseFloat(careerData.planned_monthly_expenses) || 0,
planned_monthly_debt_payments: parseFloat(careerData.planned_monthly_debt_payments) || 0,
planned_monthly_retirement_contribution: parseFloat(careerData.planned_monthly_retirement_contribution) || 0,
planned_monthly_emergency_contribution: parseFloat(careerData.planned_monthly_emergency_contribution) || 0,
planned_surplus_emergency_pct: parseFloat(careerData.planned_surplus_emergency_pct) || 0,
planned_surplus_retirement_pct: parseFloat(careerData.planned_surplus_retirement_pct) || 0,
planned_additional_income: parseFloat(careerData.planned_additional_income) || 0
};
// 1) POST career-profile (scenario)
const careerRes = await authFetch('/api/premium/career-profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(careerData),
body: JSON.stringify(scenarioPayload),
});
if (!careerRes.ok) throw new Error('Failed to save career profile');
const careerJson = await careerRes.json();
const { career_path_id } = careerJson;
if (!career_path_id) throw new Error('No career_path_id returned by server');
const mergedCollegeData = {
...collegeData,
// ensure this field isnt null
college_enrollment_status: careerData.college_enrollment_status,
career_path_id
};
// 2) POST financial-profile
const financialRes = await authFetch('/api/premium/financial-profile', {
method: 'POST',
@ -50,19 +57,20 @@ const OnboardingContainer = () => {
body: JSON.stringify(financialData),
});
if (!financialRes.ok) throw new Error('Failed to save financial profile');
// 3) POST college-profile (include career_path_id)
const mergedCollege = {
...collegeData,
college_enrollment_status: careerData.college_enrollment_status,
career_path_id };
const mergedCollege = {
...collegeData,
career_path_id,
college_enrollment_status: careerData.college_enrollment_status
};
const collegeRes = await authFetch('/api/premium/college-profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(mergedCollege),
});
if (!collegeRes.ok) throw new Error('Failed to save college profile');
// Done => navigate away
navigate('/milestone-tracker');
} catch (err) {
@ -70,6 +78,7 @@ const OnboardingContainer = () => {
// (optionally show error to user)
}
};
const onboardingSteps = [
<PremiumWelcome nextStep={nextStep} />,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,165 @@
// ScenarioEditWizard.js
import React, { useState, useEffect } from 'react';
import CareerOnboarding from './PremiumOnboarding/CareerOnboarding.js';
import FinancialOnboarding from './PremiumOnboarding/FinancialOnboarding.js';
import CollegeOnboarding from './PremiumOnboarding/CollegeOnboarding.js';
import ReviewPage from './PremiumOnboarding/ReviewPage.js';
import authFetch from '../utils/authFetch.js';
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
export default function ScenarioEditWizard({
show,
onClose,
scenarioId // or scenario object
}) {
const [step, setStep] = useState(0);
const [careerData, setCareerData] = useState({});
const [financialData, setFinancialData] = useState({});
const [collegeData, setCollegeData] = useState({});
// You might also store scenario + college IDs
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!show || !scenarioId) return;
// 1) fetch scenario => careerData
// 2) fetch financial => financialData
// 3) fetch college => collegeData
// Pre-fill the same states your Onboarding steps expect.
async function fetchExisting() {
setLoading(true);
try {
const [scenRes, finRes, colRes] = await Promise.all([
authFetch(`/api/premium/career-profile/${scenarioId}`),
authFetch(`/api/premium/financial-profile`),
authFetch(`/api/premium/college-profile?careerPathId=${scenarioId}`)
]);
if (!scenRes.ok || !finRes.ok || !colRes.ok) {
throw new Error('Failed fetching existing scenario or financial or college.');
}
const [scenData, finData, colDataRaw] = await Promise.all([
scenRes.json(),
finRes.json(),
colRes.json()
]);
let colData = Array.isArray(colDataRaw) ? colDataRaw[0] : colDataRaw;
// Now put them into the same shape as your Onboarding step states:
setCareerData({
career_name: scenData.career_name,
college_enrollment_status: scenData.college_enrollment_status,
currently_working: scenData.currently_working,
status: scenData.status,
start_date: scenData.start_date,
projected_end_date: scenData.projected_end_date,
planned_monthly_expenses: scenData.planned_monthly_expenses,
planned_monthly_debt_payments: scenData.planned_monthly_debt_payments,
planned_monthly_retirement_contribution: scenData.planned_monthly_retirement_contribution,
planned_monthly_emergency_contribution: scenData.planned_monthly_emergency_contribution,
planned_surplus_emergency_pct: scenData.planned_surplus_emergency_pct,
planned_surplus_retirement_pct: scenData.planned_surplus_retirement_pct,
planned_additional_income: scenData.planned_additional_income,
user_id: scenData.user_id,
// etc...
});
setFinancialData({
// your financial table fields
current_salary: finData.current_salary,
additional_income: finData.additional_income,
monthly_expenses: finData.monthly_expenses,
monthly_debt_payments: finData.monthly_debt_payments,
retirement_savings: finData.retirement_savings,
emergency_fund: finData.emergency_fund,
retirement_contribution: finData.retirement_contribution,
emergency_contribution: finData.emergency_contribution,
extra_cash_emergency_pct: finData.extra_cash_emergency_pct,
extra_cash_retirement_pct: finData.extra_cash_retirement_pct
});
setCollegeData({
// from colData
selected_school: colData.selected_school,
selected_program: colData.selected_program,
program_type: colData.program_type,
academic_calendar: colData.academic_calendar,
is_in_state: colData.is_in_state,
is_in_district: colData.is_in_district,
is_online: colData.is_online,
college_enrollment_status: colData.college_enrollment_status,
annual_financial_aid: colData.annual_financial_aid,
existing_college_debt: colData.existing_college_debt,
tuition: colData.tuition,
tuition_paid: colData.tuition_paid,
loan_deferral_until_graduation: colData.loan_deferral_until_graduation,
loan_term: colData.loan_term,
interest_rate: colData.interest_rate,
extra_payment: colData.extra_payment,
credit_hours_per_year: colData.credit_hours_per_year,
hours_completed: colData.hours_completed,
program_length: colData.program_length,
credit_hours_required: colData.credit_hours_required,
expected_graduation: colData.expected_graduation,
expected_salary: colData.expected_salary
});
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
}
fetchExisting();
}, [show, scenarioId]);
const nextStep = () => setStep(s => s + 1);
const prevStep = () => setStep(s => s - 1);
if (!show) return null;
if (loading) return <div className="modal">Loading existing scenario...</div>;
const steps = [
<CareerOnboarding
data={careerData}
setData={setCareerData}
nextStep={nextStep}
/>,
<FinancialOnboarding
data={{
...financialData,
currently_working: careerData.currently_working // pass along
}}
setData={setFinancialData}
nextStep={nextStep}
prevStep={prevStep}
isEditMode={true}
/>,
<CollegeOnboarding
data={{
...collegeData,
college_enrollment_status: careerData.college_enrollment_status
}}
setData={setCollegeData}
nextStep={nextStep}
prevStep={prevStep}
/>,
<ReviewPage
careerData={careerData}
financialData={financialData}
collegeData={collegeData}
onSubmit={async () => {
// same final logic from Onboarding: upsert scenario, financial, college
// Then close
onClose();
}}
onBack={prevStep}
/>
];
return (
<div className="modal-backdrop">
<div className="modal-content" style={{ padding:'1rem' }}>
{steps[step]}
</div>
</div>
);
}

View File

@ -45,8 +45,6 @@ function calculateAnnualFederalTaxSingle(annualTaxable) {
/***************************************************
* HELPER: Monthly Federal Tax (no YTD)
* We just treat (monthlyGross * 12) - standardDed
* -> bracket -> / 12
***************************************************/
function calculateMonthlyFedTaxNoYTD(monthlyGross) {
const annualGross = monthlyGross * 12;
@ -59,7 +57,6 @@ function calculateMonthlyFedTaxNoYTD(monthlyGross) {
/***************************************************
* 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;
@ -87,6 +84,9 @@ function calculateLoanPayment(principal, annualRate, years) {
* MAIN SIMULATION FUNCTION
***************************************************/
export function simulateFinancialProjection(userProfile) {
// 1) Show userProfile at the start
console.log("simulateFinancialProjection() called with userProfile:", userProfile);
/***************************************************
* 1) DESTRUCTURE USER PROFILE
***************************************************/
@ -105,7 +105,7 @@ export function simulateFinancialProjection(userProfile) {
loanDeferralUntilGraduation = false,
// College config
inCollege = false,
inCollege = false, // <<==== user-provided
programType,
hoursCompleted = 0,
creditHoursPerYear,
@ -131,12 +131,12 @@ export function simulateFinancialProjection(userProfile) {
// Program length override
programLength,
// State code for taxes (default to GA if not provided)
// State code
stateCode = 'GA',
// Financial milestone impacts
milestoneImpacts = [],
// Simulation duration
simulationYears = 20
@ -170,7 +170,7 @@ export function simulateFinancialProjection(userProfile) {
const finalProgramLength = programLength || dynamicProgramLength;
/***************************************************
* 4) TUITION CALC: lumps, deferral, etc.
* 4) TUITION CALC
***************************************************/
const netAnnualTuition = Math.max(0, (calculatedTuition || 0) - (annualFinancialAid || 0));
const totalTuitionCost = netAnnualTuition * finalProgramLength;
@ -205,10 +205,19 @@ export function simulateFinancialProjection(userProfile) {
? 0
: calculateLoanPayment(studentLoanAmount, interestRate, loanTerm);
// Log the initial loan info:
console.log("Initial loan payment setup:", {
studentLoanAmount,
interestRate,
loanTerm,
loanDeferralUntilGraduation,
monthlyLoanPayment
});
/***************************************************
* 6) SETUP FOR THE SIMULATION LOOP
***************************************************/
const maxMonths = simulationYears*12; // 20 years
const maxMonths = simulationYears * 12;
let loanBalance = Math.max(studentLoanAmount, 0);
let loanPaidOffMonth = null;
@ -217,28 +226,30 @@ export function simulateFinancialProjection(userProfile) {
let projectionData = [];
// Keep track of YTD gross & tax for reference
// Track YTD gross & tax
let fedYTDgross = 0;
let fedYTDtax = 0;
let stateYTDgross = 0;
let stateYTDtax = 0;
// We'll keep track that we started in deferral if inCollege & deferral is true
let wasInDeferral = inCollege && loanDeferralUntilGraduation;
// If there's a gradDate, let's see if we pass it:
const graduationDateObj = gradDate ? moment(gradDate).startOf('month') : null;
/***************************************************
* 7) THE MONTHLY LOOP
***************************************************/
for (let monthIndex = 0; monthIndex < maxMonths; monthIndex++) {
// date for this iteration
const currentSimDate = scenarioStartClamped.clone().add(monthIndex, 'months');
// check if loan is fully paid
// Check if loan is fully paid
if (loanBalance <= 0 && !loanPaidOffMonth) {
loanPaidOffMonth = currentSimDate.format('YYYY-MM');
}
// Are we still in college?
// Are we still in college for this month?
let stillInCollege = false;
if (inCollege) {
if (graduationDateObj) {
@ -271,7 +282,7 @@ export function simulateFinancialProjection(userProfile) {
************************************************/
let baseMonthlyIncome = 0;
if (!stillInCollege) {
// user is out of college => expected or current
// user out of college => expected or current
baseMonthlyIncome = (expectedSalary || currentSalary) / 12;
} else {
// in college => might have partTimeIncome + current
@ -315,7 +326,7 @@ export function simulateFinancialProjection(userProfile) {
});
/************************************************
* 7.4 CALCULATE TAXES (No YTD approach)
* 7.4 CALCULATE TAXES
************************************************/
const monthlyFederalTax = calculateMonthlyFedTaxNoYTD(baseMonthlyIncome);
const monthlyStateTax = calculateMonthlyStateTaxNoYTD(baseMonthlyIncome, stateCode);
@ -324,7 +335,7 @@ export function simulateFinancialProjection(userProfile) {
// net after tax
const netMonthlyIncome = baseMonthlyIncome - combinedTax;
// increment YTD gross & tax for reference
// increment YTD for reference
fedYTDgross += baseMonthlyIncome;
fedYTDtax += monthlyFederalTax;
stateYTDgross += baseMonthlyIncome;
@ -333,28 +344,47 @@ export function simulateFinancialProjection(userProfile) {
/************************************************
* 7.5 LOAN + EXPENSES
************************************************/
// Check if we're now exiting college & deferral ended => recalc monthlyLoanPayment
const nowExitingCollege = wasInDeferral && !stillInCollege;
if (nowExitingCollege) {
// recalc monthlyLoanPayment with the current loanBalance
monthlyLoanPayment = calculateLoanPayment(loanBalance, interestRate, loanTerm);
console.log(
`== Exiting deferral at monthIndex=${monthIndex}, ` +
`loanBalance=${loanBalance}, new monthlyLoanPayment=${monthlyLoanPayment}`
);
}
// sum up all monthly expenses
let totalMonthlyExpenses = monthlyExpenses + monthlyDebtPayments + tuitionCostThisMonth + extraImpactsThisMonth;
// (UPDATED) console log includes inCollege + stillInCollege + loanDeferral
console.log(
`Month ${monthIndex}, date=${currentSimDate.format('YYYY-MM')} => ` +
`inCollege=${inCollege}, stillInCollege=${stillInCollege}, ` +
`loanDeferralUntilGrad=${loanDeferralUntilGraduation}, ` +
`loanBalBefore=${loanBalance.toFixed(2)}, ` +
`monthlyLoanPayment=${monthlyLoanPayment.toFixed(2)}, extraPayment=${extraPayment}`
);
if (stillInCollege && loanDeferralUntilGraduation) {
// accumulate interest
// accumulate interest only
const interestForMonth = loanBalance * (interestRate / 100 / 12);
loanBalance += interestForMonth;
console.log(` (deferral) interest added=${interestForMonth.toFixed(2)}, loanBalAfter=${loanBalance.toFixed(2)}`);
} else {
// pay principal
if (loanBalance > 0) {
const interestForMonth = loanBalance * (interestRate / 100 / 12);
const principalForMonth = Math.min(
loanBalance,
(monthlyLoanPayment + extraPayment) - interestForMonth
);
const totalThisMonth = monthlyLoanPayment + extraPayment;
const principalForMonth = Math.min(loanBalance, totalThisMonth - interestForMonth);
loanBalance = Math.max(loanBalance - principalForMonth, 0);
totalMonthlyExpenses += totalThisMonth;
totalMonthlyExpenses += (monthlyLoanPayment + extraPayment);
console.log(
` (payment) interest=${interestForMonth.toFixed(2)}, principal=${principalForMonth.toFixed(2)}, ` +
`loanBalAfter=${loanBalance.toFixed(2)}`
);
}
}
@ -379,7 +409,6 @@ export function simulateFinancialProjection(userProfile) {
const canCover = Math.min(shortfall, currentEmergencySavings);
currentEmergencySavings -= canCover;
shortfall -= canCover;
// leftover -= shortfall; // if you want negative leftover
}
// Surplus => leftover
@ -392,11 +421,15 @@ export function simulateFinancialProjection(userProfile) {
currentRetirementSavings += retPortion;
}
// net savings
const netSavings = netMonthlyIncome - actualExpensesPaid;
// (UPDATED) add inCollege, stillInCollege, loanDeferralUntilGraduation to the result
projectionData.push({
month: currentSimDate.format('YYYY-MM'),
inCollege, // new
stillInCollege, // new
loanDeferralUntilGraduation, // new
grossMonthlyIncome: +baseMonthlyIncome.toFixed(2),
monthlyFederalTax: +monthlyFederalTax.toFixed(2),
monthlyStateTax: +monthlyStateTax.toFixed(2),
@ -408,24 +441,35 @@ export function simulateFinancialProjection(userProfile) {
effectiveEmergencyContribution: +effectiveEmergencyContribution.toFixed(2),
netSavings: +netSavings.toFixed(2),
emergencySavings: +currentEmergencySavings.toFixed(2),
retirementSavings: +currentRetirementSavings.toFixed(2),
// If you want to show the new running values,
// you can keep them as is or store them:
emergencySavings: (typeof currentEmergencySavings === 'number')
? +currentEmergencySavings.toFixed(2)
: currentEmergencySavings,
retirementSavings: (typeof currentRetirementSavings === 'number')
? +currentRetirementSavings.toFixed(2)
: currentRetirementSavings,
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),
stateYTDtax: +stateYTDtax.toFixed(2)
});
// update deferral
wasInDeferral = stillInCollege && loanDeferralUntilGraduation;
wasInDeferral = (stillInCollege && loanDeferralUntilGraduation);
}
// final loanPaidOffMonth if never set
if (loanBalance <= 0 && !loanPaidOffMonth) {
loanPaidOffMonth = scenarioStartClamped.clone().add(maxMonths, 'months').format('YYYY-MM');
}
console.log("End of simulation: finalLoanBalance=", loanBalance.toFixed(2),
"loanPaidOffMonth=", loanPaidOffMonth);
return {
projectionData,
loanPaidOffMonth,
@ -433,10 +477,9 @@ export function simulateFinancialProjection(userProfile) {
finalRetirementSavings: +currentRetirementSavings.toFixed(2),
finalLoanBalance: +loanBalance.toFixed(2),
// Final YTD totals
fedYTDgross: +fedYTDgross.toFixed(2),
fedYTDtax: +fedYTDtax.toFixed(2),
stateYTDgross: +stateYTDgross.toFixed(2),
stateYTDtax: +stateYTDtax.toFixed(2),
stateYTDtax: +stateYTDtax.toFixed(2)
};
}

Binary file not shown.

0
user_proile.db Normal file
View File