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 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;
|
|
||||||
|
@ -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 user’s 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',
|
||||||
@ -59,10 +54,9 @@ 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 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 = {
|
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, we’ll 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));
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
@ -11,15 +11,11 @@ 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 scenario’s 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,7 +146,8 @@ 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 */}
|
||||||
|
{editOpen && (
|
||||||
<ScenarioEditModal
|
<ScenarioEditModal
|
||||||
show={editOpen}
|
show={editOpen}
|
||||||
onClose={() => setEditOpen(false)}
|
onClose={() => setEditOpen(false)}
|
||||||
@ -211,6 +155,7 @@ export default function ScenarioContainer({
|
|||||||
setScenario={setLocalScenario}
|
setScenario={setLocalScenario}
|
||||||
apiURL="/api"
|
apiURL="/api"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
***************************************************/
|
***************************************************/
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user