diff --git a/backend/server3.js b/backend/server3.js
index a319b50..3efd846 100644
--- a/backend/server3.js
+++ b/backend/server3.js
@@ -115,7 +115,7 @@ app.get('/api/premium/career-profile/:careerPathId', authenticatePremiumUser, as
});
// POST a new career profile
-// server3.js
+
app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res) => {
const {
scenario_title,
@@ -143,8 +143,7 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
const newCareerPathId = uuidv4();
const now = new Date().toISOString();
- // Insert or update row in career_paths. We rely on ON CONFLICT(user_id, career_name).
- // If you want a different conflict target, change accordingly.
+ // Upsert via ON CONFLICT(user_id, career_name)
await db.run(`
INSERT INTO career_paths (
id,
@@ -156,7 +155,6 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
projected_end_date,
college_enrollment_status,
currently_working,
-
planned_monthly_expenses,
planned_monthly_debt_payments,
planned_monthly_retirement_contribution,
@@ -164,13 +162,16 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
planned_surplus_emergency_pct,
planned_surplus_retirement_pct,
planned_additional_income,
-
created_at,
updated_at
)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?,
- ?, ?, ?, ?, ?, ?, ?,
- ?, ?)
+ VALUES (
+ ?, ?, ?, ?, ?,
+ ?, ?, ?, ?,
+ ?, ?, ?, ?,
+ ?, ?, ?,
+ ?, ?
+ )
ON CONFLICT(user_id, career_name)
DO UPDATE SET
status = excluded.status,
@@ -189,19 +190,19 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
updated_at = ?
`, [
- newCareerPathId,
- req.userId,
- scenario_title || null,
- career_name,
- status || 'planned',
- start_date || now,
- projected_end_date || null,
- college_enrollment_status || null,
- currently_working || null,
+ // 18 items for the INSERT columns
+ newCareerPathId, // id
+ req.userId, // user_id
+ scenario_title || null, // scenario_title
+ career_name, // career_name
+ status || 'planned', // status
+ start_date || now, // start_date
+ projected_end_date || null, // projected_end_date
+ college_enrollment_status || null, // college_enrollment_status
+ currently_working || null, // currently_working
- // new planned columns
- planned_monthly_expenses ?? null,
- planned_monthly_debt_payments ?? null,
+ planned_monthly_expenses ?? null, // planned_monthly_expenses
+ planned_monthly_debt_payments ?? null, // planned_monthly_debt_payments
planned_monthly_retirement_contribution ?? null,
planned_monthly_emergency_contribution ?? null,
planned_surplus_emergency_pct ?? null,
@@ -210,10 +211,12 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
now, // created_at
now, // updated_at
- now // updated_at on conflict
+
+ // Then 1 more param for "updated_at = ?" in the conflict update
+ now
]);
- // Optionally fetch the row's ID (or entire row) after upsert:
+ // Optionally fetch the row's ID or entire row after upsert
const result = await db.get(`
SELECT id
FROM career_paths
@@ -231,7 +234,6 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
}
});
-// server3.js (or your premium server file)
// Delete a career path (scenario) by ID
app.delete('/api/premium/career-profile/:careerPathId', authenticatePremiumUser, async (req, res) => {
diff --git a/src/components/MilestoneCopyWizard.js b/src/components/MilestoneCopyWizard.js
new file mode 100644
index 0000000..db449ca
--- /dev/null
+++ b/src/components/MilestoneCopyWizard.js
@@ -0,0 +1,45 @@
+// src/components/MilestoneCopyWizard.js
+import React, { useState, useEffect } from 'react';
+
+export default function MilestoneCopyWizard({ milestone, authFetch, onClose }) {
+ const [scenarios, setScenarios] = useState([]);
+ const [selectedScenarios, setSelectedScenarios] = useState([]);
+
+ useEffect(() => {
+ // fetch /api/premium/career-profile/all => setScenarios
+ }, [authFetch]);
+
+ function toggleScenario(id) {
+ setSelectedScenarios((prev) =>
+ prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
+ );
+ }
+
+ async function handleCopy() {
+ // POST => /api/premium/milestone/copy
+ // with { milestoneId: milestone.id, scenarioIds: selectedScenarios }
+ // Then onClose(true)
+ }
+
+ if (!milestone) return null;
+ return (
+
+
+
Copy: {milestone.title}
+ {scenarios.map((s) => (
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/src/components/MultiScenarioView.js b/src/components/MultiScenarioView.js
index 01a5767..b12263e 100644
--- a/src/components/MultiScenarioView.js
+++ b/src/components/MultiScenarioView.js
@@ -1,8 +1,10 @@
-// src/components/MultiScenarioView.js
import React, { useEffect, useState } from 'react';
import authFetch from '../utils/authFetch.js';
import ScenarioContainer from './ScenarioContainer.js';
-import ScenarioEditModal from './ScenarioEditModal.js'; // The floating modal
+
+// This component loads the user's global financial profile
+// plus a list of all scenarios, and renders one
+// for each. It also has the "Add Scenario" and "Clone/Remove" logic.
export default function MultiScenarioView() {
const [loading, setLoading] = useState(false);
@@ -11,44 +13,38 @@ export default function MultiScenarioView() {
// The user’s single overall financial profile
const [financialProfile, setFinancialProfile] = useState(null);
- // All scenario rows (from career_paths)
+ // The list of scenario "headers" (career_paths)
const [scenarios, setScenarios] = useState([]);
- // The scenario we’re currently editing in a top-level modal:
- const [editingScenario, setEditingScenario] = useState(null);
- // The collegeProfile we load for that scenario (passed to edit modal)
- const [editingCollegeProfile, setEditingCollegeProfile] = useState(null);
-
useEffect(() => {
- async function loadData() {
- setLoading(true);
- setError(null);
- try {
- // 1) Fetch the user’s overall financial profile
- const finRes = await authFetch('/api/premium/financial-profile');
- if (!finRes.ok) throw new Error(`FinancialProfile error: ${finRes.status}`);
- const finData = await finRes.json();
-
- // 2) Fetch all scenarios (career_paths)
- const scenRes = await authFetch('/api/premium/career-profile/all');
- if (!scenRes.ok) throw new Error(`Scenarios error: ${scenRes.status}`);
- const scenData = await scenRes.json();
-
- setFinancialProfile(finData);
- setScenarios(scenData.careerPaths || []);
- } catch (err) {
- console.error('Error loading data in MultiScenarioView:', err);
- setError(err.message || 'Failed to load scenarios/financial');
- } finally {
- setLoading(false);
- }
- }
- loadData();
+ loadScenariosAndFinancial();
}, []);
- // ---------------------------
- // Add a new scenario
- // ---------------------------
+ async function loadScenariosAndFinancial() {
+ setLoading(true);
+ setError(null);
+ try {
+ // 1) fetch user’s global financialProfile
+ const finRes = await authFetch('/api/premium/financial-profile');
+ if (!finRes.ok) throw new Error(`FinancialProfile error: ${finRes.status}`);
+ const finData = await finRes.json();
+
+ // 2) fetch scenario list
+ const scenRes = await authFetch('/api/premium/career-profile/all');
+ if (!scenRes.ok) throw new Error(`Scenarios error: ${scenRes.status}`);
+ const scenData = await scenRes.json();
+
+ setFinancialProfile(finData);
+ setScenarios(scenData.careerPaths || []);
+ } catch (err) {
+ console.error('MultiScenarioView load error:', err);
+ setError(err.message || 'Failed to load multi-scenarios');
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ // Add a new scenario => then reload
async function handleAddScenario() {
try {
const body = {
@@ -64,37 +60,30 @@ export default function MultiScenarioView() {
body: JSON.stringify(body)
});
if (!res.ok) throw new Error(`Add scenario error: ${res.status}`);
- const data = await res.json();
-
- // Insert the new row into local state
- const newRow = {
- id: data.career_path_id,
- career_name: body.career_name,
- status: body.status,
- start_date: body.start_date,
- projected_end_date: null,
- college_enrollment_status: body.college_enrollment_status,
- currently_working: body.currently_working
- };
- setScenarios((prev) => [...prev, newRow]);
+ await loadScenariosAndFinancial();
} catch (err) {
- console.error('Failed adding scenario:', err);
- alert(err.message || 'Could not add scenario');
+ alert(err.message);
}
}
- // ---------------------------
- // Clone scenario
- // ---------------------------
- async function handleCloneScenario(sourceScenario) {
+ // Clone a scenario => then reload
+ async function handleCloneScenario(s) {
try {
const body = {
- career_name: sourceScenario.career_name + ' (Copy)',
- status: sourceScenario.status,
- start_date: sourceScenario.start_date,
- projected_end_date: sourceScenario.projected_end_date,
- college_enrollment_status: sourceScenario.college_enrollment_status,
- currently_working: sourceScenario.currently_working
+ scenario_title: s.scenario_title ? s.scenario_title + ' (Copy)' : null,
+ career_name: s.career_name ? s.career_name + ' (Copy)' : 'Untitled (Copy)',
+ status: s.status,
+ start_date: s.start_date,
+ projected_end_date: s.projected_end_date,
+ college_enrollment_status: s.college_enrollment_status,
+ currently_working: s.currently_working,
+ planned_monthly_expenses: s.planned_monthly_expenses,
+ planned_monthly_debt_payments: s.planned_monthly_debt_payments,
+ planned_monthly_retirement_contribution: s.planned_monthly_retirement_contribution,
+ planned_monthly_emergency_contribution: s.planned_monthly_emergency_contribution,
+ planned_surplus_emergency_pct: s.planned_surplus_emergency_pct,
+ planned_surplus_retirement_pct: s.planned_surplus_retirement_pct,
+ planned_additional_income: s.planned_additional_income
};
const res = await authFetch('/api/premium/career-profile', {
method: 'POST',
@@ -102,112 +91,51 @@ export default function MultiScenarioView() {
body: JSON.stringify(body)
});
if (!res.ok) throw new Error(`Clone scenario error: ${res.status}`);
- const data = await res.json();
-
- const newScenarioId = data.career_path_id;
- const newRow = {
- id: newScenarioId,
- career_name: body.career_name,
- status: body.status,
- start_date: body.start_date,
- projected_end_date: body.projected_end_date,
- college_enrollment_status: body.college_enrollment_status,
- currently_working: body.currently_working
- };
- setScenarios((prev) => [...prev, newRow]);
+ await loadScenariosAndFinancial();
} catch (err) {
- console.error('Failed cloning scenario:', err);
- alert(err.message || 'Could not clone scenario');
+ alert(err.message);
}
}
- // ---------------------------
- // Delete scenario
- // ---------------------------
- async function handleRemoveScenario(scenarioId) {
- // confirm
- const confirmDel = window.confirm(
- 'Delete this scenario (and associated collegeProfile/milestones)?'
- );
+ // Remove => reload
+ async function handleRemoveScenario(id) {
+ const confirmDel = window.confirm('Delete this scenario?');
if (!confirmDel) return;
-
try {
- const res = await authFetch(`/api/premium/career-profile/${scenarioId}`, {
+ const res = await authFetch(`/api/premium/career-profile/${id}`, {
method: 'DELETE'
});
if (!res.ok) throw new Error(`Delete scenario error: ${res.status}`);
- // remove from local
- setScenarios((prev) => prev.filter((s) => s.id !== scenarioId));
+ await loadScenariosAndFinancial();
} catch (err) {
- console.error('Delete scenario error:', err);
- alert(err.message || 'Could not delete scenario');
+ alert(err.message);
}
}
- // ---------------------------
- // User clicks "Edit" in ScenarioContainer => we fetch that scenario’s collegeProfile
- // set editingScenario / editingCollegeProfile => modal
- // ---------------------------
- async function handleEditScenario(scenarioObj) {
- if (!scenarioObj?.id) return;
- try {
- // fetch the collegeProfile
- const colResp = await authFetch(`/api/premium/college-profile?careerPathId=${scenarioObj.id}`);
- let colData = {};
- if (colResp.ok) {
- colData = await colResp.json();
- }
- setEditingScenario(scenarioObj);
- setEditingCollegeProfile(colData);
- } catch (err) {
- console.error('Error loading collegeProfile for editing:', err);
- setEditingScenario(scenarioObj);
- setEditingCollegeProfile({});
- }
- }
-
- // Called by on close => we optionally update local scenario
- function handleModalClose(updatedScenario, updatedCollege) {
- if (updatedScenario) {
- setScenarios(prev =>
- prev.map((s) => (s.id === updatedScenario.id ? { ...s, ...updatedScenario } : s))
- );
- }
- // We might not store the updatedCollege in local state unless we want to re-simulate immediately
- // For now, do nothing or re-fetch if needed
- setEditingScenario(null);
- setEditingCollegeProfile(null);
- }
+ // If user wants to "edit" a scenario, we'll pass it down to the container's "onEdit"
+ // or you can open a modal at this level. For now, we rely on the ScenarioContainer
+ // "onEdit" prop if needed.
if (loading) return Loading scenarios...
;
- if (error) return Error: {error}
;
+ if (error) return {error}
;
return (
-
- {scenarios.map((scen) => (
+
+ {scenarios.map(sc => (
handleCloneScenario(scen)}
- onRemove={() => handleRemoveScenario(scen.id)}
- onEdit={() => handleEditScenario(scen)} // new callback
+ onClone={(s) => handleCloneScenario(s)}
+ onRemove={(id) => handleRemoveScenario(id)}
+ onEdit={(sc) => {
+ // Example: open an edit modal or navigate to a scenario editor.
+ console.log('Edit scenario clicked:', sc);
+ }}
/>
))}
-
-
-
-
- {/* The floating modal at the bottom => only if editingScenario != null */}
- {editingScenario && (
-
- )}
+
);
}
diff --git a/src/components/PremiumOnboarding/FinancialOnboarding.js b/src/components/PremiumOnboarding/FinancialOnboarding.js
index f507dbc..6d6da21 100644
--- a/src/components/PremiumOnboarding/FinancialOnboarding.js
+++ b/src/components/PremiumOnboarding/FinancialOnboarding.js
@@ -1,6 +1,6 @@
import React from 'react';
-const FinancialOnboarding = ({ nextStep, prevStep, data, setData }) => {
+const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = false }) => {
const {
currently_working = '',
current_salary = 0,
@@ -12,7 +12,15 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData }) => {
emergency_fund = 0,
emergency_contribution = 0,
extra_cash_emergency_pct = "",
- extra_cash_retirement_pct = ""
+ extra_cash_retirement_pct = "",
+
+ planned_monthly_expenses = '',
+ planned_monthly_debt_payments = '',
+ planned_monthly_retirement_contribution = '',
+ planned_monthly_emergency_contribution = '',
+ planned_surplus_emergency_pct = '',
+ planned_surplus_retirement_pct = '',
+ planned_additional_income = ''
} = data;
const handleChange = (e) => {
@@ -134,6 +142,70 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData }) => {
onChange={handleChange}
/>
+{/* Only show the planned overrides if isEditMode is true */}
+{isEditMode && (
+ <>
+
+
Planned Scenario Overrides
+
These fields let you override your real finances for this scenario.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
diff --git a/src/components/PremiumOnboarding/OnboardingContainer.js b/src/components/PremiumOnboarding/OnboardingContainer.js
index b87353f..244f816 100644
--- a/src/components/PremiumOnboarding/OnboardingContainer.js
+++ b/src/components/PremiumOnboarding/OnboardingContainer.js
@@ -26,23 +26,30 @@ const OnboardingContainer = () => {
// Now we do the final “all done” submission when the user finishes the last step
const handleFinalSubmit = async () => {
try {
- // 1) POST career-profile
+ // Build a scenarioPayload that includes the optional planned_* fields.
+ // We parseFloat them to avoid sending strings, and default to 0 if empty.
+ const scenarioPayload = {
+ ...careerData,
+ planned_monthly_expenses: parseFloat(careerData.planned_monthly_expenses) || 0,
+ planned_monthly_debt_payments: parseFloat(careerData.planned_monthly_debt_payments) || 0,
+ planned_monthly_retirement_contribution: parseFloat(careerData.planned_monthly_retirement_contribution) || 0,
+ planned_monthly_emergency_contribution: parseFloat(careerData.planned_monthly_emergency_contribution) || 0,
+ planned_surplus_emergency_pct: parseFloat(careerData.planned_surplus_emergency_pct) || 0,
+ planned_surplus_retirement_pct: parseFloat(careerData.planned_surplus_retirement_pct) || 0,
+ planned_additional_income: parseFloat(careerData.planned_additional_income) || 0
+ };
+
+ // 1) POST career-profile (scenario)
const careerRes = await authFetch('/api/premium/career-profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(careerData),
+ body: JSON.stringify(scenarioPayload),
});
if (!careerRes.ok) throw new Error('Failed to save career profile');
const careerJson = await careerRes.json();
const { career_path_id } = careerJson;
if (!career_path_id) throw new Error('No career_path_id returned by server');
- const mergedCollegeData = {
- ...collegeData,
- // ensure this field isn’t null
- college_enrollment_status: careerData.college_enrollment_status,
- career_path_id
- };
-
+
// 2) POST financial-profile
const financialRes = await authFetch('/api/premium/financial-profile', {
method: 'POST',
@@ -50,19 +57,20 @@ const OnboardingContainer = () => {
body: JSON.stringify(financialData),
});
if (!financialRes.ok) throw new Error('Failed to save financial profile');
-
+
// 3) POST college-profile (include career_path_id)
- const mergedCollege = {
- ...collegeData,
- college_enrollment_status: careerData.college_enrollment_status,
- career_path_id };
+ const mergedCollege = {
+ ...collegeData,
+ career_path_id,
+ college_enrollment_status: careerData.college_enrollment_status
+ };
const collegeRes = await authFetch('/api/premium/college-profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(mergedCollege),
});
if (!collegeRes.ok) throw new Error('Failed to save college profile');
-
+
// Done => navigate away
navigate('/milestone-tracker');
} catch (err) {
@@ -70,6 +78,7 @@ const OnboardingContainer = () => {
// (optionally show error to user)
}
};
+
const onboardingSteps = [
,
diff --git a/src/components/ScenarioContainer.js b/src/components/ScenarioContainer.js
index ac75a62..611d934 100644
--- a/src/components/ScenarioContainer.js
+++ b/src/components/ScenarioContainer.js
@@ -1,152 +1,1039 @@
-// src/components/ScenarioContainer.js
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
import { Line } from 'react-chartjs-2';
-import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
-import MilestoneTimeline from './MilestoneTimeline.js';
-import AISuggestedMilestones from './AISuggestedMilestones.js';
-import authFetch from '../utils/authFetch.js';
+import { Chart as ChartJS } from 'chart.js';
+import annotationPlugin from 'chartjs-plugin-annotation';
+import authFetch from '../utils/authFetch.js';
+import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
+import AISuggestedMilestones from './AISuggestedMilestones.js';
+import ScenarioEditModal from './ScenarioEditModal.js';
+
+// Register the annotation plugin (though we won't use it for milestone markers).
+ChartJS.register(annotationPlugin);
+
+/**
+ * ScenarioContainer
+ * -----------------
+ * This component:
+ * - Renders a