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 // POST a new career profile
// server3.js
app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res) => { app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res) => {
const { const {
scenario_title, scenario_title,
@ -143,8 +143,7 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
const newCareerPathId = uuidv4(); const newCareerPathId = uuidv4();
const now = new Date().toISOString(); const now = new Date().toISOString();
// Insert or update row in career_paths. We rely on ON CONFLICT(user_id, career_name). // Upsert via ON CONFLICT(user_id, career_name)
// If you want a different conflict target, change accordingly.
await db.run(` await db.run(`
INSERT INTO career_paths ( INSERT INTO career_paths (
id, id,
@ -156,7 +155,6 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
projected_end_date, projected_end_date,
college_enrollment_status, college_enrollment_status,
currently_working, currently_working,
planned_monthly_expenses, planned_monthly_expenses,
planned_monthly_debt_payments, planned_monthly_debt_payments,
planned_monthly_retirement_contribution, planned_monthly_retirement_contribution,
@ -164,13 +162,16 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
planned_surplus_emergency_pct, planned_surplus_emergency_pct,
planned_surplus_retirement_pct, planned_surplus_retirement_pct,
planned_additional_income, planned_additional_income,
created_at, created_at,
updated_at updated_at
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?) ?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?,
?, ?
)
ON CONFLICT(user_id, career_name) ON CONFLICT(user_id, career_name)
DO UPDATE SET DO UPDATE SET
status = excluded.status, status = excluded.status,
@ -189,19 +190,19 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
updated_at = ? updated_at = ?
`, [ `, [
newCareerPathId, // 18 items for the INSERT columns
req.userId, newCareerPathId, // id
scenario_title || null, req.userId, // user_id
career_name, scenario_title || null, // scenario_title
status || 'planned', career_name, // career_name
start_date || now, status || 'planned', // status
projected_end_date || null, start_date || now, // start_date
college_enrollment_status || null, projected_end_date || null, // projected_end_date
currently_working || null, college_enrollment_status || null, // college_enrollment_status
currently_working || null, // currently_working
// new planned columns planned_monthly_expenses ?? null, // planned_monthly_expenses
planned_monthly_expenses ?? null, planned_monthly_debt_payments ?? null, // planned_monthly_debt_payments
planned_monthly_debt_payments ?? null,
planned_monthly_retirement_contribution ?? null, planned_monthly_retirement_contribution ?? null,
planned_monthly_emergency_contribution ?? null, planned_monthly_emergency_contribution ?? null,
planned_surplus_emergency_pct ?? null, planned_surplus_emergency_pct ?? null,
@ -210,10 +211,12 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
now, // created_at now, // created_at
now, // updated_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(` const result = await db.get(`
SELECT id SELECT id
FROM career_paths 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 // Delete a career path (scenario) by ID
app.delete('/api/premium/career-profile/:careerPathId', authenticatePremiumUser, async (req, res) => { 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 React, { useEffect, useState } from 'react';
import authFetch from '../utils/authFetch.js'; import authFetch from '../utils/authFetch.js';
import ScenarioContainer from './ScenarioContainer.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() { export default function MultiScenarioView() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -11,44 +13,38 @@ export default function MultiScenarioView() {
// The users single overall financial profile // The users single overall financial profile
const [financialProfile, setFinancialProfile] = useState(null); const [financialProfile, setFinancialProfile] = useState(null);
// All scenario rows (from career_paths) // The list of scenario "headers" (career_paths)
const [scenarios, setScenarios] = useState([]); 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(() => { useEffect(() => {
async function loadData() { loadScenariosAndFinancial();
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();
}, []); }, []);
// --------------------------- async function loadScenariosAndFinancial() {
// Add a new scenario 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() { async function handleAddScenario() {
try { try {
const body = { const body = {
@ -64,37 +60,30 @@ export default function MultiScenarioView() {
body: JSON.stringify(body) body: JSON.stringify(body)
}); });
if (!res.ok) throw new Error(`Add scenario error: ${res.status}`); if (!res.ok) throw new Error(`Add scenario error: ${res.status}`);
const data = await res.json(); await loadScenariosAndFinancial();
// 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]);
} catch (err) { } catch (err) {
console.error('Failed adding scenario:', err); alert(err.message);
alert(err.message || 'Could not add scenario');
} }
} }
// --------------------------- // Clone a scenario => then reload
// Clone scenario async function handleCloneScenario(s) {
// ---------------------------
async function handleCloneScenario(sourceScenario) {
try { try {
const body = { const body = {
career_name: sourceScenario.career_name + ' (Copy)', scenario_title: s.scenario_title ? s.scenario_title + ' (Copy)' : null,
status: sourceScenario.status, career_name: s.career_name ? s.career_name + ' (Copy)' : 'Untitled (Copy)',
start_date: sourceScenario.start_date, status: s.status,
projected_end_date: sourceScenario.projected_end_date, start_date: s.start_date,
college_enrollment_status: sourceScenario.college_enrollment_status, projected_end_date: s.projected_end_date,
currently_working: sourceScenario.currently_working 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', { const res = await authFetch('/api/premium/career-profile', {
method: 'POST', method: 'POST',
@ -102,112 +91,51 @@ export default function MultiScenarioView() {
body: JSON.stringify(body) body: JSON.stringify(body)
}); });
if (!res.ok) throw new Error(`Clone scenario error: ${res.status}`); if (!res.ok) throw new Error(`Clone scenario error: ${res.status}`);
const data = await res.json(); await loadScenariosAndFinancial();
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]);
} catch (err) { } catch (err) {
console.error('Failed cloning scenario:', err); alert(err.message);
alert(err.message || 'Could not clone scenario');
} }
} }
// --------------------------- // Remove => reload
// Delete scenario async function handleRemoveScenario(id) {
// --------------------------- const confirmDel = window.confirm('Delete this scenario?');
async function handleRemoveScenario(scenarioId) {
// confirm
const confirmDel = window.confirm(
'Delete this scenario (and associated collegeProfile/milestones)?'
);
if (!confirmDel) return; if (!confirmDel) return;
try { try {
const res = await authFetch(`/api/premium/career-profile/${scenarioId}`, { const res = await authFetch(`/api/premium/career-profile/${id}`, {
method: 'DELETE' method: 'DELETE'
}); });
if (!res.ok) throw new Error(`Delete scenario error: ${res.status}`); if (!res.ok) throw new Error(`Delete scenario error: ${res.status}`);
// remove from local await loadScenariosAndFinancial();
setScenarios((prev) => prev.filter((s) => s.id !== scenarioId));
} catch (err) { } catch (err) {
console.error('Delete scenario error:', err); alert(err.message);
alert(err.message || 'Could not delete scenario');
} }
} }
// --------------------------- // If user wants to "edit" a scenario, we'll pass it down to the container's "onEdit"
// User clicks "Edit" in ScenarioContainer => we fetch that scenarios collegeProfile // or you can open a modal at this level. For now, we rely on the ScenarioContainer
// set editingScenario / editingCollegeProfile => modal // "onEdit" prop if needed.
// ---------------------------
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 (loading) return <p>Loading scenarios...</p>; 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 ( return (
<div className="multi-scenario-view" style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem' }}> <div style={{ display:'flex', flexWrap:'wrap', gap:'1rem' }}>
{scenarios.map((scen) => ( {scenarios.map(sc => (
<ScenarioContainer <ScenarioContainer
key={scen.id} key={sc.id}
scenario={scen} scenario={sc} // pass the scenario row
financialProfile={financialProfile} financialProfile={financialProfile}
onClone={() => handleCloneScenario(scen)} onClone={(s) => handleCloneScenario(s)}
onRemove={() => handleRemoveScenario(scen.id)} onRemove={(id) => handleRemoveScenario(id)}
onEdit={() => handleEditScenario(scen)} // new callback 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>
<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}
/>
)}
</div> </div>
); );
} }

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
const FinancialOnboarding = ({ nextStep, prevStep, data, setData }) => { const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = false }) => {
const { const {
currently_working = '', currently_working = '',
current_salary = 0, current_salary = 0,
@ -12,7 +12,15 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData }) => {
emergency_fund = 0, emergency_fund = 0,
emergency_contribution = 0, emergency_contribution = 0,
extra_cash_emergency_pct = "", 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; } = data;
const handleChange = (e) => { const handleChange = (e) => {
@ -134,6 +142,70 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData }) => {
onChange={handleChange} 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={prevStep}> Previous: Career</button>
<button onClick={nextStep}>Next: College </button> <button onClick={nextStep}>Next: College </button>
</div> </div>

View File

@ -26,22 +26,29 @@ const OnboardingContainer = () => {
// Now we do the final “all done” submission when the user finishes the last step // Now we do the final “all done” submission when the user finishes the last step
const handleFinalSubmit = async () => { const handleFinalSubmit = async () => {
try { 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', { const careerRes = await authFetch('/api/premium/career-profile', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(careerData), body: JSON.stringify(scenarioPayload),
}); });
if (!careerRes.ok) throw new Error('Failed to save career profile'); if (!careerRes.ok) throw new Error('Failed to save career profile');
const careerJson = await careerRes.json(); const careerJson = await careerRes.json();
const { career_path_id } = careerJson; const { career_path_id } = careerJson;
if (!career_path_id) throw new Error('No career_path_id returned by server'); 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 // 2) POST financial-profile
const financialRes = await authFetch('/api/premium/financial-profile', { const financialRes = await authFetch('/api/premium/financial-profile', {
@ -54,8 +61,9 @@ const OnboardingContainer = () => {
// 3) POST college-profile (include career_path_id) // 3) POST college-profile (include career_path_id)
const mergedCollege = { const mergedCollege = {
...collegeData, ...collegeData,
college_enrollment_status: careerData.college_enrollment_status, career_path_id,
career_path_id }; college_enrollment_status: careerData.college_enrollment_status
};
const collegeRes = await authFetch('/api/premium/college-profile', { const collegeRes = await authFetch('/api/premium/college-profile', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@ -71,6 +79,7 @@ const OnboardingContainer = () => {
} }
}; };
const onboardingSteps = [ const onboardingSteps = [
<PremiumWelcome nextStep={nextStep} />, <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) * HELPER: Monthly Federal Tax (no YTD)
* We just treat (monthlyGross * 12) - standardDed
* -> bracket -> / 12
***************************************************/ ***************************************************/
function calculateMonthlyFedTaxNoYTD(monthlyGross) { function calculateMonthlyFedTaxNoYTD(monthlyGross) {
const annualGross = monthlyGross * 12; const annualGross = monthlyGross * 12;
@ -59,7 +57,6 @@ function calculateMonthlyFedTaxNoYTD(monthlyGross) {
/*************************************************** /***************************************************
* HELPER: Monthly State Tax (no YTD) * HELPER: Monthly State Tax (no YTD)
* Uses GA (5%) by default if user doesn't override
***************************************************/ ***************************************************/
function calculateMonthlyStateTaxNoYTD(monthlyGross, stateCode = 'GA') { function calculateMonthlyStateTaxNoYTD(monthlyGross, stateCode = 'GA') {
const rate = APPROX_STATE_TAX_RATES[stateCode] ?? 0.05; const rate = APPROX_STATE_TAX_RATES[stateCode] ?? 0.05;
@ -87,6 +84,9 @@ function calculateLoanPayment(principal, annualRate, years) {
* MAIN SIMULATION FUNCTION * MAIN SIMULATION FUNCTION
***************************************************/ ***************************************************/
export function simulateFinancialProjection(userProfile) { export function simulateFinancialProjection(userProfile) {
// 1) Show userProfile at the start
console.log("simulateFinancialProjection() called with userProfile:", userProfile);
/*************************************************** /***************************************************
* 1) DESTRUCTURE USER PROFILE * 1) DESTRUCTURE USER PROFILE
***************************************************/ ***************************************************/
@ -105,7 +105,7 @@ export function simulateFinancialProjection(userProfile) {
loanDeferralUntilGraduation = false, loanDeferralUntilGraduation = false,
// College config // College config
inCollege = false, inCollege = false, // <<==== user-provided
programType, programType,
hoursCompleted = 0, hoursCompleted = 0,
creditHoursPerYear, creditHoursPerYear,
@ -131,7 +131,7 @@ export function simulateFinancialProjection(userProfile) {
// Program length override // Program length override
programLength, programLength,
// State code for taxes (default to GA if not provided) // State code
stateCode = 'GA', stateCode = 'GA',
// Financial milestone impacts // Financial milestone impacts
@ -170,7 +170,7 @@ export function simulateFinancialProjection(userProfile) {
const finalProgramLength = programLength || dynamicProgramLength; const finalProgramLength = programLength || dynamicProgramLength;
/*************************************************** /***************************************************
* 4) TUITION CALC: lumps, deferral, etc. * 4) TUITION CALC
***************************************************/ ***************************************************/
const netAnnualTuition = Math.max(0, (calculatedTuition || 0) - (annualFinancialAid || 0)); const netAnnualTuition = Math.max(0, (calculatedTuition || 0) - (annualFinancialAid || 0));
const totalTuitionCost = netAnnualTuition * finalProgramLength; const totalTuitionCost = netAnnualTuition * finalProgramLength;
@ -205,10 +205,19 @@ export function simulateFinancialProjection(userProfile) {
? 0 ? 0
: calculateLoanPayment(studentLoanAmount, interestRate, loanTerm); : 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 * 6) SETUP FOR THE SIMULATION LOOP
***************************************************/ ***************************************************/
const maxMonths = simulationYears*12; // 20 years const maxMonths = simulationYears * 12;
let loanBalance = Math.max(studentLoanAmount, 0); let loanBalance = Math.max(studentLoanAmount, 0);
let loanPaidOffMonth = null; let loanPaidOffMonth = null;
@ -217,28 +226,30 @@ export function simulateFinancialProjection(userProfile) {
let projectionData = []; let projectionData = [];
// Keep track of YTD gross & tax for reference // Track YTD gross & tax
let fedYTDgross = 0; let fedYTDgross = 0;
let fedYTDtax = 0; let fedYTDtax = 0;
let stateYTDgross = 0; let stateYTDgross = 0;
let stateYTDtax = 0; let stateYTDtax = 0;
// We'll keep track that we started in deferral if inCollege & deferral is true
let wasInDeferral = inCollege && loanDeferralUntilGraduation; let wasInDeferral = inCollege && loanDeferralUntilGraduation;
// If there's a gradDate, let's see if we pass it:
const graduationDateObj = gradDate ? moment(gradDate).startOf('month') : null; const graduationDateObj = gradDate ? moment(gradDate).startOf('month') : null;
/*************************************************** /***************************************************
* 7) THE MONTHLY LOOP * 7) THE MONTHLY LOOP
***************************************************/ ***************************************************/
for (let monthIndex = 0; monthIndex < maxMonths; monthIndex++) { for (let monthIndex = 0; monthIndex < maxMonths; monthIndex++) {
// date for this iteration
const currentSimDate = scenarioStartClamped.clone().add(monthIndex, 'months'); const currentSimDate = scenarioStartClamped.clone().add(monthIndex, 'months');
// check if loan is fully paid // Check if loan is fully paid
if (loanBalance <= 0 && !loanPaidOffMonth) { if (loanBalance <= 0 && !loanPaidOffMonth) {
loanPaidOffMonth = currentSimDate.format('YYYY-MM'); loanPaidOffMonth = currentSimDate.format('YYYY-MM');
} }
// Are we still in college? // Are we still in college for this month?
let stillInCollege = false; let stillInCollege = false;
if (inCollege) { if (inCollege) {
if (graduationDateObj) { if (graduationDateObj) {
@ -271,7 +282,7 @@ export function simulateFinancialProjection(userProfile) {
************************************************/ ************************************************/
let baseMonthlyIncome = 0; let baseMonthlyIncome = 0;
if (!stillInCollege) { if (!stillInCollege) {
// user is out of college => expected or current // user out of college => expected or current
baseMonthlyIncome = (expectedSalary || currentSalary) / 12; baseMonthlyIncome = (expectedSalary || currentSalary) / 12;
} else { } else {
// in college => might have partTimeIncome + current // 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 monthlyFederalTax = calculateMonthlyFedTaxNoYTD(baseMonthlyIncome);
const monthlyStateTax = calculateMonthlyStateTaxNoYTD(baseMonthlyIncome, stateCode); const monthlyStateTax = calculateMonthlyStateTaxNoYTD(baseMonthlyIncome, stateCode);
@ -324,7 +335,7 @@ export function simulateFinancialProjection(userProfile) {
// net after tax // net after tax
const netMonthlyIncome = baseMonthlyIncome - combinedTax; const netMonthlyIncome = baseMonthlyIncome - combinedTax;
// increment YTD gross & tax for reference // increment YTD for reference
fedYTDgross += baseMonthlyIncome; fedYTDgross += baseMonthlyIncome;
fedYTDtax += monthlyFederalTax; fedYTDtax += monthlyFederalTax;
stateYTDgross += baseMonthlyIncome; stateYTDgross += baseMonthlyIncome;
@ -333,28 +344,47 @@ export function simulateFinancialProjection(userProfile) {
/************************************************ /************************************************
* 7.5 LOAN + EXPENSES * 7.5 LOAN + EXPENSES
************************************************/ ************************************************/
// Check if we're now exiting college & deferral ended => recalc monthlyLoanPayment
const nowExitingCollege = wasInDeferral && !stillInCollege; const nowExitingCollege = wasInDeferral && !stillInCollege;
if (nowExitingCollege) { if (nowExitingCollege) {
// recalc monthlyLoanPayment with the current loanBalance
monthlyLoanPayment = calculateLoanPayment(loanBalance, interestRate, loanTerm); 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; 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) { if (stillInCollege && loanDeferralUntilGraduation) {
// accumulate interest // accumulate interest only
const interestForMonth = loanBalance * (interestRate / 100 / 12); const interestForMonth = loanBalance * (interestRate / 100 / 12);
loanBalance += interestForMonth; loanBalance += interestForMonth;
console.log(` (deferral) interest added=${interestForMonth.toFixed(2)}, loanBalAfter=${loanBalance.toFixed(2)}`);
} else { } else {
// pay principal // 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 totalThisMonth = monthlyLoanPayment + extraPayment;
loanBalance, const principalForMonth = Math.min(loanBalance, totalThisMonth - interestForMonth);
(monthlyLoanPayment + extraPayment) - interestForMonth
);
loanBalance = Math.max(loanBalance - principalForMonth, 0); 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); const canCover = Math.min(shortfall, currentEmergencySavings);
currentEmergencySavings -= canCover; currentEmergencySavings -= canCover;
shortfall -= canCover; shortfall -= canCover;
// leftover -= shortfall; // if you want negative leftover
} }
// Surplus => leftover // Surplus => leftover
@ -392,11 +421,15 @@ export function simulateFinancialProjection(userProfile) {
currentRetirementSavings += retPortion; currentRetirementSavings += retPortion;
} }
// net savings
const netSavings = netMonthlyIncome - actualExpensesPaid; const netSavings = netMonthlyIncome - actualExpensesPaid;
// (UPDATED) add inCollege, stillInCollege, loanDeferralUntilGraduation to the result
projectionData.push({ projectionData.push({
month: currentSimDate.format('YYYY-MM'), month: currentSimDate.format('YYYY-MM'),
inCollege, // new
stillInCollege, // new
loanDeferralUntilGraduation, // new
grossMonthlyIncome: +baseMonthlyIncome.toFixed(2), grossMonthlyIncome: +baseMonthlyIncome.toFixed(2),
monthlyFederalTax: +monthlyFederalTax.toFixed(2), monthlyFederalTax: +monthlyFederalTax.toFixed(2),
monthlyStateTax: +monthlyStateTax.toFixed(2), monthlyStateTax: +monthlyStateTax.toFixed(2),
@ -408,24 +441,35 @@ export function simulateFinancialProjection(userProfile) {
effectiveEmergencyContribution: +effectiveEmergencyContribution.toFixed(2), effectiveEmergencyContribution: +effectiveEmergencyContribution.toFixed(2),
netSavings: +netSavings.toFixed(2), netSavings: +netSavings.toFixed(2),
emergencySavings: +currentEmergencySavings.toFixed(2), // If you want to show the new running values,
retirementSavings: +currentRetirementSavings.toFixed(2), // 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), loanBalance: +loanBalance.toFixed(2),
// actual loan payment
loanPaymentThisMonth: +(monthlyLoanPayment + extraPayment).toFixed(2), loanPaymentThisMonth: +(monthlyLoanPayment + extraPayment).toFixed(2),
// YTD references
fedYTDgross: +fedYTDgross.toFixed(2), fedYTDgross: +fedYTDgross.toFixed(2),
fedYTDtax: +fedYTDtax.toFixed(2), fedYTDtax: +fedYTDtax.toFixed(2),
stateYTDgross: +stateYTDgross.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 { return {
projectionData, projectionData,
loanPaidOffMonth, loanPaidOffMonth,
@ -433,10 +477,9 @@ export function simulateFinancialProjection(userProfile) {
finalRetirementSavings: +currentRetirementSavings.toFixed(2), finalRetirementSavings: +currentRetirementSavings.toFixed(2),
finalLoanBalance: +loanBalance.toFixed(2), finalLoanBalance: +loanBalance.toFixed(2),
// Final YTD totals
fedYTDgross: +fedYTDgross.toFixed(2), fedYTDgross: +fedYTDgross.toFixed(2),
fedYTDtax: +fedYTDtax.toFixed(2), fedYTDtax: +fedYTDtax.toFixed(2),
stateYTDgross: +stateYTDgross.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