UI updates for premium routes
This commit is contained in:
parent
9404135915
commit
ce53afb3d1
62
src/App.js
62
src/App.js
@ -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 */}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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 user’s overall financial snapshot
|
financialProfile, // The user’s 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 milestone’s 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)}
|
||||||
|
Loading…
Reference in New Issue
Block a user