Fixed MultiScenarioView and ScenarioContainer for UI and changes.

This commit is contained in:
Josh 2025-05-30 12:12:30 +00:00
parent 29bdb17321
commit 569626d489
4 changed files with 896 additions and 799 deletions

View File

@ -0,0 +1,120 @@
import React from 'react';
import { Button } from './ui/button.js';
export default function MilestoneModal({
show,
onClose,
milestones,
editingMilestone,
showForm,
handleNewMilestone,
handleEditMilestone,
handleDeleteMilestone,
handleAddTask,
showTaskForm,
editingTask,
handleEditTask,
deleteTask,
saveTask,
saveMilestone,
copyWizardMilestone,
setCopyWizardMilestone
}) {
if (!show) return null; // if we don't want to render at all when hidden
return (
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-start justify-center overflow-auto">
<div className="bg-white p-4 m-4 max-w-4xl w-full relative">
<h3 className="text-xl font-bold mb-4">Edit Milestones</h3>
<Button onClick={handleNewMilestone}>+ New Milestone</Button>
{/*
1) Render existing milestones
*/}
{milestones.map((m) => {
const tasks = m.tasks || [];
return (
<div key={m.id} className="border p-2 my-2">
<h5>{m.title}</h5>
{m.description && <p>{m.description}</p>}
<p>
<strong>Date:</strong> {m.date}
<strong>Progress:</strong> {m.progress}%
</p>
{/* tasks list */}
{tasks.length > 0 && (
<ul>
{tasks.map((t) => (
<li key={t.id}>
<strong>{t.title}</strong>
{t.description ? ` - ${t.description}` : ''}
{t.due_date ? ` (Due: ${t.due_date})` : ''}{' '}
<Button onClick={() => handleEditTask(m.id, t)}>Edit</Button>
<Button style={{ color: 'red' }} onClick={() => deleteTask(t.id)}>
Delete
</Button>
</li>
))}
</ul>
)}
<Button onClick={() => handleAddTask(m.id)}>+ Task</Button>
<Button onClick={() => handleEditMilestone(m)}>Edit</Button>
<Button
onClick={() => setCopyWizardMilestone(m)}
style={{ marginLeft: '0.5rem' }}
>
Copy
</Button>
<Button
style={{ marginLeft: '0.5rem', background: 'red', color: 'black' }}
onClick={() => handleDeleteMilestone(m)}
>
Delete
</Button>
{/* The "Add/Edit Task" form if showTaskForm === m.id */}
{showTaskForm === m.id && (
<div style={{ border: '1px solid #aaa', padding: '0.5rem', marginTop: '0.5rem' }}>
<h5>{editingTask.id ? 'Edit Task' : 'New Task'}</h5>
{/* same form logic... */}
<Button onClick={() => saveTask(m.id)}>
{editingTask.id ? 'Update' : 'Add'} Task
</Button>
<Button /* ... */>Cancel</Button>
</div>
)}
</div>
);
})}
{/*
2) The big milestone form if showForm is true
*/}
{showForm && (
<div className="form border p-2 my-2">
<h4>{editingMilestone ? 'Edit Milestone' : 'New Milestone'}</h4>
{/* ... your milestone form code (title, date, impacts, etc.) */}
<Button onClick={saveMilestone}>
{editingMilestone ? 'Update' : 'Add'} Milestone
</Button>
<Button onClick={onClose}>Cancel</Button>
</div>
)}
{/* Copy wizard if copyWizardMilestone */}
{copyWizardMilestone && (
<div>
{/* your copy wizard UI */}
</div>
)}
<div className="text-right">
<Button onClick={onClose}>Close</Button>
</div>
</div>
</div>
);
}

View File

@ -268,10 +268,10 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
const location = useLocation(); const location = useLocation();
const apiURL = process.env.REACT_APP_API_URL; const apiURL = process.env.REACT_APP_API_URL;
const [interestStrategy, setInterestStrategy] = useState('NONE'); // 'NONE' | 'FLAT' | 'MONTE_CARLO' const [interestStrategy, setInterestStrategy] = useState('FLAT'); // 'NONE' | 'FLAT' | 'MONTE_CARLO'
const [flatAnnualRate, setFlatAnnualRate] = useState(0.06); // 6% default const [flatAnnualRate, setFlatAnnualRate] = useState(0.06); // 6% default
const [randomRangeMin, setRandomRangeMin] = useState(-0.03); // -3% monthly const [randomRangeMin, setRandomRangeMin] = useState(-0.02); // -3% monthly
const [randomRangeMax, setRandomRangeMax] = useState(0.08); // 8% monthly const [randomRangeMax, setRandomRangeMax] = useState(0.02); // 8% monthly
// Basic states // Basic states
const [userProfile, setUserProfile] = useState(null); const [userProfile, setUserProfile] = useState(null);
@ -1077,7 +1077,7 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
> >
<option value="NONE">No Interest</option> <option value="NONE">No Interest</option>
<option value="FLAT">Flat Rate</option> <option value="FLAT">Flat Rate</option>
<option value="MONTE_CARLO">Monte Carlo</option> <option value="MONTE_CARLO">Random</option>
</select> </select>
{/* (E2) If FLAT => show the annual rate */} {/* (E2) If FLAT => show the annual rate */}
@ -1102,7 +1102,7 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
type="number" type="number"
step="0.01" step="0.01"
value={randomRangeMin} value={randomRangeMin}
onChange={(e) => setRandomRangeMin(parseFloatOrZero(e.target.value, -0.03))} onChange={(e) => setRandomRangeMin(parseFloatOrZero(e.target.value, -0.02))}
className="border rounded p-1 w-20 mr-2" className="border rounded p-1 w-20 mr-2"
/> />
<label className="mr-1">Max Return (%):</label> <label className="mr-1">Max Return (%):</label>
@ -1110,7 +1110,7 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
type="number" type="number"
step="0.01" step="0.01"
value={randomRangeMax} value={randomRangeMax}
onChange={(e) => setRandomRangeMax(parseFloatOrZero(e.target.value, 0.08))} onChange={(e) => setRandomRangeMax(parseFloatOrZero(e.target.value, 0.02))}
className="border rounded p-1 w-20" className="border rounded p-1 w-20"
/> />
</div> </div>

View File

@ -1,18 +1,9 @@
// src/components/MultiScenarioView.js // src/components/MultiScenarioView.js
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'; import { Button } from './ui/button.js';
/**
* MultiScenarioView
* -----------------
* - Loads the users global financialProfile
* - Loads all scenarios from `career_profiles`
* - Renders a <ScenarioContainer> for each scenario
* - Handles "Add Scenario", "Clone Scenario" (including college_profile), "Remove Scenario"
*/
export default function MultiScenarioView() { export default function MultiScenarioView() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
@ -61,7 +52,6 @@ export default function MultiScenarioView() {
async function handleAddScenario() { async function handleAddScenario() {
try { try {
const body = { const body = {
// minimal fields so the scenario is valid
career_name: 'New Scenario ' + new Date().toLocaleDateString(), career_name: 'New Scenario ' + new Date().toLocaleDateString(),
status: 'planned', status: 'planned',
start_date: new Date().toISOString(), start_date: new Date().toISOString(),
@ -84,7 +74,9 @@ export default function MultiScenarioView() {
} }
/** /**
* CLONE a scenario: (A) create new scenario row, (B) also clone old scenarios college_profile * Clone a scenario:
* (A) create new scenario row from old scenario fields
* (B) also clone old scenarios college_profile
*/ */
async function handleCloneScenario(oldScenario) { async function handleCloneScenario(oldScenario) {
try { try {
@ -100,7 +92,7 @@ export default function MultiScenarioView() {
start_date: oldScenario.start_date, start_date: oldScenario.start_date,
projected_end_date: oldScenario.projected_end_date, projected_end_date: oldScenario.projected_end_date,
college_enrollment_status: oldScenario.college_enrollment_status, college_enrollment_status: oldScenario.college_enrollment_status,
currently_working: oldScenario.currently_working, currently_working: oldScenario.currently_working || 'no',
planned_monthly_expenses: oldScenario.planned_monthly_expenses, planned_monthly_expenses: oldScenario.planned_monthly_expenses,
planned_monthly_debt_payments: oldScenario.planned_monthly_debt_payments, planned_monthly_debt_payments: oldScenario.planned_monthly_debt_payments,
@ -109,7 +101,8 @@ export default function MultiScenarioView() {
planned_monthly_emergency_contribution: planned_monthly_emergency_contribution:
oldScenario.planned_monthly_emergency_contribution, oldScenario.planned_monthly_emergency_contribution,
planned_surplus_emergency_pct: oldScenario.planned_surplus_emergency_pct, planned_surplus_emergency_pct: oldScenario.planned_surplus_emergency_pct,
planned_surplus_retirement_pct: oldScenario.planned_surplus_retirement_pct, planned_surplus_retirement_pct:
oldScenario.planned_surplus_retirement_pct,
planned_additional_income: oldScenario.planned_additional_income planned_additional_income: oldScenario.planned_additional_income
}; };
@ -130,13 +123,10 @@ export default function MultiScenarioView() {
// 3) reload // 3) reload
await loadScenariosAndFinancial(); await loadScenariosAndFinancial();
} catch (err) { } catch (err) {
alert(err.message); alert(`Clone scenario failed: ${err.message}`);
} }
} }
/**
* Helper to clone old scenarios college_profile => new scenario
*/
async function cloneCollegeProfile(oldScenarioId, newScenarioId) { async function cloneCollegeProfile(oldScenarioId, newScenarioId) {
try { try {
// fetch old scenarios college_profile // fetch old scenarios college_profile
@ -208,9 +198,6 @@ export default function MultiScenarioView() {
} }
} }
/**
* Remove scenario => server also deletes its college_profile => reload
*/
async function handleRemoveScenario(id) { async function handleRemoveScenario(id) {
const confirmDel = window.confirm('Delete this scenario?'); const confirmDel = window.confirm('Delete this scenario?');
if (!confirmDel) return; if (!confirmDel) return;
@ -220,8 +207,6 @@ export default function MultiScenarioView() {
method: 'DELETE' method: 'DELETE'
}); });
if (!res.ok) throw new Error(`Delete scenario error: ${res.status}`); if (!res.ok) throw new Error(`Delete scenario error: ${res.status}`);
// reload
await loadScenariosAndFinancial(); await loadScenariosAndFinancial();
} catch (err) { } catch (err) {
alert(err.message); alert(err.message);
@ -231,29 +216,32 @@ export default function MultiScenarioView() {
if (loading) return <p>Loading scenarios...</p>; if (loading) return <p>Loading scenarios...</p>;
if (error) return <p style={{ color: 'red' }}>{error}</p>; if (error) return <p style={{ color: 'red' }}>{error}</p>;
return ( // show only first 2 scenarios
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem' }}> const visibleScenarios = scenarios.slice(0, 2);
{scenarios.map(sc => (
<ScenarioContainer
key={sc.id}
scenario={sc}
financialProfile={financialProfile}
onClone={handleCloneScenario}
onRemove={handleRemoveScenario}
onEdit={(sc) => {
// Example: open an edit modal or navigate to a scenario editor
console.log('Edit scenario clicked:', sc);
}}
/>
))}
return (
<div style={{ margin: '1rem' }}>
{/* Add Scenario button */} {/* Add Scenario button */}
<div style={{ alignSelf: 'flex-start' }}> <div style={{ marginBottom: '1rem' }}>
<Button <Button onClick={handleAddScenario}>+ Add Scenario</Button>
onClick={handleAddScenario} </div>
>
+ Add Scenario {/* Display 1 or 2 scenarios side by side */}
</Button> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem' }}>
{visibleScenarios.map((sc) => (
<ScenarioContainer
key={sc.id}
scenario={sc}
financialProfile={financialProfile}
onClone={handleCloneScenario} // <--- pass down
onRemove={handleRemoveScenario} // <--- pass down
onEdit={(sc) => {
console.log('Edit scenario clicked:', sc);
// or open a modal if you prefer
}}
hideMilestones // if you want to hide milestone details
/>
))}
</div> </div>
</div> </div>
); );

File diff suppressed because it is too large Load Diff