diff --git a/src/App.js b/src/App.js index 88264fe..2e50cab 100644 --- a/src/App.js +++ b/src/App.js @@ -141,7 +141,7 @@ function App() { className="text-blue-600 hover:text-blue-800" to="/milestone-tracker" > - Milestone Tracker + Career Planner ) : ( diff --git a/src/components/MilestoneTimeline.js b/src/components/MilestoneTimeline.js index 66f677c..1c6ef40 100644 --- a/src/components/MilestoneTimeline.js +++ b/src/components/MilestoneTimeline.js @@ -3,16 +3,21 @@ import React, { useEffect, useState, useCallback } from 'react'; 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({ careerPathId, authFetch, - activeView, - setActiveView, - onMilestoneUpdated + activeView, // 'Career' or 'Financial' + setActiveView, // optional, if you need to switch between views + onMilestoneUpdated // callback after saving/deleting a milestone }) { 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({ title: '', description: '', @@ -26,14 +31,21 @@ export default function MilestoneTimeline({ const [showForm, setShowForm] = useState(false); const [editingMilestone, setEditingMilestone] = useState(null); - const [showTaskForm, setShowTaskForm] = useState(null); - const [newTask, setNewTask] = useState({ title: '', description: '', due_date: '' }); + // For CREATE/EDIT tasks + 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 [copyWizardMilestone, setCopyWizardMilestone] = useState(null); // ------------------------------------------------------------------ - // 1) Financial Impacts sub-form helpers (no change) + // 1) Financial Impacts sub-form helpers // ------------------------------------------------------------------ function addNewImpact() { setNewMilestone((prev) => ({ @@ -81,11 +93,13 @@ export default function MilestoneTimeline({ console.warn('No milestones in response:', data); return; } + // Separate them by type const categorized = { Career: [], Financial: [] }; data.milestones.forEach((m) => { if (categorized[m.milestone_type]) { categorized[m.milestone_type].push(m); } else { + // If there's a random type, log or store somewhere else console.warn(`Unknown milestone type: ${m.milestone_type}`); } }); @@ -167,7 +181,7 @@ export default function MilestoneTimeline({ const method = editingMilestone ? 'PUT' : 'POST'; const payload = { - milestone_type: activeView, + milestone_type: activeView, // 'Career' or 'Financial' title: newMilestone.title, description: newMilestone.description, date: newMilestone.date, @@ -197,6 +211,7 @@ export default function MilestoneTimeline({ const savedMilestone = await res.json(); console.log('Milestone saved/updated:', savedMilestone); + // If it's a "Financial" milestone => handle impacts if (activeView === 'Financial') { // 1) Delete old impacts for (const impactId of impactsToDelete) { @@ -253,7 +268,7 @@ export default function MilestoneTimeline({ } } - // Re-fetch + // Re-fetch milestones await fetchMilestones(); // reset form @@ -279,44 +294,91 @@ export default function MilestoneTimeline({ } // ------------------------------------------------------------------ - // 6) Add Task + // 6) TASK CRUD // ------------------------------------------------------------------ - async function addTask(milestoneId) { - try { - const taskPayload = { - milestone_id: milestoneId, - title: newTask.title, - description: newTask.description, - due_date: newTask.due_date - }; - console.log('Creating new task:', taskPayload); - const res = await authFetch('/api/premium/tasks', { - method: 'POST', + // A) “Add Task” button => sets newTask for a new item + 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, + title: newTask.title, + description: newTask.description, + due_date: newTask.due_date + }; + + let url = '/api/premium/tasks'; + 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' }, - body: JSON.stringify(taskPayload) + body: JSON.stringify(payload) }); if (!res.ok) { - const errorData = await res.json(); - console.error('Failed to create task:', errorData); - alert(errorData.error || 'Error creating task'); + const errData = await res.json().catch(() => ({})); + console.error('Failed to save task:', errData); + alert(errData.error || 'Error saving task'); return; } - const createdTask = await res.json(); - console.log('Task created:', createdTask); - // Re-fetch so the list shows the new task + // re-fetch await fetchMilestones(); - setNewTask({ title: '', description: '', due_date: '' }); + // reset setShowTaskForm(null); + setNewTask({ id: null, title: '', description: '', due_date: '' }); } 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 }) { 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]; return ( @@ -458,7 +520,7 @@ export default function MilestoneTimeline({ {showForm ? 'Cancel' : '+ New Milestone'} - {/* If showForm => the same create/edit form */} + {/* If showForm => the create/edit milestone sub-form */} {showForm && (

{editingMilestone ? 'Edit Milestone' : 'New Milestone'}

@@ -583,8 +645,7 @@ export default function MilestoneTimeline({
)} - {/* *** REPLACEMENT FOR THE OLD “TIMELINE VISUAL” *** */} - {/* Instead of a horizontal timeline, we list them in a simple vertical list. */} + {/* Render the (Career + Financial) milestones in a simple vertical list */} {Object.keys(milestones).map((typeKey) => milestones[typeKey].map((m) => { const tasks = m.tasks || []; @@ -607,21 +668,40 @@ export default function MilestoneTimeline({ {t.title} {t.description ? ` - ${t.description}` : ''} {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 */} + + ))} )} + {/* Add or edit a task */} + + )} diff --git a/src/components/MilestoneTracker.js b/src/components/MilestoneTracker.js index 4ca27c7..50943f0 100644 --- a/src/components/MilestoneTracker.js +++ b/src/components/MilestoneTracker.js @@ -1,5 +1,3 @@ -// src/components/MilestoneTracker.js - import React, { useState, useEffect } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { Line } from 'react-chartjs-2'; @@ -19,9 +17,7 @@ import authFetch from '../utils/authFetch.js'; import CareerSelectDropdown from './CareerSelectDropdown.js'; import CareerSearch from './CareerSearch.js'; -// Keep MilestoneTimeline for +Add Milestone & tasks CRUD -import MilestoneTimeline from './MilestoneTimeline.js'; - +import MilestoneTimeline from './MilestoneTimeline.js'; // Key: This handles Milestone & Task CRUD import AISuggestedMilestones from './AISuggestedMilestones.js'; import ScenarioEditModal from './ScenarioEditModal.js'; @@ -58,17 +54,15 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { const [scenarioRow, setScenarioRow] = 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([]); + const [scenarioMilestones, setScenarioMilestones] = useState([]); // for annotation const [projectionData, setProjectionData] = useState([]); const [loanPayoffMonth, setLoanPayoffMonth] = useState(null); const [simulationYearsInput, setSimulationYearsInput] = useState('20'); const simulationYears = parseInt(simulationYearsInput, 10) || 20; - // --- ADDED: showEditModal state + // Show/hide scenario edit modal const [showEditModal, setShowEditModal] = useState(false); - const [pendingCareerForModal, setPendingCareerForModal] = useState(null); const { @@ -86,6 +80,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { const data = await res.json(); setExistingCareerPaths(data.careerPaths); + // If user came from a different route passing in a selected scenario: const fromPopout = location.state?.selectedCareer; if (fromPopout) { setSelectedCareer(fromPopout); @@ -172,9 +167,9 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { } const milestonesData = await milRes.json(); 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) => authFetch(`${apiURL}/premium/milestone-impacts?milestone_id=${m.id}`) .then((r) => (r.ok ? r.json() : null)) @@ -190,10 +185,10 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { impacts: impactsForEach[i] || [] })); - // flatten all + // flatten const allImpacts = milestonesWithImpacts.flatMap((m) => m.impacts); - // mergedProfile + // Build mergedProfile const mergedProfile = { currentSalary: financialProfile.current_salary || 0, monthlyExpenses: @@ -271,12 +266,6 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { })(); }, [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 handleSimulationYearsBlur = () => { if (!simulationYearsInput.trim()) { @@ -284,7 +273,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { } }; - // Build annotation lines from scenarioMilestones + // Build chart annotations from scenarioMilestones const milestoneAnnotationLines = {}; scenarioMilestones.forEach((m) => { if (!m.date) return; @@ -295,6 +284,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { const month = String(d.getUTCMonth() + 1).padStart(2, '0'); const short = `${year}-${month}`; + // check if we have data for that month if (!projectionData.some((p) => p.month === short)) return; milestoneAnnotationLines[`milestone_${m.id}`] = { @@ -308,16 +298,11 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { content: m.title || 'Milestone', color: 'orange', 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 = {}; if (loanPayoffMonth) { annotationConfig.loanPaidOffLine = { @@ -355,7 +340,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { authFetch={authFetch} /> - {/* 2) We keep MilestoneTimeline for tasks, +Add Milestone button, etc. */} + {/* 2) MilestoneTimeline for Milestone & Task CRUD */} { )} - {/* 5) Simulation length input + the new Edit button */} + {/* 5) Simulation length + "Edit" Button => open ScenarioEditModal */}
{ onBlur={handleSimulationYearsBlur} className="border rounded p-1 w-16" /> - {/* EDIT BUTTON => open ScenarioEditModal */} @@ -474,12 +458,11 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { )} - {/* Pass scenarioRow to the modal, and optionally do a hard refresh onClose */} { setShowEditModal(false); - // Hard-refresh if you want to replicate "ScenarioContainer" approach: + // optionally reload to see scenario changes window.location.reload(); }} scenario={scenarioRow} diff --git a/src/components/ScenarioContainer.js b/src/components/ScenarioContainer.js index 9c88413..4513fd4 100644 --- a/src/components/ScenarioContainer.js +++ b/src/components/ScenarioContainer.js @@ -1,11 +1,9 @@ -// src/components/ScenarioContainer.js - import React, { useState, useEffect, useCallback } from 'react'; import { Line } from 'react-chartjs-2'; import { Chart as ChartJS } from 'chart.js'; 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 { simulateFinancialProjection } from '../utils/FinancialProjectionService.js'; import AISuggestedMilestones from './AISuggestedMilestones.js'; @@ -339,7 +337,6 @@ export default function ScenarioContainer({ // tasks const [showTaskForm, setShowTaskForm] = useState(null); - // We'll track a separate "editingTask" so we can fill in the form const [editingTask, setEditingTask] = useState({ id: null, title: '', @@ -350,7 +347,6 @@ export default function ScenarioContainer({ // copy wizard const [copyWizardMilestone, setCopyWizardMilestone] = useState(null); - // create new milestone function handleNewMilestone() { setEditingMilestone(null); setNewMilestone({ @@ -366,7 +362,6 @@ export default function ScenarioContainer({ setShowForm(true); } - // edit an existing milestone => fetch impacts async function handleEditMilestone(m) { if (!localScenario?.id) return; setEditingMilestone(m); @@ -559,7 +554,7 @@ export default function ScenarioContainer({ /************************************************************* * 6) TASK CRUD *************************************************************/ - // This can handle both new and existing tasks + // handle both new and existing tasks function handleAddTask(milestoneId) { setShowTaskForm(milestoneId); setEditingTask({ @@ -980,7 +975,6 @@ export default function ScenarioContainer({ {/* Render existing milestones */} {milestones.map((m) => { - // tasks const tasks = m.tasks || []; return (
{m.title} {m.description &&

{m.description}

}

- Date: {m.date} —{' '} - Progress: {m.progress}% + Date: {m.date} — Progress: {m.progress}%

{/* tasks list */} @@ -1043,9 +1036,15 @@ export default function ScenarioContainer({ Delete - {/* 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 && ( -
+
{editingTask.id ? 'Edit Task' : 'New Task'}