Made modal float with scroll.
This commit is contained in:
parent
733dba46a8
commit
d48f33572a
@ -118,6 +118,7 @@ app.get('/api/premium/career-profile/:careerPathId', authenticatePremiumUser, as
|
|||||||
// server3.js
|
// 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,
|
||||||
career_name,
|
career_name,
|
||||||
status,
|
status,
|
||||||
start_date,
|
start_date,
|
||||||
@ -125,7 +126,6 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
|
|||||||
college_enrollment_status,
|
college_enrollment_status,
|
||||||
currently_working,
|
currently_working,
|
||||||
|
|
||||||
// NEW planned columns
|
|
||||||
planned_monthly_expenses,
|
planned_monthly_expenses,
|
||||||
planned_monthly_debt_payments,
|
planned_monthly_debt_payments,
|
||||||
planned_monthly_retirement_contribution,
|
planned_monthly_retirement_contribution,
|
||||||
@ -149,6 +149,7 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
|
|||||||
INSERT INTO career_paths (
|
INSERT INTO career_paths (
|
||||||
id,
|
id,
|
||||||
user_id,
|
user_id,
|
||||||
|
scenario_title,
|
||||||
career_name,
|
career_name,
|
||||||
status,
|
status,
|
||||||
start_date,
|
start_date,
|
||||||
@ -190,6 +191,7 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
|
|||||||
`, [
|
`, [
|
||||||
newCareerPathId,
|
newCareerPathId,
|
||||||
req.userId,
|
req.userId,
|
||||||
|
scenario_title || null,
|
||||||
career_name,
|
career_name,
|
||||||
status || 'planned',
|
status || 'planned',
|
||||||
start_date || now,
|
start_date || now,
|
||||||
@ -229,6 +231,103 @@ 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) => {
|
||||||
|
const { careerPathId } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1) Confirm that this career_path belongs to the user
|
||||||
|
const existing = await db.get(
|
||||||
|
`
|
||||||
|
SELECT id
|
||||||
|
FROM career_paths
|
||||||
|
WHERE id = ?
|
||||||
|
AND user_id = ?
|
||||||
|
`,
|
||||||
|
[careerPathId, req.userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
return res.status(404).json({ error: 'Career path not found or not yours.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Optionally delete the college_profile for this scenario
|
||||||
|
// (If you always keep 1-to-1 relationship: careerPathId => college_profile)
|
||||||
|
await db.run(
|
||||||
|
`
|
||||||
|
DELETE FROM college_profiles
|
||||||
|
WHERE user_id = ?
|
||||||
|
AND career_path_id = ?
|
||||||
|
`,
|
||||||
|
[req.userId, careerPathId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3) Optionally delete scenario’s milestones
|
||||||
|
// (and any associated tasks, impacts, etc.)
|
||||||
|
// If you store tasks in tasks table, and impacts in milestone_impacts table:
|
||||||
|
|
||||||
|
// First find scenario milestones
|
||||||
|
const scenarioMilestones = await db.all(
|
||||||
|
`
|
||||||
|
SELECT id
|
||||||
|
FROM milestones
|
||||||
|
WHERE user_id = ?
|
||||||
|
AND career_path_id = ?
|
||||||
|
`,
|
||||||
|
[req.userId, careerPathId]
|
||||||
|
);
|
||||||
|
const milestoneIds = scenarioMilestones.map((m) => m.id);
|
||||||
|
|
||||||
|
if (milestoneIds.length > 0) {
|
||||||
|
// Delete tasks for these milestones
|
||||||
|
const placeholders = milestoneIds.map(() => '?').join(',');
|
||||||
|
await db.run(
|
||||||
|
`
|
||||||
|
DELETE FROM tasks
|
||||||
|
WHERE milestone_id IN (${placeholders})
|
||||||
|
`,
|
||||||
|
milestoneIds
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete impacts for these milestones
|
||||||
|
await db.run(
|
||||||
|
`
|
||||||
|
DELETE FROM milestone_impacts
|
||||||
|
WHERE milestone_id IN (${placeholders})
|
||||||
|
`,
|
||||||
|
milestoneIds
|
||||||
|
);
|
||||||
|
|
||||||
|
// Finally delete the milestones themselves
|
||||||
|
await db.run(
|
||||||
|
`
|
||||||
|
DELETE FROM milestones
|
||||||
|
WHERE id IN (${placeholders})
|
||||||
|
`,
|
||||||
|
milestoneIds
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Finally delete the career_path row
|
||||||
|
await db.run(
|
||||||
|
`
|
||||||
|
DELETE FROM career_paths
|
||||||
|
WHERE user_id = ?
|
||||||
|
AND id = ?
|
||||||
|
`,
|
||||||
|
[req.userId, careerPathId]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ message: 'Career path and related data successfully deleted.' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting career path:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to delete career path.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
Milestone ENDPOINTS
|
Milestone ENDPOINTS
|
||||||
------------------------------------------------------------------ */
|
------------------------------------------------------------------ */
|
||||||
|
18
src/components/MultiScenarioView.css
Normal file
18
src/components/MultiScenarioView.css
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
.modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top:0; left:0;
|
||||||
|
width:100vw; height:100vh;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
.modal-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%; left: 50%;
|
||||||
|
transform: translate(-50%,-50%);
|
||||||
|
background: #fff;
|
||||||
|
width: 600px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
@ -2,33 +2,43 @@
|
|||||||
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
|
||||||
|
|
||||||
export default function MultiScenarioView() {
|
export default function MultiScenarioView() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
// The user’s single overall financial profile
|
||||||
const [financialProfile, setFinancialProfile] = useState(null);
|
const [financialProfile, setFinancialProfile] = useState(null);
|
||||||
const [scenarios, setScenarios] = useState([]); // each scenario is a row in career_paths
|
|
||||||
|
// All scenario rows (from 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(() => {
|
useEffect(() => {
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
// A) fetch financial profile
|
// 1) Fetch the user’s overall financial profile
|
||||||
let finRes = await authFetch('/api/premium/financial-profile');
|
const finRes = await authFetch('/api/premium/financial-profile');
|
||||||
if (!finRes.ok) throw new Error(`FIN profile error: ${finRes.status}`);
|
if (!finRes.ok) throw new Error(`FinancialProfile error: ${finRes.status}`);
|
||||||
let finData = await finRes.json();
|
const finData = await finRes.json();
|
||||||
|
|
||||||
// B) fetch all career_paths (scenarios)
|
// 2) Fetch all scenarios (career_paths)
|
||||||
let scenRes = await authFetch('/api/premium/career-profile/all');
|
const scenRes = await authFetch('/api/premium/career-profile/all');
|
||||||
if (!scenRes.ok) throw new Error(`Scenarios error: ${scenRes.status}`);
|
if (!scenRes.ok) throw new Error(`Scenarios error: ${scenRes.status}`);
|
||||||
let scenData = await scenRes.json();
|
const scenData = await scenRes.json();
|
||||||
|
|
||||||
setFinancialProfile(finData);
|
setFinancialProfile(finData);
|
||||||
setScenarios(scenData.careerPaths || []);
|
setScenarios(scenData.careerPaths || []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading premium data:', err);
|
console.error('Error loading data in MultiScenarioView:', err);
|
||||||
setError(err.message || 'Failed to load data');
|
setError(err.message || 'Failed to load scenarios/financial');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -36,7 +46,9 @@ export default function MultiScenarioView() {
|
|||||||
loadData();
|
loadData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// “Add Scenario” => create a brand new row in career_paths
|
// ---------------------------
|
||||||
|
// Add a new scenario
|
||||||
|
// ---------------------------
|
||||||
async function handleAddScenario() {
|
async function handleAddScenario() {
|
||||||
try {
|
try {
|
||||||
const body = {
|
const body = {
|
||||||
@ -54,9 +66,9 @@ export default function MultiScenarioView() {
|
|||||||
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();
|
const data = await res.json();
|
||||||
|
|
||||||
|
// Insert the new row into local state
|
||||||
const newRow = {
|
const newRow = {
|
||||||
id: data.career_path_id,
|
id: data.career_path_id,
|
||||||
user_id: null,
|
|
||||||
career_name: body.career_name,
|
career_name: body.career_name,
|
||||||
status: body.status,
|
status: body.status,
|
||||||
start_date: body.start_date,
|
start_date: body.start_date,
|
||||||
@ -67,11 +79,13 @@ export default function MultiScenarioView() {
|
|||||||
setScenarios((prev) => [...prev, newRow]);
|
setScenarios((prev) => [...prev, newRow]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed adding scenario:', err);
|
console.error('Failed adding scenario:', err);
|
||||||
alert('Could not add scenario');
|
alert(err.message || 'Could not add scenario');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// “Clone” => create a new row in career_paths with copied fields
|
// ---------------------------
|
||||||
|
// Clone scenario
|
||||||
|
// ---------------------------
|
||||||
async function handleCloneScenario(sourceScenario) {
|
async function handleCloneScenario(sourceScenario) {
|
||||||
try {
|
try {
|
||||||
const body = {
|
const body = {
|
||||||
@ -90,8 +104,9 @@ export default function MultiScenarioView() {
|
|||||||
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();
|
const data = await res.json();
|
||||||
|
|
||||||
|
const newScenarioId = data.career_path_id;
|
||||||
const newRow = {
|
const newRow = {
|
||||||
id: data.career_path_id,
|
id: newScenarioId,
|
||||||
career_name: body.career_name,
|
career_name: body.career_name,
|
||||||
status: body.status,
|
status: body.status,
|
||||||
start_date: body.start_date,
|
start_date: body.start_date,
|
||||||
@ -102,23 +117,70 @@ export default function MultiScenarioView() {
|
|||||||
setScenarios((prev) => [...prev, newRow]);
|
setScenarios((prev) => [...prev, newRow]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed cloning scenario:', err);
|
console.error('Failed cloning scenario:', err);
|
||||||
alert('Could not clone scenario');
|
alert(err.message || 'Could not clone scenario');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// “Remove” => possibly remove from DB or just local
|
// ---------------------------
|
||||||
|
// Delete scenario
|
||||||
|
// ---------------------------
|
||||||
async function handleRemoveScenario(scenarioId) {
|
async function handleRemoveScenario(scenarioId) {
|
||||||
|
// confirm
|
||||||
|
const confirmDel = window.confirm(
|
||||||
|
'Delete this scenario (and associated collegeProfile/milestones)?'
|
||||||
|
);
|
||||||
|
if (!confirmDel) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const res = await authFetch(`/api/premium/career-profile/${scenarioId}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`Delete scenario error: ${res.status}`);
|
||||||
|
// remove from local
|
||||||
setScenarios((prev) => prev.filter((s) => s.id !== scenarioId));
|
setScenarios((prev) => prev.filter((s) => s.id !== scenarioId));
|
||||||
// Optionally do an API call: DELETE /api/premium/career-profile/:id
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed removing scenario:', err);
|
console.error('Delete scenario error:', err);
|
||||||
alert('Could not remove scenario');
|
alert(err.message || 'Could not delete scenario');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// 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 (loading) return <p>Loading scenarios...</p>;
|
if (loading) return <p>Loading scenarios...</p>;
|
||||||
if (error) return <p className="text-red-600">Error: {error}</p>;
|
if (error) return <p style={{ color: 'red' }}>Error: {error}</p>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="multi-scenario-view" style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem' }}>
|
<div className="multi-scenario-view" style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem' }}>
|
||||||
@ -126,15 +188,26 @@ export default function MultiScenarioView() {
|
|||||||
<ScenarioContainer
|
<ScenarioContainer
|
||||||
key={scen.id}
|
key={scen.id}
|
||||||
scenario={scen}
|
scenario={scen}
|
||||||
financialProfile={financialProfile} // shared for all
|
financialProfile={financialProfile}
|
||||||
onClone={() => handleCloneScenario(scen)}
|
onClone={() => handleCloneScenario(scen)}
|
||||||
onRemove={() => handleRemoveScenario(scen.id)}
|
onRemove={() => handleRemoveScenario(scen.id)}
|
||||||
|
onEdit={() => handleEditScenario(scen)} // new callback
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div style={{ alignSelf: 'flex-start' }}>
|
<div style={{ alignSelf: 'flex-start' }}>
|
||||||
<button onClick={handleAddScenario}>+ Add Scenario</button>
|
<button onClick={handleAddScenario}>+ Add Scenario</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* The floating modal at the bottom => only if editingScenario != null */}
|
||||||
|
{editingScenario && (
|
||||||
|
<ScenarioEditModal
|
||||||
|
show={true}
|
||||||
|
scenario={editingScenario}
|
||||||
|
collegeProfile={editingCollegeProfile}
|
||||||
|
onClose={handleModalClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,160 +2,151 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Line } from 'react-chartjs-2';
|
import { Line } from 'react-chartjs-2';
|
||||||
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
|
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
|
||||||
import ScenarioEditModal from './ScenarioEditModal.js';
|
|
||||||
import MilestoneTimeline from './MilestoneTimeline.js';
|
import MilestoneTimeline from './MilestoneTimeline.js';
|
||||||
import AISuggestedMilestones from './AISuggestedMilestones.js';
|
import AISuggestedMilestones from './AISuggestedMilestones.js';
|
||||||
import authFetch from '../utils/authFetch.js';
|
import authFetch from '../utils/authFetch.js';
|
||||||
|
|
||||||
export default function ScenarioContainer({
|
export default function ScenarioContainer({
|
||||||
scenario, // from career_paths row
|
scenario,
|
||||||
financialProfile, // single row, shared across user
|
financialProfile,
|
||||||
|
onRemove,
|
||||||
onClone,
|
onClone,
|
||||||
onRemove
|
onEdit // <-- new callback to open the floating modal
|
||||||
}) {
|
}) {
|
||||||
const [localScenario, setLocalScenario] = useState(scenario);
|
|
||||||
const [collegeProfile, setCollegeProfile] = useState(null);
|
const [collegeProfile, setCollegeProfile] = useState(null);
|
||||||
|
|
||||||
const [projectionData, setProjectionData] = useState([]);
|
const [projectionData, setProjectionData] = useState([]);
|
||||||
const [loanPaidOffMonth, setLoanPaidOffMonth] = useState(null);
|
const [loanPaidOffMonth, setLoanPaidOffMonth] = useState(null);
|
||||||
|
|
||||||
const [editOpen, setEditOpen] = useState(false);
|
// An input for sim length
|
||||||
|
const [simulationYearsInput, setSimulationYearsInput] = useState('20');
|
||||||
|
|
||||||
// Re-sync if parent updates scenario
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalScenario(scenario);
|
if (!scenario?.id) {
|
||||||
}, [scenario]);
|
setCollegeProfile(null);
|
||||||
|
return;
|
||||||
// 1) Fetch the college profile for this scenario
|
}
|
||||||
useEffect(() => {
|
|
||||||
if (!localScenario?.id) return;
|
|
||||||
async function loadCollegeProfile() {
|
async function loadCollegeProfile() {
|
||||||
try {
|
try {
|
||||||
const res = await authFetch(`/api/premium/college-profile?careerPathId=${localScenario.id}`);
|
const url = `/api/premium/college-profile?careerPathId=${scenario.id}`;
|
||||||
|
const res = await authFetch(url);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setCollegeProfile(data);
|
setCollegeProfile(data);
|
||||||
} else {
|
} else {
|
||||||
console.warn('No college profile found or error:', res.status);
|
|
||||||
setCollegeProfile({});
|
setCollegeProfile({});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed fetching college profile:', err);
|
console.error('Error loading collegeProfile:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loadCollegeProfile();
|
loadCollegeProfile();
|
||||||
}, [localScenario]);
|
}, [scenario]);
|
||||||
|
|
||||||
// 2) Whenever we have financialProfile + collegeProfile => run the simulation
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!financialProfile || !collegeProfile) return;
|
if (!financialProfile || !collegeProfile || !scenario?.id) return;
|
||||||
|
|
||||||
// Merge them into the userProfile object for the simulator:
|
// Merge the user’s base financial profile + scenario overwrites + college profile
|
||||||
const mergedProfile = {
|
const mergedProfile = {
|
||||||
currentSalary: financialProfile.current_salary || 0,
|
currentSalary: financialProfile.current_salary || 0,
|
||||||
monthlyExpenses: financialProfile.monthly_expenses || 0,
|
monthlyExpenses:
|
||||||
monthlyDebtPayments: financialProfile.monthly_debt_payments || 0,
|
scenario.planned_monthly_expenses ?? financialProfile.monthly_expenses ?? 0,
|
||||||
retirementSavings: financialProfile.retirement_savings || 0,
|
monthlyDebtPayments:
|
||||||
emergencySavings: financialProfile.emergency_fund || 0,
|
scenario.planned_monthly_debt_payments ?? financialProfile.monthly_debt_payments ?? 0,
|
||||||
monthlyRetirementContribution: financialProfile.retirement_contribution || 0,
|
// ...
|
||||||
monthlyEmergencyContribution: financialProfile.emergency_contribution || 0,
|
|
||||||
surplusEmergencyAllocation: financialProfile.extra_cash_emergency_pct || 50,
|
|
||||||
surplusRetirementAllocation: financialProfile.extra_cash_retirement_pct || 50,
|
|
||||||
|
|
||||||
// College fields (scenario-based)
|
|
||||||
studentLoanAmount: collegeProfile.existing_college_debt || 0,
|
studentLoanAmount: collegeProfile.existing_college_debt || 0,
|
||||||
interestRate: collegeProfile.interest_rate || 5,
|
interestRate: collegeProfile.interest_rate || 5,
|
||||||
loanTerm: collegeProfile.loan_term || 10,
|
loanTerm: collegeProfile.loan_term || 10,
|
||||||
loanDeferralUntilGraduation: !!collegeProfile.loan_deferral_until_graduation,
|
// ...
|
||||||
academicCalendar: collegeProfile.academic_calendar || 'semester',
|
simulationYears: parseInt(simulationYearsInput, 10) || 20,
|
||||||
annualFinancialAid: collegeProfile.annual_financial_aid || 0,
|
|
||||||
calculatedTuition: collegeProfile.tuition || 0,
|
|
||||||
extraPayment: collegeProfile.extra_payment || 0,
|
|
||||||
gradDate: collegeProfile.expected_graduation || null,
|
|
||||||
programType: collegeProfile.program_type || '',
|
|
||||||
creditHoursPerYear: collegeProfile.credit_hours_per_year || 0,
|
|
||||||
hoursCompleted: collegeProfile.hours_completed || 0,
|
|
||||||
programLength: collegeProfile.program_length || 0,
|
|
||||||
inCollege:
|
|
||||||
collegeProfile.college_enrollment_status === 'currently_enrolled' ||
|
|
||||||
collegeProfile.college_enrollment_status === 'prospective_student',
|
|
||||||
expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary || 0,
|
|
||||||
|
|
||||||
// milestoneImpacts is fetched & merged in MilestoneTimeline, not here
|
|
||||||
milestoneImpacts: []
|
milestoneImpacts: []
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3) run the simulation
|
|
||||||
const { projectionData, loanPaidOffMonth } = simulateFinancialProjection(mergedProfile);
|
const { projectionData, loanPaidOffMonth } = simulateFinancialProjection(mergedProfile);
|
||||||
setProjectionData(projectionData);
|
setProjectionData(projectionData);
|
||||||
setLoanPaidOffMonth(loanPaidOffMonth);
|
setLoanPaidOffMonth(loanPaidOffMonth);
|
||||||
}, [financialProfile, collegeProfile]);
|
}, [financialProfile, collegeProfile, scenario, simulationYearsInput]);
|
||||||
|
|
||||||
|
function handleDeleteScenario() {
|
||||||
|
// let the parent actually do the DB deletion
|
||||||
|
onRemove(scenario.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSimulationYearsBlur() {
|
||||||
|
if (simulationYearsInput.trim() === '') {
|
||||||
|
setSimulationYearsInput('20');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// chart data
|
||||||
|
const labels = projectionData.map(p => p.month);
|
||||||
|
const netSavData = projectionData.map(p => p.cumulativeNetSavings || 0);
|
||||||
|
const retData = projectionData.map(p => p.retirementSavings || 0);
|
||||||
|
const loanData = projectionData.map(p => p.loanBalance || 0);
|
||||||
|
|
||||||
|
const chartData = {
|
||||||
|
labels,
|
||||||
|
datasets: [
|
||||||
|
{ label: 'Net Savings', data: netSavData, borderColor: 'blue', fill: false },
|
||||||
|
{ label: 'Retirement', data: retData, borderColor: 'green', fill: false },
|
||||||
|
{ label: 'Loan', data: loanData, borderColor: 'red', fill: false }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: '420px', border: '1px solid #ccc', padding: '1rem' }}>
|
<div style={{ width: '420px', border: '1px solid #ccc', padding: '1rem' }}>
|
||||||
<h3>{localScenario.career_name || 'Untitled Scenario'}</h3>
|
<h4>{scenario.scenario_title || scenario.career_name || 'Untitled Scenario'}</h4>
|
||||||
<p>Status: {localScenario.status}</p>
|
|
||||||
|
|
||||||
<Line
|
<div style={{ margin: '0.5rem 0' }}>
|
||||||
data={{
|
<label>Simulation Length (yrs): </label>
|
||||||
labels: projectionData.map((p) => p.month),
|
<input
|
||||||
datasets: [
|
type="text"
|
||||||
{
|
style={{ width: '3rem' }}
|
||||||
label: 'Net Savings',
|
value={simulationYearsInput}
|
||||||
data: projectionData.map((p) => p.cumulativeNetSavings || 0),
|
onChange={e => setSimulationYearsInput(e.target.value)}
|
||||||
borderColor: 'blue',
|
onBlur={handleSimulationYearsBlur}
|
||||||
fill: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}}
|
|
||||||
options={{ responsive: true }}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div style={{ marginTop: '0.5rem' }}>
|
|
||||||
<strong>Loan Paid Off:</strong> {loanPaidOffMonth || 'N/A'} <br />
|
|
||||||
<strong>Final Retirement:</strong>{' '}
|
|
||||||
{projectionData[projectionData.length - 1]?.retirementSavings?.toFixed(0) || 0}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* The timeline that fetches scenario/universal milestones for display */}
|
<Line data={chartData} options={{ responsive: true }} />
|
||||||
|
|
||||||
|
<div style={{ marginTop: '0.5rem' }}>
|
||||||
|
<strong>Loan Paid Off:</strong> {loanPaidOffMonth || 'N/A'}
|
||||||
|
{projectionData.length > 0 && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<strong>Final Retirement:</strong> {projectionData[projectionData.length - 1].retirementSavings.toFixed(0)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{scenario?.id && (
|
||||||
<MilestoneTimeline
|
<MilestoneTimeline
|
||||||
careerPathId={localScenario.id}
|
careerPathId={scenario.id}
|
||||||
authFetch={authFetch}
|
authFetch={authFetch}
|
||||||
activeView="Financial"
|
activeView="Financial"
|
||||||
setActiveView={() => {}}
|
setActiveView={() => {}}
|
||||||
onMilestoneUpdated={() => {
|
onMilestoneUpdated={() => {}}
|
||||||
// might do scenario changes if you want
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{scenario?.id && (
|
||||||
<AISuggestedMilestones
|
<AISuggestedMilestones
|
||||||
career={localScenario.career_name}
|
career={scenario.career_name || scenario.scenario_title || ''}
|
||||||
careerPathId={localScenario.id}
|
careerPathId={scenario.id}
|
||||||
authFetch={authFetch}
|
authFetch={authFetch}
|
||||||
activeView="Financial"
|
activeView="Financial"
|
||||||
projectionData={projectionData}
|
projectionData={projectionData}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div style={{ marginTop: '0.5rem' }}>
|
<div style={{ marginTop: '0.5rem' }}>
|
||||||
<button onClick={() => setEditOpen(true)}>Edit</button>
|
<button onClick={() => onEdit(scenario)}>Edit</button>
|
||||||
<button onClick={onClone} style={{ marginLeft: '0.5rem' }}>
|
<button onClick={() => onClone(scenario)} style={{ marginLeft: '0.5rem' }}>
|
||||||
Clone
|
Clone
|
||||||
</button>
|
</button>
|
||||||
<button onClick={onRemove} style={{ marginLeft: '0.5rem', color: 'red' }}>
|
<button onClick={handleDeleteScenario} style={{ marginLeft: '0.5rem', color: 'red' }}>
|
||||||
Remove
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* If you do scenario-level editing for planned fields, show scenario edit modal */}
|
|
||||||
{editOpen && (
|
|
||||||
<ScenarioEditModal
|
|
||||||
show={editOpen}
|
|
||||||
onClose={() => setEditOpen(false)}
|
|
||||||
scenario={localScenario}
|
|
||||||
setScenario={setLocalScenario}
|
|
||||||
apiURL="/api"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user