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 PremiumRoute from './components/PremiumRoute.js';
|
||||
import SessionExpiredHandler from './components/SessionExpiredHandler.js';
|
||||
@ -19,23 +19,69 @@ function App() {
|
||||
const location = useLocation();
|
||||
|
||||
// Track whether user is authenticated
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(() => {
|
||||
return !!localStorage.getItem('token');
|
||||
});
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
|
||||
// Track the user object, including is_premium
|
||||
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 = [
|
||||
'/milestone-tracker',
|
||||
'/paywall',
|
||||
'/financial-profile',
|
||||
'/multi-scenario',
|
||||
'/premium-onboarding'
|
||||
'/premium-onboarding',
|
||||
];
|
||||
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 (
|
||||
<div className="flex min-h-screen flex-col bg-gray-50 text-gray-800">
|
||||
{/* Header */}
|
||||
@ -65,13 +111,13 @@ function App() {
|
||||
element={
|
||||
<SignIn
|
||||
setIsAuthenticated={setIsAuthenticated}
|
||||
setUser={setUser} // We pass setUser so SignIn can store user details
|
||||
setUser={setUser}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route path="/signup" element={<SignUp />} />
|
||||
|
||||
{/* Paywall - open to everyone for subscription */}
|
||||
{/* Paywall (accessible to all) */}
|
||||
<Route path="/paywall" element={<Paywall />} />
|
||||
|
||||
{/* Authenticated routes */}
|
||||
|
@ -1,72 +1,85 @@
|
||||
// src/components/AISuggestedMilestones.js
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Button } from './ui/button.js';
|
||||
|
||||
const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, activeView, projectionData }) => {
|
||||
const [suggestedMilestones, setSuggestedMilestones] = useState([]);
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Show a warning if projectionData is not an array
|
||||
useEffect(() => {
|
||||
if (!Array.isArray(projectionData)) {
|
||||
console.warn('⚠️ projectionData is not an array:', projectionData);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('📊 projectionData sample:', projectionData.slice(0, 3));
|
||||
}, [projectionData]);
|
||||
|
||||
|
||||
|
||||
// Generate AI-based suggestions from projectionData
|
||||
useEffect(() => {
|
||||
if (!career || !Array.isArray(projectionData)) return;
|
||||
|
||||
// Dynamically suggest milestones based on projection data
|
||||
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) => {
|
||||
if (index === 0) return; // Skip first month for comparison
|
||||
if (index === 0) return;
|
||||
const prevMonth = projectionData[index - 1];
|
||||
|
||||
// Example logic: suggest milestones when retirement savings hit certain thresholds
|
||||
if (monthData.totalRetirementSavings >= 50000 && prevMonth.totalRetirementSavings < 50000) {
|
||||
|
||||
// Retirement crossing 50k
|
||||
if (
|
||||
monthData.totalRetirementSavings >= 50000 &&
|
||||
prevMonth.totalRetirementSavings < 50000
|
||||
) {
|
||||
suggested.push({
|
||||
title: `Reach $50k Retirement Savings`,
|
||||
date: monthData.month + '-01',
|
||||
progress: 0,
|
||||
progress: 0
|
||||
});
|
||||
}
|
||||
|
||||
// Milestone when loan is paid off
|
||||
|
||||
// Loan paid off
|
||||
if (monthData.loanBalance <= 0 && prevMonth.loanBalance > 0) {
|
||||
suggested.push({
|
||||
title: `Student Loans Paid Off`,
|
||||
date: monthData.month + '-01',
|
||||
progress: 0,
|
||||
progress: 0
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Career-based suggestions still possible (add explicitly if desired)
|
||||
|
||||
// Career-based suggestions
|
||||
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: `Senior-Level ${career}`, date: projectionData[60]?.month + '-01' || '2030-01-01', progress: 0 }
|
||||
{
|
||||
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: `Senior-Level ${career}`,
|
||||
date: projectionData[60]?.month + '-01' || '2030-01-01',
|
||||
progress: 0
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
setSuggestedMilestones(suggested);
|
||||
setSelected([]);
|
||||
}, [career, projectionData]);
|
||||
|
||||
|
||||
|
||||
// Toggle selection of a milestone
|
||||
const toggleSelect = (index) => {
|
||||
setSelected(prev =>
|
||||
prev.includes(index) ? prev.filter(i => i !== index) : [...prev, index]
|
||||
setSelected((prev) =>
|
||||
prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index]
|
||||
);
|
||||
};
|
||||
|
||||
// Confirm the selected items => POST them as new milestones
|
||||
const confirmSelectedMilestones = async () => {
|
||||
const milestonesToSend = selected.map(index => {
|
||||
const milestonesToSend = selected.map((index) => {
|
||||
const m = suggestedMilestones[index];
|
||||
return {
|
||||
title: m.title,
|
||||
@ -74,30 +87,30 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active
|
||||
date: m.date,
|
||||
progress: m.progress,
|
||||
milestone_type: activeView || 'Career',
|
||||
career_path_id: careerPathId,
|
||||
career_path_id: careerPathId
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await authFetch(`/api/premium/milestone`, {
|
||||
method: 'POST',
|
||||
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');
|
||||
|
||||
const data = await res.json();
|
||||
console.log('Confirmed milestones:', data);
|
||||
|
||||
setSelected([]); // Clear selection
|
||||
window.location.reload();
|
||||
window.location.reload(); // Re-fetch or reload as needed
|
||||
} catch (error) {
|
||||
console.error('Error saving selected milestones:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (!suggestedMilestones.length) return null;
|
||||
|
||||
@ -116,9 +129,12 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button onClick={confirmSelectedMilestones} disabled={loading || selected.length === 0}>
|
||||
<Button
|
||||
onClick={confirmSelectedMilestones}
|
||||
disabled={loading || selected.length === 0}
|
||||
>
|
||||
{loading ? 'Saving...' : 'Confirm Selected'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button } from './ui/button.js';
|
||||
|
||||
const CareerSearch = ({ onCareerSelected }) => {
|
||||
const [careerObjects, setCareerObjects] = useState([]);
|
||||
@ -24,38 +25,30 @@ const CareerSearch = ({ onCareerSelected }) => {
|
||||
|
||||
for (let k = 0; k < careersList.length; 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 (!uniqueByTitle.has(c.title)) {
|
||||
// Add it if we haven't seen this exact title yet
|
||||
uniqueByTitle.set(c.title, {
|
||||
title: c.title,
|
||||
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()];
|
||||
setCareerObjects(dedupedArr);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading or parsing career_clusters.json:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCareerData();
|
||||
}, []);
|
||||
|
||||
// Called when user clicks "Confirm New Career"
|
||||
const handleConfirmCareer = () => {
|
||||
// Find the full object by exact title match
|
||||
// find the full object by exact title match
|
||||
const foundObj = careerObjects.find(
|
||||
(obj) => obj.title.toLowerCase() === searchInput.toLowerCase()
|
||||
);
|
||||
@ -83,9 +76,9 @@ const CareerSearch = ({ onCareerSelected }) => {
|
||||
))}
|
||||
</datalist>
|
||||
|
||||
<button onClick={handleConfirmCareer} style={{ marginLeft: '8px' }}>
|
||||
<Button onClick={handleConfirmCareer} style={{ marginLeft: '8px' }}>
|
||||
Confirm
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
// src/components/MilestoneTimeline.js
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { Button } from './ui/button.js';
|
||||
|
||||
const today = new Date();
|
||||
|
||||
@ -12,7 +12,6 @@ export default function MilestoneTimeline({
|
||||
}) {
|
||||
const [milestones, setMilestones] = useState({ Career: [], Financial: [] });
|
||||
|
||||
// "new or edit" milestone form data
|
||||
const [newMilestone, setNewMilestone] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
@ -23,7 +22,6 @@ export default function MilestoneTimeline({
|
||||
isUniversal: 0
|
||||
});
|
||||
const [impactsToDelete, setImpactsToDelete] = useState([]);
|
||||
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editingMilestone, setEditingMilestone] = useState(null);
|
||||
|
||||
@ -36,33 +34,23 @@ export default function MilestoneTimeline({
|
||||
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() {
|
||||
setNewMilestone((prev) => ({
|
||||
...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) {
|
||||
setNewMilestone((prev) => {
|
||||
const newImpacts = [...prev.impacts];
|
||||
const removed = newImpacts[idx];
|
||||
if (removed && removed.id) {
|
||||
// queue for DB DELETE
|
||||
setImpactsToDelete((old) => [...old, removed.id]);
|
||||
}
|
||||
newImpacts.splice(idx, 1);
|
||||
@ -70,7 +58,6 @@ export default function MilestoneTimeline({
|
||||
});
|
||||
}
|
||||
|
||||
// Update a specific impact property
|
||||
function updateImpact(idx, field, value) {
|
||||
setNewMilestone((prev) => {
|
||||
const newImpacts = [...prev.impacts];
|
||||
@ -92,10 +79,9 @@ export default function MilestoneTimeline({
|
||||
}
|
||||
const data = await res.json();
|
||||
if (!data.milestones) {
|
||||
console.warn('No milestones field in response:', data);
|
||||
console.warn('No milestones in response:', data);
|
||||
return;
|
||||
}
|
||||
|
||||
const categorized = { Career: [], Financial: [] };
|
||||
data.milestones.forEach((m) => {
|
||||
if (categorized[m.milestone_type]) {
|
||||
@ -104,7 +90,6 @@ export default function MilestoneTimeline({
|
||||
console.warn(`Unknown milestone type: ${m.milestone_type}`);
|
||||
}
|
||||
});
|
||||
|
||||
setMilestones(categorized);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch milestones:', err);
|
||||
@ -116,7 +101,7 @@ export default function MilestoneTimeline({
|
||||
}, [fetchMilestones]);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 3) Load scenarios for copy wizard
|
||||
// 3) Load Scenarios for copy wizard
|
||||
// ------------------------------------------------------------------
|
||||
useEffect(() => {
|
||||
async function loadScenarios() {
|
||||
@ -139,7 +124,6 @@ export default function MilestoneTimeline({
|
||||
async function handleEditMilestone(m) {
|
||||
try {
|
||||
setImpactsToDelete([]);
|
||||
|
||||
const res = await authFetch(`/api/premium/milestone-impacts?milestone_id=${m.id}`);
|
||||
if (!res.ok) {
|
||||
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();
|
||||
|
||||
// reset form
|
||||
@ -360,7 +344,6 @@ export default function MilestoneTimeline({
|
||||
|
||||
// Brute force page refresh
|
||||
window.location.reload();
|
||||
|
||||
onClose();
|
||||
} catch (err) {
|
||||
console.error('Error copying milestone:', err);
|
||||
@ -371,7 +354,9 @@ export default function MilestoneTimeline({
|
||||
<div className="modal-backdrop">
|
||||
<div className="modal-container">
|
||||
<h3>Copy Milestone to Other Scenarios</h3>
|
||||
<p>Milestone: <strong>{milestone.title}</strong></p>
|
||||
<p>
|
||||
Milestone: <strong>{milestone.title}</strong>
|
||||
</p>
|
||||
|
||||
{scenarios.map((s) => (
|
||||
<div key={s.id}>
|
||||
@ -387,8 +372,10 @@ export default function MilestoneTimeline({
|
||||
))}
|
||||
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<button onClick={onClose} style={{ marginRight: '0.5rem' }}>Cancel</button>
|
||||
<button onClick={handleCopy}>Copy</button>
|
||||
<Button onClick={onClose} style={{ marginRight: '0.5rem' }}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCopy}>Copy</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -425,17 +412,16 @@ export default function MilestoneTimeline({
|
||||
// normal => single scenario
|
||||
await deleteSingleMilestone(m);
|
||||
}
|
||||
|
||||
// done => brute force
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
async function deleteSingleMilestone(m) {
|
||||
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) {
|
||||
console.error('Failed to delete single milestone:', delRes.status);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error removing milestone from scenario:', err);
|
||||
@ -464,17 +450,17 @@ export default function MilestoneTimeline({
|
||||
<div className="milestone-timeline">
|
||||
<div className="view-selector">
|
||||
{['Career', 'Financial'].map((view) => (
|
||||
<button
|
||||
<Button
|
||||
key={view}
|
||||
className={activeView === view ? 'active' : ''}
|
||||
onClick={() => setActiveView(view)}
|
||||
>
|
||||
{view}
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (showForm) {
|
||||
// Cancel form
|
||||
@ -496,11 +482,11 @@ export default function MilestoneTimeline({
|
||||
}}
|
||||
>
|
||||
{showForm ? 'Cancel' : '+ New Milestone'}
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{/* CREATE/EDIT FORM */}
|
||||
{showForm && (
|
||||
<div className="form">
|
||||
{/* Title / Desc / Date / Progress */}
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Title"
|
||||
@ -515,11 +501,8 @@ export default function MilestoneTimeline({
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
placeholder="Milestone Date"
|
||||
value={newMilestone.date}
|
||||
onChange={(e) =>
|
||||
setNewMilestone((prev) => ({ ...prev, date: e.target.value }))
|
||||
}
|
||||
onChange={(e) => setNewMilestone({ ...newMilestone, date: e.target.value })}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
@ -531,7 +514,6 @@ export default function MilestoneTimeline({
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* If Financial => newSalary + impacts */}
|
||||
{activeView === 'Financial' && (
|
||||
<div>
|
||||
<input
|
||||
@ -587,6 +569,7 @@ export default function MilestoneTimeline({
|
||||
onChange={(e) => updateImpact(idx, 'start_date', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{imp.impact_type === 'MONTHLY' && (
|
||||
<div>
|
||||
<label>End Date (blank if indefinite): </label>
|
||||
@ -598,18 +581,14 @@ export default function MilestoneTimeline({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="text-red-500 mt-2"
|
||||
onClick={() => removeImpact(idx)}
|
||||
>
|
||||
<Button className="text-red-500 mt-2" onClick={() => removeImpact(idx)}>
|
||||
Remove Impact
|
||||
</button>
|
||||
</Button>
|
||||
</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
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -631,16 +610,15 @@ export default function MilestoneTimeline({
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button onClick={saveMilestone} style={{ marginTop: '1rem' }}>
|
||||
<Button onClick={saveMilestone} style={{ marginTop: '1rem' }}>
|
||||
{editingMilestone ? 'Update' : 'Add'} Milestone
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actual timeline */}
|
||||
{/* TIMELINE VISUAL */}
|
||||
<div className="milestone-timeline-container">
|
||||
<div className="milestone-timeline-line" />
|
||||
|
||||
{milestones[activeView].map((m) => {
|
||||
const leftPos = calcPosition(m.date);
|
||||
return (
|
||||
@ -649,10 +627,7 @@ export default function MilestoneTimeline({
|
||||
className="milestone-timeline-post"
|
||||
style={{ left: `${leftPos}%` }}
|
||||
>
|
||||
<div
|
||||
className="milestone-timeline-dot"
|
||||
onClick={() => handleEditMilestone(m)}
|
||||
/>
|
||||
<div className="milestone-timeline-dot" onClick={() => handleEditMilestone(m)} />
|
||||
<div className="milestone-content">
|
||||
<div className="title">{m.title}</div>
|
||||
{m.description && <p>{m.description}</p>}
|
||||
@ -662,6 +637,7 @@ export default function MilestoneTimeline({
|
||||
</div>
|
||||
<div className="date">{m.date}</div>
|
||||
|
||||
{/* Tasks */}
|
||||
{m.tasks && m.tasks.length > 0 && (
|
||||
<ul>
|
||||
{m.tasks.map((t) => (
|
||||
@ -674,29 +650,26 @@ export default function MilestoneTimeline({
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<button
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowTaskForm(showTaskForm === m.id ? null : m.id);
|
||||
setNewTask({ title: '', description: '', due_date: '' });
|
||||
}}
|
||||
>
|
||||
{showTaskForm === m.id ? 'Cancel Task' : 'Add Task'}
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<div style={{ marginTop: '0.5rem' }}>
|
||||
<button onClick={() => handleEditMilestone(m)}>Edit</button>
|
||||
<button
|
||||
style={{ marginLeft: '0.5rem' }}
|
||||
onClick={() => setCopyWizardMilestone(m)}
|
||||
>
|
||||
<Button onClick={() => handleEditMilestone(m)}>Edit</Button>
|
||||
<Button style={{ marginLeft: '0.5rem' }} onClick={() => setCopyWizardMilestone(m)}>
|
||||
Copy
|
||||
</button>
|
||||
<button
|
||||
style={{ marginLeft: '0.5rem', color: 'red' }}
|
||||
</Button>
|
||||
<Button
|
||||
style={{ marginLeft: '0.5rem', background: 'red', color: 'black' }}
|
||||
onClick={() => handleDeleteMilestone(m)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{showTaskForm === m.id && (
|
||||
@ -718,7 +691,7 @@ export default function MilestoneTimeline({
|
||||
value={newTask.due_date}
|
||||
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>
|
||||
|
@ -21,17 +21,9 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.view-selector button {
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.view-selector button.active {
|
||||
background: #2e7d32;
|
||||
background: #0703e2;
|
||||
}
|
||||
|
||||
.timeline-container {
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
} from 'chart.js';
|
||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||
import { Filler } from 'chart.js';
|
||||
|
||||
import { Button } from './ui/button.js';
|
||||
import authFetch from '../utils/authFetch.js';
|
||||
import CareerSelectDropdown from './CareerSelectDropdown.js';
|
||||
import CareerSearch from './CareerSearch.js';
|
||||
@ -520,7 +520,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
|
||||
{/* Confirm new career scenario */}
|
||||
{pendingCareerForModal && (
|
||||
<button
|
||||
<Button
|
||||
onClick={() => {
|
||||
// Example action
|
||||
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"
|
||||
>
|
||||
Confirm Career Change to {pendingCareerForModal}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -3,6 +3,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import authFetch from '../utils/authFetch.js';
|
||||
import ScenarioContainer from './ScenarioContainer.js';
|
||||
import { Button } from './ui/button.js';
|
||||
|
||||
/**
|
||||
* MultiScenarioView
|
||||
@ -248,19 +249,11 @@ export default function MultiScenarioView() {
|
||||
|
||||
{/* Add Scenario button */}
|
||||
<div style={{ alignSelf: 'flex-start' }}>
|
||||
<button
|
||||
<Button
|
||||
onClick={handleAddScenario}
|
||||
style={{
|
||||
padding: '0.5rem 1rem',
|
||||
height: 'auto',
|
||||
backgroundColor: '#76b900',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
+ Add Scenario
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,48 +1,45 @@
|
||||
// src/components/ScenarioContainer.js
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Line } from 'react-chartjs-2';
|
||||
import { Chart as ChartJS } from 'chart.js';
|
||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||
|
||||
import { Button } from './ui/button.js'; // <-- Universal Button
|
||||
import authFetch from '../utils/authFetch.js';
|
||||
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
|
||||
import AISuggestedMilestones from './AISuggestedMilestones.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);
|
||||
|
||||
/**
|
||||
* ScenarioContainer
|
||||
* -----------------
|
||||
* This component:
|
||||
* - Renders a <select> to pick from all scenarios, or uses the 'scenario' prop if set
|
||||
* - Fetches the collegeProfile for that scenario
|
||||
* - Fetches all milestones, plus their tasks and impacts
|
||||
* - Builds a "mergedProfile" like in single-scenario code, including:
|
||||
* -- Overridden fields from the scenario
|
||||
* -- The user's global financialProfile
|
||||
* -- 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
|
||||
* 1) Lets the user pick a scenario (via <select>), or uses the provided `scenario` prop.
|
||||
* 2) Loads the collegeProfile + milestones/impacts for that scenario.
|
||||
* 3) Merges scenario + user financial data + milestone impacts → runs `simulateFinancialProjection`.
|
||||
* 4) Shows a chart of net savings / retirement / loan balances over time.
|
||||
* 5) Allows milestone CRUD (create, edit, delete, copy).
|
||||
* 6) Offers “Clone” / “Delete” scenario callbacks from the parent.
|
||||
*/
|
||||
|
||||
export default function ScenarioContainer({
|
||||
scenario, // The scenario row from career_paths
|
||||
financialProfile, // The user’s overall financial snapshot
|
||||
onRemove,
|
||||
onClone,
|
||||
onEdit
|
||||
scenario, // The scenario row from career_paths
|
||||
financialProfile, // The user’s overall financial snapshot
|
||||
onRemove, // Callback for deleting scenario
|
||||
onClone, // Callback for cloning scenario
|
||||
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([]);
|
||||
// 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);
|
||||
|
||||
// A) On mount, fetch all scenarios to populate the dropdown
|
||||
// (A) On mount, fetch all scenarios to populate the <select>
|
||||
useEffect(() => {
|
||||
async function loadScenarios() {
|
||||
try {
|
||||
@ -59,17 +56,20 @@ export default function ScenarioContainer({
|
||||
loadScenarios();
|
||||
}, []);
|
||||
|
||||
// B) If the parent changes the scenario prop => update local
|
||||
// (B) If parent changes the `scenario` prop, update local
|
||||
useEffect(() => {
|
||||
setLocalScenario(scenario || null);
|
||||
}, [scenario]);
|
||||
|
||||
// C) scenario <select> handler
|
||||
// (C) <select> handler for picking a scenario from dropdown
|
||||
function handleScenarioSelect(e) {
|
||||
const chosenId = e.target.value;
|
||||
const found = allScenarios.find((s) => s.id === chosenId);
|
||||
if (found) setLocalScenario(found);
|
||||
else setLocalScenario(null);
|
||||
if (found) {
|
||||
setLocalScenario(found);
|
||||
} else {
|
||||
setLocalScenario(null);
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
@ -79,15 +79,17 @@ export default function ScenarioContainer({
|
||||
const [milestones, setMilestones] = useState([]);
|
||||
const [impactsByMilestone, setImpactsByMilestone] = useState({});
|
||||
|
||||
// We'll also track a scenario edit modal
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [editingScenarioData, setEditingScenarioData] = useState({
|
||||
scenario: null,
|
||||
collegeProfile: null
|
||||
});
|
||||
|
||||
// (A) Load the college profile for localScenario
|
||||
// (A) Load the college profile for the selected scenario
|
||||
useEffect(() => {
|
||||
if (!localScenario?.id) {
|
||||
// if no scenario selected, clear the collegeProfile
|
||||
setCollegeProfile(null);
|
||||
return;
|
||||
}
|
||||
@ -97,7 +99,7 @@ export default function ScenarioContainer({
|
||||
const res = await authFetch(url);
|
||||
if (res.ok) {
|
||||
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);
|
||||
} else {
|
||||
setCollegeProfile({});
|
||||
@ -110,7 +112,7 @@ export default function ScenarioContainer({
|
||||
loadCollegeProfile();
|
||||
}, [localScenario]);
|
||||
|
||||
// (B) Load milestones
|
||||
// (B) Load milestones for localScenario (and each milestone’s impacts)
|
||||
const fetchMilestones = useCallback(async () => {
|
||||
if (!localScenario?.id) {
|
||||
setMilestones([]);
|
||||
@ -160,7 +162,7 @@ export default function ScenarioContainer({
|
||||
// Wait until we have localScenario + collegeProfile + financialProfile
|
||||
if (!financialProfile || !localScenario?.id || !collegeProfile) return;
|
||||
|
||||
// Gather all milestoneImpacts into one array for the aggregator
|
||||
// Gather all milestoneImpacts
|
||||
let allImpacts = [];
|
||||
Object.keys(impactsByMilestone).forEach((mId) => {
|
||||
allImpacts = allImpacts.concat(impactsByMilestone[mId]);
|
||||
@ -168,9 +170,9 @@ export default function ScenarioContainer({
|
||||
|
||||
const simYears = parseInt(simulationYearsInput, 10) || 20;
|
||||
|
||||
// Build the mergedProfile exactly like single-scenario aggregator
|
||||
// Build mergedProfile from scenario + user financial + college + milestone
|
||||
const mergedProfile = {
|
||||
// Base user financial
|
||||
// base user data
|
||||
currentSalary: financialProfile.current_salary || 0,
|
||||
|
||||
monthlyExpenses:
|
||||
@ -178,7 +180,6 @@ export default function ScenarioContainer({
|
||||
monthlyDebtPayments:
|
||||
localScenario.planned_monthly_debt_payments ?? financialProfile.monthly_debt_payments ?? 0,
|
||||
|
||||
// Overridden savings from scenario or fallback to financialProfile
|
||||
retirementSavings: financialProfile.retirement_savings ?? 0,
|
||||
emergencySavings: financialProfile.emergency_fund ?? 0,
|
||||
|
||||
@ -203,7 +204,7 @@ export default function ScenarioContainer({
|
||||
additionalIncome:
|
||||
localScenario.planned_additional_income ?? financialProfile.additional_income ?? 0,
|
||||
|
||||
// College data
|
||||
// college-related
|
||||
studentLoanAmount: collegeProfile.existing_college_debt || 0,
|
||||
interestRate: collegeProfile.interest_rate || 5,
|
||||
loanTerm: collegeProfile.loan_term || 10,
|
||||
@ -213,7 +214,6 @@ export default function ScenarioContainer({
|
||||
calculatedTuition: collegeProfile.tuition || 0,
|
||||
extraPayment: collegeProfile.extra_payment || 0,
|
||||
|
||||
// Are we in college?
|
||||
inCollege:
|
||||
collegeProfile.college_enrollment_status === 'currently_enrolled' ||
|
||||
collegeProfile.college_enrollment_status === 'prospective_student',
|
||||
@ -225,18 +225,18 @@ export default function ScenarioContainer({
|
||||
programLength: collegeProfile.program_length || 0,
|
||||
expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary || 0,
|
||||
|
||||
// Scenario start date
|
||||
// scenario date + simulation horizon
|
||||
startDate: localScenario.start_date || new Date().toISOString(),
|
||||
simulationYears: simYears,
|
||||
|
||||
// MILESTONE IMPACTS
|
||||
// milestone impacts
|
||||
milestoneImpacts: allImpacts
|
||||
};
|
||||
|
||||
// Run the simulation
|
||||
const { projectionData, loanPaidOffMonth } = simulateFinancialProjection(mergedProfile);
|
||||
|
||||
// Optionally compute a "cumulativeNetSavings" to display
|
||||
// Optionally add a "cumulativeNetSavings" for display
|
||||
let cumulative = mergedProfile.emergencySavings || 0;
|
||||
const finalData = projectionData.map((monthRow) => {
|
||||
cumulative += (monthRow.netSavings || 0);
|
||||
@ -245,7 +245,6 @@ export default function ScenarioContainer({
|
||||
|
||||
setProjectionData(finalData);
|
||||
setLoanPaidOffMonth(loanPaidOffMonth);
|
||||
|
||||
}, [
|
||||
financialProfile,
|
||||
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);
|
||||
// We'll show netSavings, retirementSavings, and loanBalance
|
||||
|
||||
// dataset arrays
|
||||
const netSavingsData = projectionData.map((p) => p.cumulativeNetSavings || 0);
|
||||
const retData = projectionData.map((p) => p.retirementSavings || 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) {
|
||||
if (!m.date) return -1;
|
||||
const short = m.date.slice(0, 7); // "YYYY-MM"
|
||||
@ -284,11 +284,7 @@ export default function ScenarioContainer({
|
||||
.map((m) => {
|
||||
const xIndex = getLabelIndexForMilestone(m);
|
||||
if (xIndex < 0) return null;
|
||||
return {
|
||||
x: xIndex,
|
||||
y: 0, // place them at 0 or some other reference
|
||||
milestoneObj: m
|
||||
};
|
||||
return { x: xIndex, y: 0, milestoneObj: m };
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
@ -314,7 +310,6 @@ export default function ScenarioContainer({
|
||||
fill: false
|
||||
},
|
||||
{
|
||||
// The milestone dataset for clickable markers
|
||||
label: 'Milestones',
|
||||
data: milestonePoints,
|
||||
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 [editingMilestone, setEditingMilestone] = useState(null);
|
||||
@ -397,12 +392,12 @@ export default function ScenarioContainer({
|
||||
setShowForm(true);
|
||||
}
|
||||
|
||||
// edit an existing milestone => fetch impacts
|
||||
async function handleEditMilestone(m) {
|
||||
if (!localScenario?.id) return;
|
||||
setEditingMilestone(m);
|
||||
setImpactsToDelete([]);
|
||||
|
||||
// fetch impacts for this milestone
|
||||
try {
|
||||
const impRes = await authFetch(`/api/premium/milestone-impacts?milestone_id=${m.id}`);
|
||||
if (impRes.ok) {
|
||||
@ -436,16 +431,11 @@ export default function ScenarioContainer({
|
||||
...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) {
|
||||
setNewMilestone((prev) => {
|
||||
const copy = [...prev.impacts];
|
||||
@ -457,6 +447,7 @@ export default function ScenarioContainer({
|
||||
return { ...prev, impacts: copy };
|
||||
});
|
||||
}
|
||||
|
||||
function updateImpact(idx, field, value) {
|
||||
setNewMilestone((prev) => {
|
||||
const copy = [...prev.impacts];
|
||||
@ -465,6 +456,7 @@ export default function ScenarioContainer({
|
||||
});
|
||||
}
|
||||
|
||||
// create or update milestone
|
||||
async function saveMilestone() {
|
||||
if (!localScenario?.id) return;
|
||||
|
||||
@ -474,7 +466,7 @@ export default function ScenarioContainer({
|
||||
const method = editingMilestone ? 'PUT' : 'POST';
|
||||
|
||||
const payload = {
|
||||
milestone_type: 'Financial', // or "Career" if needed
|
||||
milestone_type: 'Financial', // or "Career" if your scenario is about career
|
||||
title: newMilestone.title,
|
||||
description: newMilestone.description,
|
||||
date: newMilestone.date,
|
||||
@ -513,14 +505,14 @@ export default function ScenarioContainer({
|
||||
end_date: imp.end_date || null
|
||||
};
|
||||
if (imp.id) {
|
||||
// PUT
|
||||
// update existing
|
||||
await authFetch(`/api/premium/milestone-impacts/${imp.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(impPayload)
|
||||
});
|
||||
} else {
|
||||
// POST
|
||||
// create new
|
||||
await authFetch('/api/premium/milestone-impacts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@ -529,6 +521,7 @@ export default function ScenarioContainer({
|
||||
}
|
||||
}
|
||||
|
||||
// done => re-fetch
|
||||
await fetchMilestones();
|
||||
|
||||
// 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) {
|
||||
if (m.is_universal === 1) {
|
||||
const userChoice = window.confirm(
|
||||
@ -569,6 +563,7 @@ export default function ScenarioContainer({
|
||||
}
|
||||
await fetchMilestones();
|
||||
}
|
||||
|
||||
async function deleteSingleMilestone(m) {
|
||||
try {
|
||||
await authFetch(`/api/premium/milestones/${m.id}`, { method: 'DELETE' });
|
||||
@ -577,6 +572,7 @@ export default function ScenarioContainer({
|
||||
}
|
||||
}
|
||||
|
||||
// tasks
|
||||
async function addTask(milestoneId) {
|
||||
try {
|
||||
const payload = {
|
||||
@ -602,11 +598,9 @@ export default function ScenarioContainer({
|
||||
}
|
||||
}
|
||||
|
||||
// Instead of immediately calling `onEdit(scenario)`, we handle locally:
|
||||
// scenario-level editing
|
||||
function handleEditScenario() {
|
||||
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({
|
||||
scenario: localScenario,
|
||||
collegeProfile
|
||||
@ -614,14 +608,12 @@ export default function ScenarioContainer({
|
||||
setShowEditModal(true);
|
||||
}
|
||||
|
||||
// Scenario-level
|
||||
function handleDeleteScenario() {
|
||||
if (localScenario) onRemove(localScenario.id);
|
||||
}
|
||||
function handleCloneScenario() {
|
||||
if (localScenario) onClone(localScenario);
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************
|
||||
* 6) COPY WIZARD
|
||||
@ -649,7 +641,7 @@ export default function ScenarioContainer({
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to copy milestone');
|
||||
onClose();
|
||||
// Optionally reload the page or call fetchMilestones again
|
||||
// Optionally reload
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
console.error('Error copying milestone:', err);
|
||||
@ -696,10 +688,10 @@ export default function ScenarioContainer({
|
||||
))}
|
||||
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<button onClick={onClose} style={{ marginRight: '0.5rem' }}>
|
||||
<Button onClick={onClose} style={{ marginRight: '0.5rem' }}>
|
||||
Cancel
|
||||
</button>
|
||||
<button onClick={handleCopy}>Copy</button>
|
||||
</Button>
|
||||
<Button onClick={handleCopy}>Copy</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -761,11 +753,11 @@ export default function ScenarioContainer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button onClick={handleNewMilestone} style={{ marginTop: '0.5rem' }}>
|
||||
<Button onClick={handleNewMilestone} style={{ marginTop: '0.5rem' }}>
|
||||
+ New Milestone
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{/* AI-Suggested Milestones (unchanged) */}
|
||||
{/* AI-Suggested Milestones */}
|
||||
<AISuggestedMilestones
|
||||
career={localScenario.career_name || localScenario.scenario_title || ''}
|
||||
careerPathId={localScenario.id}
|
||||
@ -775,17 +767,16 @@ export default function ScenarioContainer({
|
||||
/>
|
||||
|
||||
<div style={{ marginTop: '0.5rem' }}>
|
||||
{/* Instead of calling onEdit(localScenario), show local modal */}
|
||||
<button onClick={handleEditScenario}>Edit</button>
|
||||
<button onClick={handleCloneScenario} style={{ marginLeft: '0.5rem' }}>
|
||||
<Button onClick={handleEditScenario}>Edit</Button>
|
||||
<Button onClick={handleCloneScenario} style={{ marginLeft: '0.5rem' }}>
|
||||
Clone
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDeleteScenario}
|
||||
style={{ marginLeft: '0.5rem', color: 'red' }}
|
||||
style={{ marginLeft: '0.5rem', background: 'red', color: 'black' }}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* The inline form for milestone creation/edit */}
|
||||
@ -821,7 +812,7 @@ export default function ScenarioContainer({
|
||||
progress: parseInt(e.target.value || '0', 10)
|
||||
}))
|
||||
}
|
||||
/>
|
||||
/>
|
||||
|
||||
{/* Impacts sub-form */}
|
||||
<div style={{ border: '1px solid #ccc', padding: '1rem', marginTop: '1rem' }}>
|
||||
@ -878,22 +869,22 @@ export default function ScenarioContainer({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
<Button
|
||||
style={{ marginLeft: '0.5rem', color: 'red' }}
|
||||
onClick={() => removeImpact(idx)}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={addNewImpact}>+ Add Impact</button>
|
||||
<Button onClick={addNewImpact}>+ Add Impact</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<button onClick={saveMilestone}>
|
||||
<Button onClick={saveMilestone}>
|
||||
{editingMilestone ? 'Update' : 'Add'} Milestone
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
style={{ marginLeft: '0.5rem' }}
|
||||
onClick={() => {
|
||||
setShowForm(false);
|
||||
@ -911,7 +902,7 @@ export default function ScenarioContainer({
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -943,29 +934,32 @@ export default function ScenarioContainer({
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<button
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowTaskForm(showTaskForm === m.id ? null : m.id);
|
||||
setNewTask({ title: '', description: '', due_date: '' });
|
||||
}}
|
||||
>
|
||||
{showTaskForm === m.id ? 'Cancel Task' : 'Add Task'}
|
||||
</button>
|
||||
<button style={{ marginLeft: '0.5rem' }} onClick={() => handleEditMilestone(m)}>
|
||||
</Button>
|
||||
<Button
|
||||
style={{ marginLeft: '0.5rem' }}
|
||||
onClick={() => handleEditMilestone(m)}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
style={{ marginLeft: '0.5rem' }}
|
||||
onClick={() => setCopyWizardMilestone(m)}
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
<button
|
||||
style={{ marginLeft: '0.5rem', color: 'red' }}
|
||||
</Button>
|
||||
<Button
|
||||
style={{ marginLeft: '0.5rem', background: 'red', color: 'black' }}
|
||||
onClick={() => handleDeleteMilestone(m)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{/* Task form */}
|
||||
{showTaskForm === m.id && (
|
||||
@ -987,20 +981,20 @@ export default function ScenarioContainer({
|
||||
value={newTask.due_date}
|
||||
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>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* (B) RENDER THE EDIT MODAL IF showEditModal */}
|
||||
<ScenarioEditModal
|
||||
show={showEditModal}
|
||||
onClose={() => setShowEditModal(false)}
|
||||
scenario={editingScenarioData.scenario}
|
||||
collegeProfile={editingScenarioData.collegeProfile}
|
||||
/>
|
||||
{/* (B) Show the scenario edit modal if needed */}
|
||||
<ScenarioEditModal
|
||||
show={showEditModal}
|
||||
onClose={() => setShowEditModal(false)}
|
||||
scenario={editingScenarioData.scenario}
|
||||
collegeProfile={editingScenarioData.collegeProfile}
|
||||
/>
|
||||
|
||||
{/* The copy wizard if copying a milestone */}
|
||||
{copyWizardMilestone && (
|
||||
|
Loading…
Reference in New Issue
Block a user