UI updates for premium routes

This commit is contained in:
Josh 2025-05-01 16:50:18 +00:00
parent 9404135915
commit ce53afb3d1
8 changed files with 258 additions and 251 deletions

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom'; import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';
import PremiumRoute from './components/PremiumRoute.js'; import PremiumRoute from './components/PremiumRoute.js';
import SessionExpiredHandler from './components/SessionExpiredHandler.js'; import SessionExpiredHandler from './components/SessionExpiredHandler.js';
@ -19,23 +19,69 @@ function App() {
const location = useLocation(); const location = useLocation();
// Track whether user is authenticated // Track whether user is authenticated
const [isAuthenticated, setIsAuthenticated] = useState(() => { const [isAuthenticated, setIsAuthenticated] = useState(false);
return !!localStorage.getItem('token');
});
// Track the user object, including is_premium // Track the user object, including is_premium
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
// We hide the "Upgrade to Premium" CTA if we're already on certain premium routes // We might also track a "loading" state so we don't redirect prematurely
const [isLoading, setIsLoading] = useState(true);
// Hide the "Upgrade to Premium" CTA on certain premium routes
const premiumPaths = [ const premiumPaths = [
'/milestone-tracker', '/milestone-tracker',
'/paywall', '/paywall',
'/financial-profile', '/financial-profile',
'/multi-scenario', '/multi-scenario',
'/premium-onboarding' '/premium-onboarding',
]; ];
const showPremiumCTA = !premiumPaths.includes(location.pathname); const showPremiumCTA = !premiumPaths.includes(location.pathname);
// On first mount, rehydrate user if there's a token
useEffect(() => {
const token = localStorage.getItem('token');
if (!token) {
setIsLoading(false);
return; // No token => not authenticated
}
// If we have a token, let's validate it and fetch user data
fetch('https://dev1.aptivaai.com/api/user-profile', {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then((res) => {
if (!res.ok) {
// e.g. 401 means token invalid/expired
throw new Error('Token invalid or expired');
}
return res.json();
})
.then((profile) => {
// We have a valid user profile -> set states
setUser(profile);
setIsAuthenticated(true);
})
.catch((err) => {
console.error(err);
// If invalid, remove the token to avoid loops
localStorage.removeItem('token');
})
.finally(() => {
setIsLoading(false);
});
}, []);
if (isLoading) {
// Optionally show a loading spinner or placeholder
return (
<div className="flex h-screen items-center justify-center">
<p>Loading...</p>
</div>
);
}
return ( return (
<div className="flex min-h-screen flex-col bg-gray-50 text-gray-800"> <div className="flex min-h-screen flex-col bg-gray-50 text-gray-800">
{/* Header */} {/* Header */}
@ -65,13 +111,13 @@ function App() {
element={ element={
<SignIn <SignIn
setIsAuthenticated={setIsAuthenticated} setIsAuthenticated={setIsAuthenticated}
setUser={setUser} // We pass setUser so SignIn can store user details setUser={setUser}
/> />
} }
/> />
<Route path="/signup" element={<SignUp />} /> <Route path="/signup" element={<SignUp />} />
{/* Paywall - open to everyone for subscription */} {/* Paywall (accessible to all) */}
<Route path="/paywall" element={<Paywall />} /> <Route path="/paywall" element={<Paywall />} />
{/* Authenticated routes */} {/* Authenticated routes */}

View File

@ -1,72 +1,85 @@
// src/components/AISuggestedMilestones.js
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button } from './ui/button.js';
const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, activeView, projectionData }) => { const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, activeView, projectionData }) => {
const [suggestedMilestones, setSuggestedMilestones] = useState([]); const [suggestedMilestones, setSuggestedMilestones] = useState([]);
const [selected, setSelected] = useState([]); const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// Show a warning if projectionData is not an array
useEffect(() => { useEffect(() => {
if (!Array.isArray(projectionData)) { if (!Array.isArray(projectionData)) {
console.warn('⚠️ projectionData is not an array:', projectionData); console.warn('⚠️ projectionData is not an array:', projectionData);
return; return;
} }
console.log('📊 projectionData sample:', projectionData.slice(0, 3)); console.log('📊 projectionData sample:', projectionData.slice(0, 3));
}, [projectionData]); }, [projectionData]);
// Generate AI-based suggestions from projectionData
useEffect(() => { useEffect(() => {
if (!career || !Array.isArray(projectionData)) return; if (!career || !Array.isArray(projectionData)) return;
// Dynamically suggest milestones based on projection data
const suggested = []; const suggested = [];
// Find salary or savings growth points from projectionData: // Example: if retirement crosses $50k or if loans are paid off, we suggest a milestone
projectionData.forEach((monthData, index) => { projectionData.forEach((monthData, index) => {
if (index === 0) return; // Skip first month for comparison if (index === 0) return;
const prevMonth = projectionData[index - 1]; const prevMonth = projectionData[index - 1];
// Example logic: suggest milestones when retirement savings hit certain thresholds // Retirement crossing 50k
if (monthData.totalRetirementSavings >= 50000 && prevMonth.totalRetirementSavings < 50000) { if (
monthData.totalRetirementSavings >= 50000 &&
prevMonth.totalRetirementSavings < 50000
) {
suggested.push({ suggested.push({
title: `Reach $50k Retirement Savings`, title: `Reach $50k Retirement Savings`,
date: monthData.month + '-01', date: monthData.month + '-01',
progress: 0, progress: 0
}); });
} }
// Milestone when loan is paid off // Loan paid off
if (monthData.loanBalance <= 0 && prevMonth.loanBalance > 0) { if (monthData.loanBalance <= 0 && prevMonth.loanBalance > 0) {
suggested.push({ suggested.push({
title: `Student Loans Paid Off`, title: `Student Loans Paid Off`,
date: monthData.month + '-01', date: monthData.month + '-01',
progress: 0, progress: 0
}); });
} }
}); });
// Career-based suggestions still possible (add explicitly if desired) // Career-based suggestions
suggested.push( suggested.push(
{ title: `Entry-Level ${career}`, date: projectionData[6]?.month + '-01' || '2025-06-01', progress: 0 }, {
{ title: `Mid-Level ${career}`, date: projectionData[24]?.month + '-01' || '2027-01-01', progress: 0 }, title: `Entry-Level ${career}`,
{ title: `Senior-Level ${career}`, date: projectionData[60]?.month + '-01' || '2030-01-01', progress: 0 } date: projectionData[6]?.month + '-01' || '2025-06-01',
progress: 0
},
{
title: `Mid-Level ${career}`,
date: projectionData[24]?.month + '-01' || '2027-01-01',
progress: 0
},
{
title: `Senior-Level ${career}`,
date: projectionData[60]?.month + '-01' || '2030-01-01',
progress: 0
}
); );
setSuggestedMilestones(suggested); setSuggestedMilestones(suggested);
setSelected([]); setSelected([]);
}, [career, projectionData]); }, [career, projectionData]);
// Toggle selection of a milestone
const toggleSelect = (index) => { const toggleSelect = (index) => {
setSelected(prev => setSelected((prev) =>
prev.includes(index) ? prev.filter(i => i !== index) : [...prev, index] prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index]
); );
}; };
// Confirm the selected items => POST them as new milestones
const confirmSelectedMilestones = async () => { const confirmSelectedMilestones = async () => {
const milestonesToSend = selected.map(index => { const milestonesToSend = selected.map((index) => {
const m = suggestedMilestones[index]; const m = suggestedMilestones[index];
return { return {
title: m.title, title: m.title,
@ -74,7 +87,7 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active
date: m.date, date: m.date,
progress: m.progress, progress: m.progress,
milestone_type: activeView || 'Career', milestone_type: activeView || 'Career',
career_path_id: careerPathId, career_path_id: careerPathId
}; };
}); });
@ -83,14 +96,15 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active
const res = await authFetch(`/api/premium/milestone`, { const res = await authFetch(`/api/premium/milestone`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ milestones: milestonesToSend }), body: JSON.stringify({ milestones: milestonesToSend }),
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' }
}); });
if (!res.ok) throw new Error('Failed to save selected milestones'); if (!res.ok) throw new Error('Failed to save selected milestones');
const data = await res.json(); const data = await res.json();
console.log('Confirmed milestones:', data); console.log('Confirmed milestones:', data);
setSelected([]); // Clear selection setSelected([]); // Clear selection
window.location.reload(); window.location.reload(); // Re-fetch or reload as needed
} catch (error) { } catch (error) {
console.error('Error saving selected milestones:', error); console.error('Error saving selected milestones:', error);
} finally { } finally {
@ -98,7 +112,6 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active
} }
}; };
if (!suggestedMilestones.length) return null; if (!suggestedMilestones.length) return null;
return ( return (
@ -116,9 +129,12 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active
</li> </li>
))} ))}
</ul> </ul>
<button onClick={confirmSelectedMilestones} disabled={loading || selected.length === 0}> <Button
onClick={confirmSelectedMilestones}
disabled={loading || selected.length === 0}
>
{loading ? 'Saving...' : 'Confirm Selected'} {loading ? 'Saving...' : 'Confirm Selected'}
</button> </Button>
</div> </div>
); );
}; };

View File

@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button } from './ui/button.js';
const CareerSearch = ({ onCareerSelected }) => { const CareerSearch = ({ onCareerSelected }) => {
const [careerObjects, setCareerObjects] = useState([]); const [careerObjects, setCareerObjects] = useState([]);
@ -24,38 +25,30 @@ const CareerSearch = ({ onCareerSelected }) => {
for (let k = 0; k < careersList.length; k++) { for (let k = 0; k < careersList.length; k++) {
const c = careersList[k]; const c = careersList[k];
// If there's a title and soc_code, store the first we encounter for that title.
if (c.title && c.soc_code && c.cip_code !== undefined) { if (c.title && c.soc_code && c.cip_code !== undefined) {
if (!uniqueByTitle.has(c.title)) { if (!uniqueByTitle.has(c.title)) {
// Add it if we haven't seen this exact title yet
uniqueByTitle.set(c.title, { uniqueByTitle.set(c.title, {
title: c.title, title: c.title,
soc_code: c.soc_code, soc_code: c.soc_code,
cip_code: c.cip_code, cip_code: c.cip_code
}); });
} }
// If you truly only want to keep the first occurrence,
// just do nothing if we see the same title again.
} }
} }
} }
} }
// Convert Map to array
const dedupedArr = [...uniqueByTitle.values()]; const dedupedArr = [...uniqueByTitle.values()];
setCareerObjects(dedupedArr); setCareerObjects(dedupedArr);
} catch (error) { } catch (error) {
console.error('Error loading or parsing career_clusters.json:', error); console.error('Error loading or parsing career_clusters.json:', error);
} }
}; };
fetchCareerData(); fetchCareerData();
}, []); }, []);
// Called when user clicks "Confirm New Career"
const handleConfirmCareer = () => { const handleConfirmCareer = () => {
// Find the full object by exact title match // find the full object by exact title match
const foundObj = careerObjects.find( const foundObj = careerObjects.find(
(obj) => obj.title.toLowerCase() === searchInput.toLowerCase() (obj) => obj.title.toLowerCase() === searchInput.toLowerCase()
); );
@ -83,9 +76,9 @@ const CareerSearch = ({ onCareerSelected }) => {
))} ))}
</datalist> </datalist>
<button onClick={handleConfirmCareer} style={{ marginLeft: '8px' }}> <Button onClick={handleConfirmCareer} style={{ marginLeft: '8px' }}>
Confirm Confirm
</button> </Button>
</div> </div>
); );
}; };

View File

@ -1,5 +1,5 @@
// src/components/MilestoneTimeline.js
import React, { useEffect, useState, useCallback } from 'react'; import React, { useEffect, useState, useCallback } from 'react';
import { Button } from './ui/button.js';
const today = new Date(); const today = new Date();
@ -12,7 +12,6 @@ export default function MilestoneTimeline({
}) { }) {
const [milestones, setMilestones] = useState({ Career: [], Financial: [] }); const [milestones, setMilestones] = useState({ Career: [], Financial: [] });
// "new or edit" milestone form data
const [newMilestone, setNewMilestone] = useState({ const [newMilestone, setNewMilestone] = useState({
title: '', title: '',
description: '', description: '',
@ -23,7 +22,6 @@ export default function MilestoneTimeline({
isUniversal: 0 isUniversal: 0
}); });
const [impactsToDelete, setImpactsToDelete] = useState([]); const [impactsToDelete, setImpactsToDelete] = useState([]);
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
const [editingMilestone, setEditingMilestone] = useState(null); const [editingMilestone, setEditingMilestone] = useState(null);
@ -36,33 +34,23 @@ export default function MilestoneTimeline({
const [copyWizardMilestone, setCopyWizardMilestone] = useState(null); const [copyWizardMilestone, setCopyWizardMilestone] = useState(null);
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// 1) HELPER FUNCTIONS (defined above usage) // 1) HELPER: Add or remove an impact from newMilestone
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// Insert a new blank impact
function addNewImpact() { function addNewImpact() {
setNewMilestone((prev) => ({ setNewMilestone((prev) => ({
...prev, ...prev,
impacts: [ impacts: [
...prev.impacts, ...prev.impacts,
{ { impact_type: 'ONE_TIME', direction: 'subtract', amount: 0, start_date: '', end_date: '' }
impact_type: 'ONE_TIME',
direction: 'subtract',
amount: 0,
start_date: '',
end_date: ''
}
] ]
})); }));
} }
// Remove an impact from newMilestone.impacts
function 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 && removed.id) { if (removed && removed.id) {
// queue for DB DELETE
setImpactsToDelete((old) => [...old, removed.id]); setImpactsToDelete((old) => [...old, removed.id]);
} }
newImpacts.splice(idx, 1); newImpacts.splice(idx, 1);
@ -70,7 +58,6 @@ export default function MilestoneTimeline({
}); });
} }
// Update a specific impact property
function updateImpact(idx, field, value) { function updateImpact(idx, field, value) {
setNewMilestone((prev) => { setNewMilestone((prev) => {
const newImpacts = [...prev.impacts]; const newImpacts = [...prev.impacts];
@ -92,10 +79,9 @@ export default function MilestoneTimeline({
} }
const data = await res.json(); const data = await res.json();
if (!data.milestones) { if (!data.milestones) {
console.warn('No milestones field in response:', data); console.warn('No milestones 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]) {
@ -104,7 +90,6 @@ export default function MilestoneTimeline({
console.warn(`Unknown milestone type: ${m.milestone_type}`); console.warn(`Unknown milestone type: ${m.milestone_type}`);
} }
}); });
setMilestones(categorized); setMilestones(categorized);
} catch (err) { } catch (err) {
console.error('Failed to fetch milestones:', err); console.error('Failed to fetch milestones:', err);
@ -116,7 +101,7 @@ export default function MilestoneTimeline({
}, [fetchMilestones]); }, [fetchMilestones]);
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// 3) Load scenarios for copy wizard // 3) Load Scenarios for copy wizard
// ------------------------------------------------------------------ // ------------------------------------------------------------------
useEffect(() => { useEffect(() => {
async function loadScenarios() { async function loadScenarios() {
@ -139,7 +124,6 @@ export default function MilestoneTimeline({
async function handleEditMilestone(m) { async function handleEditMilestone(m) {
try { try {
setImpactsToDelete([]); setImpactsToDelete([]);
const res = await authFetch(`/api/premium/milestone-impacts?milestone_id=${m.id}`); const res = await authFetch(`/api/premium/milestone-impacts?milestone_id=${m.id}`);
if (!res.ok) { if (!res.ok) {
console.error('Failed to fetch milestone impacts. Status:', res.status); console.error('Failed to fetch milestone impacts. Status:', res.status);
@ -270,7 +254,7 @@ export default function MilestoneTimeline({
} }
} }
// Optionally re-fetch or update local // Re-fetch or update local
await fetchMilestones(); await fetchMilestones();
// reset form // reset form
@ -360,7 +344,6 @@ export default function MilestoneTimeline({
// Brute force page refresh // Brute force page refresh
window.location.reload(); window.location.reload();
onClose(); onClose();
} catch (err) { } catch (err) {
console.error('Error copying milestone:', err); console.error('Error copying milestone:', err);
@ -371,7 +354,9 @@ export default function MilestoneTimeline({
<div className="modal-backdrop"> <div className="modal-backdrop">
<div className="modal-container"> <div className="modal-container">
<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}>
@ -387,8 +372,10 @@ export default function MilestoneTimeline({
))} ))}
<div style={{ marginTop: '1rem' }}> <div style={{ marginTop: '1rem' }}>
<button onClick={onClose} style={{ marginRight: '0.5rem' }}>Cancel</button> <Button onClick={onClose} style={{ marginRight: '0.5rem' }}>
<button onClick={handleCopy}>Copy</button> Cancel
</Button>
<Button onClick={handleCopy}>Copy</Button>
</div> </div>
</div> </div>
</div> </div>
@ -425,17 +412,16 @@ export default function MilestoneTimeline({
// normal => single scenario // normal => single scenario
await deleteSingleMilestone(m); await deleteSingleMilestone(m);
} }
// done => brute force
window.location.reload(); window.location.reload();
} }
async function deleteSingleMilestone(m) { async function deleteSingleMilestone(m) {
try { try {
const delRes = await authFetch(`/api/premium/milestones/${m.id}`, { method: 'DELETE' }); const delRes = await authFetch(`/api/premium/milestones/${m.id}`, {
method: 'DELETE'
});
if (!delRes.ok) { if (!delRes.ok) {
console.error('Failed to delete single milestone:', delRes.status); console.error('Failed to delete single milestone:', delRes.status);
return;
} }
} catch (err) { } catch (err) {
console.error('Error removing milestone from scenario:', err); console.error('Error removing milestone from scenario:', err);
@ -464,17 +450,17 @@ export default function MilestoneTimeline({
<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' : ''}
onClick={() => setActiveView(view)} onClick={() => setActiveView(view)}
> >
{view} {view}
</button> </Button>
))} ))}
</div> </div>
<button <Button
onClick={() => { onClick={() => {
if (showForm) { if (showForm) {
// Cancel form // Cancel form
@ -496,11 +482,11 @@ export default function MilestoneTimeline({
}} }}
> >
{showForm ? 'Cancel' : '+ New Milestone'} {showForm ? 'Cancel' : '+ New Milestone'}
</button> </Button>
{/* CREATE/EDIT FORM */}
{showForm && ( {showForm && (
<div className="form"> <div className="form">
{/* Title / Desc / Date / Progress */}
<input <input
type="text" type="text"
placeholder="Title" placeholder="Title"
@ -515,11 +501,8 @@ export default function MilestoneTimeline({
/> />
<input <input
type="date" type="date"
placeholder="Milestone Date"
value={newMilestone.date} value={newMilestone.date}
onChange={(e) => onChange={(e) => setNewMilestone({ ...newMilestone, date: e.target.value })}
setNewMilestone((prev) => ({ ...prev, date: e.target.value }))
}
/> />
<input <input
type="number" type="number"
@ -531,7 +514,6 @@ export default function MilestoneTimeline({
}} }}
/> />
{/* If Financial => newSalary + impacts */}
{activeView === 'Financial' && ( {activeView === 'Financial' && (
<div> <div>
<input <input
@ -587,6 +569,7 @@ export default function MilestoneTimeline({
onChange={(e) => updateImpact(idx, 'start_date', e.target.value)} onChange={(e) => updateImpact(idx, 'start_date', e.target.value)}
/> />
</div> </div>
{imp.impact_type === 'MONTHLY' && ( {imp.impact_type === 'MONTHLY' && (
<div> <div>
<label>End Date (blank if indefinite): </label> <label>End Date (blank if indefinite): </label>
@ -598,18 +581,14 @@ export default function MilestoneTimeline({
</div> </div>
)} )}
<button <Button className="text-red-500 mt-2" onClick={() => removeImpact(idx)}>
className="text-red-500 mt-2"
onClick={() => removeImpact(idx)}
>
Remove Impact Remove Impact
</button> </Button>
</div> </div>
))} ))}
<Button onClick={addNewImpact} className="bg-gray-200 px-2 py-1 mt-2">
<button onClick={addNewImpact} className="bg-gray-200 px-2 py-1 mt-2">
+ Add Impact + Add Impact
</button> </Button>
</div> </div>
</div> </div>
)} )}
@ -631,16 +610,15 @@ export default function MilestoneTimeline({
</label> </label>
</div> </div>
<button onClick={saveMilestone} style={{ marginTop: '1rem' }}> <Button onClick={saveMilestone} style={{ marginTop: '1rem' }}>
{editingMilestone ? 'Update' : 'Add'} Milestone {editingMilestone ? 'Update' : 'Add'} Milestone
</button> </Button>
</div> </div>
)} )}
{/* Actual timeline */} {/* TIMELINE VISUAL */}
<div className="milestone-timeline-container"> <div className="milestone-timeline-container">
<div className="milestone-timeline-line" /> <div className="milestone-timeline-line" />
{milestones[activeView].map((m) => { {milestones[activeView].map((m) => {
const leftPos = calcPosition(m.date); const leftPos = calcPosition(m.date);
return ( return (
@ -649,10 +627,7 @@ export default function MilestoneTimeline({
className="milestone-timeline-post" className="milestone-timeline-post"
style={{ left: `${leftPos}%` }} style={{ left: `${leftPos}%` }}
> >
<div <div className="milestone-timeline-dot" onClick={() => handleEditMilestone(m)} />
className="milestone-timeline-dot"
onClick={() => handleEditMilestone(m)}
/>
<div className="milestone-content"> <div className="milestone-content">
<div className="title">{m.title}</div> <div className="title">{m.title}</div>
{m.description && <p>{m.description}</p>} {m.description && <p>{m.description}</p>}
@ -662,6 +637,7 @@ export default function MilestoneTimeline({
</div> </div>
<div className="date">{m.date}</div> <div className="date">{m.date}</div>
{/* Tasks */}
{m.tasks && m.tasks.length > 0 && ( {m.tasks && m.tasks.length > 0 && (
<ul> <ul>
{m.tasks.map((t) => ( {m.tasks.map((t) => (
@ -674,29 +650,26 @@ export default function MilestoneTimeline({
</ul> </ul>
)} )}
<button <Button
onClick={() => { onClick={() => {
setShowTaskForm(showTaskForm === m.id ? null : m.id); setShowTaskForm(showTaskForm === m.id ? null : m.id);
setNewTask({ title: '', description: '', due_date: '' }); setNewTask({ title: '', description: '', due_date: '' });
}} }}
> >
{showTaskForm === m.id ? 'Cancel Task' : 'Add Task'} {showTaskForm === m.id ? 'Cancel Task' : 'Add Task'}
</button> </Button>
<div style={{ marginTop: '0.5rem' }}> <div style={{ marginTop: '0.5rem' }}>
<button onClick={() => handleEditMilestone(m)}>Edit</button> <Button onClick={() => handleEditMilestone(m)}>Edit</Button>
<button <Button style={{ marginLeft: '0.5rem' }} onClick={() => setCopyWizardMilestone(m)}>
style={{ marginLeft: '0.5rem' }}
onClick={() => setCopyWizardMilestone(m)}
>
Copy Copy
</button> </Button>
<button <Button
style={{ marginLeft: '0.5rem', color: 'red' }} style={{ marginLeft: '0.5rem', background: 'red', color: 'black' }}
onClick={() => handleDeleteMilestone(m)} onClick={() => handleDeleteMilestone(m)}
> >
Delete Delete
</button> </Button>
</div> </div>
{showTaskForm === m.id && ( {showTaskForm === m.id && (
@ -718,7 +691,7 @@ export default function MilestoneTimeline({
value={newTask.due_date} value={newTask.due_date}
onChange={(e) => setNewTask({ ...newTask, due_date: e.target.value })} onChange={(e) => setNewTask({ ...newTask, due_date: e.target.value })}
/> />
<button onClick={() => addTask(m.id)}>Save Task</button> <Button onClick={() => addTask(m.id)}>Save Task</Button>
</div> </div>
)} )}
</div> </div>

View File

@ -21,17 +21,9 @@
margin-bottom: 20px; margin-bottom: 20px;
} }
.view-selector button {
padding: 10px 15px;
border: none;
cursor: pointer;
background: #4caf50;
color: white;
border-radius: 5px;
}
.view-selector button.active { .view-selector button.active {
background: #2e7d32; background: #0703e2;
} }
.timeline-container { .timeline-container {

View File

@ -14,7 +14,7 @@ import {
} from 'chart.js'; } from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation'; import annotationPlugin from 'chartjs-plugin-annotation';
import { Filler } from 'chart.js'; import { Filler } from 'chart.js';
import { Button } from './ui/button.js';
import authFetch from '../utils/authFetch.js'; 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';
@ -520,7 +520,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
{/* Confirm new career scenario */} {/* Confirm new career scenario */}
{pendingCareerForModal && ( {pendingCareerForModal && (
<button <Button
onClick={() => { onClick={() => {
// Example action // Example action
console.log('User confirmed new career path:', pendingCareerForModal); console.log('User confirmed new career path:', pendingCareerForModal);
@ -529,7 +529,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
className="bg-blue-500 hover:bg-blue-600 text-white font-semibold px-4 py-2 rounded" className="bg-blue-500 hover:bg-blue-600 text-white font-semibold px-4 py-2 rounded"
> >
Confirm Career Change to {pendingCareerForModal} Confirm Career Change to {pendingCareerForModal}
</button> </Button>
)} )}
</div> </div>
); );

View File

@ -3,6 +3,7 @@
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 { Button } from './ui/button.js';
/** /**
* MultiScenarioView * MultiScenarioView
@ -248,19 +249,11 @@ export default function MultiScenarioView() {
{/* Add Scenario button */} {/* Add Scenario button */}
<div style={{ alignSelf: 'flex-start' }}> <div style={{ alignSelf: 'flex-start' }}>
<button <Button
onClick={handleAddScenario} onClick={handleAddScenario}
style={{
padding: '0.5rem 1rem',
height: 'auto',
backgroundColor: '#76b900',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
> >
+ Add Scenario + Add Scenario
</button> </Button>
</div> </div>
</div> </div>
); );

View File

@ -1,48 +1,45 @@
// 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 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';
import ScenarioEditModal from './ScenarioEditModal.js'; import ScenarioEditModal from './ScenarioEditModal.js';
// Register the annotation plugin (though we won't use it for milestone markers). // Register the annotation plugin
ChartJS.register(annotationPlugin); ChartJS.register(annotationPlugin);
/** /**
* ScenarioContainer * ScenarioContainer
* ----------------- * -----------------
* This component: * This component:
* - Renders a <select> to pick from all scenarios, or uses the 'scenario' prop if set * 1) Lets the user pick a scenario (via <select>), or uses the provided `scenario` prop.
* - Fetches the collegeProfile for that scenario * 2) Loads the collegeProfile + milestones/impacts for that scenario.
* - Fetches all milestones, plus their tasks and impacts * 3) Merges scenario + user financial data + milestone impacts runs `simulateFinancialProjection`.
* - Builds a "mergedProfile" like in single-scenario code, including: * 4) Shows a chart of net savings / retirement / loan balances over time.
* -- Overridden fields from the scenario * 5) Allows milestone CRUD (create, edit, delete, copy).
* -- The user's global financialProfile * 6) Offers Clone / Delete scenario callbacks from the parent.
* -- milestoneImpacts from all milestones
* -- The "inCollege" flag, etc.
* - Calls simulateFinancialProjection(mergedProfile)
* - Displays the results (chart, loan payoff, final retirement)
* - Provides Milestone CRUD
* - "Clone" / "Remove" scenario methods are delegated to parent
*/ */
export default function ScenarioContainer({ export default function ScenarioContainer({
scenario, // The scenario row from career_paths scenario, // The scenario row from career_paths
financialProfile, // The users overall financial snapshot financialProfile, // The users overall financial snapshot
onRemove, onRemove, // Callback for deleting scenario
onClone, onClone, // Callback for cloning scenario
onEdit onEdit // (Optional) If you want a scenario editing callback
}) { }) {
/************************************************************* /*************************************************************
* 1) SCENARIO DROPDOWN: allScenarios, localScenario, etc. * 1) SCENARIO DROPDOWN: Load, store, and let user pick
*************************************************************/ *************************************************************/
const [allScenarios, setAllScenarios] = useState([]); const [allScenarios, setAllScenarios] = useState([]);
// We keep a local copy of scenario so you can switch from dropdown // We keep a local copy of `scenario` so user can switch from the dropdown
const [localScenario, setLocalScenario] = useState(scenario || null); const [localScenario, setLocalScenario] = useState(scenario || null);
// A) On mount, fetch all scenarios to populate the dropdown // (A) On mount, fetch all scenarios to populate the <select>
useEffect(() => { useEffect(() => {
async function loadScenarios() { async function loadScenarios() {
try { try {
@ -59,17 +56,20 @@ export default function ScenarioContainer({
loadScenarios(); loadScenarios();
}, []); }, []);
// B) If the parent changes the scenario prop => update local // (B) If parent changes the `scenario` prop, update local
useEffect(() => { useEffect(() => {
setLocalScenario(scenario || null); setLocalScenario(scenario || null);
}, [scenario]); }, [scenario]);
// C) scenario <select> handler // (C) <select> handler for picking a scenario from dropdown
function handleScenarioSelect(e) { function handleScenarioSelect(e) {
const chosenId = e.target.value; const chosenId = e.target.value;
const found = allScenarios.find((s) => s.id === chosenId); const found = allScenarios.find((s) => s.id === chosenId);
if (found) setLocalScenario(found); if (found) {
else setLocalScenario(null); setLocalScenario(found);
} else {
setLocalScenario(null);
}
} }
/************************************************************* /*************************************************************
@ -79,15 +79,17 @@ export default function ScenarioContainer({
const [milestones, setMilestones] = useState([]); const [milestones, setMilestones] = useState([]);
const [impactsByMilestone, setImpactsByMilestone] = useState({}); const [impactsByMilestone, setImpactsByMilestone] = useState({});
// We'll also track a scenario edit modal
const [showEditModal, setShowEditModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false);
const [editingScenarioData, setEditingScenarioData] = useState({ const [editingScenarioData, setEditingScenarioData] = useState({
scenario: null, scenario: null,
collegeProfile: null collegeProfile: null
}); });
// (A) Load the college profile for localScenario // (A) Load the college profile for the selected scenario
useEffect(() => { useEffect(() => {
if (!localScenario?.id) { if (!localScenario?.id) {
// if no scenario selected, clear the collegeProfile
setCollegeProfile(null); setCollegeProfile(null);
return; return;
} }
@ -97,7 +99,7 @@ export default function ScenarioContainer({
const res = await authFetch(url); const res = await authFetch(url);
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
// Might be an object or array // in some setups, the endpoint returns an array, in others an object
setCollegeProfile(Array.isArray(data) ? data[0] || {} : data); setCollegeProfile(Array.isArray(data) ? data[0] || {} : data);
} else { } else {
setCollegeProfile({}); setCollegeProfile({});
@ -110,7 +112,7 @@ export default function ScenarioContainer({
loadCollegeProfile(); loadCollegeProfile();
}, [localScenario]); }, [localScenario]);
// (B) Load milestones // (B) Load milestones for localScenario (and each milestones impacts)
const fetchMilestones = useCallback(async () => { const fetchMilestones = useCallback(async () => {
if (!localScenario?.id) { if (!localScenario?.id) {
setMilestones([]); setMilestones([]);
@ -160,7 +162,7 @@ export default function ScenarioContainer({
// Wait until we have localScenario + collegeProfile + financialProfile // Wait until we have localScenario + collegeProfile + financialProfile
if (!financialProfile || !localScenario?.id || !collegeProfile) return; if (!financialProfile || !localScenario?.id || !collegeProfile) return;
// Gather all milestoneImpacts into one array for the aggregator // Gather all milestoneImpacts
let allImpacts = []; let allImpacts = [];
Object.keys(impactsByMilestone).forEach((mId) => { Object.keys(impactsByMilestone).forEach((mId) => {
allImpacts = allImpacts.concat(impactsByMilestone[mId]); allImpacts = allImpacts.concat(impactsByMilestone[mId]);
@ -168,9 +170,9 @@ export default function ScenarioContainer({
const simYears = parseInt(simulationYearsInput, 10) || 20; const simYears = parseInt(simulationYearsInput, 10) || 20;
// Build the mergedProfile exactly like single-scenario aggregator // Build mergedProfile from scenario + user financial + college + milestone
const mergedProfile = { const mergedProfile = {
// Base user financial // base user data
currentSalary: financialProfile.current_salary || 0, currentSalary: financialProfile.current_salary || 0,
monthlyExpenses: monthlyExpenses:
@ -178,7 +180,6 @@ export default function ScenarioContainer({
monthlyDebtPayments: monthlyDebtPayments:
localScenario.planned_monthly_debt_payments ?? financialProfile.monthly_debt_payments ?? 0, localScenario.planned_monthly_debt_payments ?? financialProfile.monthly_debt_payments ?? 0,
// Overridden savings from scenario or fallback to financialProfile
retirementSavings: financialProfile.retirement_savings ?? 0, retirementSavings: financialProfile.retirement_savings ?? 0,
emergencySavings: financialProfile.emergency_fund ?? 0, emergencySavings: financialProfile.emergency_fund ?? 0,
@ -203,7 +204,7 @@ export default function ScenarioContainer({
additionalIncome: additionalIncome:
localScenario.planned_additional_income ?? financialProfile.additional_income ?? 0, localScenario.planned_additional_income ?? financialProfile.additional_income ?? 0,
// College data // college-related
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,
@ -213,7 +214,6 @@ export default function ScenarioContainer({
calculatedTuition: collegeProfile.tuition || 0, calculatedTuition: collegeProfile.tuition || 0,
extraPayment: collegeProfile.extra_payment || 0, extraPayment: collegeProfile.extra_payment || 0,
// Are we in college?
inCollege: inCollege:
collegeProfile.college_enrollment_status === 'currently_enrolled' || collegeProfile.college_enrollment_status === 'currently_enrolled' ||
collegeProfile.college_enrollment_status === 'prospective_student', collegeProfile.college_enrollment_status === 'prospective_student',
@ -225,18 +225,18 @@ export default function ScenarioContainer({
programLength: collegeProfile.program_length || 0, programLength: collegeProfile.program_length || 0,
expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary || 0, expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary || 0,
// Scenario start date // scenario date + simulation horizon
startDate: localScenario.start_date || new Date().toISOString(), startDate: localScenario.start_date || new Date().toISOString(),
simulationYears: simYears, simulationYears: simYears,
// MILESTONE IMPACTS // milestone impacts
milestoneImpacts: allImpacts milestoneImpacts: allImpacts
}; };
// Run the simulation // Run the simulation
const { projectionData, loanPaidOffMonth } = simulateFinancialProjection(mergedProfile); const { projectionData, loanPaidOffMonth } = simulateFinancialProjection(mergedProfile);
// Optionally compute a "cumulativeNetSavings" to display // Optionally add a "cumulativeNetSavings" for display
let cumulative = mergedProfile.emergencySavings || 0; let cumulative = mergedProfile.emergencySavings || 0;
const finalData = projectionData.map((monthRow) => { const finalData = projectionData.map((monthRow) => {
cumulative += (monthRow.netSavings || 0); cumulative += (monthRow.netSavings || 0);
@ -245,7 +245,6 @@ export default function ScenarioContainer({
setProjectionData(finalData); setProjectionData(finalData);
setLoanPaidOffMonth(loanPaidOffMonth); setLoanPaidOffMonth(loanPaidOffMonth);
}, [ }, [
financialProfile, financialProfile,
localScenario, localScenario,
@ -264,16 +263,17 @@ export default function ScenarioContainer({
} }
/************************************************************* /*************************************************************
* 4) CHART: lines + milestone markers * 4) CHART: build data, handle milestone markers
*************************************************************/ *************************************************************/
// Create arrays from projectionData // x-axis labels (e.g. "2025-01", "2025-02", etc.)
const chartLabels = projectionData.map((p) => p.month); const chartLabels = projectionData.map((p) => p.month);
// We'll show netSavings, retirementSavings, and loanBalance
// dataset arrays
const netSavingsData = projectionData.map((p) => p.cumulativeNetSavings || 0); const netSavingsData = projectionData.map((p) => p.cumulativeNetSavings || 0);
const retData = projectionData.map((p) => p.retirementSavings || 0); const retData = projectionData.map((p) => p.retirementSavings || 0);
const loanData = projectionData.map((p) => p.loanBalance || 0); const loanData = projectionData.map((p) => p.loanBalance || 0);
// Build a "milestone points" array to place markers on the chart // milestone markers => we find index by matching YYYY-MM
function getLabelIndexForMilestone(m) { function getLabelIndexForMilestone(m) {
if (!m.date) return -1; if (!m.date) return -1;
const short = m.date.slice(0, 7); // "YYYY-MM" const short = m.date.slice(0, 7); // "YYYY-MM"
@ -284,11 +284,7 @@ export default function ScenarioContainer({
.map((m) => { .map((m) => {
const xIndex = getLabelIndexForMilestone(m); const xIndex = getLabelIndexForMilestone(m);
if (xIndex < 0) return null; if (xIndex < 0) return null;
return { return { x: xIndex, y: 0, milestoneObj: m };
x: xIndex,
y: 0, // place them at 0 or some other reference
milestoneObj: m
};
}) })
.filter(Boolean); .filter(Boolean);
@ -314,7 +310,6 @@ export default function ScenarioContainer({
fill: false fill: false
}, },
{ {
// The milestone dataset for clickable markers
label: 'Milestones', label: 'Milestones',
data: milestonePoints, data: milestonePoints,
showLine: false, showLine: false,
@ -360,7 +355,7 @@ export default function ScenarioContainer({
}; };
/************************************************************* /*************************************************************
* 5) MILESTONE CRUD: same as your code (Add, Copy, etc.) * 5) MILESTONE CRUD
*************************************************************/ *************************************************************/
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
const [editingMilestone, setEditingMilestone] = useState(null); const [editingMilestone, setEditingMilestone] = useState(null);
@ -397,12 +392,12 @@ 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);
setImpactsToDelete([]); setImpactsToDelete([]);
// fetch impacts for this milestone
try { try {
const impRes = await authFetch(`/api/premium/milestone-impacts?milestone_id=${m.id}`); const impRes = await authFetch(`/api/premium/milestone-impacts?milestone_id=${m.id}`);
if (impRes.ok) { if (impRes.ok) {
@ -436,16 +431,11 @@ export default function ScenarioContainer({
...prev, ...prev,
impacts: [ impacts: [
...prev.impacts, ...prev.impacts,
{ { impact_type: 'ONE_TIME', direction: 'subtract', amount: 0, start_date: '', end_date: '' }
impact_type: 'ONE_TIME',
direction: 'subtract',
amount: 0,
start_date: '',
end_date: ''
}
] ]
})); }));
} }
function removeImpact(idx) { function removeImpact(idx) {
setNewMilestone((prev) => { setNewMilestone((prev) => {
const copy = [...prev.impacts]; const copy = [...prev.impacts];
@ -457,6 +447,7 @@ export default function ScenarioContainer({
return { ...prev, impacts: copy }; return { ...prev, impacts: copy };
}); });
} }
function updateImpact(idx, field, value) { function updateImpact(idx, field, value) {
setNewMilestone((prev) => { setNewMilestone((prev) => {
const copy = [...prev.impacts]; const copy = [...prev.impacts];
@ -465,6 +456,7 @@ export default function ScenarioContainer({
}); });
} }
// create or update milestone
async function saveMilestone() { async function saveMilestone() {
if (!localScenario?.id) return; if (!localScenario?.id) return;
@ -474,7 +466,7 @@ export default function ScenarioContainer({
const method = editingMilestone ? 'PUT' : 'POST'; const method = editingMilestone ? 'PUT' : 'POST';
const payload = { const payload = {
milestone_type: 'Financial', // or "Career" if needed milestone_type: 'Financial', // or "Career" if your scenario is about career
title: newMilestone.title, title: newMilestone.title,
description: newMilestone.description, description: newMilestone.description,
date: newMilestone.date, date: newMilestone.date,
@ -513,14 +505,14 @@ export default function ScenarioContainer({
end_date: imp.end_date || null end_date: imp.end_date || null
}; };
if (imp.id) { if (imp.id) {
// PUT // update existing
await authFetch(`/api/premium/milestone-impacts/${imp.id}`, { await authFetch(`/api/premium/milestone-impacts/${imp.id}`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(impPayload) body: JSON.stringify(impPayload)
}); });
} else { } else {
// POST // create new
await authFetch('/api/premium/milestone-impacts', { await authFetch('/api/premium/milestone-impacts', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@ -529,6 +521,7 @@ export default function ScenarioContainer({
} }
} }
// done => re-fetch
await fetchMilestones(); await fetchMilestones();
// reset form // reset form
@ -550,6 +543,7 @@ export default function ScenarioContainer({
} }
} }
// delete milestone => if universal, ask user if removing from all or just this scenario
async function handleDeleteMilestone(m) { async function handleDeleteMilestone(m) {
if (m.is_universal === 1) { if (m.is_universal === 1) {
const userChoice = window.confirm( const userChoice = window.confirm(
@ -569,6 +563,7 @@ export default function ScenarioContainer({
} }
await fetchMilestones(); await fetchMilestones();
} }
async function deleteSingleMilestone(m) { async function deleteSingleMilestone(m) {
try { try {
await authFetch(`/api/premium/milestones/${m.id}`, { method: 'DELETE' }); await authFetch(`/api/premium/milestones/${m.id}`, { method: 'DELETE' });
@ -577,6 +572,7 @@ export default function ScenarioContainer({
} }
} }
// tasks
async function addTask(milestoneId) { async function addTask(milestoneId) {
try { try {
const payload = { const payload = {
@ -602,11 +598,9 @@ export default function ScenarioContainer({
} }
} }
// Instead of immediately calling `onEdit(scenario)`, we handle locally: // scenario-level editing
function handleEditScenario() { function handleEditScenario() {
if (!localScenario) return; if (!localScenario) return;
// We'll store the scenario and also fetch the collegeProfile if needed
// or we can just reuse the existing `collegeProfile` state.
setEditingScenarioData({ setEditingScenarioData({
scenario: localScenario, scenario: localScenario,
collegeProfile collegeProfile
@ -614,7 +608,6 @@ export default function ScenarioContainer({
setShowEditModal(true); setShowEditModal(true);
} }
// Scenario-level
function handleDeleteScenario() { function handleDeleteScenario() {
if (localScenario) onRemove(localScenario.id); if (localScenario) onRemove(localScenario.id);
} }
@ -622,7 +615,6 @@ export default function ScenarioContainer({
if (localScenario) onClone(localScenario); if (localScenario) onClone(localScenario);
} }
/************************************************************* /*************************************************************
* 6) COPY WIZARD * 6) COPY WIZARD
*************************************************************/ *************************************************************/
@ -649,7 +641,7 @@ export default function ScenarioContainer({
}); });
if (!res.ok) throw new Error('Failed to copy milestone'); if (!res.ok) throw new Error('Failed to copy milestone');
onClose(); onClose();
// Optionally reload the page or call fetchMilestones again // Optionally reload
window.location.reload(); window.location.reload();
} catch (err) { } catch (err) {
console.error('Error copying milestone:', err); console.error('Error copying milestone:', err);
@ -696,10 +688,10 @@ export default function ScenarioContainer({
))} ))}
<div style={{ marginTop: '1rem' }}> <div style={{ marginTop: '1rem' }}>
<button onClick={onClose} style={{ marginRight: '0.5rem' }}> <Button onClick={onClose} style={{ marginRight: '0.5rem' }}>
Cancel Cancel
</button> </Button>
<button onClick={handleCopy}>Copy</button> <Button onClick={handleCopy}>Copy</Button>
</div> </div>
</div> </div>
</div> </div>
@ -761,11 +753,11 @@ export default function ScenarioContainer({
</div> </div>
)} )}
<button onClick={handleNewMilestone} style={{ marginTop: '0.5rem' }}> <Button onClick={handleNewMilestone} style={{ marginTop: '0.5rem' }}>
+ New Milestone + New Milestone
</button> </Button>
{/* AI-Suggested Milestones (unchanged) */} {/* AI-Suggested Milestones */}
<AISuggestedMilestones <AISuggestedMilestones
career={localScenario.career_name || localScenario.scenario_title || ''} career={localScenario.career_name || localScenario.scenario_title || ''}
careerPathId={localScenario.id} careerPathId={localScenario.id}
@ -775,17 +767,16 @@ export default function ScenarioContainer({
/> />
<div style={{ marginTop: '0.5rem' }}> <div style={{ marginTop: '0.5rem' }}>
{/* Instead of calling onEdit(localScenario), show local modal */} <Button onClick={handleEditScenario}>Edit</Button>
<button onClick={handleEditScenario}>Edit</button> <Button onClick={handleCloneScenario} style={{ marginLeft: '0.5rem' }}>
<button onClick={handleCloneScenario} style={{ marginLeft: '0.5rem' }}>
Clone Clone
</button> </Button>
<button <Button
onClick={handleDeleteScenario} onClick={handleDeleteScenario}
style={{ marginLeft: '0.5rem', color: 'red' }} style={{ marginLeft: '0.5rem', background: 'red', color: 'black' }}
> >
Delete Delete
</button> </Button>
</div> </div>
{/* The inline form for milestone creation/edit */} {/* The inline form for milestone creation/edit */}
@ -878,22 +869,22 @@ export default function ScenarioContainer({
/> />
</div> </div>
)} )}
<button <Button
style={{ marginLeft: '0.5rem', color: 'red' }} style={{ marginLeft: '0.5rem', color: 'red' }}
onClick={() => removeImpact(idx)} onClick={() => removeImpact(idx)}
> >
Remove Remove
</button> </Button>
</div> </div>
))} ))}
<button onClick={addNewImpact}>+ Add Impact</button> <Button onClick={addNewImpact}>+ Add Impact</Button>
</div> </div>
<div style={{ marginTop: '1rem' }}> <div style={{ marginTop: '1rem' }}>
<button onClick={saveMilestone}> <Button onClick={saveMilestone}>
{editingMilestone ? 'Update' : 'Add'} Milestone {editingMilestone ? 'Update' : 'Add'} Milestone
</button> </Button>
<button <Button
style={{ marginLeft: '0.5rem' }} style={{ marginLeft: '0.5rem' }}
onClick={() => { onClick={() => {
setShowForm(false); setShowForm(false);
@ -911,7 +902,7 @@ export default function ScenarioContainer({
}} }}
> >
Cancel Cancel
</button> </Button>
</div> </div>
</div> </div>
)} )}
@ -943,29 +934,32 @@ export default function ScenarioContainer({
</ul> </ul>
)} )}
<button <Button
onClick={() => { onClick={() => {
setShowTaskForm(showTaskForm === m.id ? null : m.id); setShowTaskForm(showTaskForm === m.id ? null : m.id);
setNewTask({ title: '', description: '', due_date: '' }); setNewTask({ title: '', description: '', due_date: '' });
}} }}
> >
{showTaskForm === m.id ? 'Cancel Task' : 'Add Task'} {showTaskForm === m.id ? 'Cancel Task' : 'Add Task'}
</button> </Button>
<button style={{ marginLeft: '0.5rem' }} onClick={() => handleEditMilestone(m)}> <Button
style={{ marginLeft: '0.5rem' }}
onClick={() => handleEditMilestone(m)}
>
Edit Edit
</button> </Button>
<button <Button
style={{ marginLeft: '0.5rem' }} style={{ marginLeft: '0.5rem' }}
onClick={() => setCopyWizardMilestone(m)} onClick={() => setCopyWizardMilestone(m)}
> >
Copy Copy
</button> </Button>
<button <Button
style={{ marginLeft: '0.5rem', color: 'red' }} style={{ marginLeft: '0.5rem', background: 'red', color: 'black' }}
onClick={() => handleDeleteMilestone(m)} onClick={() => handleDeleteMilestone(m)}
> >
Delete Delete
</button> </Button>
{/* Task form */} {/* Task form */}
{showTaskForm === m.id && ( {showTaskForm === m.id && (
@ -987,14 +981,14 @@ export default function ScenarioContainer({
value={newTask.due_date} value={newTask.due_date}
onChange={(e) => setNewTask({ ...newTask, due_date: e.target.value })} onChange={(e) => setNewTask({ ...newTask, due_date: e.target.value })}
/> />
<button onClick={() => addTask(m.id)}>Save Task</button> <Button onClick={() => addTask(m.id)}>Save Task</Button>
</div> </div>
)} )}
</div> </div>
); );
})} })}
{/* (B) RENDER THE EDIT MODAL IF showEditModal */} {/* (B) Show the scenario edit modal if needed */}
<ScenarioEditModal <ScenarioEditModal
show={showEditModal} show={showEditModal}
onClose={() => setShowEditModal(false)} onClose={() => setShowEditModal(false)}