Milestone hard refresh added.
This commit is contained in:
parent
23ac7260ab
commit
733dba46a8
@ -3,13 +3,13 @@ import React, { useEffect, useState, useCallback } from 'react';
|
||||
|
||||
const today = new Date();
|
||||
|
||||
const MilestoneTimeline = ({
|
||||
export default function MilestoneTimeline({
|
||||
careerPathId,
|
||||
authFetch,
|
||||
activeView,
|
||||
setActiveView,
|
||||
onMilestoneUpdated // optional callback if you want the parent to be notified of changes
|
||||
}) => {
|
||||
onMilestoneUpdated
|
||||
}) {
|
||||
const [milestones, setMilestones] = useState({ Career: [], Financial: [] });
|
||||
|
||||
// "new or edit" milestone form data
|
||||
@ -22,8 +22,6 @@ const MilestoneTimeline = ({
|
||||
impacts: [],
|
||||
isUniversal: 0
|
||||
});
|
||||
|
||||
// We'll track which existing impacts are removed so we can do a DELETE if needed
|
||||
const [impactsToDelete, setImpactsToDelete] = useState([]);
|
||||
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
@ -33,17 +31,17 @@ const MilestoneTimeline = ({
|
||||
const [showTaskForm, setShowTaskForm] = useState(null);
|
||||
const [newTask, setNewTask] = useState({ title: '', description: '', due_date: '' });
|
||||
|
||||
// For the Copy wizard
|
||||
// The copy wizard
|
||||
const [scenarios, setScenarios] = useState([]);
|
||||
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
|
||||
const addNewImpact = () => {
|
||||
setNewMilestone(prev => ({
|
||||
// Insert a new blank impact
|
||||
function addNewImpact() {
|
||||
setNewMilestone((prev) => ({
|
||||
...prev,
|
||||
impacts: [
|
||||
...prev.impacts,
|
||||
@ -56,53 +54,33 @@ const MilestoneTimeline = ({
|
||||
}
|
||||
]
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
// Remove an impact from newMilestone.impacts
|
||||
const removeImpact = (idx) => {
|
||||
setNewMilestone(prev => {
|
||||
function removeImpact(idx) {
|
||||
setNewMilestone((prev) => {
|
||||
const newImpacts = [...prev.impacts];
|
||||
const removed = newImpacts[idx];
|
||||
if (removed.id) {
|
||||
// queue up for DB DELETE
|
||||
setImpactsToDelete(old => [...old, removed.id]);
|
||||
if (removed && removed.id) {
|
||||
// queue for DB DELETE
|
||||
setImpactsToDelete((old) => [...old, removed.id]);
|
||||
}
|
||||
newImpacts.splice(idx, 1);
|
||||
return { ...prev, impacts: newImpacts };
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Update a specific impact property
|
||||
const updateImpact = (idx, field, value) => {
|
||||
setNewMilestone(prev => {
|
||||
function updateImpact(idx, field, value) {
|
||||
setNewMilestone((prev) => {
|
||||
const newImpacts = [...prev.impacts];
|
||||
newImpacts[idx] = { ...newImpacts[idx], [field]: value };
|
||||
return { ...prev, impacts: newImpacts };
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 2) Load scenarios (for copy wizard)
|
||||
// ------------------------------------------------------------------
|
||||
useEffect(() => {
|
||||
async function loadScenarios() {
|
||||
try {
|
||||
const res = await authFetch('/api/premium/career-profile/all');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setScenarios(data.careerPaths || []);
|
||||
} else {
|
||||
console.error('Failed to load scenarios. Status:', res.status);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading scenarios for copy wizard:', err);
|
||||
}
|
||||
}
|
||||
loadScenarios();
|
||||
}, [authFetch]);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 3) Fetch milestones for the current scenario
|
||||
// 2) fetchMilestones => local state
|
||||
// ------------------------------------------------------------------
|
||||
const fetchMilestones = useCallback(async () => {
|
||||
if (!careerPathId) return;
|
||||
@ -114,12 +92,12 @@ const MilestoneTimeline = ({
|
||||
}
|
||||
const data = await res.json();
|
||||
if (!data.milestones) {
|
||||
console.warn('No milestones returned:', data);
|
||||
console.warn('No milestones field in response:', data);
|
||||
return;
|
||||
}
|
||||
|
||||
const categorized = { Career: [], Financial: [] };
|
||||
data.milestones.forEach(m => {
|
||||
data.milestones.forEach((m) => {
|
||||
if (categorized[m.milestone_type]) {
|
||||
categorized[m.milestone_type].push(m);
|
||||
} else {
|
||||
@ -138,9 +116,27 @@ const MilestoneTimeline = ({
|
||||
}, [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 {
|
||||
setImpactsToDelete([]);
|
||||
|
||||
@ -158,7 +154,7 @@ const MilestoneTimeline = ({
|
||||
date: m.date || '',
|
||||
progress: m.progress || 0,
|
||||
newSalary: m.new_salary || '',
|
||||
impacts: fetchedImpacts.map(imp => ({
|
||||
impacts: fetchedImpacts.map((imp) => ({
|
||||
id: imp.id,
|
||||
impact_type: imp.impact_type || 'ONE_TIME',
|
||||
direction: imp.direction || 'subtract',
|
||||
@ -171,16 +167,15 @@ const MilestoneTimeline = ({
|
||||
|
||||
setEditingMilestone(m);
|
||||
setShowForm(true);
|
||||
|
||||
} 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;
|
||||
|
||||
const url = editingMilestone
|
||||
@ -219,7 +214,6 @@ const MilestoneTimeline = ({
|
||||
const savedMilestone = await res.json();
|
||||
console.log('Milestone saved/updated:', savedMilestone);
|
||||
|
||||
// If financial => handle impacts
|
||||
if (activeView === 'Financial') {
|
||||
// 1) Delete old impacts
|
||||
for (const impactId of impactsToDelete) {
|
||||
@ -232,7 +226,6 @@ const MilestoneTimeline = ({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Insert/Update new impacts
|
||||
for (let i = 0; i < newMilestone.impacts.length; i++) {
|
||||
const imp = newMilestone.impacts[i];
|
||||
@ -253,7 +246,7 @@ const MilestoneTimeline = ({
|
||||
});
|
||||
if (!impRes.ok) {
|
||||
const errImp = await impRes.json();
|
||||
console.error('Failed updating existing impact:', errImp);
|
||||
console.error('Failed updating impact:', errImp);
|
||||
}
|
||||
} else {
|
||||
// new => POST
|
||||
@ -277,20 +270,10 @@ const MilestoneTimeline = ({
|
||||
}
|
||||
}
|
||||
|
||||
// optional local state update to avoid re-fetch
|
||||
setMilestones((prev) => {
|
||||
const newState = { ...prev };
|
||||
if (editingMilestone) {
|
||||
newState[activeView] = newState[activeView].map(m =>
|
||||
m.id === editingMilestone.id ? savedMilestone : m
|
||||
);
|
||||
} else {
|
||||
newState[activeView].push(savedMilestone);
|
||||
}
|
||||
return newState;
|
||||
});
|
||||
// Optionally re-fetch or update local
|
||||
await fetchMilestones();
|
||||
|
||||
// reset the form
|
||||
// reset form
|
||||
setShowForm(false);
|
||||
setEditingMilestone(null);
|
||||
setNewMilestone({
|
||||
@ -304,22 +287,18 @@ const MilestoneTimeline = ({
|
||||
});
|
||||
setImpactsToDelete([]);
|
||||
|
||||
// optionally re-fetch from DB
|
||||
// await fetchMilestones();
|
||||
|
||||
if (onMilestoneUpdated) {
|
||||
onMilestoneUpdated();
|
||||
}
|
||||
|
||||
} catch (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 {
|
||||
const taskPayload = {
|
||||
milestone_id: milestoneId,
|
||||
@ -343,32 +322,18 @@ const MilestoneTimeline = ({
|
||||
const createdTask = await res.json();
|
||||
console.log('Task created:', createdTask);
|
||||
|
||||
// update local state
|
||||
setMilestones((prev) => {
|
||||
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;
|
||||
});
|
||||
// Re-fetch so the timeline shows the new task
|
||||
await fetchMilestones();
|
||||
|
||||
setNewTask({ title: '', description: '', due_date: '' });
|
||||
setShowTaskForm(null);
|
||||
} catch (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 }) {
|
||||
const [selectedScenarios, setSelectedScenarios] = useState([]);
|
||||
@ -376,13 +341,9 @@ const MilestoneTimeline = ({
|
||||
if (!milestone) return null;
|
||||
|
||||
function toggleScenario(scenarioId) {
|
||||
setSelectedScenarios(prev => {
|
||||
if (prev.includes(scenarioId)) {
|
||||
return prev.filter(id => id !== scenarioId);
|
||||
} else {
|
||||
return [...prev, scenarioId];
|
||||
}
|
||||
});
|
||||
setSelectedScenarios((prev) =>
|
||||
prev.includes(scenarioId) ? prev.filter((id) => id !== scenarioId) : [...prev, scenarioId]
|
||||
);
|
||||
}
|
||||
|
||||
async function handleCopy() {
|
||||
@ -397,16 +358,10 @@ const MilestoneTimeline = ({
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to copy milestone');
|
||||
|
||||
const data = await res.json();
|
||||
console.log('Copied milestone to new scenarios:', data);
|
||||
// Brute force page refresh
|
||||
window.location.reload();
|
||||
|
||||
onClose(); // close wizard
|
||||
|
||||
// re-fetch or update local
|
||||
await fetchMilestones();
|
||||
if (onMilestoneUpdated) {
|
||||
onMilestoneUpdated();
|
||||
}
|
||||
onClose();
|
||||
} catch (err) {
|
||||
console.error('Error copying milestone:', err);
|
||||
}
|
||||
@ -418,7 +373,7 @@ const MilestoneTimeline = ({
|
||||
<h3>Copy Milestone to Other Scenarios</h3>
|
||||
<p>Milestone: <strong>{milestone.title}</strong></p>
|
||||
|
||||
{scenarios.map(s => (
|
||||
{scenarios.map((s) => (
|
||||
<div key={s.id}>
|
||||
<label>
|
||||
<input
|
||||
@ -441,7 +396,7 @@ const MilestoneTimeline = ({
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 8) Delete milestone => single or all
|
||||
// 8) handleDelete => also brute force refresh
|
||||
// ------------------------------------------------------------------
|
||||
async function handleDeleteMilestone(m) {
|
||||
if (m.is_universal === 1) {
|
||||
@ -458,22 +413,21 @@ const MilestoneTimeline = ({
|
||||
console.error('Failed removing universal from all. Status:', delAll.status);
|
||||
return;
|
||||
}
|
||||
// re-fetch
|
||||
await fetchMilestones();
|
||||
if (onMilestoneUpdated) {
|
||||
onMilestoneUpdated();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error deleting universal milestone from all:', err);
|
||||
}
|
||||
} else {
|
||||
// remove from single scenario
|
||||
await deleteSingleMilestone(m);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// normal => single scenario
|
||||
await deleteSingleMilestone(m);
|
||||
}
|
||||
|
||||
// done => brute force
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
async function deleteSingleMilestone(m) {
|
||||
@ -483,18 +437,13 @@ const MilestoneTimeline = ({
|
||||
console.error('Failed to delete single milestone:', delRes.status);
|
||||
return;
|
||||
}
|
||||
// re-fetch
|
||||
await fetchMilestones();
|
||||
if (onMilestoneUpdated) {
|
||||
onMilestoneUpdated();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error removing milestone from scenario:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 9) Positioning in the timeline
|
||||
// 9) Render the timeline
|
||||
// ------------------------------------------------------------------
|
||||
const allMilestonesCombined = [...milestones.Career, ...milestones.Financial];
|
||||
const lastDate = allMilestonesCombined.reduce((latest, m) => {
|
||||
@ -502,22 +451,19 @@ const MilestoneTimeline = ({
|
||||
return d > latest ? d : latest;
|
||||
}, today);
|
||||
|
||||
const calcPosition = (dateString) => {
|
||||
function calcPosition(dateString) {
|
||||
const start = today.getTime();
|
||||
const end = lastDate.getTime();
|
||||
const dateVal = new Date(dateString).getTime();
|
||||
if (end === start) return 0;
|
||||
const ratio = (dateVal - start) / (end - start);
|
||||
return Math.min(Math.max(ratio * 100, 0), 100);
|
||||
};
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Render
|
||||
// ------------------------------------------------------------------
|
||||
return (
|
||||
<div className="milestone-timeline">
|
||||
<div className="view-selector">
|
||||
{['Career', 'Financial'].map(view => (
|
||||
{['Career', 'Financial'].map((view) => (
|
||||
<button
|
||||
key={view}
|
||||
className={activeView === view ? 'active' : ''}
|
||||
@ -528,11 +474,10 @@ const MilestoneTimeline = ({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* + New Milestone button */}
|
||||
<button
|
||||
onClick={() => {
|
||||
if (showForm) {
|
||||
// Cancel
|
||||
// Cancel form
|
||||
setShowForm(false);
|
||||
setEditingMilestone(null);
|
||||
setNewMilestone({
|
||||
@ -555,6 +500,7 @@ const MilestoneTimeline = ({
|
||||
|
||||
{showForm && (
|
||||
<div className="form">
|
||||
{/* Title / Desc / Date / Progress */}
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Title"
|
||||
@ -571,7 +517,9 @@ const MilestoneTimeline = ({
|
||||
type="date"
|
||||
placeholder="Milestone Date"
|
||||
value={newMilestone.date}
|
||||
onChange={(e) => setNewMilestone(prev => ({ ...prev, date: e.target.value }))}
|
||||
onChange={(e) =>
|
||||
setNewMilestone((prev) => ({ ...prev, date: e.target.value }))
|
||||
}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
@ -579,27 +527,26 @@ const MilestoneTimeline = ({
|
||||
value={newMilestone.progress === 0 ? '' : newMilestone.progress}
|
||||
onChange={(e) => {
|
||||
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' && (
|
||||
<div>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Full New Salary (e.g. 70000)"
|
||||
placeholder="Full New Salary (e.g., 70000)"
|
||||
value={newMilestone.newSalary}
|
||||
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">
|
||||
<h4>Financial Impacts</h4>
|
||||
{newMilestone.impacts.map((imp, idx) => (
|
||||
<div key={idx} className="impact-item border p-2 my-2">
|
||||
{imp.id && (
|
||||
<p className="text-xs text-gray-500">Impact ID: {imp.id}</p>
|
||||
)}
|
||||
{imp.id && <p className="text-xs text-gray-500">Impact ID: {imp.id}</p>}
|
||||
|
||||
<div>
|
||||
<label>Type: </label>
|
||||
@ -674,7 +621,7 @@ const MilestoneTimeline = ({
|
||||
type="checkbox"
|
||||
checked={!!newMilestone.isUniversal}
|
||||
onChange={(e) =>
|
||||
setNewMilestone(prev => ({
|
||||
setNewMilestone((prev) => ({
|
||||
...prev,
|
||||
isUniversal: e.target.checked ? 1 : 0
|
||||
}))
|
||||
@ -690,7 +637,7 @@ const MilestoneTimeline = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timeline */}
|
||||
{/* Actual timeline */}
|
||||
<div className="milestone-timeline-container">
|
||||
<div className="milestone-timeline-line" />
|
||||
|
||||
@ -736,7 +683,6 @@ const MilestoneTimeline = ({
|
||||
{showTaskForm === m.id ? 'Cancel Task' : 'Add Task'}
|
||||
</button>
|
||||
|
||||
{/* Edit, Copy, Delete Buttons */}
|
||||
<div style={{ marginTop: '0.5rem' }}>
|
||||
<button onClick={() => handleEditMilestone(m)}>Edit</button>
|
||||
<button
|
||||
@ -754,7 +700,7 @@ const MilestoneTimeline = ({
|
||||
</div>
|
||||
|
||||
{showTaskForm === m.id && (
|
||||
<div className="task-form">
|
||||
<div className="task-form" style={{ marginTop: '0.5rem' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Task Title"
|
||||
@ -781,18 +727,14 @@ const MilestoneTimeline = ({
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* CopyWizard modal if copying */}
|
||||
{copyWizardMilestone && (
|
||||
<CopyMilestoneWizard
|
||||
milestone={copyWizardMilestone}
|
||||
scenarios={scenarios}
|
||||
onClose={() => setCopyWizardMilestone(null)}
|
||||
authFetch={authFetch}
|
||||
onMilestoneUpdated={onMilestoneUpdated}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MilestoneTimeline;
|
||||
}
|
||||
|
@ -2,17 +2,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import authFetch from '../utils/authFetch.js';
|
||||
import ScenarioContainer from './ScenarioContainer.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export default function MultiScenarioView() {
|
||||
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 [financialProfile, setFinancialProfile] = useState(null);
|
||||
const [scenarios, setScenarios] = useState([]); // each scenario is a row in career_paths
|
||||
|
||||
// 1) On mount, fetch the user’s single financial profile + all career_paths.
|
||||
useEffect(() => {
|
||||
async function loadData() {
|
||||
setLoading(true);
|
||||
@ -40,10 +36,9 @@ export default function MultiScenarioView() {
|
||||
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() {
|
||||
try {
|
||||
// You might prompt user for a scenario name, or just default
|
||||
const body = {
|
||||
career_name: 'New Scenario ' + new Date().toLocaleDateString(),
|
||||
status: 'planned',
|
||||
@ -58,11 +53,10 @@ export default function MultiScenarioView() {
|
||||
});
|
||||
if (!res.ok) throw new Error(`Add scenario error: ${res.status}`);
|
||||
const data = await res.json();
|
||||
|
||||
// re-fetch scenarios or just push a new scenario object
|
||||
|
||||
const newRow = {
|
||||
id: data.career_path_id,
|
||||
user_id: null, // we can skip if not needed
|
||||
user_id: null,
|
||||
career_name: body.career_name,
|
||||
status: body.status,
|
||||
start_date: body.start_date,
|
||||
@ -70,18 +64,16 @@ export default function MultiScenarioView() {
|
||||
college_enrollment_status: body.college_enrollment_status,
|
||||
currently_working: body.currently_working
|
||||
};
|
||||
setScenarios(prev => [...prev, newRow]);
|
||||
setScenarios((prev) => [...prev, newRow]);
|
||||
} catch (err) {
|
||||
console.error('Failed adding scenario:', err);
|
||||
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) {
|
||||
try {
|
||||
// A simple approach: just create a new row with the same fields
|
||||
// Then copy the existing scenario fields
|
||||
const body = {
|
||||
career_name: sourceScenario.career_name + ' (Copy)',
|
||||
status: sourceScenario.status,
|
||||
@ -98,15 +90,8 @@ export default function MultiScenarioView() {
|
||||
if (!res.ok) throw new Error(`Clone scenario error: ${res.status}`);
|
||||
const data = await res.json();
|
||||
|
||||
const newScenarioId = data.career_path_id;
|
||||
|
||||
// Optionally, also clone the scenario’s milestones, if you want them duplicated:
|
||||
// (You’d 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 = {
|
||||
id: newScenarioId,
|
||||
id: data.career_path_id,
|
||||
career_name: body.career_name,
|
||||
status: body.status,
|
||||
start_date: body.start_date,
|
||||
@ -114,21 +99,18 @@ export default function MultiScenarioView() {
|
||||
college_enrollment_status: body.college_enrollment_status,
|
||||
currently_working: body.currently_working
|
||||
};
|
||||
setScenarios(prev => [...prev, newRow]);
|
||||
setScenarios((prev) => [...prev, newRow]);
|
||||
} catch (err) {
|
||||
console.error('Failed cloning scenario:', err);
|
||||
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) {
|
||||
try {
|
||||
// If you have a real DELETE endpoint for career_paths, use it:
|
||||
// For now, we’ll just remove from the local UI:
|
||||
setScenarios(prev => prev.filter(s => s.id !== scenarioId));
|
||||
// Optionally, implement an API call:
|
||||
// await authFetch(`/api/premium/career-profile/${scenarioId}`, { method: 'DELETE' });
|
||||
setScenarios((prev) => prev.filter((s) => s.id !== scenarioId));
|
||||
// Optionally do an API call: DELETE /api/premium/career-profile/:id
|
||||
} catch (err) {
|
||||
console.error('Failed removing scenario:', err);
|
||||
alert('Could not remove scenario');
|
||||
@ -144,13 +126,9 @@ export default function MultiScenarioView() {
|
||||
<ScenarioContainer
|
||||
key={scen.id}
|
||||
scenario={scen}
|
||||
financialProfile={financialProfile} // shared for all scenarios
|
||||
financialProfile={financialProfile} // shared for all
|
||||
onClone={() => handleCloneScenario(scen)}
|
||||
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));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
@ -8,18 +8,14 @@ import AISuggestedMilestones from './AISuggestedMilestones.js';
|
||||
import authFetch from '../utils/authFetch.js';
|
||||
|
||||
export default function ScenarioContainer({
|
||||
scenario, // from career_paths row
|
||||
financialProfile, // single row, shared across user
|
||||
scenario, // from career_paths row
|
||||
financialProfile, // single row, shared across user
|
||||
onClone,
|
||||
onRemove,
|
||||
onScenarioUpdated
|
||||
onRemove
|
||||
}) {
|
||||
const [localScenario, setLocalScenario] = useState(scenario);
|
||||
const [collegeProfile, setCollegeProfile] = useState(null);
|
||||
|
||||
const [milestones, setMilestones] = useState([]);
|
||||
const [universalMilestones, setUniversalMilestones] = useState([]);
|
||||
|
||||
const [projectionData, setProjectionData] = useState([]);
|
||||
const [loanPaidOffMonth, setLoanPaidOffMonth] = useState(null);
|
||||
|
||||
@ -35,9 +31,7 @@ export default function ScenarioContainer({
|
||||
if (!localScenario?.id) return;
|
||||
async function loadCollegeProfile() {
|
||||
try {
|
||||
const res = await authFetch(
|
||||
`/api/premium/college-profile?careerPathId=${localScenario.id}`
|
||||
);
|
||||
const res = await authFetch(`/api/premium/college-profile?careerPathId=${localScenario.id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setCollegeProfile(data);
|
||||
@ -52,62 +46,23 @@ export default function ScenarioContainer({
|
||||
loadCollegeProfile();
|
||||
}, [localScenario]);
|
||||
|
||||
// 2) Fetch scenario’s milestones (and universal)
|
||||
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
|
||||
// 2) Whenever we have financialProfile + collegeProfile => run the simulation
|
||||
useEffect(() => {
|
||||
if (!financialProfile || !collegeProfile) return;
|
||||
|
||||
// Merge the scenario's planned overrides if not null,
|
||||
// else fallback to the real snapshot in financialProfile
|
||||
// Merge them into the userProfile object for the simulator:
|
||||
const mergedProfile = {
|
||||
currentSalary: financialProfile.current_salary || 0,
|
||||
monthlyExpenses:
|
||||
localScenario.planned_monthly_expenses ?? financialProfile.monthly_expenses ?? 0,
|
||||
monthlyDebtPayments:
|
||||
localScenario.planned_monthly_debt_payments ?? financialProfile.monthly_debt_payments ?? 0,
|
||||
retirementSavings: financialProfile.retirement_savings ?? 0,
|
||||
emergencySavings: financialProfile.emergency_fund ?? 0,
|
||||
monthlyRetirementContribution:
|
||||
localScenario.planned_monthly_retirement_contribution ??
|
||||
financialProfile.retirement_contribution ??
|
||||
0,
|
||||
monthlyEmergencyContribution:
|
||||
localScenario.planned_monthly_emergency_contribution ??
|
||||
financialProfile.emergency_contribution ??
|
||||
0,
|
||||
surplusEmergencyAllocation:
|
||||
localScenario.planned_surplus_emergency_pct ??
|
||||
financialProfile.extra_cash_emergency_pct ??
|
||||
50,
|
||||
surplusRetirementAllocation:
|
||||
localScenario.planned_surplus_retirement_pct ??
|
||||
financialProfile.extra_cash_retirement_pct ??
|
||||
50,
|
||||
additionalIncome:
|
||||
localScenario.planned_additional_income ?? financialProfile.additional_income ?? 0,
|
||||
monthlyExpenses: financialProfile.monthly_expenses || 0,
|
||||
monthlyDebtPayments: financialProfile.monthly_debt_payments || 0,
|
||||
retirementSavings: financialProfile.retirement_savings || 0,
|
||||
emergencySavings: financialProfile.emergency_fund || 0,
|
||||
monthlyRetirementContribution: financialProfile.retirement_contribution || 0,
|
||||
monthlyEmergencyContribution: financialProfile.emergency_contribution || 0,
|
||||
surplusEmergencyAllocation: financialProfile.extra_cash_emergency_pct || 50,
|
||||
surplusRetirementAllocation: financialProfile.extra_cash_retirement_pct || 50,
|
||||
|
||||
// College fields
|
||||
// College fields (scenario-based)
|
||||
studentLoanAmount: collegeProfile.existing_college_debt || 0,
|
||||
interestRate: collegeProfile.interest_rate || 5,
|
||||
loanTerm: collegeProfile.loan_term || 10,
|
||||
@ -126,28 +81,16 @@ export default function ScenarioContainer({
|
||||
collegeProfile.college_enrollment_status === 'prospective_student',
|
||||
expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary || 0,
|
||||
|
||||
// Flatten scenario + universal milestoneImpacts
|
||||
milestoneImpacts: buildAllImpacts([...milestones, ...universalMilestones])
|
||||
// milestoneImpacts is fetched & merged in MilestoneTimeline, not here
|
||||
milestoneImpacts: []
|
||||
};
|
||||
|
||||
const { projectionData, loanPaidOffMonth } =
|
||||
simulateFinancialProjection(mergedProfile);
|
||||
// 3) run the simulation
|
||||
const { projectionData, loanPaidOffMonth } = simulateFinancialProjection(mergedProfile);
|
||||
setProjectionData(projectionData);
|
||||
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 (
|
||||
<div style={{ width: '420px', border: '1px solid #ccc', padding: '1rem' }}>
|
||||
<h3>{localScenario.career_name || 'Untitled Scenario'}</h3>
|
||||
@ -170,18 +113,18 @@ export default function ScenarioContainer({
|
||||
|
||||
<div style={{ marginTop: '0.5rem' }}>
|
||||
<strong>Loan Paid Off:</strong> {loanPaidOffMonth || 'N/A'} <br />
|
||||
<strong>Retirement (final):</strong> ${
|
||||
projectionData[projectionData.length - 1]?.retirementSavings?.toFixed(0) || 0
|
||||
}
|
||||
<strong>Final Retirement:</strong>{' '}
|
||||
{projectionData[projectionData.length - 1]?.retirementSavings?.toFixed(0) || 0}
|
||||
</div>
|
||||
|
||||
{/* The timeline that fetches scenario/universal milestones for display */}
|
||||
<MilestoneTimeline
|
||||
careerPathId={localScenario.id}
|
||||
authFetch={authFetch}
|
||||
activeView="Financial"
|
||||
setActiveView={() => {}}
|
||||
onMilestoneUpdated={() => {
|
||||
// re-fetch or something
|
||||
// might do scenario changes if you want
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -203,14 +146,16 @@ export default function ScenarioContainer({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Updated ScenarioEditModal that references localScenario + setLocalScenario */}
|
||||
<ScenarioEditModal
|
||||
show={editOpen}
|
||||
onClose={() => setEditOpen(false)}
|
||||
scenario={localScenario}
|
||||
setScenario={setLocalScenario}
|
||||
apiURL="/api"
|
||||
/>
|
||||
{/* If you do scenario-level editing for planned fields, show scenario edit modal */}
|
||||
{editOpen && (
|
||||
<ScenarioEditModal
|
||||
show={editOpen}
|
||||
onClose={() => setEditOpen(false)}
|
||||
scenario={localScenario}
|
||||
setScenario={setLocalScenario}
|
||||
apiURL="/api"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -226,9 +226,6 @@ export function simulateFinancialProjection(userProfile) {
|
||||
let wasInDeferral = inCollege && loanDeferralUntilGraduation;
|
||||
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
|
||||
***************************************************/
|
||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user