diff --git a/backend/server3.js b/backend/server3.js index b9bfe92..61e835b 100644 --- a/backend/server3.js +++ b/backend/server3.js @@ -359,7 +359,7 @@ app.post('/api/premium/ai/next-steps', authenticatePremiumUser, async (req, res) const threeYearsFromNow = new Date(now); threeYearsFromNow.setFullYear(threeYearsFromNow.getFullYear() + 3); - const isoThreeYearsFromNow = threeYearsFromNow.toISOString().slice(0, 10); + const isoThreeYearsFromNow = threeYearsFromNow.toISOString().slice(0, 10).slice(0, 10); // 4) Construct ChatGPT messages const messages = [ @@ -2146,7 +2146,7 @@ app.post( SET resume_optimizations_used = 0, resume_limit_reset = ? WHERE id = ? - `, [resetDate.toISOString(), id]); + `, [resetDate.toISOString().slice(0, 10), id]); userProfile.resume_optimizations_used = 0; } @@ -2202,7 +2202,7 @@ app.post( res.json({ optimizedResume, remainingOptimizations, - resetDate: resetDate.toISOString() + resetDate: resetDate.toISOString().slice(0, 10) }); } catch (err) { console.error('Error optimizing resume:', err); @@ -2246,7 +2246,7 @@ app.get('/api/premium/resume/remaining', authenticatePremiumUser, async (req, re SET resume_optimizations_used = 0, resume_limit_reset = ? WHERE id = ? - `, [resetDate.toISOString(), id]); + `, [resetDate.toISOString().slice(0, 10), id]); userProfile.resume_optimizations_used = 0; } diff --git a/src/App.js b/src/App.js index db7f8ec..78862de 100644 --- a/src/App.js +++ b/src/App.js @@ -243,28 +243,45 @@ function App() { {/* 5) Profile */} -
- -
- {/* Account Profile, Financial Profile links */} +
+ + + {/* DROPDOWN MENU FOR PROFILE */} +
+ {/* Account (Links to UserProfile.js) */} + + Account + + + {/* Financial Profile (Links to FinancialProfileForm.js) */} + + Financial Profile + +
-
+ {/* LOGOUT + UPGRADE BUTTONS */} diff --git a/src/components/MilestoneAddModal.js b/src/components/MilestoneAddModal.js index 07b580b..6a99095 100644 --- a/src/components/MilestoneAddModal.js +++ b/src/components/MilestoneAddModal.js @@ -117,8 +117,8 @@ const MilestoneAddModal = ({ end_month: impact.end_month !== null ? parseInt(impact.end_month, 10) : null, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString() + created_at: new Date()..toISOString().slice(0, 10), + updated_at: new Date()..toISOString().slice(0, 10) }) }); } diff --git a/src/components/MilestoneCopyWizard.js b/src/components/MilestoneCopyWizard.js index db449ca..cccc547 100644 --- a/src/components/MilestoneCopyWizard.js +++ b/src/components/MilestoneCopyWizard.js @@ -1,13 +1,30 @@ // src/components/MilestoneCopyWizard.js -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; +import { Button } from './ui/button.js'; +import authFetch from '../utils/authFetch.js'; -export default function MilestoneCopyWizard({ milestone, authFetch, onClose }) { +export default function MilestoneCopyWizard({ milestone, onClose }) { const [scenarios, setScenarios] = useState([]); const [selectedScenarios, setSelectedScenarios] = useState([]); useEffect(() => { - // fetch /api/premium/career-profile/all => setScenarios - }, [authFetch]); + if (!milestone) return; + // 1) load all scenarios + async function loadAllScenarios() { + try { + const resp = await authFetch('/api/premium/career-profile/all'); + if (!resp.ok) { + console.error('Failed to load all scenarios =>', resp.status); + return; + } + const data = await resp.json(); + setScenarios(data.careerProfiles || []); + } catch (err) { + console.error('MilestoneCopyWizard => error loading scenarios:', err); + } + } + loadAllScenarios(); + }, [milestone]); function toggleScenario(id) { setSelectedScenarios((prev) => @@ -16,29 +33,85 @@ export default function MilestoneCopyWizard({ milestone, authFetch, onClose }) { } async function handleCopy() { - // POST => /api/premium/milestone/copy - // with { milestoneId: milestone.id, scenarioIds: selectedScenarios } - // Then onClose(true) + if (!milestone || !selectedScenarios.length) { + onClose(false); + return; + } + try { + // 2) call your copy endpoint + const payload = { + milestoneId: milestone.id, + scenarioIds: selectedScenarios + }; + const res = await authFetch('/api/premium/milestone/copy', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + if (!res.ok) { + const txt = await res.text(); + alert(txt || 'Failed to copy milestone'); + onClose(false); + return; + } + onClose(true); // success + } catch (err) { + console.error('Error copying milestone =>', err); + alert('Error copying milestone'); + onClose(false); + } } if (!milestone) return null; + return ( -
-
-

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 (
setRandomRangeMax(parseFloatOrZero(e.target.value, 0.02))} + onChange={(e) => + setRandomRangeMax(parseFloatOrZero(e.target.value, 0.02)) + } />
)}
- {/* 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})` : ''}
))} -