-
Copy: {milestone.title}
- {scenarios.map((s) => (
-
- ))}
-
-
-
+
+
+
Copy Milestone:
+
{milestone.title}
+
+
+ {scenarios.length === 0 &&
No scenarios found.
}
+ {scenarios.map((s) => (
+
+
+
+ ))}
+
+
+
+
+
+
);
diff --git a/src/components/MilestoneTracker.js b/src/components/MilestoneTracker.js
index 50d34af..3af07f7 100644
--- a/src/components/MilestoneTracker.js
+++ b/src/components/MilestoneTracker.js
@@ -642,7 +642,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
programLength: collegeData.programLength,
expectedSalary: collegeData.expectedSalary,
- startDate: new Date().toISOString(),
+ startDate: new Date().toISOString().slice(0, 10),
simulationYears,
milestoneImpacts: allImpacts,
@@ -703,7 +703,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
const [clickCount, setClickCount] = useState(() => {
const storedCount = localStorage.getItem('aiClickCount');
const storedDate = localStorage.getItem('aiClickDate');
- const today = new Date().toISOString().slice(0, 10);
+ const today = new Date().toISOString().slice(0, 10).slice(0, 10);
if (storedDate !== today) {
localStorage.setItem('aiClickDate', today);
localStorage.setItem('aiClickCount', '0');
diff --git a/src/components/MultiScenarioView.js b/src/components/MultiScenarioView.js
index d1f0700..6a26c60 100644
--- a/src/components/MultiScenarioView.js
+++ b/src/components/MultiScenarioView.js
@@ -7,31 +7,21 @@ import { Button } from './ui/button.js';
export default function MultiScenarioView() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
-
- // The user’s single overall financial profile
const [financialProfile, setFinancialProfile] = useState(null);
-
- // The list of scenario "headers" (rows from career_profiles)
const [scenarios, setScenarios] = useState([]);
useEffect(() => {
loadScenariosAndFinancial();
}, []);
- /**
- * Fetch user’s financial profile + scenario list
- */
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();
@@ -39,58 +29,57 @@ export default function MultiScenarioView() {
setFinancialProfile(finData);
setScenarios(scenData.careerProfiles || []);
} catch (err) {
- console.error('MultiScenarioView load error:', err);
- setError(err.message || 'Failed to load multi-scenarios');
+ console.error('MultiScenarioView =>', err);
+ setError(err.message || 'Failed to load');
} finally {
setLoading(false);
}
}
- /**
- * Create a brand-new scenario with minimal defaults
- */
async function handleAddScenario() {
try {
const body = {
career_name: 'New Scenario ' + new Date().toLocaleDateString(),
status: 'planned',
- start_date: new Date().toISOString(),
+ // slice(0,10) to avoid timestamps
+ start_date: new Date().toISOString().slice(0, 10),
college_enrollment_status: 'not_enrolled',
currently_working: 'no'
};
-
- const res = await authFetch('/api/premium/career-profile', {
+ const r = await authFetch('/api/premium/career-profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
- if (!res.ok) throw new Error(`Add scenario error: ${res.status}`);
-
- // reload
+ if (!r.ok) throw new Error(`Add scenario error => ${r.status}`);
await loadScenariosAndFinancial();
} catch (err) {
alert(err.message);
}
}
- /**
- * Clone a scenario:
- * (A) create new scenario row from old scenario fields
- * (B) also clone old scenario’s college_profile
- */
async function handleCloneScenario(oldScenario) {
try {
- // 1) create the new scenario row
+ // convert oldScenario.start_date to just YYYY-MM-DD
+ const cloneStart = oldScenario.start_date
+ ? oldScenario.start_date.slice(0, 10)
+ : new Date().toISOString().slice(0, 10);
+
const scenarioPayload = {
scenario_title: oldScenario.scenario_title
? oldScenario.scenario_title + ' (Copy)'
- : null,
+ : 'Untitled (Copy)',
career_name: oldScenario.career_name
? oldScenario.career_name + ' (Copy)'
- : 'Untitled (Copy)',
+ : 'Unknown Career',
status: oldScenario.status,
- start_date: oldScenario.start_date,
- projected_end_date: oldScenario.projected_end_date,
+ // also do the slice if projected_end_date is set
+ start_date: oldScenario.start_date
+ ? oldScenario.start_date.slice(0, 10)
+ : '',
+ projected_end_date: oldScenario.projected_end_date
+ ? oldScenario.projected_end_date.slice(0, 10)
+ : '',
college_enrollment_status: oldScenario.college_enrollment_status,
currently_working: oldScenario.currently_working || 'no',
@@ -106,107 +95,195 @@ export default function MultiScenarioView() {
planned_additional_income: oldScenario.planned_additional_income
};
- const res = await authFetch('/api/premium/career-profile', {
+ const createRes = await authFetch('/api/premium/career-profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(scenarioPayload)
});
- if (!res.ok) throw new Error(`Clone scenario error: ${res.status}`);
-
- // parse the newly created scenario_id
- const newScenarioData = await res.json();
+ if (!createRes.ok) {
+ throw new Error(`Clone scenario error: ${createRes.status}`);
+ }
+ const newScenarioData = await createRes.json();
const newScenarioId = newScenarioData.career_profile_id;
- // 2) Clone the old scenario’s college_profile => new scenario
+ // clone college
await cloneCollegeProfile(oldScenario.id, newScenarioId);
- // 3) reload
+ // clone milestones
+ await cloneAllMilestones(oldScenario.id, newScenarioId);
+
await loadScenariosAndFinancial();
} catch (err) {
- alert(`Clone scenario failed: ${err.message}`);
+ alert('Failed to clone scenario => ' + err.message);
}
}
- async function cloneCollegeProfile(oldScenarioId, newScenarioId) {
+ async function cloneCollegeProfile(oldId, newId) {
try {
- // fetch old scenario’s college_profile
- const getRes = await authFetch(
- `/api/premium/college-profile?careerProfileId=${oldScenarioId}`
- );
- if (!getRes.ok) {
- console.warn(
- 'Could not fetch old college profile for scenarioId=' + oldScenarioId
- );
- return;
- }
+ const cRes = await authFetch(`/api/premium/college-profile?careerProfileId=${oldId}`);
+ if (!cRes.ok) return;
+ let oldC = await cRes.json();
+ if (Array.isArray(oldC)) oldC = oldC[0] || null;
+ if (!oldC || !oldC.id) return;
- let oldCollegeData = await getRes.json();
- if (Array.isArray(oldCollegeData)) {
- oldCollegeData = oldCollegeData[0] || null;
- }
-
- if (!oldCollegeData || !oldCollegeData.id) {
- // no old college profile => nothing to clone
- return;
- }
-
- // build new payload
- const clonePayload = {
- career_profile_id: newScenarioId,
-
- selected_school: oldCollegeData.selected_school,
- selected_program: oldCollegeData.selected_program,
- program_type: oldCollegeData.program_type,
- academic_calendar: oldCollegeData.academic_calendar,
-
- is_in_state: oldCollegeData.is_in_state,
- is_in_district: oldCollegeData.is_in_district,
- is_online: oldCollegeData.is_online,
- college_enrollment_status: oldCollegeData.college_enrollment_status,
-
- annual_financial_aid: oldCollegeData.annual_financial_aid,
- existing_college_debt: oldCollegeData.existing_college_debt,
- tuition_paid: oldCollegeData.tuition_paid,
- tuition: oldCollegeData.tuition,
- loan_deferral_until_graduation: oldCollegeData.loan_deferral_until_graduation,
- loan_term: oldCollegeData.loan_term,
- interest_rate: oldCollegeData.interest_rate,
- extra_payment: oldCollegeData.extra_payment,
-
- credit_hours_per_year: oldCollegeData.credit_hours_per_year,
- hours_completed: oldCollegeData.hours_completed,
- program_length: oldCollegeData.program_length,
- credit_hours_required: oldCollegeData.credit_hours_required,
- expected_graduation: oldCollegeData.expected_graduation,
- expected_salary: oldCollegeData.expected_salary
+ // you can do date-slice on expected_graduation if needed
+ const pay = {
+ career_profile_id: newId,
+ selected_school: oldC.selected_school,
+ selected_program: oldC.selected_program,
+ program_type: oldC.program_type,
+ academic_calendar: oldC.academic_calendar,
+ is_in_state: oldC.is_in_state,
+ is_in_district: oldC.is_in_district,
+ is_online: oldC.is_online,
+ college_enrollment_status: oldC.college_enrollment_status,
+ annual_financial_aid: oldC.annual_financial_aid,
+ existing_college_debt: oldC.existing_college_debt,
+ tuition_paid: oldC.tuition_paid,
+ tuition: oldC.tuition,
+ loan_deferral_until_graduation: oldC.loan_deferral_until_graduation,
+ loan_term: oldC.loan_term,
+ interest_rate: oldC.interest_rate,
+ extra_payment: oldC.extra_payment,
+ credit_hours_per_year: oldC.credit_hours_per_year,
+ hours_completed: oldC.hours_completed,
+ program_length: oldC.program_length,
+ credit_hours_required: oldC.credit_hours_required,
+ expected_graduation: oldC.expected_graduation
+ ? oldC.expected_graduation.slice(0, 10)
+ : '',
+ expected_salary: oldC.expected_salary
};
-
- // insert new row in college_profiles
- const postRes = await authFetch('/api/premium/college-profile', {
+ const pRes = await authFetch('/api/premium/college-profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(clonePayload)
+ body: JSON.stringify(pay)
});
- if (!postRes.ok) {
- console.warn(
- 'Could not clone old collegeProfile => new scenario',
- postRes.status
- );
+ if (!pRes.ok) {
+ console.warn('Clone college failed =>', pRes.status);
}
} catch (err) {
- console.error('Error cloning college profile:', err);
+ console.error('cloneCollegeProfile =>', err);
+ }
+ }
+
+ async function cloneAllMilestones(oldId, newId) {
+ try {
+ const mRes = await authFetch(
+ `/api/premium/milestones?careerProfileId=${oldId}`
+ );
+ if (!mRes.ok) {
+ console.warn('No old milestones => skip');
+ return;
+ }
+ const d = await mRes.json();
+ const oldList = d.milestones || [];
+ for (const m of oldList) {
+ // create new milestone
+ const newMileId = await cloneSingleMilestone(m, newId);
+ // tasks
+ await cloneTasks(m.id, newMileId);
+ }
+ } catch (err) {
+ console.error('cloneAllMilestones =>', err);
+ }
+ }
+ async function cloneSingleMilestone(oldM, newScenarioId) {
+ try {
+ // remove timestamps from oldM.date
+ const justDate = oldM.date ? oldM.date.slice(0, 10) : '';
+ const pay = {
+ title: oldM.title,
+ description: oldM.description,
+ date: justDate,
+ career_profile_id: newScenarioId,
+ progress: oldM.progress,
+ status: oldM.status,
+ is_universal: oldM.is_universal
+ };
+ const r = await authFetch('/api/premium/milestone', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(pay)
+ });
+ if (!r.ok) {
+ console.warn('cloneSingleMilestone =>', r.status);
+ return null;
+ }
+ const j = await r.json();
+ let mid = null;
+ if (Array.isArray(j)) {
+ mid = j[0]?.id || null;
+ } else if (j?.id) {
+ mid = j.id;
+ }
+ // impacts
+ if (mid) {
+ await cloneMilestoneImpacts(oldM.id, mid);
+ }
+ return mid;
+ } catch (err) {
+ console.error('cloneSingleMilestone =>', err);
+ return null;
+ }
+ }
+ async function cloneMilestoneImpacts(oldMId, newMId) {
+ try {
+ const iRes = await authFetch(`/api/premium/milestone-impacts?milestone_id=${oldMId}`);
+ if (!iRes.ok) return;
+ const d = await iRes.json();
+ const arr = d.impacts || [];
+ for (const imp of arr) {
+ const justStart = imp.start_date ? imp.start_date.slice(0, 10) : null;
+ const justEnd = imp.end_date ? imp.end_date.slice(0, 10) : null;
+ const pay = {
+ milestone_id: newMId,
+ impact_type: imp.impact_type,
+ direction: imp.direction,
+ amount: imp.amount,
+ start_date: justStart,
+ end_date: justEnd
+ };
+ await authFetch('/api/premium/milestone-impacts', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(pay)
+ });
+ }
+ } catch (err) {
+ console.error('cloneMilestoneImpacts =>', err);
+ }
+ }
+ async function cloneTasks(oldMId, newMId) {
+ try {
+ const tRes = await authFetch(`/api/premium/tasks?milestone_id=${oldMId}`);
+ if (!tRes.ok) return;
+ const d = await tRes.json();
+ const arr = d.tasks || [];
+ for (const tk of arr) {
+ const pay = {
+ milestone_id: newMId,
+ title: tk.title,
+ description: tk.description,
+ due_date: tk.due_date ? tk.due_date.slice(0, 10) : ''
+ };
+ await authFetch('/api/premium/tasks', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(pay)
+ });
+ }
+ } catch (err) {
+ console.error('cloneTasks =>', err);
}
}
async function handleRemoveScenario(id) {
- const confirmDel = window.confirm('Delete this scenario?');
- if (!confirmDel) return;
-
+ const c = window.confirm('Delete scenario?');
+ if (!c) return;
try {
- const res = await authFetch(`/api/premium/career-profile/${id}`, {
- method: 'DELETE'
- });
- if (!res.ok) throw new Error(`Delete scenario error: ${res.status}`);
+ const r = await authFetch(`/api/premium/career-profile/${id}`, { method: 'DELETE' });
+ if (!r.ok) throw new Error(`Delete scenario => ${r.status}`);
await loadScenariosAndFinancial();
} catch (err) {
alert(err.message);
@@ -216,30 +293,22 @@ export default function MultiScenarioView() {
if (loading) return
Loading scenarios...
;
if (error) return
{error}
;
- // show only first 2 scenarios
- const visibleScenarios = scenarios.slice(0, 2);
+ const visible = scenarios.slice(0, 2);
return (
- {/* Add Scenario button */}
-
-
-
+
- {/* Display 1 or 2 scenarios side by side */}
- {visibleScenarios.map((sc) => (
+ {visible.map(sc => (
{
- console.log('Edit scenario clicked:', sc);
- // or open a modal if you prefer
- }}
- hideMilestones // if you want to hide milestone details
+ onClone={handleCloneScenario}
+ onRemove={handleRemoveScenario}
/>
))}
diff --git a/src/components/PremiumOnboarding/CareerOnboarding.js b/src/components/PremiumOnboarding/CareerOnboarding.js
index aa4a0e2..6411b9e 100644
--- a/src/components/PremiumOnboarding/CareerOnboarding.js
+++ b/src/components/PremiumOnboarding/CareerOnboarding.js
@@ -56,7 +56,7 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
inCollege: isInCollege,
// fallback defaults, or use user-provided
status: prevData.status || 'planned',
- start_date: prevData.start_date || new Date().toISOString().slice(0, 10),
+ start_date: prevData.start_date || new Date().toISOString().slice(0, 10).slice(0, 10),
projected_end_date: prevData.projected_end_date || null
}));
diff --git a/src/components/ScenarioContainer.js b/src/components/ScenarioContainer.js
index 45c5b44..70d2fe8 100644
--- a/src/components/ScenarioContainer.js
+++ b/src/components/ScenarioContainer.js
@@ -8,46 +8,44 @@ import authFetch from '../utils/authFetch.js';
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
import parseFloatOrZero from '../utils/ParseFloatorZero.js';
import ScenarioEditModal from './ScenarioEditModal.js';
+import MilestoneCopyWizard from './MilestoneCopyWizard.js';
-// Register the annotation plugin globally
ChartJS.register(annotationPlugin);
export default function ScenarioContainer({
scenario,
financialProfile,
onRemove,
- onClone,
- onEdit
+ onClone
}) {
- /*************************************************************
- * 1) States
- *************************************************************/
+ // -------------------------------------------------------------
+ // 1) States
+ // -------------------------------------------------------------
const [allScenarios, setAllScenarios] = useState([]);
const [localScenario, setLocalScenario] = useState(scenario || null);
- // scenario edit modal
const [showScenarioModal, setShowScenarioModal] = useState(false);
- // college + milestones
+ // Data from DB for college + milestones
const [collegeProfile, setCollegeProfile] = useState(null);
const [milestones, setMilestones] = useState([]);
const [impactsByMilestone, setImpactsByMilestone] = useState({});
- // projection
+ // Projection
const [projectionData, setProjectionData] = useState([]);
const [loanPaidOffMonth, setLoanPaidOffMonth] = useState(null);
const [simulationYearsInput, setSimulationYearsInput] = useState('20');
- // interest
+ // Interest
const [interestStrategy, setInterestStrategy] = useState('NONE');
const [flatAnnualRate, setFlatAnnualRate] = useState(0.06);
const [randomRangeMin, setRandomRangeMin] = useState(-0.02);
const [randomRangeMax, setRandomRangeMax] = useState(0.02);
- // milestone modal
+ // The “milestone modal” for editing
const [showMilestoneModal, setShowMilestoneModal] = useState(false);
- // local milestone states
+ // Tasks
const [showTaskForm, setShowTaskForm] = useState(null);
const [editingTask, setEditingTask] = useState({
id: null,
@@ -56,16 +54,12 @@ export default function ScenarioContainer({
due_date: ''
});
- // big form for adding/editing a milestone => we do "inline" in the box
+ // “Inline editing” of existing milestone
const [editingMilestoneId, setEditingMilestoneId] = useState(null);
- const [newMilestoneMap, setNewMilestoneMap] = useState({});
- // This is a map of milestoneId => milestoneData so each milestone is edited in place
- // If a milestoneId is "new", we handle it as well.
+ const [newMilestoneMap, setNewMilestoneMap] = useState({});
+ const [impactsToDeleteMap, setImpactsToDeleteMap] = useState({});
- // copy wizard
- const [copyWizardMilestone, setCopyWizardMilestone] = useState(null);
-
- // For new "Add Milestone" flow
+ // For brand-new milestone creation
const [addingNewMilestone, setAddingNewMilestone] = useState(false);
const [newMilestoneData, setNewMilestoneData] = useState({
title: '',
@@ -73,14 +67,16 @@ export default function ScenarioContainer({
date: '',
progress: 0,
newSalary: '',
- impacts: [],
+ impacts: [], // same structure as your “impacts” arrays
isUniversal: 0
});
- const [impactsToDeleteMap, setImpactsToDeleteMap] = useState({});
- /*************************************************************
- * 2) Load scenario list
- *************************************************************/
+ // The “Copy Wizard”
+ const [copyWizardMilestone, setCopyWizardMilestone] = useState(null);
+
+ // -------------------------------------------------------------
+ // 2) Load scenario list
+ // -------------------------------------------------------------
useEffect(() => {
async function loadScenarios() {
try {
@@ -107,9 +103,9 @@ export default function ScenarioContainer({
setLocalScenario(found || null);
}
- /*************************************************************
- * 3) College + Milestones
- *************************************************************/
+ // -------------------------------------------------------------
+ // 3) College + Milestones
+ // -------------------------------------------------------------
useEffect(() => {
if (!localScenario?.id) {
setCollegeProfile(null);
@@ -166,7 +162,7 @@ export default function ScenarioContainer({
}
setImpactsByMilestone(impactsData);
- // Reset our editing states so we don't keep old data
+ // reset new creation & inline editing states
setAddingNewMilestone(false);
setNewMilestoneData({
title: '',
@@ -190,9 +186,9 @@ export default function ScenarioContainer({
fetchMilestones();
}, [fetchMilestones]);
- /*************************************************************
- * 4) Simulation
- *************************************************************/
+ // -------------------------------------------------------------
+ // 4) Simulation
+ // -------------------------------------------------------------
useEffect(() => {
if (!financialProfile || !localScenario?.id || !collegeProfile) return;
@@ -202,7 +198,7 @@ export default function ScenarioContainer({
return Number.isNaN(parsed) ? fallbackVal : parsed;
}
- // build financial base
+ // Build financial base
const financialBase = {
currentSalary: parseFloatOrZero(financialProfile.current_salary, 0),
monthlyExpenses: parseFloatOrZero(financialProfile.monthly_expenses, 0),
@@ -215,7 +211,7 @@ export default function ScenarioContainer({
extraCashRetirementPct: parseFloatOrZero(financialProfile.extra_cash_retirement_pct, 50)
};
- // gather milestone impacts
+ // Gather milestoneImpacts
let allImpacts = [];
Object.keys(impactsByMilestone).forEach((mId) => {
allImpacts = allImpacts.concat(impactsByMilestone[mId]);
@@ -255,7 +251,7 @@ export default function ScenarioContainer({
)
};
- // college data
+ // college
const c = collegeProfile;
const collegeData = {
studentLoanAmount: parseFloatOrZero(c.existing_college_debt, 0),
@@ -279,7 +275,6 @@ export default function ScenarioContainer({
parseFloatOrZero(financialProfile.current_salary, 0)
};
- // build mergedProfile
const mergedProfile = {
currentSalary: financialBase.currentSalary,
monthlyExpenses: scenarioOverrides.monthlyExpenses,
@@ -292,7 +287,6 @@ export default function ScenarioContainer({
surplusRetirementAllocation: scenarioOverrides.surplusRetirementAllocation,
additionalIncome: scenarioOverrides.additionalIncome,
- // college
studentLoanAmount: collegeData.studentLoanAmount,
interestRate: collegeData.interestRate,
loanTerm: collegeData.loanTerm,
@@ -309,7 +303,7 @@ export default function ScenarioContainer({
programLength: collegeData.programLength,
expectedSalary: collegeData.expectedSalary,
- startDate: localScenario.start_date || new Date().toISOString(),
+ startDate: (localScenario.start_date || new Date().toISOString().slice(0,10)),
simulationYears: simYears,
milestoneImpacts: allImpacts,
@@ -324,14 +318,13 @@ export default function ScenarioContainer({
simulateFinancialProjection(mergedProfile);
let cumulative = mergedProfile.emergencySavings || 0;
- const finalData = pData.map((monthRow) => {
- cumulative += monthRow.netSavings || 0;
- return { ...monthRow, cumulativeNetSavings: cumulative };
+ const finalData = pData.map((row) => {
+ cumulative += row.netSavings || 0;
+ return { ...row, cumulativeNetSavings: cumulative };
});
setProjectionData(finalData);
setLoanPaidOffMonth(loanPaidOffMonth);
-
}, [
financialProfile,
localScenario,
@@ -344,9 +337,9 @@ export default function ScenarioContainer({
randomRangeMax
]);
- /*************************************************************
- * 5) Chart
- *************************************************************/
+ // -------------------------------------------------------------
+ // 5) Chart
+ // -------------------------------------------------------------
const chartLabels = projectionData.map((p) => p.month);
const hasStudentLoan = projectionData.some((p) => (p.loanBalance ?? 0) > 0);
@@ -391,7 +384,6 @@ export default function ScenarioContainer({
if (hasStudentLoan) chartDatasets.push(loanBalanceData);
chartDatasets.push(totalSavingsData);
- // Build milestone annotations
const milestoneAnnotations = milestones
.map((m) => {
if (!m.date) return null;
@@ -415,8 +407,7 @@ export default function ScenarioContainer({
content: m.title || 'Milestone',
color: 'orange',
position: 'end'
- },
- milestoneObj: m
+ }
};
})
.filter(Boolean);
@@ -429,9 +420,7 @@ export default function ScenarioContainer({
const chartOptions = {
responsive: true,
plugins: {
- annotation: {
- annotations: milestoneAnnotations
- },
+ annotation: { annotations: milestoneAnnotations },
tooltip: {
callbacks: {
label: (ctx) => `${ctx.dataset.label}: ${ctx.formattedValue}`
@@ -448,9 +437,9 @@ export default function ScenarioContainer({
}
};
- /*************************************************************
- * 6) Task Handlers (unchanged from prior code)
- *************************************************************/
+ // -------------------------------------------------------------
+ // 6) Task CRUD
+ // -------------------------------------------------------------
function handleAddTask(milestoneId) {
setShowTaskForm(milestoneId);
setEditingTask({
@@ -490,7 +479,6 @@ export default function ScenarioContainer({
url = `/api/premium/tasks/${editingTask.id}`;
method = 'PUT';
}
-
const res = await authFetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
@@ -529,17 +517,14 @@ export default function ScenarioContainer({
}
}
- /*************************************************************
- * 7) Inline Milestone Editing
- *************************************************************/
+ // -------------------------------------------------------------
+ // 7) Inline Milestone Editing
+ // -------------------------------------------------------------
function handleEditMilestoneInline(m) {
- // This toggles that milestone's edit mode
if (editingMilestoneId === m.id) {
- // if already editing this one => collapse
setEditingMilestoneId(null);
return;
}
- // otherwise fetch impacts, load into newMilestoneMap
loadMilestoneImpacts(m);
}
@@ -551,7 +536,7 @@ export default function ScenarioContainer({
if (impRes.ok) {
const data = await impRes.json();
const fetchedImpacts = data.impacts || [];
- setNewMilestoneMap(prev => ({
+ setNewMilestoneMap((prev) => ({
...prev,
[m.id]: {
title: m.title || '',
@@ -571,7 +556,7 @@ export default function ScenarioContainer({
}
}));
setEditingMilestoneId(m.id);
- setImpactsToDeleteMap(prev => ({ ...prev, [m.id]: [] }));
+ setImpactsToDeleteMap((prev) => ({ ...prev, [m.id]: [] }));
}
} catch (err) {
console.error('Error loading milestone impacts:', err);
@@ -579,47 +564,43 @@ export default function ScenarioContainer({
}
function updateInlineImpact(milestoneId, idx, field, value) {
- setNewMilestoneMap(prev => {
- const clone = { ...prev };
- const item = clone[milestoneId];
+ setNewMilestoneMap((prev) => {
+ const copy = { ...prev };
+ const item = copy[milestoneId];
if (!item) return prev;
const impactsClone = [...item.impacts];
impactsClone[idx] = { ...impactsClone[idx], [field]: value };
- clone[milestoneId] = { ...item, impacts: impactsClone };
- return clone;
+ copy[milestoneId] = { ...item, impacts: impactsClone };
+ return copy;
});
}
function removeInlineImpact(milestoneId, idx) {
- setNewMilestoneMap(prev => {
- const clone = { ...prev };
- const item = clone[milestoneId];
+ setNewMilestoneMap((prev) => {
+ const copy = { ...prev };
+ const item = copy[milestoneId];
if (!item) return prev;
const impactsClone = [...item.impacts];
const removed = impactsClone[idx];
impactsClone.splice(idx, 1);
- // track deletions
- setImpactsToDeleteMap(old => {
+ setImpactsToDeleteMap((old) => {
const sub = old[milestoneId] || [];
if (removed.id) {
- return {
- ...old,
- [milestoneId]: [...sub, removed.id]
- };
+ return { ...old, [milestoneId]: [...sub, removed.id] };
}
return old;
});
- clone[milestoneId] = { ...item, impacts: impactsClone };
- return clone;
+ copy[milestoneId] = { ...item, impacts: impactsClone };
+ return copy;
});
}
function addInlineImpact(milestoneId) {
- setNewMilestoneMap(prev => {
- const clone = { ...prev };
- const item = clone[milestoneId];
+ setNewMilestoneMap((prev) => {
+ const copy = { ...prev };
+ const item = copy[milestoneId];
if (!item) return prev;
const impactsClone = [...item.impacts];
impactsClone.push({
@@ -629,18 +610,14 @@ export default function ScenarioContainer({
start_date: '',
end_date: ''
});
- clone[milestoneId] = { ...item, impacts: impactsClone };
- return clone;
+ copy[milestoneId] = { ...item, impacts: impactsClone };
+ return copy;
});
}
async function saveInlineMilestone(m) {
if (!localScenario?.id) return;
-
const data = newMilestoneMap[m.id];
- const url = `/api/premium/milestones/${m.id}`;
- const method = 'PUT';
-
const payload = {
milestone_type: 'Financial',
title: data.title,
@@ -654,30 +631,28 @@ export default function ScenarioContainer({
};
try {
- const res = await authFetch(url, {
- method,
+ const res = await authFetch(`/api/premium/milestones/${m.id}`, {
+ method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!res.ok) {
- const errData = await res.json();
- alert(errData.error || 'Error saving milestone');
+ const errMsg = await res.text();
+ alert(errMsg || 'Error saving milestone');
return;
}
- const savedMilestone = await res.json();
+ const saved = await res.json();
// handle impacts
const milestoneId = m.id;
- const impactsToDelete = impactsToDeleteMap[milestoneId] || [];
- for (const id of impactsToDelete) {
- await authFetch(`/api/premium/milestone-impacts/${id}`, {
- method: 'DELETE'
- });
+ const toDelete = impactsToDeleteMap[milestoneId] || [];
+ for (const id of toDelete) {
+ await authFetch(`/api/premium/milestone-impacts/${id}`, { method: 'DELETE' });
}
for (let i = 0; i < data.impacts.length; i++) {
const imp = data.impacts[i];
const impPayload = {
- milestone_id: savedMilestone.id,
+ milestone_id: saved.id,
impact_type: imp.impact_type,
direction: imp.direction,
amount: parseFloat(imp.amount) || 0,
@@ -699,10 +674,7 @@ export default function ScenarioContainer({
}
}
- // re-fetch
await fetchMilestones();
-
- // close the inline form
setEditingMilestoneId(null);
} catch (err) {
console.error('Error saving milestone:', err);
@@ -711,25 +683,126 @@ export default function ScenarioContainer({
}
async function handleDeleteMilestone(m) {
- // same as your old logic
if (!localScenario?.id) return;
- // ...
- // or just remove for brevity
+ const confirmDel = window.confirm('Delete milestone?');
+ if (!confirmDel) return;
+ try {
+ const res = await authFetch(`/api/premium/milestones/${m.id}`, { method: 'DELETE' });
+ if (!res.ok) {
+ alert('Failed to delete milestone');
+ return;
+ }
+ await fetchMilestones();
+ } catch (err) {
+ console.error('Error deleting milestone:', err);
+ }
}
- /*************************************************************
- * 8) Scenario editing
- *************************************************************/
+ // -------------------------------------------------------------
+ // 8) BRAND NEW MILESTONE
+ // -------------------------------------------------------------
+ function addNewImpactToNewMilestone() {
+ setNewMilestoneData((prev) => ({
+ ...prev,
+ impacts: [
+ ...prev.impacts,
+ {
+ impact_type: 'ONE_TIME',
+ direction: 'subtract',
+ amount: 0,
+ start_date: '',
+ end_date: ''
+ }
+ ]
+ }));
+ }
+ function removeImpactFromNewMilestone(idx) {
+ setNewMilestoneData((prev) => {
+ const copy = [...prev.impacts];
+ copy.splice(idx, 1);
+ return { ...prev, impacts: copy };
+ });
+ }
+ async function saveNewMilestone() {
+ if (!localScenario?.id) return;
+ if (!newMilestoneData.title.trim() || !newMilestoneData.date.trim()) {
+ alert('Need title and date');
+ return;
+ }
+ const payload = {
+ title: newMilestoneData.title,
+ description: newMilestoneData.description,
+ date: newMilestoneData.date,
+ career_profile_id: localScenario.id,
+ progress: newMilestoneData.progress,
+ status: newMilestoneData.progress >= 100 ? 'completed' : 'planned',
+ is_universal: newMilestoneData.isUniversal || 0
+ };
+ try {
+ // create milestone
+ const createRes = await authFetch('/api/premium/milestone', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ });
+ if (!createRes.ok) {
+ const txt = await createRes.text();
+ alert(txt || 'Failed to create milestone');
+ return;
+ }
+ let created = await createRes.json(); // might be single object or array
+
+ if (Array.isArray(created) && created.length > 0) {
+ created = created[0];
+ } else if (!Array.isArray(created)) {
+ // single object
+ }
+ // handle impacts
+ if (newMilestoneData.impacts.length > 0 && created.id) {
+ for (const imp of newMilestoneData.impacts) {
+ const impPayload = {
+ milestone_id: created.id,
+ 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
+ };
+ await authFetch('/api/premium/milestone-impacts', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(impPayload)
+ });
+ }
+ }
+
+ await fetchMilestones();
+ setAddingNewMilestone(false);
+ setNewMilestoneData({
+ title: '',
+ description: '',
+ date: '',
+ progress: 0,
+ newSalary: '',
+ impacts: [],
+ isUniversal: 0
+ });
+ } catch (err) {
+ console.error('Error creating new milestone =>', err);
+ alert('Error saving new milestone');
+ }
+ }
+
+ // -------------------------------------------------------------
+ // 9) Scenario Edit
+ // -------------------------------------------------------------
function handleEditScenario() {
setShowScenarioModal(true);
}
- function handleScenarioSave(updatedPayload) {
- // update localScenario, or make an API call if you want
- // For demonstration, we'll just console.log
- console.log('Saving scenario to server =>', updatedPayload);
+ function handleScenarioSave(updated) {
+ console.log('TODO => Save scenario', updated);
setShowScenarioModal(false);
}
-
function handleDeleteScenario() {
if (localScenario) onRemove(localScenario.id);
}
@@ -737,9 +810,9 @@ export default function ScenarioContainer({
if (localScenario) onClone(localScenario);
}
- /*************************************************************
- * 9) RENDER
- *************************************************************/
+ // -------------------------------------------------------------
+ // 10) Render
+ // -------------------------------------------------------------
return (
)}
- {/* chart */}
{projectionData.length > 0 && (
@@ -856,10 +934,7 @@ export default function ScenarioContainer({
onSave={handleScenarioSave}
/>
- {/*
- 10) Milestone modal with "inline editing"
- We'll display each milestone, show a progress bar, etc.
- */}
+ {/* The “Milestone Modal” => inline editing + new milestone + copy wizard */}
{showMilestoneModal && (
Edit Milestones
- {/* List existing milestones */}
{milestones.map((m) => {
- // for a progress bar, we can do:
- const progressPct = m.progress || 0;
const hasEditOpen = editingMilestoneId === m.id;
const data = newMilestoneMap[m.id] || null;
return (
-
+
{m.title}
-
{m.description}
-
- Date: {m.date}
-
- {/* simple progress bar */}
-
-
- Progress: {m.progress}%
+
{m.description}
+
+ Date: {m.date}
+
Progress: {m.progress}%
- {/* tasks, as in your old code */}
+ {/* tasks */}
{(m.tasks || []).map((t) => (
- {t.title} {t.description ? ` - ${t.description}` : ''}
+ {t.title}{' '}
+ {t.description ? ` - ${t.description}` : ''}
{t.due_date ? ` (Due: ${t.due_date})` : ''}
))}
-