diff --git a/backend/server3.js b/backend/server3.js
index 64e09f5..29caa9f 100644
--- a/backend/server3.js
+++ b/backend/server3.js
@@ -92,7 +92,30 @@ app.get('/api/premium/career-profile/all', authenticatePremiumUser, async (req,
}
});
+// GET a single career profile (scenario) by ID
+app.get('/api/premium/career-profile/:careerPathId', authenticatePremiumUser, async (req, res) => {
+ const { careerPathId } = req.params;
+ try {
+ const row = await db.get(`
+ SELECT *
+ FROM career_paths
+ WHERE id = ?
+ AND user_id = ?
+ `, [careerPathId, req.userId]);
+
+ if (!row) {
+ return res.status(404).json({ error: 'Career path (scenario) not found or not yours.' });
+ }
+
+ res.json(row);
+ } catch (error) {
+ console.error('Error fetching single career profile:', error);
+ res.status(500).json({ error: 'Failed to fetch career profile by ID.' });
+ }
+});
+
// POST a new career profile
+// server3.js
app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res) => {
const {
career_name,
@@ -100,10 +123,18 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
start_date,
projected_end_date,
college_enrollment_status,
- currently_working
+ currently_working,
+
+ // NEW planned columns
+ 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
} = req.body;
- // If you need to ensure the user gave us a career_name:
if (!career_name) {
return res.status(400).json({ error: 'career_name is required.' });
}
@@ -112,6 +143,8 @@ 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.
await db.run(`
INSERT INTO career_paths (
id,
@@ -122,10 +155,21 @@ 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,
+ planned_monthly_emergency_contribution,
+ 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,
@@ -133,22 +177,41 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
projected_end_date = excluded.projected_end_date,
college_enrollment_status = excluded.college_enrollment_status,
currently_working = excluded.currently_working,
+
+ planned_monthly_expenses = excluded.planned_monthly_expenses,
+ planned_monthly_debt_payments = excluded.planned_monthly_debt_payments,
+ planned_monthly_retirement_contribution = excluded.planned_monthly_retirement_contribution,
+ planned_monthly_emergency_contribution = excluded.planned_monthly_emergency_contribution,
+ planned_surplus_emergency_pct = excluded.planned_surplus_emergency_pct,
+ planned_surplus_retirement_pct = excluded.planned_surplus_retirement_pct,
+ planned_additional_income = excluded.planned_additional_income,
+
updated_at = ?
`, [
- newCareerPathId, // id
- req.userId, // user_id
- career_name, // career_name
- status || 'planned', // status (if null, default to 'planned')
+ newCareerPathId,
+ req.userId,
+ career_name,
+ status || 'planned',
start_date || now,
projected_end_date || null,
college_enrollment_status || null,
currently_working || null,
- now, // created_at
- now, // updated_at on initial insert
- now // updated_at on conflict
+
+ // new planned columns
+ planned_monthly_expenses ?? null,
+ planned_monthly_debt_payments ?? null,
+ planned_monthly_retirement_contribution ?? null,
+ planned_monthly_emergency_contribution ?? null,
+ planned_surplus_emergency_pct ?? null,
+ planned_surplus_retirement_pct ?? null,
+ planned_additional_income ?? null,
+
+ now, // created_at
+ now, // updated_at
+ now // updated_at on conflict
]);
- // Optionally fetch the row's ID after upsert
+ // Optionally fetch the row's ID (or entire row) after upsert:
const result = await db.get(`
SELECT id
FROM career_paths
@@ -166,7 +229,11 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
}
});
+/* ------------------------------------------------------------------
+ Milestone ENDPOINTS
+ ------------------------------------------------------------------ */
+// CREATE one or more milestones
app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => {
try {
const body = req.body;
@@ -183,12 +250,12 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
career_path_id,
progress,
status,
- new_salary
+ new_salary,
+ is_universal
} = m;
// Validate some required fields
if (!milestone_type || !title || !date || !career_path_id) {
- // Optionally handle partial errors, but let's do a quick check
return res.status(400).json({
error: 'One or more milestones missing required fields',
details: m
@@ -210,9 +277,10 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
progress,
status,
new_salary,
+ is_universal,
created_at,
updated_at
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
id,
req.userId,
@@ -224,6 +292,7 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
progress || 0,
status || 'planned',
new_salary || null,
+ is_universal ? 1 : 0, // store 1 or 0
now,
now
]);
@@ -239,6 +308,7 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
progress: progress || 0,
status: status || 'planned',
new_salary: new_salary || null,
+ is_universal: is_universal ? 1 : 0,
tasks: []
});
}
@@ -246,7 +316,7 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
return res.status(201).json(createdMilestones);
}
- // CASE 2: Handle single milestone (the old logic)
+ // CASE 2: Single milestone creation
const {
milestone_type,
title,
@@ -255,7 +325,8 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
career_path_id,
progress,
status,
- new_salary
+ new_salary,
+ is_universal
} = body;
if (!milestone_type || !title || !date || !career_path_id) {
@@ -280,9 +351,10 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
progress,
status,
new_salary,
+ is_universal,
created_at,
updated_at
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
id,
req.userId,
@@ -294,6 +366,7 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
progress || 0,
status || 'planned',
new_salary || null,
+ is_universal ? 1 : 0,
now,
now
]);
@@ -310,6 +383,7 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
progress: progress || 0,
status: status || 'planned',
new_salary: new_salary || null,
+ is_universal: is_universal ? 1 : 0,
tasks: []
};
@@ -320,6 +394,7 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
}
});
+// UPDATE an existing milestone
app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (req, res) => {
try {
const { milestoneId } = req.params;
@@ -331,7 +406,8 @@ app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (
career_path_id,
progress,
status,
- new_salary
+ new_salary,
+ is_universal
} = req.body;
// Check if milestone exists and belongs to user
@@ -346,8 +422,21 @@ app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (
return res.status(404).json({ error: 'Milestone not found or not yours.' });
}
- // Update
const now = new Date().toISOString();
+
+ // Merge fields with existing if not provided
+ const finalMilestoneType = milestone_type || existing.milestone_type;
+ const finalTitle = title || existing.title;
+ const finalDesc = description || existing.description;
+ const finalDate = date || existing.date;
+ const finalCareerPath = career_path_id || existing.career_path_id;
+ const finalProgress = progress != null ? progress : existing.progress;
+ const finalStatus = status || existing.status;
+ const finalSalary = new_salary != null ? new_salary : existing.new_salary;
+ const finalIsUniversal =
+ is_universal != null ? (is_universal ? 1 : 0) : existing.is_universal;
+
+ // Update row
await db.run(`
UPDATE milestones
SET
@@ -359,19 +448,23 @@ app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (
progress = ?,
status = ?,
new_salary = ?,
+ is_universal = ?,
updated_at = ?
WHERE id = ?
+ AND user_id = ?
`, [
- milestone_type || existing.milestone_type,
- title || existing.title,
- description || existing.description,
- date || existing.date,
- career_path_id || existing.career_path_id,
- progress != null ? progress : existing.progress,
- status || existing.status,
- new_salary != null ? new_salary : existing.new_salary,
+ finalMilestoneType,
+ finalTitle,
+ finalDesc,
+ finalDate,
+ finalCareerPath,
+ finalProgress,
+ finalStatus,
+ finalSalary,
+ finalIsUniversal,
now,
- milestoneId
+ milestoneId,
+ req.userId
]);
// Return the updated record with tasks
@@ -400,11 +493,44 @@ app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (
}
});
+// GET all milestones for a given careerPathId
app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => {
const { careerPathId } = req.query;
try {
- // 1. Fetch the milestones for this user + path
+ // if user wants universal=1 only, e.g. careerPathId=universal
+ if (careerPathId === 'universal') {
+ // For example, fetch all is_universal=1 for the user:
+ const universalRows = await db.all(`
+ SELECT *
+ FROM milestones
+ WHERE user_id = ?
+ AND is_universal = 1
+ `, [req.userId]);
+
+ // attach tasks if needed
+ const milestoneIds = universalRows.map(m => m.id);
+ let tasksByMilestone = {};
+ if (milestoneIds.length > 0) {
+ const tasks = await db.all(`
+ SELECT *
+ FROM tasks
+ WHERE milestone_id IN (${milestoneIds.map(() => '?').join(',')})
+ `, milestoneIds);
+ tasksByMilestone = tasks.reduce((acc, t) => {
+ if (!acc[t.milestone_id]) acc[t.milestone_id] = [];
+ acc[t.milestone_id].push(t);
+ return acc;
+ }, {});
+ }
+ const uniMils = universalRows.map(m => ({
+ ...m,
+ tasks: tasksByMilestone[m.id] || []
+ }));
+ return res.json({ milestones: uniMils });
+ }
+
+ // else fetch by careerPathId
const milestones = await db.all(`
SELECT *
FROM milestones
@@ -412,7 +538,6 @@ app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) =>
AND career_path_id = ?
`, [req.userId, careerPathId]);
- // 2. For each milestone, fetch tasks
const milestoneIds = milestones.map(m => m.id);
let tasksByMilestone = {};
if (milestoneIds.length > 0) {
@@ -429,7 +554,6 @@ app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) =>
}, {});
}
- // 3. Attach tasks to each milestone object
const milestonesWithTasks = milestones.map(m => ({
...m,
tasks: tasksByMilestone[m.id] || []
@@ -442,6 +566,259 @@ app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) =>
}
});
+// COPY an existing milestone to other scenarios
+app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res) => {
+ try {
+ const { milestoneId, scenarioIds } = req.body;
+ if (!milestoneId || !Array.isArray(scenarioIds) || scenarioIds.length === 0) {
+ return res.status(400).json({ error: 'Missing milestoneId or scenarioIds.' });
+ }
+
+ // 1) Fetch the original
+ const original = await db.get(`
+ SELECT *
+ FROM milestones
+ WHERE id = ?
+ AND user_id = ?
+ `, [milestoneId, req.userId]);
+
+ if (!original) {
+ return res.status(404).json({ error: 'Milestone not found or not owned by user.' });
+ }
+
+ // 2) Force is_universal=1 on the original
+ if (original.is_universal !== 1) {
+ await db.run(`
+ UPDATE milestones
+ SET is_universal = 1
+ WHERE id = ?
+ AND user_id = ?
+ `, [ milestoneId, req.userId ]);
+
+ // Also refresh "original" object if you want
+ original.is_universal = 1;
+ }
+
+ // 3) If no origin_milestone_id, set it
+ let originId = original.origin_milestone_id || original.id;
+ if (!original.origin_milestone_id) {
+ await db.run(`
+ UPDATE milestones
+ SET origin_milestone_id = ?
+ WHERE id = ?
+ AND user_id = ?
+ `, [ originId, milestoneId, req.userId ]);
+ }
+
+ // 4) fetch tasks & impacts
+ const tasks = await db.all(`
+ SELECT *
+ FROM tasks
+ WHERE milestone_id = ?
+ `, [milestoneId]);
+
+ const impacts = await db.all(`
+ SELECT *
+ FROM milestone_impacts
+ WHERE milestone_id = ?
+ `, [milestoneId]);
+
+ const now = new Date().toISOString();
+ const copiesCreated = [];
+
+ for (let scenarioId of scenarioIds) {
+ if (scenarioId === original.career_path_id) {
+ continue;
+ }
+
+ const newMilestoneId = uuidv4();
+
+ // Always set isUniversal=1 on copies
+ const isUniversal = 1;
+
+ await db.run(`
+ INSERT INTO milestones (
+ id,
+ user_id,
+ career_path_id,
+ milestone_type,
+ title,
+ description,
+ date,
+ progress,
+ status,
+ new_salary,
+ is_universal,
+ origin_milestone_id,
+ created_at,
+ updated_at
+ )
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `, [
+ newMilestoneId,
+ req.userId,
+ scenarioId,
+ original.milestone_type,
+ original.title,
+ original.description,
+ original.date,
+ original.progress,
+ original.status,
+ original.new_salary,
+ isUniversal,
+ originId,
+ now,
+ now
+ ]);
+
+ // copy tasks
+ for (let t of tasks) {
+ const newTaskId = uuidv4();
+ await db.run(`
+ INSERT INTO tasks (
+ id,
+ milestone_id,
+ user_id,
+ title,
+ description,
+ due_date,
+ status,
+ created_at,
+ updated_at
+ )
+ VALUES (?, ?, ?, ?, ?, ?, 'not_started', ?, ?)
+ `, [
+ newTaskId,
+ newMilestoneId,
+ req.userId,
+ t.title,
+ t.description,
+ t.due_date || null,
+ now,
+ now
+ ]);
+ }
+
+ // copy impacts
+ for (let imp of impacts) {
+ const newImpactId = uuidv4();
+ await db.run(`
+ INSERT INTO milestone_impacts (
+ id,
+ milestone_id,
+ impact_type,
+ direction,
+ amount,
+ start_date,
+ end_date,
+ created_at,
+ updated_at
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `, [
+ newImpactId,
+ newMilestoneId,
+ imp.impact_type,
+ imp.direction,
+ imp.amount,
+ imp.start_date || null,
+ imp.end_date || null,
+ now,
+ now
+ ]);
+ }
+
+ copiesCreated.push(newMilestoneId);
+ }
+
+ return res.json({
+ originalId: milestoneId,
+ origin_milestone_id: originId,
+ copiesCreated
+ });
+ } catch (err) {
+ console.error('Error copying milestone:', err);
+ res.status(500).json({ error: 'Failed to copy milestone.' });
+ }
+});
+
+// DELETE milestone from ALL scenarios
+app.delete('/api/premium/milestones/:milestoneId/all', authenticatePremiumUser, async (req, res) => {
+ const { milestoneId } = req.params;
+
+ try {
+ // 1) Fetch the milestone
+ const existing = await db.get(`
+ SELECT id, user_id, origin_milestone_id
+ FROM milestones
+ WHERE id = ?
+ AND user_id = ?
+ `, [milestoneId, req.userId]);
+
+ if (!existing) {
+ return res.status(404).json({ error: 'Milestone not found or not owned by user.' });
+ }
+
+ const originId = existing.origin_milestone_id || existing.id;
+
+ // 2) Delete all copies referencing that origin
+ await db.run(`
+ DELETE FROM milestones
+ WHERE user_id = ?
+ AND origin_milestone_id = ?
+ `, [req.userId, originId]);
+
+ // Also delete the original if it doesn't store itself in origin_milestone_id
+ await db.run(`
+ DELETE FROM milestones
+ WHERE user_id = ?
+ AND id = ?
+ AND origin_milestone_id IS NULL
+ `, [req.userId, originId]);
+
+ res.json({ message: 'Deleted from all scenarios' });
+ } catch (err) {
+ console.error('Error deleting milestone from all scenarios:', err);
+ res.status(500).json({ error: 'Failed to delete milestone from all scenarios.' });
+ }
+});
+
+// DELETE milestone from this scenario only
+app.delete('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (req, res) => {
+ const { milestoneId } = req.params;
+
+ try {
+ // 1) check user ownership
+ const existing = await db.get(`
+ SELECT id, user_id
+ FROM milestones
+ WHERE id = ?
+ AND user_id = ?
+ `, [milestoneId, req.userId]);
+
+ if (!existing) {
+ return res.status(404).json({ error: 'Milestone not found or not owned by user.' });
+ }
+
+ // 2) Delete the single row
+ await db.run(`
+ DELETE FROM milestones
+ WHERE id = ?
+ AND user_id = ?
+ `, [milestoneId, req.userId]);
+
+ // optionally also remove tasks + impacts if you want
+ // e.g.:
+ // await db.run('DELETE FROM tasks WHERE milestone_id = ?', [milestoneId]);
+ // await db.run('DELETE FROM milestone_impacts WHERE milestone_id = ?', [milestoneId]);
+
+ res.json({ message: 'Milestone deleted from this scenario.' });
+ } catch (err) {
+ console.error('Error deleting single milestone:', err);
+ res.status(500).json({ error: 'Failed to delete milestone.' });
+ }
+});
+
+
/* ------------------------------------------------------------------
FINANCIAL PROFILES (Renamed emergency_contribution)
------------------------------------------------------------------ */
diff --git a/src/App.js b/src/App.js
index 3391a8e..4c2593c 100644
--- a/src/App.js
+++ b/src/App.js
@@ -13,6 +13,9 @@ import MilestoneTracker from "./components/MilestoneTracker.js";
import Paywall from "./components/Paywall.js";
import OnboardingContainer from './components/PremiumOnboarding/OnboardingContainer.js';
+// NEW: import your MultiScenarioView component
+import MultiScenarioView from './components/MultiScenarioView.js';
+
import './App.css';
function App() {
@@ -21,7 +24,13 @@ function App() {
const [isAuthenticated, setIsAuthenticated] = useState(() => !!localStorage.getItem('token'));
- const premiumPaths = ['/milestone-tracker', '/paywall', '/financial-profile'];
+ // Any paths that are specifically “premium” (where you might not want to show an Upgrade CTA).
+ const premiumPaths = [
+ '/milestone-tracker',
+ '/paywall',
+ '/financial-profile',
+ '/multi-scenario', // ADDED here so the CTA is hidden on the multi-scenario page
+ ];
const showPremiumCTA = !premiumPaths.includes(location.pathname);
@@ -55,11 +64,14 @@ function App() {
} />
} />
+ {/* NEW multi-scenario route */}
+ } />
>
)}
} />
+
);
diff --git a/src/components/MilestoneTimeline.js b/src/components/MilestoneTimeline.js
index d7f6c7d..996c37b 100644
--- a/src/components/MilestoneTimeline.js
+++ b/src/components/MilestoneTimeline.js
@@ -3,21 +3,27 @@ import React, { useEffect, useState, useCallback } from 'react';
const today = new Date();
-const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView, onMilestoneUpdated }) => {
+const MilestoneTimeline = ({
+ careerPathId,
+ authFetch,
+ activeView,
+ setActiveView,
+ onMilestoneUpdated // optional callback if you want the parent to be notified of changes
+}) => {
const [milestones, setMilestones] = useState({ Career: [], Financial: [] });
- // The "new or edit" milestone form state
+ // "new or edit" milestone form data
const [newMilestone, setNewMilestone] = useState({
title: '',
description: '',
date: '',
progress: 0,
newSalary: '',
- // Each impact can have: { id?, impact_type, direction, amount, start_date, end_date }
- impacts: []
+ impacts: [],
+ isUniversal: 0
});
- // We track which existing impacts the user removed, so we can DELETE them
+ // We'll track which existing impacts are removed so we can do a DELETE if needed
const [impactsToDelete, setImpactsToDelete] = useState([]);
const [showForm, setShowForm] = useState(false);
@@ -27,10 +33,77 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView,
const [showTaskForm, setShowTaskForm] = useState(null);
const [newTask, setNewTask] = useState({ title: '', description: '', due_date: '' });
- /**
- * Fetch all milestones (and their tasks) for this careerPathId.
- * Then categorize them by milestone_type: 'Career' or 'Financial'.
- */
+ // For the Copy wizard
+ const [scenarios, setScenarios] = useState([]);
+ const [copyWizardMilestone, setCopyWizardMilestone] = useState(null);
+
+ // ------------------------------------------------------------------
+ // 1) Impact Helper Functions (define them first to avoid scoping errors)
+ // ------------------------------------------------------------------
+
+ // Insert a new blank impact into newMilestone.impacts
+ const addNewImpact = () => {
+ setNewMilestone(prev => ({
+ ...prev,
+ impacts: [
+ ...prev.impacts,
+ {
+ impact_type: 'ONE_TIME',
+ direction: 'subtract',
+ amount: 0,
+ start_date: '',
+ end_date: ''
+ }
+ ]
+ }));
+ };
+
+ // Remove an impact from newMilestone.impacts
+ const removeImpact = (idx) => {
+ setNewMilestone(prev => {
+ const newImpacts = [...prev.impacts];
+ const removed = newImpacts[idx];
+ if (removed.id) {
+ // queue up for DB DELETE
+ setImpactsToDelete(old => [...old, removed.id]);
+ }
+ newImpacts.splice(idx, 1);
+ return { ...prev, impacts: newImpacts };
+ });
+ };
+
+ // Update a specific impact property
+ const updateImpact = (idx, field, value) => {
+ setNewMilestone(prev => {
+ const newImpacts = [...prev.impacts];
+ newImpacts[idx] = { ...newImpacts[idx], [field]: value };
+ return { ...prev, impacts: newImpacts };
+ });
+ };
+
+ // ------------------------------------------------------------------
+ // 2) Load scenarios (for copy wizard)
+ // ------------------------------------------------------------------
+ useEffect(() => {
+ async function loadScenarios() {
+ try {
+ const res = await authFetch('/api/premium/career-profile/all');
+ if (res.ok) {
+ const data = await res.json();
+ setScenarios(data.careerPaths || []);
+ } else {
+ console.error('Failed to load scenarios. Status:', res.status);
+ }
+ } catch (err) {
+ console.error('Error loading scenarios for copy wizard:', err);
+ }
+ }
+ loadScenarios();
+ }, [authFetch]);
+
+ // ------------------------------------------------------------------
+ // 3) Fetch milestones for the current scenario
+ // ------------------------------------------------------------------
const fetchMilestones = useCallback(async () => {
if (!careerPathId) return;
try {
@@ -41,12 +114,12 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView,
}
const data = await res.json();
if (!data.milestones) {
- console.warn('No milestones field in response:', data);
+ console.warn('No milestones returned:', data);
return;
}
const categorized = { Career: [], Financial: [] };
- data.milestones.forEach((m) => {
+ data.milestones.forEach(m => {
if (categorized[m.milestone_type]) {
categorized[m.milestone_type].push(m);
} else {
@@ -64,57 +137,52 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView,
fetchMilestones();
}, [fetchMilestones]);
- /**
- * Async function to edit an existing milestone.
- * Fetch its impacts, populate newMilestone, show the form.
- */
+ // ------------------------------------------------------------------
+ // 4) "Edit" an existing milestone => load impacts
+ // ------------------------------------------------------------------
const handleEditMilestone = async (m) => {
try {
- // Reset impactsToDelete whenever we edit a new milestone
setImpactsToDelete([]);
- // Fetch existing impacts for milestone "m"
const res = await authFetch(`/api/premium/milestone-impacts?milestone_id=${m.id}`);
if (!res.ok) {
- console.error('Failed to fetch milestone impacts, status:', res.status);
- throw new Error(`HTTP ${res.status}`);
+ console.error('Failed to fetch milestone impacts. Status:', res.status);
+ return;
}
const data = await res.json();
const fetchedImpacts = data.impacts || [];
- // Populate the newMilestone form
setNewMilestone({
title: m.title || '',
description: m.description || '',
date: m.date || '',
progress: m.progress || 0,
newSalary: m.new_salary || '',
- impacts: fetchedImpacts.map((imp) => ({
- // If the DB row has id, we'll store it for PUT or DELETE
+ impacts: fetchedImpacts.map(imp => ({
id: imp.id,
impact_type: imp.impact_type || 'ONE_TIME',
direction: imp.direction || 'subtract',
amount: imp.amount || 0,
start_date: imp.start_date || '',
end_date: imp.end_date || ''
- }))
+ })),
+ isUniversal: m.is_universal ? 1 : 0
});
setEditingMilestone(m);
setShowForm(true);
} catch (err) {
- console.error('Error in handleEditMilestone:', err);
+ console.error('Error editing milestone:', err);
}
};
- /**
- * Create or update a milestone (plus handle impacts).
- */
+ // ------------------------------------------------------------------
+ // 5) Save (create or update) a milestone => handle impacts if needed
+ // ------------------------------------------------------------------
const saveMilestone = async () => {
if (!activeView) return;
- // If editing, we do PUT; otherwise POST
const url = editingMilestone
? `/api/premium/milestones/${editingMilestone.id}`
: `/api/premium/milestone`;
@@ -131,143 +199,126 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView,
new_salary:
activeView === 'Financial' && newMilestone.newSalary
? parseFloat(newMilestone.newSalary)
- : null
+ : null,
+ is_universal: newMilestone.isUniversal || 0
};
try {
- console.log('Sending request:', method, url, payload);
const res = await authFetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
-
if (!res.ok) {
- const errorData = await res.json();
- console.error('Failed to save milestone:', errorData);
- alert(errorData.error || 'Error saving milestone');
+ const errData = await res.json();
+ console.error('Failed to save milestone:', errData);
+ alert(errData.error || 'Error saving milestone');
return;
}
- if (onMilestoneUpdated) onMilestoneUpdated();
const savedMilestone = await res.json();
console.log('Milestone saved/updated:', savedMilestone);
- // If Financial, handle the "impacts"
+ // If financial => handle impacts
if (activeView === 'Financial') {
- // 1) Delete impacts that user removed
+ // 1) Delete old impacts
for (const impactId of impactsToDelete) {
if (impactId) {
- console.log('Deleting old impact', impactId);
const delRes = await authFetch(`/api/premium/milestone-impacts/${impactId}`, {
method: 'DELETE'
});
if (!delRes.ok) {
- console.error('Failed to delete old impact', impactId, await delRes.text());
+ console.error('Failed deleting old impact', impactId, await delRes.text());
}
}
}
- // 2) For each current impact in newMilestone.impacts
- // We'll track the index so we can store the newly created ID if needed
+ // 2) Insert/Update new impacts
for (let i = 0; i < newMilestone.impacts.length; i++) {
- const impact = newMilestone.impacts[i];
- if (impact.id) {
- // existing row => PUT
+ const imp = newMilestone.impacts[i];
+ if (imp.id) {
+ // existing => PUT
const putPayload = {
milestone_id: savedMilestone.id,
- impact_type: impact.impact_type,
- direction: impact.direction,
- amount: parseFloat(impact.amount) || 0,
- start_date: impact.start_date || null,
- end_date: impact.end_date || null
+ impact_type: imp.impact_type,
+ direction: imp.direction,
+ amount: parseFloat(imp.amount) || 0,
+ start_date: imp.start_date || null,
+ end_date: imp.end_date || null
};
- console.log('Updating milestone impact:', impact.id, putPayload);
- const impRes = await authFetch(`/api/premium/milestone-impacts/${impact.id}`, {
+ const impRes = await authFetch(`/api/premium/milestone-impacts/${imp.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(putPayload)
});
if (!impRes.ok) {
const errImp = await impRes.json();
- console.error('Failed to update milestone impact:', errImp);
- } else {
- const updatedImpact = await impRes.json();
- console.log('Updated Impact:', updatedImpact);
+ console.error('Failed updating existing impact:', errImp);
}
} else {
- // [FIX HERE] If no id => POST to create new
- const impactPayload = {
+ // new => POST
+ const postPayload = {
milestone_id: savedMilestone.id,
- impact_type: impact.impact_type,
- direction: impact.direction,
- amount: parseFloat(impact.amount) || 0,
- start_date: impact.start_date || null,
- end_date: impact.end_date || null
+ impact_type: imp.impact_type,
+ direction: imp.direction,
+ amount: parseFloat(imp.amount) || 0,
+ start_date: imp.start_date || null,
+ end_date: imp.end_date || null
};
- console.log('Creating milestone impact:', impactPayload);
const impRes = await authFetch('/api/premium/milestone-impacts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(impactPayload)
+ body: JSON.stringify(postPayload)
});
-
if (!impRes.ok) {
- const errImp = await impRes.json();
- console.error('Failed to create milestone impact:', errImp);
- } else {
- const createdImpact = await impRes.json();
- if (createdImpact && createdImpact.id) {
- setNewMilestone(prev => {
- const newImpacts = [...prev.impacts];
- newImpacts[i] = { ...newImpacts[i], id: createdImpact.id };
- return { ...prev, impacts: newImpacts };
- });
- }
-
+ console.error('Failed creating new impact:', await impRes.text());
}
}
}
}
- // Update local state so we don't have to refetch everything
+ // optional local state update to avoid re-fetch
setMilestones((prev) => {
- const updated = { ...prev };
+ const newState = { ...prev };
if (editingMilestone) {
- updated[activeView] = updated[activeView].map((m) =>
+ newState[activeView] = newState[activeView].map(m =>
m.id === editingMilestone.id ? savedMilestone : m
);
} else {
- updated[activeView].push(savedMilestone);
+ newState[activeView].push(savedMilestone);
}
- return updated;
+ return newState;
});
- // Reset form
+ // reset the form
setShowForm(false);
setEditingMilestone(null);
-
- // [FIX HERE] The next line ensures the updated or newly created impact IDs
- // stay in the local state if the user tries to edit the milestone again
- // in the same session.
setNewMilestone({
title: '',
description: '',
date: '',
progress: 0,
newSalary: '',
- impacts: []
+ impacts: [],
+ isUniversal: 0
});
setImpactsToDelete([]);
+ // optionally re-fetch from DB
+ // await fetchMilestones();
+
+ if (onMilestoneUpdated) {
+ onMilestoneUpdated();
+ }
+
} catch (err) {
console.error('Error saving milestone:', err);
}
};
- /**
- * Add a new task to an existing milestone
- */
+ // ------------------------------------------------------------------
+ // 6) addTask => attach a new task to an existing milestone
+ // ------------------------------------------------------------------
const addTask = async (milestoneId) => {
try {
const taskPayload = {
@@ -292,11 +343,11 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView,
const createdTask = await res.json();
console.log('Task created:', createdTask);
- // Update the milestone's tasks in local state
+ // update local state
setMilestones((prev) => {
const newState = { ...prev };
- ['Career', 'Financial'].forEach((category) => {
- newState[category] = newState[category].map((m) => {
+ ['Career', 'Financial'].forEach((cat) => {
+ newState[cat] = newState[cat].map((m) => {
if (m.id === milestoneId) {
return {
...m,
@@ -316,7 +367,135 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView,
}
};
- // For timeline
+ // ------------------------------------------------------------------
+ // 7) "Copy" wizard -> after copying => re-fetch or local update
+ // ------------------------------------------------------------------
+ function CopyMilestoneWizard({ milestone, scenarios, onClose, authFetch }) {
+ const [selectedScenarios, setSelectedScenarios] = useState([]);
+
+ if (!milestone) return null;
+
+ function toggleScenario(scenarioId) {
+ setSelectedScenarios(prev => {
+ if (prev.includes(scenarioId)) {
+ return prev.filter(id => id !== scenarioId);
+ } else {
+ return [...prev, scenarioId];
+ }
+ });
+ }
+
+ async function handleCopy() {
+ try {
+ const res = await authFetch('/api/premium/milestone/copy', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ milestoneId: milestone.id,
+ scenarioIds: selectedScenarios
+ })
+ });
+ if (!res.ok) throw new Error('Failed to copy milestone');
+
+ const data = await res.json();
+ console.log('Copied milestone to new scenarios:', data);
+
+ onClose(); // close wizard
+
+ // re-fetch or update local
+ await fetchMilestones();
+ if (onMilestoneUpdated) {
+ onMilestoneUpdated();
+ }
+ } catch (err) {
+ console.error('Error copying milestone:', err);
+ }
+ }
+
+ return (
+
+
+
Copy Milestone to Other Scenarios
+
Milestone: {milestone.title}
+
+ {scenarios.map(s => (
+
+
+ toggleScenario(s.id)}
+ />
+ {s.career_name}
+
+
+ ))}
+
+
+ Cancel
+ Copy
+
+
+
+ );
+ }
+
+ // ------------------------------------------------------------------
+ // 8) Delete milestone => single or all
+ // ------------------------------------------------------------------
+ async function handleDeleteMilestone(m) {
+ if (m.is_universal === 1) {
+ const userChoice = window.confirm(
+ 'This milestone is universal. OK => remove from ALL scenarios, Cancel => remove only from this scenario.'
+ );
+ if (userChoice) {
+ // delete from all
+ try {
+ const delAll = await authFetch(`/api/premium/milestones/${m.id}/all`, {
+ method: 'DELETE'
+ });
+ if (!delAll.ok) {
+ console.error('Failed removing universal from all. Status:', delAll.status);
+ return;
+ }
+ // re-fetch
+ await fetchMilestones();
+ if (onMilestoneUpdated) {
+ onMilestoneUpdated();
+ }
+ } catch (err) {
+ console.error('Error deleting universal milestone from all:', err);
+ }
+ } else {
+ // remove from single scenario
+ await deleteSingleMilestone(m);
+ }
+ } else {
+ // normal => single scenario
+ await deleteSingleMilestone(m);
+ }
+ }
+
+ async function deleteSingleMilestone(m) {
+ try {
+ const delRes = await authFetch(`/api/premium/milestones/${m.id}`, { method: 'DELETE' });
+ if (!delRes.ok) {
+ console.error('Failed to delete single milestone:', delRes.status);
+ return;
+ }
+ // re-fetch
+ await fetchMilestones();
+ if (onMilestoneUpdated) {
+ onMilestoneUpdated();
+ }
+ } catch (err) {
+ console.error('Error removing milestone from scenario:', err);
+ }
+ }
+
+ // ------------------------------------------------------------------
+ // 9) Positioning in the timeline
+ // ------------------------------------------------------------------
const allMilestonesCombined = [...milestones.Career, ...milestones.Financial];
const lastDate = allMilestonesCombined.reduce((latest, m) => {
const d = new Date(m.date);
@@ -332,61 +511,13 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView,
return Math.min(Math.max(ratio * 100, 0), 100);
};
- /**
- * Add a new empty impact (no id => new)
- */
- const addNewImpact = () => {
- setNewMilestone((prev) => ({
- ...prev,
- impacts: [
- ...prev.impacts,
- {
- // no 'id' => brand new
- impact_type: 'ONE_TIME',
- direction: 'subtract',
- amount: 0,
- start_date: '',
- end_date: ''
- }
- ]
- }));
- };
-
- /**
- * Remove an impact from the UI. If it had an `id`, track it in impactsToDelete for later DELETE call.
- */
- const removeImpact = (idx) => {
- setNewMilestone((prev) => {
- const newImpacts = [...prev.impacts];
- const removed = newImpacts[idx];
- if (removed.id) {
- setImpactsToDelete((old) => [...old, removed.id]);
- }
- newImpacts.splice(idx, 1);
- return { ...prev, impacts: newImpacts };
- });
- };
-
- const updateImpact = (idx, field, value) => {
- setNewMilestone((prev) => {
- const newImpacts = [...prev.impacts];
- newImpacts[idx] = { ...newImpacts[idx], [field]: value };
- return { ...prev, impacts: newImpacts };
- });
- };
-
- if (!activeView || !milestones[activeView]) {
- return (
-
-
Loading or no milestones in this view...
-
- );
- }
-
+ // ------------------------------------------------------------------
+ // Render
+ // ------------------------------------------------------------------
return (
- {['Career', 'Financial'].map((view) => (
+ {['Career', 'Financial'].map(view => (
- {/* New Milestone button */}
+ {/* + New Milestone button */}
{
if (showForm) {
- // Cancel form
+ // Cancel
setShowForm(false);
setEditingMilestone(null);
setNewMilestone({
@@ -410,7 +541,8 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView,
date: '',
progress: 0,
newSalary: '',
- impacts: []
+ impacts: [],
+ isUniversal: 0
});
setImpactsToDelete([]);
} else {
@@ -439,7 +571,7 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView,
type="date"
placeholder="Milestone Date"
value={newMilestone.date}
- onChange={(e) => setNewMilestone((prev) => ({ ...prev, date: e.target.value }))}
+ onChange={(e) => setNewMilestone(prev => ({ ...prev, date: e.target.value }))}
/>
{
const val = e.target.value === '' ? 0 : parseInt(e.target.value, 10);
- setNewMilestone((prev) => ({ ...prev, progress: val }));
+ setNewMilestone(prev => ({ ...prev, progress: val }));
}}
/>
@@ -455,7 +587,7 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView,
setNewMilestone({ ...newMilestone, newSalary: e.target.value })}
/>
@@ -535,12 +667,30 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView,
)}
-
+ {/* universal checkbox */}
+
+
+
+ setNewMilestone(prev => ({
+ ...prev,
+ isUniversal: e.target.checked ? 1 : 0
+ }))
+ }
+ />
+ {' '}Apply this milestone to all scenarios?
+
+
+
+
{editingMilestone ? 'Update' : 'Add'} Milestone
)}
+ {/* Timeline */}
@@ -586,6 +736,23 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView,
{showTaskForm === m.id ? 'Cancel Task' : 'Add Task'}
+ {/* Edit, Copy, Delete Buttons */}
+
+ handleEditMilestone(m)}>Edit
+ setCopyWizardMilestone(m)}
+ >
+ Copy
+
+ handleDeleteMilestone(m)}
+ >
+ Delete
+
+
+
{showTaskForm === m.id && (
+
+ {/* CopyWizard modal if copying */}
+ {copyWizardMilestone && (
+ setCopyWizardMilestone(null)}
+ authFetch={authFetch}
+ onMilestoneUpdated={onMilestoneUpdated}
+ />
+ )}
);
};
diff --git a/src/components/MilestoneTracker.js b/src/components/MilestoneTracker.js
index 723a7a4..9d644f1 100644
--- a/src/components/MilestoneTracker.js
+++ b/src/components/MilestoneTracker.js
@@ -40,27 +40,35 @@ ChartJS.register(
const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
const location = useLocation();
const navigate = useNavigate();
-
const apiURL = process.env.REACT_APP_API_URL;
- // -------------------------
+ // --------------------------------------------------
// State
- // -------------------------
+ // --------------------------------------------------
const [selectedCareer, setSelectedCareer] = useState(initialCareer || null);
const [careerPathId, setCareerPathId] = useState(null);
const [existingCareerPaths, setExistingCareerPaths] = useState([]);
- const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
const [activeView, setActiveView] = useState("Career");
+ // Real user snapshot
const [financialProfile, setFinancialProfile] = useState(null);
+
+ // Scenario row (with planned_* overrides) from GET /api/premium/career-profile/:careerPathId
+ const [scenarioRow, setScenarioRow] = useState(null);
+
+ // scenario's collegeProfile row
const [collegeProfile, setCollegeProfile] = useState(null);
+ // Simulation results
const [projectionData, setProjectionData] = useState([]);
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
+ // Possibly let user type the simulation length
const [simulationYearsInput, setSimulationYearsInput] = useState("20");
+ const simulationYears = parseInt(simulationYearsInput, 10) || 20;
const [showEditModal, setShowEditModal] = useState(false);
+ const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
// Possibly loaded from location.state
const {
@@ -68,10 +76,9 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
loanPayoffMonth: initialLoanPayoffMonth = null
} = location.state || {};
- const simulationYears = parseInt(simulationYearsInput, 10) || 20;
- // -------------------------
- // 1. Fetch career paths + financialProfile on mount
- // -------------------------
+ // --------------------------------------------------
+ // 1) Fetch user’s scenario list + financialProfile
+ // --------------------------------------------------
useEffect(() => {
const fetchCareerPaths = async () => {
const res = await authFetch(`${apiURL}/premium/career-profile/all`);
@@ -84,7 +91,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
setSelectedCareer(fromPopout);
setCareerPathId(fromPopout.career_path_id);
} else if (!selectedCareer) {
- // Try to fetch the latest
+ // fallback to latest
const latest = await authFetch(`${apiURL}/premium/career-profile/latest`);
if (latest && latest.ok) {
const latestData = await latest.json();
@@ -98,7 +105,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
const fetchFinancialProfile = async () => {
const res = await authFetch(`${apiURL}/premium/financial-profile`);
- if (res && res.ok) {
+ if (res?.ok) {
const data = await res.json();
setFinancialProfile(data);
}
@@ -108,256 +115,265 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
fetchFinancialProfile();
}, [apiURL, location.state, selectedCareer]);
- // -------------------------
- // 2. Fetch the college profile for the selected careerPathId
- // -------------------------
+ // --------------------------------------------------
+ // 2) When careerPathId changes => fetch scenarioRow + collegeProfile
+ // --------------------------------------------------
useEffect(() => {
if (!careerPathId) {
+ setScenarioRow(null);
setCollegeProfile(null);
return;
}
- const fetchCollegeProfile = async () => {
- const res = await authFetch(`${apiURL}/premium/college-profile?careerPathId=${careerPathId}`);
- if (!res || !res.ok) {
+ async function fetchScenario() {
+ const scenRes = await authFetch(`${apiURL}/premium/career-profile/${careerPathId}`);
+ if (scenRes.ok) {
+ const data = await scenRes.json();
+ setScenarioRow(data);
+ } else {
+ console.error('Failed to fetch scenario row:', scenRes.status);
+ setScenarioRow(null);
+ }
+ }
+
+ async function fetchCollege() {
+ const colRes = await authFetch(`${apiURL}/premium/college-profile?careerPathId=${careerPathId}`);
+ if (!colRes?.ok) {
setCollegeProfile(null);
return;
}
- const data = await res.json();
+ const data = await colRes.json();
setCollegeProfile(data);
- };
+ }
- fetchCollegeProfile();
+ fetchScenario();
+ fetchCollege();
}, [careerPathId, apiURL]);
- // -------------------------
- // 3. Initial simulation when profiles + career loaded
- // (But this does NOT update after milestone changes yet)
- // -------------------------
+ // --------------------------------------------------
+ // 3) Once we have (financialProfile, scenarioRow, collegeProfile),
+ // run initial simulation with the scenario's milestones + impacts
+ // --------------------------------------------------
useEffect(() => {
- if (!financialProfile || !collegeProfile || !selectedCareer || !careerPathId) return;
-
- // 1) Fetch the raw milestones for this careerPath
+ if (!financialProfile || !scenarioRow || !collegeProfile) return;
+
(async () => {
try {
+ // 1) load milestones for scenario
const milRes = await authFetch(`${apiURL}/premium/milestones?careerPathId=${careerPathId}`);
if (!milRes.ok) {
- console.error('Failed to fetch initial milestones');
+ console.error('Failed to fetch initial milestones for scenario', careerPathId);
return;
}
const milestonesData = await milRes.json();
const allMilestones = milestonesData.milestones || [];
-
- // 2) For each milestone, fetch impacts
- const impactPromises = allMilestones.map((m) =>
+
+ // 2) fetch impacts for each
+ const impactPromises = allMilestones.map(m =>
authFetch(`${apiURL}/premium/milestone-impacts?milestone_id=${m.id}`)
- .then((r) => (r.ok ? r.json() : null))
- .then((data) => data?.impacts || [])
- .catch((err) => {
- console.error('Failed fetching impacts for milestone', m.id, err);
+ .then(r => r.ok ? r.json() : null)
+ .then(data => data?.impacts || [])
+ .catch(err => {
+ console.warn('Error fetching impacts for milestone', m.id, err);
return [];
})
);
const impactsForEach = await Promise.all(impactPromises);
const milestonesWithImpacts = allMilestones.map((m, i) => ({
...m,
- impacts: impactsForEach[i] || [],
+ impacts: impactsForEach[i] || []
}));
-
- // 3) Flatten them
- const allImpacts = milestonesWithImpacts.flatMap((m) => m.impacts || []);
-
- // 4) Build the mergedProfile (like you already do)
- const mergedProfile = {
- // From financialProfile
- currentSalary: financialProfile.current_salary || 0,
- monthlyExpenses: financialProfile.monthly_expenses || 0,
- monthlyDebtPayments: financialProfile.monthly_debt_payments || 0,
- retirementSavings: financialProfile.retirement_savings || 0,
- emergencySavings: financialProfile.emergency_fund || 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,
-
- // From collegeProfile
- studentLoanAmount: collegeProfile.existing_college_debt || 0,
- interestRate: collegeProfile.interest_rate || 5,
- loanTerm: collegeProfile.loan_term || 10,
- loanDeferralUntilGraduation: !!collegeProfile.loan_deferral_until_graduation,
- academicCalendar: collegeProfile.academic_calendar || 'monthly',
- annualFinancialAid: collegeProfile.annual_financial_aid || 0,
- calculatedTuition: collegeProfile.tuition || 0,
- extraPayment: collegeProfile.extra_payment || 0,
- inCollege:
- collegeProfile.college_enrollment_status === 'currently_enrolled' ||
- collegeProfile.college_enrollment_status === 'prospective_student',
- 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,
- startDate: new Date().toISOString(),
- expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary,
-
- // The key: impacts
- milestoneImpacts: allImpacts,
+ const allImpacts = milestonesWithImpacts.flatMap(m => m.impacts);
- simulationYears,
- };
-
- // 5) Run the simulation
- const { projectionData: initialProjData, loanPaidOffMonth: payoff } =
+ // 3) Build the merged profile w/ scenario overrides
+ const mergedProfile = buildMergedProfile(
+ financialProfile,
+ scenarioRow,
+ collegeProfile,
+ allImpacts,
+ simulationYears
+ );
+
+ // 4) run the simulation
+ const { projectionData: pData, loanPaidOffMonth: payoff } =
simulateFinancialProjection(mergedProfile);
-
- let cumulativeSavings = mergedProfile.emergencySavings || 0;
- const finalData = initialProjData.map((month) => {
- cumulativeSavings += (month.netSavings || 0);
- return { ...month, cumulativeNetSavings: cumulativeSavings };
+
+ // 5) If you track cumulative net
+ let cumu = mergedProfile.emergencySavings || 0;
+ const finalData = pData.map(mo => {
+ cumu += (mo.netSavings || 0);
+ return { ...mo, cumulativeNetSavings: cumu };
});
-
+
setProjectionData(finalData);
setLoanPayoffMonth(payoff);
-
} catch (err) {
- console.error('Error fetching initial milestones/impacts or simulating:', err);
+ console.error('Error in initial scenario simulation:', err);
}
})();
- }, [financialProfile, collegeProfile, simulationYears, selectedCareer, careerPathId]);
+ }, [
+ financialProfile,
+ scenarioRow,
+ collegeProfile,
+ simulationYears,
+ careerPathId,
+ apiURL
+ ]);
- const handleSimulationYearsChange = (e) => {
- setSimulationYearsInput(e.target.value); // let user type partial/blank
- };
+ // Merges the real snapshot w/ scenario overrides + milestones
+ function buildMergedProfile(finProf, scenRow, colProf, milestoneImpacts, simYears) {
+ return {
+ // Real snapshot fallback
+ currentSalary: finProf.current_salary || 0,
+ monthlyExpenses:
+ scenRow.planned_monthly_expenses ?? finProf.monthly_expenses ?? 0,
+ monthlyDebtPayments:
+ scenRow.planned_monthly_debt_payments ?? finProf.monthly_debt_payments ?? 0,
+ retirementSavings: finProf.retirement_savings ?? 0,
+ emergencySavings: finProf.emergency_fund ?? 0,
+ monthlyRetirementContribution:
+ scenRow.planned_monthly_retirement_contribution ??
+ finProf.retirement_contribution ??
+ 0,
+ monthlyEmergencyContribution:
+ scenRow.planned_monthly_emergency_contribution ??
+ finProf.emergency_contribution ??
+ 0,
+ surplusEmergencyAllocation:
+ scenRow.planned_surplus_emergency_pct ??
+ finProf.extra_cash_emergency_pct ??
+ 50,
+ surplusRetirementAllocation:
+ scenRow.planned_surplus_retirement_pct ??
+ finProf.extra_cash_retirement_pct ??
+ 50,
+ additionalIncome:
+ scenRow.planned_additional_income ?? finProf.additional_income ?? 0,
- const handleSimulationYearsBlur = () => {
- // Optionally, onBlur you can “normalize” the value
- // (e.g. if they left it blank, revert to "20").
- if (simulationYearsInput.trim() === "") {
- setSimulationYearsInput("20");
- }
- };
+ // College stuff
+ studentLoanAmount: colProf.existing_college_debt || 0,
+ interestRate: colProf.interest_rate || 5,
+ loanTerm: colProf.loan_term || 10,
+ loanDeferralUntilGraduation: !!colProf.loan_deferral_until_graduation,
+ academicCalendar: colProf.academic_calendar || 'monthly',
+ annualFinancialAid: colProf.annual_financial_aid || 0,
+ calculatedTuition: colProf.tuition || 0,
+ extraPayment: colProf.extra_payment || 0,
+ inCollege:
+ colProf.college_enrollment_status === 'currently_enrolled' ||
+ colProf.college_enrollment_status === 'prospective_student',
+ gradDate: colProf.expected_graduation || null,
+ programType: colProf.program_type,
+ creditHoursPerYear: colProf.credit_hours_per_year || 0,
+ hoursCompleted: colProf.hours_completed || 0,
+ programLength: colProf.program_length || 0,
+ expectedSalary: colProf.expected_salary || finProf.current_salary || 0,
- // -------------------------------------------------
- // 4. reSimulate() => re-fetch everything (financial, college, milestones & impacts),
- // re-run the simulation. This is triggered AFTER user updates a milestone in MilestoneTimeline.
- // -------------------------------------------------
+ // Additional
+ startDate: new Date().toISOString(),
+ simulationYears: simYears,
+
+ // Milestone Impacts
+ milestoneImpacts: milestoneImpacts || []
+ };
+ }
+
+ // ------------------------------------------------------
+ // 4) reSimulate => after milestone changes or user toggles something
+ // ------------------------------------------------------
const reSimulate = async () => {
if (!careerPathId) return;
-
try {
- // 1) Fetch financial + college + raw milestones
- const [finResp, colResp, milResp] = await Promise.all([
+ // 1) fetch everything again
+ const [finResp, scenResp, colResp, milResp] = await Promise.all([
authFetch(`${apiURL}/premium/financial-profile`),
+ authFetch(`${apiURL}/premium/career-profile/${careerPathId}`),
authFetch(`${apiURL}/premium/college-profile?careerPathId=${careerPathId}`),
authFetch(`${apiURL}/premium/milestones?careerPathId=${careerPathId}`)
]);
- if (!finResp.ok || !colResp.ok || !milResp.ok) {
- console.error('One reSimulate fetch failed:', finResp.status, colResp.status, milResp.status);
+ if (!finResp.ok || !scenResp.ok || !colResp.ok || !milResp.ok) {
+ console.error(
+ 'One reSimulate fetch failed:',
+ finResp.status,
+ scenResp.status,
+ colResp.status,
+ milResp.status
+ );
return;
}
- const [updatedFinancial, updatedCollege, milestonesData] = await Promise.all([
- finResp.json(),
- colResp.json(),
- milResp.json()
- ]);
+ const [updatedFinancial, updatedScenario, updatedCollege, milData] =
+ await Promise.all([
+ finResp.json(),
+ scenResp.json(),
+ colResp.json(),
+ milResp.json()
+ ]);
- // 2) For each milestone, fetch its impacts separately (if not already included)
- const allMilestones = milestonesData.milestones || [];
+ const allMilestones = milData.milestones || [];
const impactsPromises = allMilestones.map(m =>
authFetch(`${apiURL}/premium/milestone-impacts?milestone_id=${m.id}`)
.then(r => r.ok ? r.json() : null)
.then(data => data?.impacts || [])
.catch(err => {
- console.error('Failed fetching impacts for milestone', m.id, err);
+ console.warn('Impact fetch err for milestone', m.id, err);
return [];
})
);
-
const impactsForEach = await Promise.all(impactsPromises);
- // Merge them onto the milestone array if desired
const milestonesWithImpacts = allMilestones.map((m, i) => ({
...m,
impacts: impactsForEach[i] || []
}));
+ const allImpacts = milestonesWithImpacts.flatMap(m => m.impacts);
- // Flatten or gather all impacts if your simulation function needs them
- const allImpacts = milestonesWithImpacts.flatMap(m => m.impacts || []);
+ // 2) Build merged
+ const mergedProfile = buildMergedProfile(
+ updatedFinancial,
+ updatedScenario,
+ updatedCollege,
+ allImpacts,
+ simulationYears
+ );
- // 3) Build mergedProfile
- const mergedProfile = {
- // From updatedFinancial
- currentSalary: updatedFinancial.current_salary || 0,
- monthlyExpenses: updatedFinancial.monthly_expenses || 0,
- monthlyDebtPayments: updatedFinancial.monthly_debt_payments || 0,
- retirementSavings: updatedFinancial.retirement_savings || 0,
- emergencySavings: updatedFinancial.emergency_fund || 0,
- monthlyRetirementContribution: updatedFinancial.retirement_contribution || 0,
- monthlyEmergencyContribution: updatedFinancial.emergency_contribution || 0,
- surplusEmergencyAllocation: updatedFinancial.extra_cash_emergency_pct || 50,
- surplusRetirementAllocation: updatedFinancial.extra_cash_retirement_pct || 50,
-
- // From updatedCollege
- studentLoanAmount: updatedCollege.existing_college_debt || 0,
- interestRate: updatedCollege.interest_rate || 5,
- loanTerm: updatedCollege.loan_term || 10,
- loanDeferralUntilGraduation: !!updatedCollege.loan_deferral_until_graduation,
- academicCalendar: updatedCollege.academic_calendar || 'monthly',
- annualFinancialAid: updatedCollege.annual_financial_aid || 0,
- calculatedTuition: updatedCollege.tuition || 0,
- extraPayment: updatedCollege.extra_payment || 0,
- inCollege:
- updatedCollege.college_enrollment_status === 'currently_enrolled' ||
- updatedCollege.college_enrollment_status === 'prospective_student',
- gradDate: updatedCollege.expected_graduation || null,
- programType: updatedCollege.program_type,
- creditHoursPerYear: updatedCollege.credit_hours_per_year || 0,
- hoursCompleted: updatedCollege.hours_completed || 0,
- programLength: updatedCollege.program_length || 0,
- startDate: new Date().toISOString(),
- expectedSalary: updatedCollege.expected_salary || updatedFinancial.current_salary,
-
- // The key: pass the impacts to the simulation if needed
- milestoneImpacts: allImpacts
- };
-
- // 4) Re-run simulation
+ // 3) run
const { projectionData: newProjData, loanPaidOffMonth: payoff } =
simulateFinancialProjection(mergedProfile);
- // 5) If you track cumulative net savings:
- let cumulativeSavings = mergedProfile.emergencySavings || 0;
- const finalData = newProjData.map(month => {
- cumulativeSavings += (month.netSavings || 0);
- return { ...month, cumulativeNetSavings: cumulativeSavings };
+ // 4) cumulative
+ let csum = mergedProfile.emergencySavings || 0;
+ const finalData = newProjData.map(mo => {
+ csum += (mo.netSavings || 0);
+ return { ...mo, cumulativeNetSavings: csum };
});
- // 6) Update states => triggers chart refresh
setProjectionData(finalData);
setLoanPayoffMonth(payoff);
- // Optionally store the new profiles in state if you like
+ // also store updated scenario, financial, college
setFinancialProfile(updatedFinancial);
+ setScenarioRow(updatedScenario);
setCollegeProfile(updatedCollege);
- console.log('Re-simulated after Milestone update!', {
- mergedProfile,
- milestonesWithImpacts
- });
-
+ console.log('Re-simulated after milestone update', { mergedProfile, finalData });
} catch (err) {
console.error('Error in reSimulate:', err);
}
};
- // ...
- // The rest of your component logic
- // ...
+ // handle user typing simulation length
+ const handleSimulationYearsChange = (e) => setSimulationYearsInput(e.target.value);
+ const handleSimulationYearsBlur = () => {
+ if (!simulationYearsInput.trim()) {
+ setSimulationYearsInput("20");
+ }
+ };
+ // Logging
console.log(
'First 5 items of projectionData:',
- Array.isArray(projectionData) ? projectionData.slice(0, 5) : 'projectionData not yet available'
+ Array.isArray(projectionData) ? projectionData.slice(0, 5) : 'none'
);
return (
@@ -373,7 +389,6 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
authFetch={authFetch}
/>
- {/* Pass reSimulate as onMilestoneUpdated: */}
{
)}
-
- Simulation Length (years):
-
-
+
+ Simulation Length (years):
+
+
setPendingCareerForModal(careerName)}
@@ -500,7 +515,8 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
{pendingCareerForModal && (
{
- // handleConfirmCareerSelection logic
+ // Example Confirm
+ console.log('TODO: handleConfirmCareerSelection => new scenario?');
}}
>
Confirm Career Change to {pendingCareerForModal}
diff --git a/src/components/ScenarioContainer.js b/src/components/ScenarioContainer.js
index 960bf71..382b685 100644
--- a/src/components/ScenarioContainer.js
+++ b/src/components/ScenarioContainer.js
@@ -2,8 +2,6 @@
import React, { useState, useEffect } from 'react';
import { Line } from 'react-chartjs-2';
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
-
-// Reuse your existing:
import ScenarioEditModal from './ScenarioEditModal.js';
import MilestoneTimeline from './MilestoneTimeline.js';
import AISuggestedMilestones from './AISuggestedMilestones.js';
@@ -14,23 +12,32 @@ export default function ScenarioContainer({
financialProfile, // single row, shared across user
onClone,
onRemove,
- onScenarioUpdated // callback to parent to store updated scenario data
+ onScenarioUpdated
}) {
+ const [localScenario, setLocalScenario] = useState(scenario);
const [collegeProfile, setCollegeProfile] = useState(null);
+
const [milestones, setMilestones] = useState([]);
const [universalMilestones, setUniversalMilestones] = useState([]);
-
+
const [projectionData, setProjectionData] = useState([]);
const [loanPaidOffMonth, setLoanPaidOffMonth] = useState(null);
const [editOpen, setEditOpen] = useState(false);
+ // Re-sync if parent updates scenario
+ useEffect(() => {
+ setLocalScenario(scenario);
+ }, [scenario]);
+
// 1) Fetch the college profile for this scenario
useEffect(() => {
- if (!scenario?.id) return;
+ if (!localScenario?.id) return;
async function loadCollegeProfile() {
try {
- const res = await authFetch(`/api/premium/college-profile?careerPathId=${scenario.id}`);
+ const res = await authFetch(
+ `/api/premium/college-profile?careerPathId=${localScenario.id}`
+ );
if (res.ok) {
const data = await res.json();
setCollegeProfile(data);
@@ -43,20 +50,16 @@ export default function ScenarioContainer({
}
}
loadCollegeProfile();
- }, [scenario]);
+ }, [localScenario]);
- // 2) Fetch scenario’s milestones (where is_universal=0) + universal (is_universal=1)
+ // 2) Fetch scenario’s milestones (and universal)
useEffect(() => {
- if (!scenario?.id) return;
+ if (!localScenario?.id) return;
async function loadMilestones() {
try {
const [scenRes, uniRes] = await Promise.all([
- authFetch(`/api/premium/milestones?careerPathId=${scenario.id}`),
- // for universal: we do an extra call with no careerPathId.
- // But your current code always requires a careerPathId. So you might
- // create a new endpoint /api/premium/milestones?is_universal=1 or something.
- // We'll assume you have it:
- authFetch(`/api/premium/milestones?careerPathId=universal`)
+ authFetch(`/api/premium/milestones?careerPathId=${localScenario.id}`),
+ authFetch(`/api/premium/milestones?careerPathId=universal`) // if you have that route
]);
let scenarioData = scenRes.ok ? (await scenRes.json()) : { milestones: [] };
@@ -69,26 +72,42 @@ export default function ScenarioContainer({
}
}
loadMilestones();
- }, [scenario]);
+ }, [localScenario]);
- // 3) Whenever we have financialProfile + collegeProfile + milestones, run the simulation
+ // 3) Merge real snapshot + scenario overrides => run simulation
useEffect(() => {
if (!financialProfile || !collegeProfile) return;
- // Merge them into the userProfile object for the simulator:
+ // Merge the scenario's planned overrides if not null,
+ // else fallback to the real snapshot in financialProfile
const mergedProfile = {
- // Financial fields
currentSalary: financialProfile.current_salary || 0,
- monthlyExpenses: financialProfile.monthly_expenses || 0,
- monthlyDebtPayments: financialProfile.monthly_debt_payments || 0,
- retirementSavings: financialProfile.retirement_savings || 0,
- emergencySavings: financialProfile.emergency_fund || 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,
+ monthlyExpenses:
+ localScenario.planned_monthly_expenses ?? financialProfile.monthly_expenses ?? 0,
+ monthlyDebtPayments:
+ localScenario.planned_monthly_debt_payments ?? financialProfile.monthly_debt_payments ?? 0,
+ retirementSavings: financialProfile.retirement_savings ?? 0,
+ emergencySavings: financialProfile.emergency_fund ?? 0,
+ monthlyRetirementContribution:
+ localScenario.planned_monthly_retirement_contribution ??
+ financialProfile.retirement_contribution ??
+ 0,
+ monthlyEmergencyContribution:
+ localScenario.planned_monthly_emergency_contribution ??
+ financialProfile.emergency_contribution ??
+ 0,
+ surplusEmergencyAllocation:
+ localScenario.planned_surplus_emergency_pct ??
+ financialProfile.extra_cash_emergency_pct ??
+ 50,
+ surplusRetirementAllocation:
+ localScenario.planned_surplus_retirement_pct ??
+ financialProfile.extra_cash_retirement_pct ??
+ 50,
+ additionalIncome:
+ localScenario.planned_additional_income ?? financialProfile.additional_income ?? 0,
- // College fields (scenario-based)
+ // College fields
studentLoanAmount: collegeProfile.existing_college_debt || 0,
interestRate: collegeProfile.interest_rate || 5,
loanTerm: collegeProfile.loan_term || 10,
@@ -102,66 +121,45 @@ export default function ScenarioContainer({
creditHoursPerYear: collegeProfile.credit_hours_per_year || 0,
hoursCompleted: collegeProfile.hours_completed || 0,
programLength: collegeProfile.program_length || 0,
-
- // We assume user’s baseline “inCollege” from the DB:
inCollege:
collegeProfile.college_enrollment_status === 'currently_enrolled' ||
collegeProfile.college_enrollment_status === 'prospective_student',
+ expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary || 0,
- // If you store expected_salary in collegeProfile
- expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary,
-
- // Flatten the scenario + universal milestones’ impacts
+ // Flatten scenario + universal milestoneImpacts
milestoneImpacts: buildAllImpacts([...milestones, ...universalMilestones])
};
- const { projectionData, loanPaidOffMonth } = simulateFinancialProjection(mergedProfile);
+ const { projectionData, loanPaidOffMonth } =
+ simulateFinancialProjection(mergedProfile);
setProjectionData(projectionData);
setLoanPaidOffMonth(loanPaidOffMonth);
- }, [financialProfile, collegeProfile, milestones, universalMilestones]);
+ }, [financialProfile, collegeProfile, localScenario, milestones, universalMilestones]);
- // Helper: Flatten all milestone impacts into one array for the simulator
function buildAllImpacts(allMilestones) {
let impacts = [];
for (let m of allMilestones) {
- // Possibly fetch m.impacts if you store them directly on the milestone
- // or if you fetch them separately.
- // If your code stores them as `m.impacts = [ { direction, amount, ... } ]`
if (m.impacts) {
impacts.push(...m.impacts);
}
- // If you also want a milestone that sets a new salary, handle that logic too
- // E.g., { impact_type: 'SALARY_CHANGE', start_date: m.date, newSalary: m.new_salary }
+ // If new_salary logic is relevant, handle it here
}
return impacts;
}
- // 4) We’ll display a single line chart with Net Savings (or cumulativeNetSavings)
- const labels = projectionData.map((p) => p.month);
- const netSavingsData = projectionData.map((p) => p.cumulativeNetSavings || 0);
-
- // Grab final row for some KPIs
- const finalRow = projectionData[projectionData.length - 1] || {};
- const finalRet = finalRow.retirementSavings?.toFixed(0) || '0';
- const finalEmerg = finalRow.emergencySavings?.toFixed(0) || '0';
-
- // 5) Handle “Edit” scenario -> open your existing `ScenarioEditModal.js`
- // But that modal currently references setFinancialProfile, setCollegeProfile directly,
- // so you may want a specialized version that changes only this scenario’s row.
- // For simplicity, we’ll just show how to open it:
-
+ // Edit => open modal
return (
-
{scenario.career_name || 'Untitled Scenario'}
-
Status: {scenario.status}
+
{localScenario.career_name || 'Untitled Scenario'}
+
Status: {localScenario.status}
p.month),
datasets: [
{
label: 'Net Savings',
- data: netSavingsData,
+ data: projectionData.map((p) => p.cumulativeNetSavings || 0),
borderColor: 'blue',
fill: false
}
@@ -172,27 +170,24 @@ export default function ScenarioContainer({
Loan Paid Off: {loanPaidOffMonth || 'N/A'}
- Final Retirement: ${finalRet}
- Final Emergency: ${finalEmerg}
+ Retirement (final): ${
+ projectionData[projectionData.length - 1]?.retirementSavings?.toFixed(0) || 0
+ }
- {/* The timeline for this scenario. We pass careerPathId */}
{}}
onMilestoneUpdated={() => {
// re-fetch or something
- // We'll just force a re-fetch of scenario’s milestones
- // or re-run the entire load effect
}}
/>
- {/* Show AI suggestions if you like */}
setEditOpen(true)}>Edit
- Clone
+
+ Clone
+
Remove
- {/* Reuse your existing ScenarioEditModal that expects
- setFinancialProfile, setCollegeProfile, etc.
- However, you might want a specialized "ScenarioEditModal" that updates
- the DB fields for *this* scenario. For now, we just show how to open. */}
+ {/* Updated ScenarioEditModal that references localScenario + setLocalScenario */}
setEditOpen(false)}
- financialProfile={financialProfile}
- setFinancialProfile={() => {
- // If you truly want scenario-specific financial data,
- // you’d do a more advanced approach.
- // For now, do nothing or re-fetch from server.
- }}
- collegeProfile={collegeProfile}
- setCollegeProfile={(updated) => {
- setCollegeProfile((prev) => ({ ...prev, ...updated }));
- }}
+ scenario={localScenario}
+ setScenario={setLocalScenario}
apiURL="/api"
- authFetch={authFetch}
/>
);
diff --git a/src/components/ScenarioEditModal.js b/src/components/ScenarioEditModal.js
index 880abda..be03aeb 100644
--- a/src/components/ScenarioEditModal.js
+++ b/src/components/ScenarioEditModal.js
@@ -5,234 +5,195 @@ import authFetch from '../utils/authFetch.js';
const ScenarioEditModal = ({
show,
onClose,
- financialProfile,
- setFinancialProfile,
- collegeProfile,
- setCollegeProfile,
- apiURL,
- authFetch,
+ scenario, // <== We'll need the scenario object here
+ setScenario, // callback to update the scenario in parent
+ apiURL
}) => {
const [formData, setFormData] = useState({});
- // Populate local formData whenever show=true
useEffect(() => {
- if (!show) return;
+ if (!show || !scenario) return;
setFormData({
- // From financialProfile:
- currentSalary: financialProfile?.current_salary ?? 0,
- monthlyExpenses: financialProfile?.monthly_expenses ?? 0,
- monthlyDebtPayments: financialProfile?.monthly_debt_payments ?? 0,
- retirementSavings: financialProfile?.retirement_savings ?? 0,
- emergencySavings: financialProfile?.emergency_fund ?? 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,
-
- // From collegeProfile:
- studentLoanAmount: collegeProfile?.existing_college_debt ?? 0,
- interestRate: collegeProfile?.interest_rate ?? 5,
- loanTerm: collegeProfile?.loan_term ?? 10,
- loanDeferralUntilGraduation: !!collegeProfile?.loan_deferral_until_graduation,
- academicCalendar: collegeProfile?.academic_calendar ?? 'monthly',
- annualFinancialAid: collegeProfile?.annual_financial_aid ?? 0,
- calculatedTuition: collegeProfile?.tuition ?? 0,
- extraPayment: collegeProfile?.extra_payment ?? 0,
- partTimeIncome: 0, // or fetch from DB if you store it
- gradDate: collegeProfile?.expected_graduation ?? '',
- 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,
+ careerName: scenario.career_name || '',
+ status: scenario.status || 'planned',
+ startDate: scenario.start_date || '',
+ projectedEndDate: scenario.projected_end_date || '',
+ // existing fields
+ // newly added columns:
+ plannedMonthlyExpenses: scenario.planned_monthly_expenses ?? '',
+ plannedMonthlyDebt: scenario.planned_monthly_debt_payments ?? '',
+ plannedMonthlyRetirement: scenario.planned_monthly_retirement_contribution ?? '',
+ plannedMonthlyEmergency: scenario.planned_monthly_emergency_contribution ?? '',
+ plannedSurplusEmergencyPct: scenario.planned_surplus_emergency_pct ?? '',
+ plannedSurplusRetirementPct: scenario.planned_surplus_retirement_pct ?? '',
+ plannedAdditionalIncome: scenario.planned_additional_income ?? '',
+ // ...
});
- }, [show, financialProfile, collegeProfile]);
+ }, [show, scenario]);
- // Handle form changes in local state
const handleChange = (e) => {
- const { name, type, value, checked } = e.target;
- setFormData((prev) => ({
- ...prev,
- [name]:
- type === 'checkbox'
- ? checked
- : type === 'number'
- ? parseFloat(value) || 0
- : value
- }));
+ const { name, value } = e.target;
+ setFormData(prev => ({ ...prev, [name]: value }));
};
- // SAVE: Update DB and local states, then close
const handleSave = async () => {
+ if (!scenario) return;
try {
- // 1) Update the backend (financialProfile + collegeProfile):
- // (Adjust endpoints/methods as needed in your codebase)
- await authFetch(`${apiURL}/premium/financial-profile`, {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- current_salary: formData.currentSalary,
- monthly_expenses: formData.monthlyExpenses,
- monthly_debt_payments: formData.monthlyDebtPayments,
- retirement_savings: formData.retirementSavings,
- emergency_fund: formData.emergencySavings,
- retirement_contribution: formData.monthlyRetirementContribution,
- emergency_contribution: formData.monthlyEmergencyContribution,
- extra_cash_emergency_pct: formData.surplusEmergencyAllocation,
- extra_cash_retirement_pct: formData.surplusRetirementAllocation
- })
- });
+ // We'll call POST /api/premium/career-profile or a separate PUT.
+ // Because the code is "upsert," we can do the same POST
+ // and rely on ON CONFLICT.
+ const payload = {
+ career_name: formData.careerName,
+ status: formData.status,
+ start_date: formData.startDate,
+ projected_end_date: formData.projectedEndDate,
- await authFetch(`${apiURL}/premium/college-profile`, {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- existing_college_debt: formData.studentLoanAmount,
- interest_rate: formData.interestRate,
- loan_term: formData.loanTerm,
- loan_deferral_until_graduation: formData.loanDeferralUntilGraduation,
- academic_calendar: formData.academicCalendar,
- annual_financial_aid: formData.annualFinancialAid,
- tuition: formData.calculatedTuition,
- extra_payment: formData.extraPayment,
- expected_graduation: formData.gradDate,
- program_type: formData.programType,
- credit_hours_per_year: formData.creditHoursPerYear,
- hours_completed: formData.hoursCompleted,
- program_length: formData.programLength,
- college_enrollment_status: formData.inCollege
- ? 'currently_enrolled'
- : 'not_enrolled',
- expected_salary: formData.expectedSalary
- })
- });
+ planned_monthly_expenses: formData.plannedMonthlyExpenses === ''
+ ? null
+ : parseFloat(formData.plannedMonthlyExpenses),
+ planned_monthly_debt_payments: formData.plannedMonthlyDebt === ''
+ ? null
+ : parseFloat(formData.plannedMonthlyDebt),
+ planned_monthly_retirement_contribution: formData.plannedMonthlyRetirement === ''
+ ? null
+ : parseFloat(formData.plannedMonthlyRetirement),
+ planned_monthly_emergency_contribution: formData.plannedMonthlyEmergency === ''
+ ? null
+ : parseFloat(formData.plannedMonthlyEmergency),
+ planned_surplus_emergency_pct: formData.plannedSurplusEmergencyPct === ''
+ ? null
+ : parseFloat(formData.plannedSurplusEmergencyPct),
+ planned_surplus_retirement_pct: formData.plannedSurplusRetirementPct === ''
+ ? null
+ : parseFloat(formData.plannedSurplusRetirementPct),
+ planned_additional_income: formData.plannedAdditionalIncome === ''
+ ? null
+ : parseFloat(formData.plannedAdditionalIncome),
+ };
- // 2) Update local React state so your useEffect triggers re-simulation
- setFinancialProfile((prev) => ({
+ const res = await authFetch(`${apiURL}/premium/career-profile`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ });
+ if (!res.ok) throw new Error(`HTTP ${res.status} - failed to update scenario`);
+
+ // If successful, we can optionally fetch the updated row or just
+ // update local scenario:
+ const data = await res.json();
+ console.log('Scenario upserted:', data);
+
+ // Optionally call setScenario if you want to reflect changes in UI
+ setScenario(prev => ({
...prev,
- current_salary: formData.currentSalary,
- monthly_expenses: formData.monthlyExpenses,
- monthly_debt_payments: formData.monthlyDebtPayments,
- retirement_savings: formData.retirementSavings,
- emergency_fund: formData.emergencySavings,
- retirement_contribution: formData.monthlyRetirementContribution,
- emergency_contribution: formData.monthlyEmergencyContribution,
- extra_cash_emergency_pct: formData.surplusEmergencyAllocation,
- extra_cash_retirement_pct: formData.surplusRetirementAllocation
+ career_name: formData.careerName,
+ status: formData.status,
+ start_date: formData.startDate,
+ projected_end_date: formData.projectedEndDate,
+ planned_monthly_expenses: payload.planned_monthly_expenses,
+ planned_monthly_debt_payments: payload.planned_monthly_debt_payments,
+ planned_monthly_retirement_contribution: payload.planned_monthly_retirement_contribution,
+ planned_monthly_emergency_contribution: payload.planned_monthly_emergency_contribution,
+ planned_surplus_emergency_pct: payload.planned_surplus_emergency_pct,
+ planned_surplus_retirement_pct: payload.planned_surplus_retirement_pct,
+ planned_additional_income: payload.planned_additional_income
}));
- setCollegeProfile((prev) => ({
- ...prev,
- existing_college_debt: formData.studentLoanAmount,
- interest_rate: formData.interestRate,
- loan_term: formData.loanTerm,
- loan_deferral_until_graduation: formData.loanDeferralUntilGraduation,
- academic_calendar: formData.academicCalendar,
- annual_financial_aid: formData.annualFinancialAid,
- tuition: formData.calculatedTuition,
- extra_payment: formData.extraPayment,
- expected_graduation: formData.gradDate,
- program_type: formData.programType,
- credit_hours_per_year: formData.creditHoursPerYear,
- hours_completed: formData.hoursCompleted,
- program_length: formData.programLength,
- college_enrollment_status: formData.inCollege
- ? 'currently_enrolled'
- : 'not_enrolled',
- expected_salary: formData.expectedSalary
- }));
-
- // 3) Close the modal
onClose();
} catch (err) {
console.error('Error saving scenario changes:', err);
- // Optionally show a toast or error UI
+ alert('Failed to save scenario. See console for details.');
}
};
- // If show=false, don't render anything
if (!show) return null;
return (