Fixed MultiScenarioView and ScenarioContainer for UI and changes.
This commit is contained in:
parent
29bdb17321
commit
569626d489
120
src/components/MilestoneModal.js
Normal file
120
src/components/MilestoneModal.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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,12 +1110,12 @@ 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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 7) AI Next Steps */}
|
{/* 7) AI Next Steps */}
|
||||||
<div className="bg-white p-4 rounded shadow mt-4">
|
<div className="bg-white p-4 rounded shadow mt-4">
|
||||||
<Button onClick={handleAiClick} disabled={aiLoading || clickCount >= DAILY_CLICK_LIMIT}>
|
<Button onClick={handleAiClick} disabled={aiLoading || clickCount >= DAILY_CLICK_LIMIT}>
|
||||||
|
@ -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 user’s 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 scenario’s college_profile
|
* Clone a scenario:
|
||||||
|
* (A) create new scenario row from old scenario fields
|
||||||
|
* (B) also clone old scenario’s 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 scenario’s college_profile => new scenario
|
|
||||||
*/
|
|
||||||
async function cloneCollegeProfile(oldScenarioId, newScenarioId) {
|
async function cloneCollegeProfile(oldScenarioId, newScenarioId) {
|
||||||
try {
|
try {
|
||||||
// fetch old scenario’s college_profile
|
// fetch old scenario’s 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
Loading…
Reference in New Issue
Block a user