Milestone hard refresh added.

This commit is contained in:
Josh 2025-04-24 11:53:31 +00:00
parent 0e9a1e605a
commit b080036e6a
5 changed files with 134 additions and 272 deletions

View File

@ -3,13 +3,13 @@ import React, { useEffect, useState, useCallback } from 'react';
const today = new Date(); const today = new Date();
const MilestoneTimeline = ({ export default function MilestoneTimeline({
careerPathId, careerPathId,
authFetch, authFetch,
activeView, activeView,
setActiveView, setActiveView,
onMilestoneUpdated // optional callback if you want the parent to be notified of changes onMilestoneUpdated
}) => { }) {
const [milestones, setMilestones] = useState({ Career: [], Financial: [] }); const [milestones, setMilestones] = useState({ Career: [], Financial: [] });
// "new or edit" milestone form data // "new or edit" milestone form data
@ -22,8 +22,6 @@ const MilestoneTimeline = ({
impacts: [], impacts: [],
isUniversal: 0 isUniversal: 0
}); });
// We'll track which existing impacts are removed so we can do a DELETE if needed
const [impactsToDelete, setImpactsToDelete] = useState([]); const [impactsToDelete, setImpactsToDelete] = useState([]);
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
@ -33,17 +31,17 @@ const MilestoneTimeline = ({
const [showTaskForm, setShowTaskForm] = useState(null); const [showTaskForm, setShowTaskForm] = useState(null);
const [newTask, setNewTask] = useState({ title: '', description: '', due_date: '' }); const [newTask, setNewTask] = useState({ title: '', description: '', due_date: '' });
// For the Copy wizard // The copy wizard
const [scenarios, setScenarios] = useState([]); const [scenarios, setScenarios] = useState([]);
const [copyWizardMilestone, setCopyWizardMilestone] = useState(null); const [copyWizardMilestone, setCopyWizardMilestone] = useState(null);
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// 1) Impact Helper Functions (define them first to avoid scoping errors) // 1) HELPER FUNCTIONS (defined above usage)
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// Insert a new blank impact into newMilestone.impacts // Insert a new blank impact
const addNewImpact = () => { function addNewImpact() {
setNewMilestone(prev => ({ setNewMilestone((prev) => ({
...prev, ...prev,
impacts: [ impacts: [
...prev.impacts, ...prev.impacts,
@ -56,53 +54,33 @@ const MilestoneTimeline = ({
} }
] ]
})); }));
}; }
// Remove an impact from newMilestone.impacts // Remove an impact from newMilestone.impacts
const removeImpact = (idx) => { function removeImpact(idx) {
setNewMilestone(prev => { setNewMilestone((prev) => {
const newImpacts = [...prev.impacts]; const newImpacts = [...prev.impacts];
const removed = newImpacts[idx]; const removed = newImpacts[idx];
if (removed.id) { if (removed && removed.id) {
// queue up for DB DELETE // queue for DB DELETE
setImpactsToDelete(old => [...old, removed.id]); setImpactsToDelete((old) => [...old, removed.id]);
} }
newImpacts.splice(idx, 1); newImpacts.splice(idx, 1);
return { ...prev, impacts: newImpacts }; return { ...prev, impacts: newImpacts };
}); });
}; }
// Update a specific impact property // Update a specific impact property
const updateImpact = (idx, field, value) => { function updateImpact(idx, field, value) {
setNewMilestone(prev => { setNewMilestone((prev) => {
const newImpacts = [...prev.impacts]; const newImpacts = [...prev.impacts];
newImpacts[idx] = { ...newImpacts[idx], [field]: value }; newImpacts[idx] = { ...newImpacts[idx], [field]: value };
return { ...prev, impacts: newImpacts }; return { ...prev, impacts: newImpacts };
}); });
}; }
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// 2) Load scenarios (for copy wizard) // 2) fetchMilestones => local state
// ------------------------------------------------------------------
useEffect(() => {
async function loadScenarios() {
try {
const res = await authFetch('/api/premium/career-profile/all');
if (res.ok) {
const data = await res.json();
setScenarios(data.careerPaths || []);
} else {
console.error('Failed to load scenarios. Status:', res.status);
}
} catch (err) {
console.error('Error loading scenarios for copy wizard:', err);
}
}
loadScenarios();
}, [authFetch]);
// ------------------------------------------------------------------
// 3) Fetch milestones for the current scenario
// ------------------------------------------------------------------ // ------------------------------------------------------------------
const fetchMilestones = useCallback(async () => { const fetchMilestones = useCallback(async () => {
if (!careerPathId) return; if (!careerPathId) return;
@ -114,12 +92,12 @@ const MilestoneTimeline = ({
} }
const data = await res.json(); const data = await res.json();
if (!data.milestones) { if (!data.milestones) {
console.warn('No milestones returned:', data); console.warn('No milestones field in response:', data);
return; return;
} }
const categorized = { Career: [], Financial: [] }; const categorized = { Career: [], Financial: [] };
data.milestones.forEach(m => { data.milestones.forEach((m) => {
if (categorized[m.milestone_type]) { if (categorized[m.milestone_type]) {
categorized[m.milestone_type].push(m); categorized[m.milestone_type].push(m);
} else { } else {
@ -138,9 +116,27 @@ const MilestoneTimeline = ({
}, [fetchMilestones]); }, [fetchMilestones]);
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// 4) "Edit" an existing milestone => load impacts // 3) Load scenarios for copy wizard
// ------------------------------------------------------------------ // ------------------------------------------------------------------
const handleEditMilestone = async (m) => { useEffect(() => {
async function loadScenarios() {
try {
const res = await authFetch('/api/premium/career-profile/all');
if (res.ok) {
const data = await res.json();
setScenarios(data.careerPaths || []);
}
} catch (err) {
console.error('Error loading scenarios for copy wizard:', err);
}
}
loadScenarios();
}, [authFetch]);
// ------------------------------------------------------------------
// 4) Edit Milestone => fetch impacts
// ------------------------------------------------------------------
async function handleEditMilestone(m) {
try { try {
setImpactsToDelete([]); setImpactsToDelete([]);
@ -158,7 +154,7 @@ const MilestoneTimeline = ({
date: m.date || '', date: m.date || '',
progress: m.progress || 0, progress: m.progress || 0,
newSalary: m.new_salary || '', newSalary: m.new_salary || '',
impacts: fetchedImpacts.map(imp => ({ impacts: fetchedImpacts.map((imp) => ({
id: imp.id, id: imp.id,
impact_type: imp.impact_type || 'ONE_TIME', impact_type: imp.impact_type || 'ONE_TIME',
direction: imp.direction || 'subtract', direction: imp.direction || 'subtract',
@ -171,16 +167,15 @@ const MilestoneTimeline = ({
setEditingMilestone(m); setEditingMilestone(m);
setShowForm(true); setShowForm(true);
} catch (err) { } catch (err) {
console.error('Error editing milestone:', err); console.error('Error in handleEditMilestone:', err);
} }
}; }
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// 5) Save (create or update) a milestone => handle impacts if needed // 5) Save (create/update) => handle impacts
// ------------------------------------------------------------------ // ------------------------------------------------------------------
const saveMilestone = async () => { async function saveMilestone() {
if (!activeView) return; if (!activeView) return;
const url = editingMilestone const url = editingMilestone
@ -219,7 +214,6 @@ const MilestoneTimeline = ({
const savedMilestone = await res.json(); const savedMilestone = await res.json();
console.log('Milestone saved/updated:', savedMilestone); console.log('Milestone saved/updated:', savedMilestone);
// If financial => handle impacts
if (activeView === 'Financial') { if (activeView === 'Financial') {
// 1) Delete old impacts // 1) Delete old impacts
for (const impactId of impactsToDelete) { for (const impactId of impactsToDelete) {
@ -232,7 +226,6 @@ const MilestoneTimeline = ({
} }
} }
} }
// 2) Insert/Update new impacts // 2) Insert/Update new impacts
for (let i = 0; i < newMilestone.impacts.length; i++) { for (let i = 0; i < newMilestone.impacts.length; i++) {
const imp = newMilestone.impacts[i]; const imp = newMilestone.impacts[i];
@ -253,7 +246,7 @@ const MilestoneTimeline = ({
}); });
if (!impRes.ok) { if (!impRes.ok) {
const errImp = await impRes.json(); const errImp = await impRes.json();
console.error('Failed updating existing impact:', errImp); console.error('Failed updating impact:', errImp);
} }
} else { } else {
// new => POST // new => POST
@ -277,20 +270,10 @@ const MilestoneTimeline = ({
} }
} }
// optional local state update to avoid re-fetch // Optionally re-fetch or update local
setMilestones((prev) => { await fetchMilestones();
const newState = { ...prev };
if (editingMilestone) {
newState[activeView] = newState[activeView].map(m =>
m.id === editingMilestone.id ? savedMilestone : m
);
} else {
newState[activeView].push(savedMilestone);
}
return newState;
});
// reset the form // reset form
setShowForm(false); setShowForm(false);
setEditingMilestone(null); setEditingMilestone(null);
setNewMilestone({ setNewMilestone({
@ -304,22 +287,18 @@ const MilestoneTimeline = ({
}); });
setImpactsToDelete([]); setImpactsToDelete([]);
// optionally re-fetch from DB
// await fetchMilestones();
if (onMilestoneUpdated) { if (onMilestoneUpdated) {
onMilestoneUpdated(); onMilestoneUpdated();
} }
} catch (err) { } catch (err) {
console.error('Error saving milestone:', err); console.error('Error saving milestone:', err);
} }
}; }
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// 6) addTask => attach a new task to an existing milestone // 6) Add Task
// ------------------------------------------------------------------ // ------------------------------------------------------------------
const addTask = async (milestoneId) => { async function addTask(milestoneId) {
try { try {
const taskPayload = { const taskPayload = {
milestone_id: milestoneId, milestone_id: milestoneId,
@ -343,32 +322,18 @@ const MilestoneTimeline = ({
const createdTask = await res.json(); const createdTask = await res.json();
console.log('Task created:', createdTask); console.log('Task created:', createdTask);
// update local state // Re-fetch so the timeline shows the new task
setMilestones((prev) => { await fetchMilestones();
const newState = { ...prev };
['Career', 'Financial'].forEach((cat) => {
newState[cat] = newState[cat].map((m) => {
if (m.id === milestoneId) {
return {
...m,
tasks: [...(m.tasks || []), createdTask]
};
}
return m;
});
});
return newState;
});
setNewTask({ title: '', description: '', due_date: '' }); setNewTask({ title: '', description: '', due_date: '' });
setShowTaskForm(null); setShowTaskForm(null);
} catch (err) { } catch (err) {
console.error('Error adding task:', err); console.error('Error adding task:', err);
} }
}; }
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// 7) "Copy" wizard -> after copying => re-fetch or local update // 7) Copy Wizard => now with brute force refresh
// ------------------------------------------------------------------ // ------------------------------------------------------------------
function CopyMilestoneWizard({ milestone, scenarios, onClose, authFetch }) { function CopyMilestoneWizard({ milestone, scenarios, onClose, authFetch }) {
const [selectedScenarios, setSelectedScenarios] = useState([]); const [selectedScenarios, setSelectedScenarios] = useState([]);
@ -376,13 +341,9 @@ const MilestoneTimeline = ({
if (!milestone) return null; if (!milestone) return null;
function toggleScenario(scenarioId) { function toggleScenario(scenarioId) {
setSelectedScenarios(prev => { setSelectedScenarios((prev) =>
if (prev.includes(scenarioId)) { prev.includes(scenarioId) ? prev.filter((id) => id !== scenarioId) : [...prev, scenarioId]
return prev.filter(id => id !== scenarioId); );
} else {
return [...prev, scenarioId];
}
});
} }
async function handleCopy() { async function handleCopy() {
@ -397,16 +358,10 @@ const MilestoneTimeline = ({
}); });
if (!res.ok) throw new Error('Failed to copy milestone'); if (!res.ok) throw new Error('Failed to copy milestone');
const data = await res.json(); // Brute force page refresh
console.log('Copied milestone to new scenarios:', data); window.location.reload();
onClose(); // close wizard onClose();
// re-fetch or update local
await fetchMilestones();
if (onMilestoneUpdated) {
onMilestoneUpdated();
}
} catch (err) { } catch (err) {
console.error('Error copying milestone:', err); console.error('Error copying milestone:', err);
} }
@ -418,7 +373,7 @@ const MilestoneTimeline = ({
<h3>Copy Milestone to Other Scenarios</h3> <h3>Copy Milestone to Other Scenarios</h3>
<p>Milestone: <strong>{milestone.title}</strong></p> <p>Milestone: <strong>{milestone.title}</strong></p>
{scenarios.map(s => ( {scenarios.map((s) => (
<div key={s.id}> <div key={s.id}>
<label> <label>
<input <input
@ -441,7 +396,7 @@ const MilestoneTimeline = ({
} }
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// 8) Delete milestone => single or all // 8) handleDelete => also brute force refresh
// ------------------------------------------------------------------ // ------------------------------------------------------------------
async function handleDeleteMilestone(m) { async function handleDeleteMilestone(m) {
if (m.is_universal === 1) { if (m.is_universal === 1) {
@ -458,22 +413,21 @@ const MilestoneTimeline = ({
console.error('Failed removing universal from all. Status:', delAll.status); console.error('Failed removing universal from all. Status:', delAll.status);
return; return;
} }
// re-fetch
await fetchMilestones();
if (onMilestoneUpdated) {
onMilestoneUpdated();
}
} catch (err) { } catch (err) {
console.error('Error deleting universal milestone from all:', err); console.error('Error deleting universal milestone from all:', err);
} }
} else { } else {
// remove from single scenario // remove from single scenario
await deleteSingleMilestone(m); await deleteSingleMilestone(m);
return;
} }
} else { } else {
// normal => single scenario // normal => single scenario
await deleteSingleMilestone(m); await deleteSingleMilestone(m);
} }
// done => brute force
window.location.reload();
} }
async function deleteSingleMilestone(m) { async function deleteSingleMilestone(m) {
@ -483,18 +437,13 @@ const MilestoneTimeline = ({
console.error('Failed to delete single milestone:', delRes.status); console.error('Failed to delete single milestone:', delRes.status);
return; return;
} }
// re-fetch
await fetchMilestones();
if (onMilestoneUpdated) {
onMilestoneUpdated();
}
} catch (err) { } catch (err) {
console.error('Error removing milestone from scenario:', err); console.error('Error removing milestone from scenario:', err);
} }
} }
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// 9) Positioning in the timeline // 9) Render the timeline
// ------------------------------------------------------------------ // ------------------------------------------------------------------
const allMilestonesCombined = [...milestones.Career, ...milestones.Financial]; const allMilestonesCombined = [...milestones.Career, ...milestones.Financial];
const lastDate = allMilestonesCombined.reduce((latest, m) => { const lastDate = allMilestonesCombined.reduce((latest, m) => {
@ -502,22 +451,19 @@ const MilestoneTimeline = ({
return d > latest ? d : latest; return d > latest ? d : latest;
}, today); }, today);
const calcPosition = (dateString) => { function calcPosition(dateString) {
const start = today.getTime(); const start = today.getTime();
const end = lastDate.getTime(); const end = lastDate.getTime();
const dateVal = new Date(dateString).getTime(); const dateVal = new Date(dateString).getTime();
if (end === start) return 0; if (end === start) return 0;
const ratio = (dateVal - start) / (end - start); const ratio = (dateVal - start) / (end - start);
return Math.min(Math.max(ratio * 100, 0), 100); return Math.min(Math.max(ratio * 100, 0), 100);
}; }
// ------------------------------------------------------------------
// Render
// ------------------------------------------------------------------
return ( return (
<div className="milestone-timeline"> <div className="milestone-timeline">
<div className="view-selector"> <div className="view-selector">
{['Career', 'Financial'].map(view => ( {['Career', 'Financial'].map((view) => (
<button <button
key={view} key={view}
className={activeView === view ? 'active' : ''} className={activeView === view ? 'active' : ''}
@ -528,11 +474,10 @@ const MilestoneTimeline = ({
))} ))}
</div> </div>
{/* + New Milestone button */}
<button <button
onClick={() => { onClick={() => {
if (showForm) { if (showForm) {
// Cancel // Cancel form
setShowForm(false); setShowForm(false);
setEditingMilestone(null); setEditingMilestone(null);
setNewMilestone({ setNewMilestone({
@ -555,6 +500,7 @@ const MilestoneTimeline = ({
{showForm && ( {showForm && (
<div className="form"> <div className="form">
{/* Title / Desc / Date / Progress */}
<input <input
type="text" type="text"
placeholder="Title" placeholder="Title"
@ -571,7 +517,9 @@ const MilestoneTimeline = ({
type="date" type="date"
placeholder="Milestone Date" placeholder="Milestone Date"
value={newMilestone.date} value={newMilestone.date}
onChange={(e) => setNewMilestone(prev => ({ ...prev, date: e.target.value }))} onChange={(e) =>
setNewMilestone((prev) => ({ ...prev, date: e.target.value }))
}
/> />
<input <input
type="number" type="number"
@ -579,27 +527,26 @@ const MilestoneTimeline = ({
value={newMilestone.progress === 0 ? '' : newMilestone.progress} value={newMilestone.progress === 0 ? '' : newMilestone.progress}
onChange={(e) => { onChange={(e) => {
const val = e.target.value === '' ? 0 : parseInt(e.target.value, 10); const val = e.target.value === '' ? 0 : parseInt(e.target.value, 10);
setNewMilestone(prev => ({ ...prev, progress: val })); setNewMilestone((prev) => ({ ...prev, progress: val }));
}} }}
/> />
{/* If Financial => newSalary + impacts */}
{activeView === 'Financial' && ( {activeView === 'Financial' && (
<div> <div>
<input <input
type="number" type="number"
placeholder="Full New Salary (e.g. 70000)" placeholder="Full New Salary (e.g., 70000)"
value={newMilestone.newSalary} value={newMilestone.newSalary}
onChange={(e) => setNewMilestone({ ...newMilestone, newSalary: e.target.value })} onChange={(e) => setNewMilestone({ ...newMilestone, newSalary: e.target.value })}
/> />
<p>Enter the full new salary (not just the increase) after the milestone occurs.</p> <p>Enter the full new salary after the milestone occurs.</p>
<div className="impacts-section border p-2 mt-3"> <div className="impacts-section border p-2 mt-3">
<h4>Financial Impacts</h4> <h4>Financial Impacts</h4>
{newMilestone.impacts.map((imp, idx) => ( {newMilestone.impacts.map((imp, idx) => (
<div key={idx} className="impact-item border p-2 my-2"> <div key={idx} className="impact-item border p-2 my-2">
{imp.id && ( {imp.id && <p className="text-xs text-gray-500">Impact ID: {imp.id}</p>}
<p className="text-xs text-gray-500">Impact ID: {imp.id}</p>
)}
<div> <div>
<label>Type: </label> <label>Type: </label>
@ -674,7 +621,7 @@ const MilestoneTimeline = ({
type="checkbox" type="checkbox"
checked={!!newMilestone.isUniversal} checked={!!newMilestone.isUniversal}
onChange={(e) => onChange={(e) =>
setNewMilestone(prev => ({ setNewMilestone((prev) => ({
...prev, ...prev,
isUniversal: e.target.checked ? 1 : 0 isUniversal: e.target.checked ? 1 : 0
})) }))
@ -690,7 +637,7 @@ const MilestoneTimeline = ({
</div> </div>
)} )}
{/* Timeline */} {/* Actual timeline */}
<div className="milestone-timeline-container"> <div className="milestone-timeline-container">
<div className="milestone-timeline-line" /> <div className="milestone-timeline-line" />
@ -736,7 +683,6 @@ const MilestoneTimeline = ({
{showTaskForm === m.id ? 'Cancel Task' : 'Add Task'} {showTaskForm === m.id ? 'Cancel Task' : 'Add Task'}
</button> </button>
{/* Edit, Copy, Delete Buttons */}
<div style={{ marginTop: '0.5rem' }}> <div style={{ marginTop: '0.5rem' }}>
<button onClick={() => handleEditMilestone(m)}>Edit</button> <button onClick={() => handleEditMilestone(m)}>Edit</button>
<button <button
@ -754,7 +700,7 @@ const MilestoneTimeline = ({
</div> </div>
{showTaskForm === m.id && ( {showTaskForm === m.id && (
<div className="task-form"> <div className="task-form" style={{ marginTop: '0.5rem' }}>
<input <input
type="text" type="text"
placeholder="Task Title" placeholder="Task Title"
@ -781,18 +727,14 @@ const MilestoneTimeline = ({
})} })}
</div> </div>
{/* CopyWizard modal if copying */}
{copyWizardMilestone && ( {copyWizardMilestone && (
<CopyMilestoneWizard <CopyMilestoneWizard
milestone={copyWizardMilestone} milestone={copyWizardMilestone}
scenarios={scenarios} scenarios={scenarios}
onClose={() => setCopyWizardMilestone(null)} onClose={() => setCopyWizardMilestone(null)}
authFetch={authFetch} authFetch={authFetch}
onMilestoneUpdated={onMilestoneUpdated}
/> />
)} )}
</div> </div>
); );
}; }
export default MilestoneTimeline;

View File

@ -2,17 +2,13 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import authFetch from '../utils/authFetch.js'; import authFetch from '../utils/authFetch.js';
import ScenarioContainer from './ScenarioContainer.js'; import ScenarioContainer from './ScenarioContainer.js';
import { v4 as uuidv4 } from 'uuid';
export default function MultiScenarioView() { export default function MultiScenarioView() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [financialProfile, setFinancialProfile] = useState(null);
const [scenarios, setScenarios] = useState([]); // each scenario corresponds to a row in career_paths
// For error reporting
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [financialProfile, setFinancialProfile] = useState(null);
const [scenarios, setScenarios] = useState([]); // each scenario is a row in career_paths
// 1) On mount, fetch the users single financial profile + all career_paths.
useEffect(() => { useEffect(() => {
async function loadData() { async function loadData() {
setLoading(true); setLoading(true);
@ -40,10 +36,9 @@ export default function MultiScenarioView() {
loadData(); loadData();
}, []); }, []);
// 2) “Add Scenario” => create a brand new row in career_paths // “Add Scenario” => create a brand new row in career_paths
async function handleAddScenario() { async function handleAddScenario() {
try { try {
// You might prompt user for a scenario name, or just default
const body = { const body = {
career_name: 'New Scenario ' + new Date().toLocaleDateString(), career_name: 'New Scenario ' + new Date().toLocaleDateString(),
status: 'planned', status: 'planned',
@ -58,11 +53,10 @@ export default function MultiScenarioView() {
}); });
if (!res.ok) throw new Error(`Add scenario error: ${res.status}`); if (!res.ok) throw new Error(`Add scenario error: ${res.status}`);
const data = await res.json(); const data = await res.json();
// re-fetch scenarios or just push a new scenario object
const newRow = { const newRow = {
id: data.career_path_id, id: data.career_path_id,
user_id: null, // we can skip if not needed user_id: null,
career_name: body.career_name, career_name: body.career_name,
status: body.status, status: body.status,
start_date: body.start_date, start_date: body.start_date,
@ -70,18 +64,16 @@ export default function MultiScenarioView() {
college_enrollment_status: body.college_enrollment_status, college_enrollment_status: body.college_enrollment_status,
currently_working: body.currently_working currently_working: body.currently_working
}; };
setScenarios(prev => [...prev, newRow]); setScenarios((prev) => [...prev, newRow]);
} catch (err) { } catch (err) {
console.error('Failed adding scenario:', err); console.error('Failed adding scenario:', err);
alert('Could not add scenario'); alert('Could not add scenario');
} }
} }
// 3) “Clone” => POST a new row in career_paths with copied fields // “Clone” => create a new row in career_paths with copied fields
async function handleCloneScenario(sourceScenario) { async function handleCloneScenario(sourceScenario) {
try { try {
// A simple approach: just create a new row with the same fields
// Then copy the existing scenario fields
const body = { const body = {
career_name: sourceScenario.career_name + ' (Copy)', career_name: sourceScenario.career_name + ' (Copy)',
status: sourceScenario.status, status: sourceScenario.status,
@ -98,15 +90,8 @@ export default function MultiScenarioView() {
if (!res.ok) throw new Error(`Clone scenario error: ${res.status}`); if (!res.ok) throw new Error(`Clone scenario error: ${res.status}`);
const data = await res.json(); const data = await res.json();
const newScenarioId = data.career_path_id;
// Optionally, also clone the scenarios milestones, if you want them duplicated:
// (Youd fetch all existing milestones for sourceScenario, then re-insert them for newScenario.)
// This example just leaves that out for brevity.
// Add it to local state
const newRow = { const newRow = {
id: newScenarioId, id: data.career_path_id,
career_name: body.career_name, career_name: body.career_name,
status: body.status, status: body.status,
start_date: body.start_date, start_date: body.start_date,
@ -114,21 +99,18 @@ export default function MultiScenarioView() {
college_enrollment_status: body.college_enrollment_status, college_enrollment_status: body.college_enrollment_status,
currently_working: body.currently_working currently_working: body.currently_working
}; };
setScenarios(prev => [...prev, newRow]); setScenarios((prev) => [...prev, newRow]);
} catch (err) { } catch (err) {
console.error('Failed cloning scenario:', err); console.error('Failed cloning scenario:', err);
alert('Could not clone scenario'); alert('Could not clone scenario');
} }
} }
// 4) “Remove” => (If you want a delete scenario) // “Remove” => possibly remove from DB or just local
async function handleRemoveScenario(scenarioId) { async function handleRemoveScenario(scenarioId) {
try { try {
// If you have a real DELETE endpoint for career_paths, use it: setScenarios((prev) => prev.filter((s) => s.id !== scenarioId));
// For now, well just remove from the local UI: // Optionally do an API call: DELETE /api/premium/career-profile/:id
setScenarios(prev => prev.filter(s => s.id !== scenarioId));
// Optionally, implement an API call:
// await authFetch(`/api/premium/career-profile/${scenarioId}`, { method: 'DELETE' });
} catch (err) { } catch (err) {
console.error('Failed removing scenario:', err); console.error('Failed removing scenario:', err);
alert('Could not remove scenario'); alert('Could not remove scenario');
@ -144,13 +126,9 @@ export default function MultiScenarioView() {
<ScenarioContainer <ScenarioContainer
key={scen.id} key={scen.id}
scenario={scen} scenario={scen}
financialProfile={financialProfile} // shared for all scenarios financialProfile={financialProfile} // shared for all
onClone={() => handleCloneScenario(scen)} onClone={() => handleCloneScenario(scen)}
onRemove={() => handleRemoveScenario(scen.id)} onRemove={() => handleRemoveScenario(scen.id)}
// Optionally refresh the scenario if user changes it
onScenarioUpdated={(updated) => {
setScenarios(prev => prev.map(s => s.id === scen.id ? { ...s, ...updated } : s));
}}
/> />
))} ))}

View File

@ -8,18 +8,14 @@ import AISuggestedMilestones from './AISuggestedMilestones.js';
import authFetch from '../utils/authFetch.js'; import authFetch from '../utils/authFetch.js';
export default function ScenarioContainer({ export default function ScenarioContainer({
scenario, // from career_paths row scenario, // from career_paths row
financialProfile, // single row, shared across user financialProfile, // single row, shared across user
onClone, onClone,
onRemove, onRemove
onScenarioUpdated
}) { }) {
const [localScenario, setLocalScenario] = useState(scenario); const [localScenario, setLocalScenario] = useState(scenario);
const [collegeProfile, setCollegeProfile] = useState(null); const [collegeProfile, setCollegeProfile] = useState(null);
const [milestones, setMilestones] = useState([]);
const [universalMilestones, setUniversalMilestones] = useState([]);
const [projectionData, setProjectionData] = useState([]); const [projectionData, setProjectionData] = useState([]);
const [loanPaidOffMonth, setLoanPaidOffMonth] = useState(null); const [loanPaidOffMonth, setLoanPaidOffMonth] = useState(null);
@ -35,9 +31,7 @@ export default function ScenarioContainer({
if (!localScenario?.id) return; if (!localScenario?.id) return;
async function loadCollegeProfile() { async function loadCollegeProfile() {
try { try {
const res = await authFetch( const res = await authFetch(`/api/premium/college-profile?careerPathId=${localScenario.id}`);
`/api/premium/college-profile?careerPathId=${localScenario.id}`
);
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
setCollegeProfile(data); setCollegeProfile(data);
@ -52,62 +46,23 @@ export default function ScenarioContainer({
loadCollegeProfile(); loadCollegeProfile();
}, [localScenario]); }, [localScenario]);
// 2) Fetch scenarios milestones (and universal) // 2) Whenever we have financialProfile + collegeProfile => run the simulation
useEffect(() => {
if (!localScenario?.id) return;
async function loadMilestones() {
try {
const [scenRes, uniRes] = await Promise.all([
authFetch(`/api/premium/milestones?careerPathId=${localScenario.id}`),
authFetch(`/api/premium/milestones?careerPathId=universal`) // if you have that route
]);
let scenarioData = scenRes.ok ? (await scenRes.json()) : { milestones: [] };
let universalData = uniRes.ok ? (await uniRes.json()) : { milestones: [] };
setMilestones(scenarioData.milestones || []);
setUniversalMilestones(universalData.milestones || []);
} catch (err) {
console.error('Failed to load milestones:', err);
}
}
loadMilestones();
}, [localScenario]);
// 3) Merge real snapshot + scenario overrides => run simulation
useEffect(() => { useEffect(() => {
if (!financialProfile || !collegeProfile) return; if (!financialProfile || !collegeProfile) return;
// Merge the scenario's planned overrides if not null, // Merge them into the userProfile object for the simulator:
// else fallback to the real snapshot in financialProfile
const mergedProfile = { const mergedProfile = {
currentSalary: financialProfile.current_salary || 0, currentSalary: financialProfile.current_salary || 0,
monthlyExpenses: monthlyExpenses: financialProfile.monthly_expenses || 0,
localScenario.planned_monthly_expenses ?? financialProfile.monthly_expenses ?? 0, monthlyDebtPayments: financialProfile.monthly_debt_payments || 0,
monthlyDebtPayments: retirementSavings: financialProfile.retirement_savings || 0,
localScenario.planned_monthly_debt_payments ?? financialProfile.monthly_debt_payments ?? 0, emergencySavings: financialProfile.emergency_fund || 0,
retirementSavings: financialProfile.retirement_savings ?? 0, monthlyRetirementContribution: financialProfile.retirement_contribution || 0,
emergencySavings: financialProfile.emergency_fund ?? 0, monthlyEmergencyContribution: financialProfile.emergency_contribution || 0,
monthlyRetirementContribution: surplusEmergencyAllocation: financialProfile.extra_cash_emergency_pct || 50,
localScenario.planned_monthly_retirement_contribution ?? surplusRetirementAllocation: financialProfile.extra_cash_retirement_pct || 50,
financialProfile.retirement_contribution ??
0,
monthlyEmergencyContribution:
localScenario.planned_monthly_emergency_contribution ??
financialProfile.emergency_contribution ??
0,
surplusEmergencyAllocation:
localScenario.planned_surplus_emergency_pct ??
financialProfile.extra_cash_emergency_pct ??
50,
surplusRetirementAllocation:
localScenario.planned_surplus_retirement_pct ??
financialProfile.extra_cash_retirement_pct ??
50,
additionalIncome:
localScenario.planned_additional_income ?? financialProfile.additional_income ?? 0,
// College fields // College fields (scenario-based)
studentLoanAmount: collegeProfile.existing_college_debt || 0, studentLoanAmount: collegeProfile.existing_college_debt || 0,
interestRate: collegeProfile.interest_rate || 5, interestRate: collegeProfile.interest_rate || 5,
loanTerm: collegeProfile.loan_term || 10, loanTerm: collegeProfile.loan_term || 10,
@ -126,28 +81,16 @@ export default function ScenarioContainer({
collegeProfile.college_enrollment_status === 'prospective_student', collegeProfile.college_enrollment_status === 'prospective_student',
expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary || 0, expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary || 0,
// Flatten scenario + universal milestoneImpacts // milestoneImpacts is fetched & merged in MilestoneTimeline, not here
milestoneImpacts: buildAllImpacts([...milestones, ...universalMilestones]) milestoneImpacts: []
}; };
const { projectionData, loanPaidOffMonth } = // 3) run the simulation
simulateFinancialProjection(mergedProfile); const { projectionData, loanPaidOffMonth } = simulateFinancialProjection(mergedProfile);
setProjectionData(projectionData); setProjectionData(projectionData);
setLoanPaidOffMonth(loanPaidOffMonth); setLoanPaidOffMonth(loanPaidOffMonth);
}, [financialProfile, collegeProfile, localScenario, milestones, universalMilestones]); }, [financialProfile, collegeProfile]);
function buildAllImpacts(allMilestones) {
let impacts = [];
for (let m of allMilestones) {
if (m.impacts) {
impacts.push(...m.impacts);
}
// If new_salary logic is relevant, handle it here
}
return impacts;
}
// Edit => open modal
return ( return (
<div style={{ width: '420px', border: '1px solid #ccc', padding: '1rem' }}> <div style={{ width: '420px', border: '1px solid #ccc', padding: '1rem' }}>
<h3>{localScenario.career_name || 'Untitled Scenario'}</h3> <h3>{localScenario.career_name || 'Untitled Scenario'}</h3>
@ -170,18 +113,18 @@ export default function ScenarioContainer({
<div style={{ marginTop: '0.5rem' }}> <div style={{ marginTop: '0.5rem' }}>
<strong>Loan Paid Off:</strong> {loanPaidOffMonth || 'N/A'} <br /> <strong>Loan Paid Off:</strong> {loanPaidOffMonth || 'N/A'} <br />
<strong>Retirement (final):</strong> ${ <strong>Final Retirement:</strong>{' '}
projectionData[projectionData.length - 1]?.retirementSavings?.toFixed(0) || 0 {projectionData[projectionData.length - 1]?.retirementSavings?.toFixed(0) || 0}
}
</div> </div>
{/* The timeline that fetches scenario/universal milestones for display */}
<MilestoneTimeline <MilestoneTimeline
careerPathId={localScenario.id} careerPathId={localScenario.id}
authFetch={authFetch} authFetch={authFetch}
activeView="Financial" activeView="Financial"
setActiveView={() => {}} setActiveView={() => {}}
onMilestoneUpdated={() => { onMilestoneUpdated={() => {
// re-fetch or something // might do scenario changes if you want
}} }}
/> />
@ -203,14 +146,16 @@ export default function ScenarioContainer({
</button> </button>
</div> </div>
{/* Updated ScenarioEditModal that references localScenario + setLocalScenario */} {/* If you do scenario-level editing for planned fields, show scenario edit modal */}
<ScenarioEditModal {editOpen && (
show={editOpen} <ScenarioEditModal
onClose={() => setEditOpen(false)} show={editOpen}
scenario={localScenario} onClose={() => setEditOpen(false)}
setScenario={setLocalScenario} scenario={localScenario}
apiURL="/api" setScenario={setLocalScenario}
/> apiURL="/api"
/>
)}
</div> </div>
); );
} }

View File

@ -226,9 +226,6 @@ export function simulateFinancialProjection(userProfile) {
let wasInDeferral = inCollege && loanDeferralUntilGraduation; let wasInDeferral = inCollege && loanDeferralUntilGraduation;
const graduationDateObj = gradDate ? moment(gradDate).startOf('month') : null; const graduationDateObj = gradDate ? moment(gradDate).startOf('month') : null;
console.log('simulateFinancialProjection - monthly tax approach');
console.log('scenarioStartClamped:', scenarioStartClamped.format('YYYY-MM-DD'));
/*************************************************** /***************************************************
* 7) THE MONTHLY LOOP * 7) THE MONTHLY LOOP
***************************************************/ ***************************************************/

Binary file not shown.