Fixed multiscenarioView to simulate properly.
This commit is contained in:
parent
d48f33572a
commit
1330ccc366
@ -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) => {
|
||||
|
45
src/components/MilestoneCopyWizard.js
Normal file
45
src/components/MilestoneCopyWizard.js
Normal 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>
|
||||
);
|
||||
}
|
@ -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 user’s 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 we’re 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 user’s 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 user’s 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 scenario’s 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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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 isn’t 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
165
src/components/ScenarioEditWizard.js
Normal file
165
src/components/ScenarioEditWizard.js
Normal 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>
|
||||
);
|
||||
}
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
0
user_proile.db
Normal file
0
user_proile.db
Normal file
Loading…
Reference in New Issue
Block a user