Added Task CRUD to MilestoneTimeline.js and ScenarioContainer.js
This commit is contained in:
parent
a2c9dc7157
commit
b3c1d522ee
@ -141,7 +141,7 @@ function App() {
|
|||||||
className="text-blue-600 hover:text-blue-800"
|
className="text-blue-600 hover:text-blue-800"
|
||||||
to="/milestone-tracker"
|
to="/milestone-tracker"
|
||||||
>
|
>
|
||||||
Milestone Tracker
|
Career Planner
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-400 cursor-not-allowed">
|
<span className="text-gray-400 cursor-not-allowed">
|
||||||
|
@ -3,16 +3,21 @@
|
|||||||
import React, { useEffect, useState, useCallback } from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import { Button } from './ui/button.js';
|
import { Button } from './ui/button.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a simple vertical list of milestones for the given careerPathId.
|
||||||
|
* Also includes Task CRUD (create/edit/delete) for each milestone,
|
||||||
|
* plus a small "copy milestone" wizard, "financial impacts" form, etc.
|
||||||
|
*/
|
||||||
export default function MilestoneTimeline({
|
export default function MilestoneTimeline({
|
||||||
careerPathId,
|
careerPathId,
|
||||||
authFetch,
|
authFetch,
|
||||||
activeView,
|
activeView, // 'Career' or 'Financial'
|
||||||
setActiveView,
|
setActiveView, // optional, if you need to switch between views
|
||||||
onMilestoneUpdated
|
onMilestoneUpdated // callback after saving/deleting a milestone
|
||||||
}) {
|
}) {
|
||||||
const [milestones, setMilestones] = useState({ Career: [], Financial: [] });
|
const [milestones, setMilestones] = useState({ Career: [], Financial: [] });
|
||||||
|
|
||||||
// We'll keep your existing milestone form state, tasks, copy wizard, etc.
|
// For CREATE/EDIT milestone
|
||||||
const [newMilestone, setNewMilestone] = useState({
|
const [newMilestone, setNewMilestone] = useState({
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
@ -26,14 +31,21 @@ export default function MilestoneTimeline({
|
|||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
const [editingMilestone, setEditingMilestone] = useState(null);
|
const [editingMilestone, setEditingMilestone] = useState(null);
|
||||||
|
|
||||||
const [showTaskForm, setShowTaskForm] = useState(null);
|
// For CREATE/EDIT tasks
|
||||||
const [newTask, setNewTask] = useState({ title: '', description: '', due_date: '' });
|
const [showTaskForm, setShowTaskForm] = useState(null); // which milestone ID is showing the form
|
||||||
|
const [newTask, setNewTask] = useState({
|
||||||
|
id: null,
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
due_date: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// For the "Copy to other scenarios" wizard
|
||||||
const [scenarios, setScenarios] = useState([]);
|
const [scenarios, setScenarios] = useState([]);
|
||||||
const [copyWizardMilestone, setCopyWizardMilestone] = useState(null);
|
const [copyWizardMilestone, setCopyWizardMilestone] = useState(null);
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 1) Financial Impacts sub-form helpers (no change)
|
// 1) Financial Impacts sub-form helpers
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
function addNewImpact() {
|
function addNewImpact() {
|
||||||
setNewMilestone((prev) => ({
|
setNewMilestone((prev) => ({
|
||||||
@ -81,11 +93,13 @@ export default function MilestoneTimeline({
|
|||||||
console.warn('No milestones in response:', data);
|
console.warn('No milestones in response:', data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Separate them by type
|
||||||
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 {
|
||||||
|
// If there's a random type, log or store somewhere else
|
||||||
console.warn(`Unknown milestone type: ${m.milestone_type}`);
|
console.warn(`Unknown milestone type: ${m.milestone_type}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -167,7 +181,7 @@ export default function MilestoneTimeline({
|
|||||||
const method = editingMilestone ? 'PUT' : 'POST';
|
const method = editingMilestone ? 'PUT' : 'POST';
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
milestone_type: activeView,
|
milestone_type: activeView, // 'Career' or 'Financial'
|
||||||
title: newMilestone.title,
|
title: newMilestone.title,
|
||||||
description: newMilestone.description,
|
description: newMilestone.description,
|
||||||
date: newMilestone.date,
|
date: newMilestone.date,
|
||||||
@ -197,6 +211,7 @@ export default function MilestoneTimeline({
|
|||||||
const savedMilestone = await res.json();
|
const savedMilestone = await res.json();
|
||||||
console.log('Milestone saved/updated:', savedMilestone);
|
console.log('Milestone saved/updated:', savedMilestone);
|
||||||
|
|
||||||
|
// If it's a "Financial" milestone => 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) {
|
||||||
@ -253,7 +268,7 @@ export default function MilestoneTimeline({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-fetch
|
// Re-fetch milestones
|
||||||
await fetchMilestones();
|
await fetchMilestones();
|
||||||
|
|
||||||
// reset form
|
// reset form
|
||||||
@ -279,44 +294,91 @@ export default function MilestoneTimeline({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 6) Add Task
|
// 6) TASK CRUD
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
async function addTask(milestoneId) {
|
|
||||||
try {
|
// A) “Add Task” button => sets newTask for a new item
|
||||||
const taskPayload = {
|
function handleAddTask(milestoneId) {
|
||||||
|
setShowTaskForm(milestoneId);
|
||||||
|
setNewTask({ id: null, title: '', description: '', due_date: '' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// B) “Edit Task” => fill newTask with the existing fields
|
||||||
|
function handleEditTask(milestoneId, task) {
|
||||||
|
setShowTaskForm(milestoneId);
|
||||||
|
setNewTask({
|
||||||
|
id: task.id,
|
||||||
|
title: task.title,
|
||||||
|
description: task.description || '',
|
||||||
|
due_date: task.due_date || ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// C) Save (create or update) task
|
||||||
|
async function saveTask(milestoneId) {
|
||||||
|
if (!newTask.title.trim()) {
|
||||||
|
alert('Task needs a title');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const payload = {
|
||||||
milestone_id: milestoneId,
|
milestone_id: milestoneId,
|
||||||
title: newTask.title,
|
title: newTask.title,
|
||||||
description: newTask.description,
|
description: newTask.description,
|
||||||
due_date: newTask.due_date
|
due_date: newTask.due_date
|
||||||
};
|
};
|
||||||
console.log('Creating new task:', taskPayload);
|
|
||||||
|
|
||||||
const res = await authFetch('/api/premium/tasks', {
|
let url = '/api/premium/tasks';
|
||||||
method: 'POST',
|
let method = 'POST';
|
||||||
|
|
||||||
|
if (newTask.id) {
|
||||||
|
// existing => PUT
|
||||||
|
url = `/api/premium/tasks/${newTask.id}`;
|
||||||
|
method = 'PUT';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await authFetch(url, {
|
||||||
|
method,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(taskPayload)
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errorData = await res.json();
|
const errData = await res.json().catch(() => ({}));
|
||||||
console.error('Failed to create task:', errorData);
|
console.error('Failed to save task:', errData);
|
||||||
alert(errorData.error || 'Error creating task');
|
alert(errData.error || 'Error saving task');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const createdTask = await res.json();
|
|
||||||
console.log('Task created:', createdTask);
|
|
||||||
|
|
||||||
// Re-fetch so the list shows the new task
|
// re-fetch
|
||||||
await fetchMilestones();
|
await fetchMilestones();
|
||||||
|
|
||||||
setNewTask({ title: '', description: '', due_date: '' });
|
// reset
|
||||||
setShowTaskForm(null);
|
setShowTaskForm(null);
|
||||||
|
setNewTask({ id: null, title: '', description: '', due_date: '' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error adding task:', err);
|
console.error('Error saving task:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// D) Delete an existing task
|
||||||
|
async function deleteTask(taskId) {
|
||||||
|
if (!taskId) return;
|
||||||
|
try {
|
||||||
|
const res = await authFetch(`/api/premium/tasks/${taskId}`, { method: 'DELETE' });
|
||||||
|
if (!res.ok) {
|
||||||
|
const errData = await res.json().catch(() => ({}));
|
||||||
|
console.error('Failed to delete task:', errData);
|
||||||
|
alert(errData.error || 'Error deleting task');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await fetchMilestones();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error deleting task:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 7) Copy Wizard
|
// 7) Copy Wizard for universal/cross-scenario
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
function CopyMilestoneWizard({ milestone, scenarios, onClose, authFetch }) {
|
function CopyMilestoneWizard({ milestone, scenarios, onClose, authFetch }) {
|
||||||
const [selectedScenarios, setSelectedScenarios] = useState([]);
|
const [selectedScenarios, setSelectedScenarios] = useState([]);
|
||||||
@ -425,9 +487,9 @@ export default function MilestoneTimeline({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 9) RENDER: remove the "timeline" code, show a list instead
|
// 9) Render
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Combined array if you want to show them all in one list
|
// Combine "Career" + "Financial" if you want them in a single list:
|
||||||
const allMilestones = [...milestones.Career, ...milestones.Financial];
|
const allMilestones = [...milestones.Career, ...milestones.Financial];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -458,7 +520,7 @@ export default function MilestoneTimeline({
|
|||||||
{showForm ? 'Cancel' : '+ New Milestone'}
|
{showForm ? 'Cancel' : '+ New Milestone'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* If showForm => the same create/edit form */}
|
{/* If showForm => the create/edit milestone sub-form */}
|
||||||
{showForm && (
|
{showForm && (
|
||||||
<div className="border p-2 my-2">
|
<div className="border p-2 my-2">
|
||||||
<h4>{editingMilestone ? 'Edit Milestone' : 'New Milestone'}</h4>
|
<h4>{editingMilestone ? 'Edit Milestone' : 'New Milestone'}</h4>
|
||||||
@ -583,8 +645,7 @@ export default function MilestoneTimeline({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* *** REPLACEMENT FOR THE OLD “TIMELINE VISUAL” *** */}
|
{/* Render the (Career + Financial) milestones in a simple vertical list */}
|
||||||
{/* Instead of a horizontal timeline, we list them in a simple vertical list. */}
|
|
||||||
{Object.keys(milestones).map((typeKey) =>
|
{Object.keys(milestones).map((typeKey) =>
|
||||||
milestones[typeKey].map((m) => {
|
milestones[typeKey].map((m) => {
|
||||||
const tasks = m.tasks || [];
|
const tasks = m.tasks || [];
|
||||||
@ -607,21 +668,40 @@ export default function MilestoneTimeline({
|
|||||||
<strong>{t.title}</strong>
|
<strong>{t.title}</strong>
|
||||||
{t.description ? ` - ${t.description}` : ''}
|
{t.description ? ` - ${t.description}` : ''}
|
||||||
{t.due_date ? ` (Due: ${t.due_date})` : ''}{' '}
|
{t.due_date ? ` (Due: ${t.due_date})` : ''}{' '}
|
||||||
{/* If you'd like to add “Edit”/“Delete” for tasks, replicate scenario container logic */}
|
{/* EDIT & DELETE Task buttons */}
|
||||||
|
<Button
|
||||||
|
onClick={() => handleEditTask(m.id, t)}
|
||||||
|
style={{ marginLeft: '0.5rem' }}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => deleteTask(t.id)}
|
||||||
|
style={{ marginLeft: '0.5rem', color: 'red' }}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Add or edit a task */}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowTaskForm(showTaskForm === m.id ? null : m.id);
|
// if we are already showing the form for this milestone => Cancel
|
||||||
setNewTask({ title: '', description: '', due_date: '' });
|
if (showTaskForm === m.id) {
|
||||||
|
setShowTaskForm(null);
|
||||||
|
setNewTask({ id: null, title: '', description: '', due_date: '' });
|
||||||
|
} else {
|
||||||
|
handleAddTask(m.id);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
style={{ marginRight: '0.5rem' }}
|
style={{ marginRight: '0.5rem' }}
|
||||||
>
|
>
|
||||||
{showTaskForm === m.id ? 'Cancel Task' : 'Add Task'}
|
{showTaskForm === m.id ? 'Cancel Task' : '+ Task'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button onClick={() => handleEditMilestone(m)}>Edit</Button>
|
<Button onClick={() => handleEditMilestone(m)}>Edit</Button>
|
||||||
<Button
|
<Button
|
||||||
style={{ marginLeft: '0.5rem' }}
|
style={{ marginLeft: '0.5rem' }}
|
||||||
@ -636,30 +716,42 @@ export default function MilestoneTimeline({
|
|||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* The "Add Task" form if showTaskForm === m.id */}
|
{/* If this is the milestone whose tasks we're editing => show the form */}
|
||||||
{showTaskForm === m.id && (
|
{showTaskForm === m.id && (
|
||||||
<div
|
<div
|
||||||
style={{ marginTop: '0.5rem', border: '1px solid #aaa', padding: '0.5rem' }}
|
style={{
|
||||||
|
marginTop: '0.5rem',
|
||||||
|
border: '1px solid #aaa',
|
||||||
|
padding: '0.5rem'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<h5>New Task</h5>
|
<h5>{newTask.id ? 'Edit Task' : 'New Task'}</h5>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Task Title"
|
placeholder="Task Title"
|
||||||
value={newTask.title}
|
value={newTask.title}
|
||||||
onChange={(e) => setNewTask({ ...newTask, title: e.target.value })}
|
onChange={(e) =>
|
||||||
|
setNewTask((prev) => ({ ...prev, title: e.target.value }))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Task Description"
|
placeholder="Task Description"
|
||||||
value={newTask.description}
|
value={newTask.description}
|
||||||
onChange={(e) => setNewTask({ ...newTask, description: e.target.value })}
|
onChange={(e) =>
|
||||||
|
setNewTask((prev) => ({ ...prev, description: e.target.value }))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={newTask.due_date}
|
value={newTask.due_date || ''}
|
||||||
onChange={(e) => setNewTask({ ...newTask, due_date: e.target.value })}
|
onChange={(e) =>
|
||||||
|
setNewTask((prev) => ({ ...prev, due_date: e.target.value }))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Button onClick={() => addTask(m.id)}>Save Task</Button>
|
<Button onClick={() => saveTask(m.id)}>
|
||||||
|
{newTask.id ? 'Update' : 'Add'} Task
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
// src/components/MilestoneTracker.js
|
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { Line } from 'react-chartjs-2';
|
import { Line } from 'react-chartjs-2';
|
||||||
@ -19,9 +17,7 @@ import authFetch from '../utils/authFetch.js';
|
|||||||
import CareerSelectDropdown from './CareerSelectDropdown.js';
|
import CareerSelectDropdown from './CareerSelectDropdown.js';
|
||||||
import CareerSearch from './CareerSearch.js';
|
import CareerSearch from './CareerSearch.js';
|
||||||
|
|
||||||
// Keep MilestoneTimeline for +Add Milestone & tasks CRUD
|
import MilestoneTimeline from './MilestoneTimeline.js'; // Key: This handles Milestone & Task CRUD
|
||||||
import MilestoneTimeline from './MilestoneTimeline.js';
|
|
||||||
|
|
||||||
import AISuggestedMilestones from './AISuggestedMilestones.js';
|
import AISuggestedMilestones from './AISuggestedMilestones.js';
|
||||||
import ScenarioEditModal from './ScenarioEditModal.js';
|
import ScenarioEditModal from './ScenarioEditModal.js';
|
||||||
|
|
||||||
@ -58,17 +54,15 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
const [scenarioRow, setScenarioRow] = useState(null);
|
const [scenarioRow, setScenarioRow] = useState(null);
|
||||||
const [collegeProfile, setCollegeProfile] = useState(null);
|
const [collegeProfile, setCollegeProfile] = useState(null);
|
||||||
|
|
||||||
// We will store the scenario’s milestones in state so we can build annotation lines
|
const [scenarioMilestones, setScenarioMilestones] = useState([]); // for annotation
|
||||||
const [scenarioMilestones, setScenarioMilestones] = useState([]);
|
|
||||||
|
|
||||||
const [projectionData, setProjectionData] = useState([]);
|
const [projectionData, setProjectionData] = useState([]);
|
||||||
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
|
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
|
||||||
const [simulationYearsInput, setSimulationYearsInput] = useState('20');
|
const [simulationYearsInput, setSimulationYearsInput] = useState('20');
|
||||||
const simulationYears = parseInt(simulationYearsInput, 10) || 20;
|
const simulationYears = parseInt(simulationYearsInput, 10) || 20;
|
||||||
|
|
||||||
// --- ADDED: showEditModal state
|
// Show/hide scenario edit modal
|
||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
|
|
||||||
const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
|
const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -86,6 +80,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setExistingCareerPaths(data.careerPaths);
|
setExistingCareerPaths(data.careerPaths);
|
||||||
|
|
||||||
|
// If user came from a different route passing in a selected scenario:
|
||||||
const fromPopout = location.state?.selectedCareer;
|
const fromPopout = location.state?.selectedCareer;
|
||||||
if (fromPopout) {
|
if (fromPopout) {
|
||||||
setSelectedCareer(fromPopout);
|
setSelectedCareer(fromPopout);
|
||||||
@ -172,9 +167,9 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
}
|
}
|
||||||
const milestonesData = await milRes.json();
|
const milestonesData = await milRes.json();
|
||||||
const allMilestones = milestonesData.milestones || [];
|
const allMilestones = milestonesData.milestones || [];
|
||||||
setScenarioMilestones(allMilestones); // store them for annotation lines
|
setScenarioMilestones(allMilestones);
|
||||||
|
|
||||||
// fetch impacts for each
|
// fetch impacts for each milestone
|
||||||
const impactPromises = allMilestones.map((m) =>
|
const impactPromises = allMilestones.map((m) =>
|
||||||
authFetch(`${apiURL}/premium/milestone-impacts?milestone_id=${m.id}`)
|
authFetch(`${apiURL}/premium/milestone-impacts?milestone_id=${m.id}`)
|
||||||
.then((r) => (r.ok ? r.json() : null))
|
.then((r) => (r.ok ? r.json() : null))
|
||||||
@ -190,10 +185,10 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
impacts: impactsForEach[i] || []
|
impacts: impactsForEach[i] || []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// flatten all
|
// flatten
|
||||||
const allImpacts = milestonesWithImpacts.flatMap((m) => m.impacts);
|
const allImpacts = milestonesWithImpacts.flatMap((m) => m.impacts);
|
||||||
|
|
||||||
// mergedProfile
|
// Build mergedProfile
|
||||||
const mergedProfile = {
|
const mergedProfile = {
|
||||||
currentSalary: financialProfile.current_salary || 0,
|
currentSalary: financialProfile.current_salary || 0,
|
||||||
monthlyExpenses:
|
monthlyExpenses:
|
||||||
@ -271,12 +266,6 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
})();
|
})();
|
||||||
}, [financialProfile, scenarioRow, collegeProfile, careerPathId, apiURL, simulationYears]);
|
}, [financialProfile, scenarioRow, collegeProfile, careerPathId, apiURL, simulationYears]);
|
||||||
|
|
||||||
// If you want to re-run simulation after any milestone changes:
|
|
||||||
const reSimulate = async () => {
|
|
||||||
// Put your logic to re-fetch scenario + milestones, then re-run sim (if needed).
|
|
||||||
};
|
|
||||||
|
|
||||||
// handle user typing simulation length
|
|
||||||
const handleSimulationYearsChange = (e) => setSimulationYearsInput(e.target.value);
|
const handleSimulationYearsChange = (e) => setSimulationYearsInput(e.target.value);
|
||||||
const handleSimulationYearsBlur = () => {
|
const handleSimulationYearsBlur = () => {
|
||||||
if (!simulationYearsInput.trim()) {
|
if (!simulationYearsInput.trim()) {
|
||||||
@ -284,7 +273,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build annotation lines from scenarioMilestones
|
// Build chart annotations from scenarioMilestones
|
||||||
const milestoneAnnotationLines = {};
|
const milestoneAnnotationLines = {};
|
||||||
scenarioMilestones.forEach((m) => {
|
scenarioMilestones.forEach((m) => {
|
||||||
if (!m.date) return;
|
if (!m.date) return;
|
||||||
@ -295,6 +284,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
const month = String(d.getUTCMonth() + 1).padStart(2, '0');
|
const month = String(d.getUTCMonth() + 1).padStart(2, '0');
|
||||||
const short = `${year}-${month}`;
|
const short = `${year}-${month}`;
|
||||||
|
|
||||||
|
// check if we have data for that month
|
||||||
if (!projectionData.some((p) => p.month === short)) return;
|
if (!projectionData.some((p) => p.month === short)) return;
|
||||||
|
|
||||||
milestoneAnnotationLines[`milestone_${m.id}`] = {
|
milestoneAnnotationLines[`milestone_${m.id}`] = {
|
||||||
@ -308,16 +298,11 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
content: m.title || 'Milestone',
|
content: m.title || 'Milestone',
|
||||||
color: 'orange',
|
color: 'orange',
|
||||||
position: 'end'
|
position: 'end'
|
||||||
},
|
|
||||||
// If you want them clickable:
|
|
||||||
onClick: () => {
|
|
||||||
console.log('Clicked milestone line => open editing for', m.title);
|
|
||||||
// e.g. open the MilestoneTimeline's edit feature, or do something
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// If we also show a line for payoff:
|
// If we also want a line for payoff:
|
||||||
const annotationConfig = {};
|
const annotationConfig = {};
|
||||||
if (loanPayoffMonth) {
|
if (loanPayoffMonth) {
|
||||||
annotationConfig.loanPaidOffLine = {
|
annotationConfig.loanPaidOffLine = {
|
||||||
@ -355,7 +340,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
authFetch={authFetch}
|
authFetch={authFetch}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 2) We keep MilestoneTimeline for tasks, +Add Milestone button, etc. */}
|
{/* 2) MilestoneTimeline for Milestone & Task CRUD */}
|
||||||
<MilestoneTimeline
|
<MilestoneTimeline
|
||||||
careerPathId={careerPathId}
|
careerPathId={careerPathId}
|
||||||
authFetch={authFetch}
|
authFetch={authFetch}
|
||||||
@ -439,7 +424,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 5) Simulation length input + the new Edit button */}
|
{/* 5) Simulation length + "Edit" Button => open ScenarioEditModal */}
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
<label className="font-medium">Simulation Length (years):</label>
|
<label className="font-medium">Simulation Length (years):</label>
|
||||||
<input
|
<input
|
||||||
@ -449,7 +434,6 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
onBlur={handleSimulationYearsBlur}
|
onBlur={handleSimulationYearsBlur}
|
||||||
className="border rounded p-1 w-16"
|
className="border rounded p-1 w-16"
|
||||||
/>
|
/>
|
||||||
{/* EDIT BUTTON => open ScenarioEditModal */}
|
|
||||||
<Button onClick={() => setShowEditModal(true)} className="ml-2">
|
<Button onClick={() => setShowEditModal(true)} className="ml-2">
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
@ -474,12 +458,11 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pass scenarioRow to the modal, and optionally do a hard refresh onClose */}
|
|
||||||
<ScenarioEditModal
|
<ScenarioEditModal
|
||||||
show={showEditModal}
|
show={showEditModal}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowEditModal(false);
|
setShowEditModal(false);
|
||||||
// Hard-refresh if you want to replicate "ScenarioContainer" approach:
|
// optionally reload to see scenario changes
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}}
|
}}
|
||||||
scenario={scenarioRow}
|
scenario={scenarioRow}
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
// src/components/ScenarioContainer.js
|
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { Line } from 'react-chartjs-2';
|
import { Line } from 'react-chartjs-2';
|
||||||
import { Chart as ChartJS } from 'chart.js';
|
import { Chart as ChartJS } from 'chart.js';
|
||||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||||
|
|
||||||
import { Button } from './ui/button.js'; // universal Button
|
import { Button } from './ui/button.js';
|
||||||
import authFetch from '../utils/authFetch.js';
|
import authFetch from '../utils/authFetch.js';
|
||||||
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
|
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
|
||||||
import AISuggestedMilestones from './AISuggestedMilestones.js';
|
import AISuggestedMilestones from './AISuggestedMilestones.js';
|
||||||
@ -339,7 +337,6 @@ export default function ScenarioContainer({
|
|||||||
|
|
||||||
// tasks
|
// tasks
|
||||||
const [showTaskForm, setShowTaskForm] = useState(null);
|
const [showTaskForm, setShowTaskForm] = useState(null);
|
||||||
// We'll track a separate "editingTask" so we can fill in the form
|
|
||||||
const [editingTask, setEditingTask] = useState({
|
const [editingTask, setEditingTask] = useState({
|
||||||
id: null,
|
id: null,
|
||||||
title: '',
|
title: '',
|
||||||
@ -350,7 +347,6 @@ export default function ScenarioContainer({
|
|||||||
// copy wizard
|
// copy wizard
|
||||||
const [copyWizardMilestone, setCopyWizardMilestone] = useState(null);
|
const [copyWizardMilestone, setCopyWizardMilestone] = useState(null);
|
||||||
|
|
||||||
// create new milestone
|
|
||||||
function handleNewMilestone() {
|
function handleNewMilestone() {
|
||||||
setEditingMilestone(null);
|
setEditingMilestone(null);
|
||||||
setNewMilestone({
|
setNewMilestone({
|
||||||
@ -366,7 +362,6 @@ export default function ScenarioContainer({
|
|||||||
setShowForm(true);
|
setShowForm(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// edit an existing milestone => fetch impacts
|
|
||||||
async function handleEditMilestone(m) {
|
async function handleEditMilestone(m) {
|
||||||
if (!localScenario?.id) return;
|
if (!localScenario?.id) return;
|
||||||
setEditingMilestone(m);
|
setEditingMilestone(m);
|
||||||
@ -559,7 +554,7 @@ export default function ScenarioContainer({
|
|||||||
/*************************************************************
|
/*************************************************************
|
||||||
* 6) TASK CRUD
|
* 6) TASK CRUD
|
||||||
*************************************************************/
|
*************************************************************/
|
||||||
// This can handle both new and existing tasks
|
// handle both new and existing tasks
|
||||||
function handleAddTask(milestoneId) {
|
function handleAddTask(milestoneId) {
|
||||||
setShowTaskForm(milestoneId);
|
setShowTaskForm(milestoneId);
|
||||||
setEditingTask({
|
setEditingTask({
|
||||||
@ -980,7 +975,6 @@ export default function ScenarioContainer({
|
|||||||
|
|
||||||
{/* Render existing milestones */}
|
{/* Render existing milestones */}
|
||||||
{milestones.map((m) => {
|
{milestones.map((m) => {
|
||||||
// tasks
|
|
||||||
const tasks = m.tasks || [];
|
const tasks = m.tasks || [];
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -994,8 +988,7 @@ export default function ScenarioContainer({
|
|||||||
<h5>{m.title}</h5>
|
<h5>{m.title}</h5>
|
||||||
{m.description && <p>{m.description}</p>}
|
{m.description && <p>{m.description}</p>}
|
||||||
<p>
|
<p>
|
||||||
<strong>Date:</strong> {m.date} —{' '}
|
<strong>Date:</strong> {m.date} — <strong>Progress:</strong> {m.progress}%
|
||||||
<strong>Progress:</strong> {m.progress}%
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* tasks list */}
|
{/* tasks list */}
|
||||||
@ -1043,9 +1036,15 @@ export default function ScenarioContainer({
|
|||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* If this is the milestone whose tasks we're editing => show the task form */}
|
{/* The "Add/Edit Task" form if showTaskForm === this milestone */}
|
||||||
{showTaskForm === m.id && (
|
{showTaskForm === m.id && (
|
||||||
<div style={{ marginTop: '0.5rem', border: '1px solid #aaa', padding: '0.5rem' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: '0.5rem',
|
||||||
|
border: '1px solid #aaa',
|
||||||
|
padding: '0.5rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<h5>{editingTask.id ? 'Edit Task' : 'New Task'}</h5>
|
<h5>{editingTask.id ? 'Edit Task' : 'New Task'}</h5>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user