773 lines
25 KiB
JavaScript
773 lines
25 KiB
JavaScript
// src/components/MilestoneTimeline.js
|
|
|
|
import React, { useEffect, useState, useCallback } from 'react';
|
|
import { Button } from './ui/button.js';
|
|
|
|
/**
|
|
* Renders a simple vertical list of milestones for the given careerPathId.
|
|
* Also includes Task CRUD (create/edit/delete) for each milestone,
|
|
* plus a small "copy milestone" wizard, "financial impacts" form, etc.
|
|
*/
|
|
export default function MilestoneTimeline({
|
|
careerPathId,
|
|
authFetch,
|
|
activeView, // 'Career' or 'Financial'
|
|
setActiveView, // optional, if you need to switch between views
|
|
onMilestoneUpdated // callback after saving/deleting a milestone
|
|
}) {
|
|
const [milestones, setMilestones] = useState({ Career: [], Financial: [] });
|
|
|
|
// For CREATE/EDIT milestone
|
|
const [newMilestone, setNewMilestone] = useState({
|
|
title: '',
|
|
description: '',
|
|
date: '',
|
|
progress: 0,
|
|
newSalary: '',
|
|
impacts: [],
|
|
isUniversal: 0
|
|
});
|
|
const [impactsToDelete, setImpactsToDelete] = useState([]);
|
|
const [showForm, setShowForm] = useState(false);
|
|
const [editingMilestone, setEditingMilestone] = useState(null);
|
|
|
|
// For CREATE/EDIT tasks
|
|
const [showTaskForm, setShowTaskForm] = useState(null); // which milestone ID is showing the form
|
|
const [newTask, setNewTask] = useState({
|
|
id: null,
|
|
title: '',
|
|
description: '',
|
|
due_date: ''
|
|
});
|
|
|
|
// For the "Copy to other scenarios" wizard
|
|
const [scenarios, setScenarios] = useState([]);
|
|
const [copyWizardMilestone, setCopyWizardMilestone] = useState(null);
|
|
|
|
// ------------------------------------------------------------------
|
|
// 1) Financial Impacts sub-form helpers
|
|
// ------------------------------------------------------------------
|
|
function addNewImpact() {
|
|
setNewMilestone((prev) => ({
|
|
...prev,
|
|
impacts: [
|
|
...prev.impacts,
|
|
{ impact_type: 'ONE_TIME', direction: 'subtract', amount: 0, start_date: '', end_date: '' }
|
|
]
|
|
}));
|
|
}
|
|
|
|
function removeImpact(idx) {
|
|
setNewMilestone((prev) => {
|
|
const newImpacts = [...prev.impacts];
|
|
const removed = newImpacts[idx];
|
|
if (removed && removed.id) {
|
|
setImpactsToDelete((old) => [...old, removed.id]);
|
|
}
|
|
newImpacts.splice(idx, 1);
|
|
return { ...prev, impacts: newImpacts };
|
|
});
|
|
}
|
|
|
|
function updateImpact(idx, field, value) {
|
|
setNewMilestone((prev) => {
|
|
const newImpacts = [...prev.impacts];
|
|
newImpacts[idx] = { ...newImpacts[idx], [field]: value };
|
|
return { ...prev, impacts: newImpacts };
|
|
});
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// 2) Fetch milestones => store in "milestones[Career]" / "milestones[Financial]"
|
|
// ------------------------------------------------------------------
|
|
const fetchMilestones = useCallback(async () => {
|
|
if (!careerPathId) return;
|
|
try {
|
|
const res = await authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`);
|
|
if (!res.ok) {
|
|
console.error('Failed to fetch milestones. Status:', res.status);
|
|
return;
|
|
}
|
|
const data = await res.json();
|
|
if (!data.milestones) {
|
|
console.warn('No milestones in response:', data);
|
|
return;
|
|
}
|
|
// Separate them by type
|
|
const categorized = { Career: [], Financial: [] };
|
|
data.milestones.forEach((m) => {
|
|
if (categorized[m.milestone_type]) {
|
|
categorized[m.milestone_type].push(m);
|
|
} else {
|
|
// If there's a random type, log or store somewhere else
|
|
console.warn(`Unknown milestone type: ${m.milestone_type}`);
|
|
}
|
|
});
|
|
setMilestones(categorized);
|
|
} catch (err) {
|
|
console.error('Failed to fetch milestones:', err);
|
|
}
|
|
}, [careerPathId, authFetch]);
|
|
|
|
useEffect(() => {
|
|
fetchMilestones();
|
|
}, [fetchMilestones]);
|
|
|
|
// ------------------------------------------------------------------
|
|
// 3) Load all scenarios for the copy wizard
|
|
// ------------------------------------------------------------------
|
|
useEffect(() => {
|
|
async function loadScenarios() {
|
|
try {
|
|
const res = await authFetch('/api/premium/career-profile/all');
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setScenarios(data.careerPaths || []);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error loading scenarios for copy wizard:', err);
|
|
}
|
|
}
|
|
loadScenarios();
|
|
}, [authFetch]);
|
|
|
|
// ------------------------------------------------------------------
|
|
// 4) Edit Milestone => fetch impacts
|
|
// ------------------------------------------------------------------
|
|
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);
|
|
return;
|
|
}
|
|
const data = await res.json();
|
|
const fetchedImpacts = data.impacts || [];
|
|
|
|
setNewMilestone({
|
|
title: m.title || '',
|
|
description: m.description || '',
|
|
date: m.date || '',
|
|
progress: m.progress || 0,
|
|
newSalary: m.new_salary || '',
|
|
impacts: fetchedImpacts.map((imp) => ({
|
|
id: imp.id,
|
|
impact_type: imp.impact_type || 'ONE_TIME',
|
|
direction: imp.direction || 'subtract',
|
|
amount: imp.amount || 0,
|
|
start_date: imp.start_date || '',
|
|
end_date: imp.end_date || ''
|
|
})),
|
|
isUniversal: m.is_universal ? 1 : 0
|
|
});
|
|
|
|
setEditingMilestone(m);
|
|
setShowForm(true);
|
|
} catch (err) {
|
|
console.error('Error in handleEditMilestone:', err);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// 5) Save (create/update) => handle impacts
|
|
// ------------------------------------------------------------------
|
|
async function saveMilestone() {
|
|
if (!activeView) return;
|
|
|
|
const url = editingMilestone
|
|
? `/api/premium/milestones/${editingMilestone.id}`
|
|
: `/api/premium/milestone`;
|
|
const method = editingMilestone ? 'PUT' : 'POST';
|
|
|
|
const payload = {
|
|
milestone_type: activeView, // 'Career' or 'Financial'
|
|
title: newMilestone.title,
|
|
description: newMilestone.description,
|
|
date: newMilestone.date,
|
|
career_path_id: careerPathId,
|
|
progress: newMilestone.progress,
|
|
status: newMilestone.progress >= 100 ? 'completed' : 'planned',
|
|
new_salary:
|
|
activeView === 'Financial' && newMilestone.newSalary
|
|
? parseFloat(newMilestone.newSalary)
|
|
: null,
|
|
is_universal: newMilestone.isUniversal || 0
|
|
};
|
|
|
|
try {
|
|
const res = await authFetch(url, {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
if (!res.ok) {
|
|
const errData = await res.json();
|
|
console.error('Failed to save milestone:', errData);
|
|
alert(errData.error || 'Error saving milestone');
|
|
return;
|
|
}
|
|
|
|
const savedMilestone = await res.json();
|
|
console.log('Milestone saved/updated:', savedMilestone);
|
|
|
|
// If it's a "Financial" milestone => handle impacts
|
|
if (activeView === 'Financial') {
|
|
// 1) Delete old impacts
|
|
for (const impactId of impactsToDelete) {
|
|
if (impactId) {
|
|
const delRes = await authFetch(`/api/premium/milestone-impacts/${impactId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
if (!delRes.ok) {
|
|
console.error('Failed deleting old impact', impactId, await delRes.text());
|
|
}
|
|
}
|
|
}
|
|
// 2) Insert/Update new impacts
|
|
for (let i = 0; i < newMilestone.impacts.length; i++) {
|
|
const imp = newMilestone.impacts[i];
|
|
if (imp.id) {
|
|
// existing => PUT
|
|
const putPayload = {
|
|
milestone_id: savedMilestone.id,
|
|
impact_type: imp.impact_type,
|
|
direction: imp.direction,
|
|
amount: parseFloat(imp.amount) || 0,
|
|
start_date: imp.start_date || null,
|
|
end_date: imp.end_date || null
|
|
};
|
|
const impRes = await authFetch(`/api/premium/milestone-impacts/${imp.id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(putPayload)
|
|
});
|
|
if (!impRes.ok) {
|
|
const errImp = await impRes.json();
|
|
console.error('Failed updating impact:', errImp);
|
|
}
|
|
} else {
|
|
// new => POST
|
|
const postPayload = {
|
|
milestone_id: savedMilestone.id,
|
|
impact_type: imp.impact_type,
|
|
direction: imp.direction,
|
|
amount: parseFloat(imp.amount) || 0,
|
|
start_date: imp.start_date || null,
|
|
end_date: imp.end_date || null
|
|
};
|
|
const impRes = await authFetch('/api/premium/milestone-impacts', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(postPayload)
|
|
});
|
|
if (!impRes.ok) {
|
|
console.error('Failed creating new impact:', await impRes.text());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Re-fetch milestones
|
|
await fetchMilestones();
|
|
|
|
// reset form
|
|
setShowForm(false);
|
|
setEditingMilestone(null);
|
|
setNewMilestone({
|
|
title: '',
|
|
description: '',
|
|
date: '',
|
|
progress: 0,
|
|
newSalary: '',
|
|
impacts: [],
|
|
isUniversal: 0
|
|
});
|
|
setImpactsToDelete([]);
|
|
|
|
if (onMilestoneUpdated) {
|
|
onMilestoneUpdated();
|
|
}
|
|
} catch (err) {
|
|
console.error('Error saving milestone:', err);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// 6) TASK CRUD
|
|
// ------------------------------------------------------------------
|
|
|
|
// A) “Add Task” button => sets newTask for a new item
|
|
function handleAddTask(milestoneId) {
|
|
setShowTaskForm(milestoneId);
|
|
setNewTask({ id: null, title: '', description: '', due_date: '' });
|
|
}
|
|
|
|
// B) “Edit Task” => fill newTask with the existing fields
|
|
function handleEditTask(milestoneId, task) {
|
|
setShowTaskForm(milestoneId);
|
|
setNewTask({
|
|
id: task.id,
|
|
title: task.title,
|
|
description: task.description || '',
|
|
due_date: task.due_date || ''
|
|
});
|
|
}
|
|
|
|
// C) Save (create or update) task
|
|
async function saveTask(milestoneId) {
|
|
if (!newTask.title.trim()) {
|
|
alert('Task needs a title');
|
|
return;
|
|
}
|
|
const payload = {
|
|
milestone_id: milestoneId,
|
|
title: newTask.title,
|
|
description: newTask.description,
|
|
due_date: newTask.due_date
|
|
};
|
|
|
|
let url = '/api/premium/tasks';
|
|
let method = 'POST';
|
|
|
|
if (newTask.id) {
|
|
// existing => PUT
|
|
url = `/api/premium/tasks/${newTask.id}`;
|
|
method = 'PUT';
|
|
}
|
|
|
|
try {
|
|
const res = await authFetch(url, {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
if (!res.ok) {
|
|
const errData = await res.json().catch(() => ({}));
|
|
console.error('Failed to save task:', errData);
|
|
alert(errData.error || 'Error saving task');
|
|
return;
|
|
}
|
|
|
|
// re-fetch
|
|
await fetchMilestones();
|
|
|
|
// reset
|
|
setShowTaskForm(null);
|
|
setNewTask({ id: null, title: '', description: '', due_date: '' });
|
|
} catch (err) {
|
|
console.error('Error saving task:', err);
|
|
}
|
|
}
|
|
|
|
// D) Delete an existing task
|
|
async function deleteTask(taskId) {
|
|
if (!taskId) return;
|
|
try {
|
|
const res = await authFetch(`/api/premium/tasks/${taskId}`, { method: 'DELETE' });
|
|
if (!res.ok) {
|
|
const errData = await res.json().catch(() => ({}));
|
|
console.error('Failed to delete task:', errData);
|
|
alert(errData.error || 'Error deleting task');
|
|
return;
|
|
}
|
|
await fetchMilestones();
|
|
} catch (err) {
|
|
console.error('Error deleting task:', err);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// 7) Copy Wizard for universal/cross-scenario
|
|
// ------------------------------------------------------------------
|
|
function CopyMilestoneWizard({ milestone, scenarios, onClose, authFetch }) {
|
|
const [selectedScenarios, setSelectedScenarios] = useState([]);
|
|
|
|
if (!milestone) return null;
|
|
|
|
function toggleScenario(scenarioId) {
|
|
setSelectedScenarios((prev) =>
|
|
prev.includes(scenarioId) ? prev.filter((id) => id !== scenarioId) : [...prev, scenarioId]
|
|
);
|
|
}
|
|
|
|
async function handleCopy() {
|
|
try {
|
|
const res = await authFetch('/api/premium/milestone/copy', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
milestoneId: milestone.id,
|
|
scenarioIds: selectedScenarios
|
|
})
|
|
});
|
|
if (!res.ok) throw new Error('Failed to copy milestone');
|
|
|
|
window.location.reload();
|
|
onClose();
|
|
} catch (err) {
|
|
console.error('Error copying milestone:', err);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="modal-backdrop">
|
|
<div className="modal-container">
|
|
<h3>Copy Milestone to Other Scenarios</h3>
|
|
<p>
|
|
Milestone: <strong>{milestone.title}</strong>
|
|
</p>
|
|
{scenarios.map((s) => (
|
|
<div key={s.id}>
|
|
<label>
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedScenarios.includes(s.id)}
|
|
onChange={() => toggleScenario(s.id)}
|
|
/>
|
|
{s.career_name || s.scenario_title || '(untitled)'}
|
|
</label>
|
|
</div>
|
|
))}
|
|
<div style={{ marginTop: '1rem' }}>
|
|
<Button onClick={onClose} style={{ marginRight: '0.5rem' }}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleCopy}>Copy</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// 8) Delete Milestone
|
|
// ------------------------------------------------------------------
|
|
async function handleDeleteMilestone(m) {
|
|
if (m.is_universal === 1) {
|
|
const userChoice = window.confirm(
|
|
'This milestone is universal. OK => remove from ALL scenarios, Cancel => only remove from this scenario.'
|
|
);
|
|
if (userChoice) {
|
|
// delete from all
|
|
try {
|
|
const delAll = await authFetch(`/api/premium/milestones/${m.id}/all`, {
|
|
method: 'DELETE'
|
|
});
|
|
if (!delAll.ok) {
|
|
console.error('Failed removing universal from all. Status:', delAll.status);
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
console.error('Error deleting universal milestone from all:', err);
|
|
}
|
|
} else {
|
|
// remove from single scenario
|
|
await deleteSingleMilestone(m);
|
|
return;
|
|
}
|
|
} else {
|
|
// normal => single scenario
|
|
await deleteSingleMilestone(m);
|
|
}
|
|
window.location.reload();
|
|
}
|
|
|
|
async function deleteSingleMilestone(m) {
|
|
try {
|
|
const delRes = await authFetch(`/api/premium/milestones/${m.id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
if (!delRes.ok) {
|
|
console.error('Failed to delete milestone:', delRes.status);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error removing milestone from scenario:', err);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// 9) Render
|
|
// ------------------------------------------------------------------
|
|
// Combine "Career" + "Financial" if you want them in a single list:
|
|
|
|
return (
|
|
<div className="milestone-timeline" style={{ padding: '1rem' }}>
|
|
{/* “+ New Milestone” toggles the same form as before */}
|
|
<Button
|
|
onClick={() => {
|
|
if (showForm) {
|
|
// Cancel form
|
|
setShowForm(false);
|
|
setEditingMilestone(null);
|
|
setNewMilestone({
|
|
title: '',
|
|
description: '',
|
|
date: '',
|
|
progress: 0,
|
|
newSalary: '',
|
|
impacts: [],
|
|
isUniversal: 0
|
|
});
|
|
setImpactsToDelete([]);
|
|
} else {
|
|
setShowForm(true);
|
|
}
|
|
}}
|
|
style={{ marginBottom: '0.5rem' }}
|
|
>
|
|
{showForm ? 'Cancel' : '+ New Milestone'}
|
|
</Button>
|
|
|
|
{/* If showForm => the create/edit milestone sub-form */}
|
|
{showForm && (
|
|
<div className="border p-2 my-2">
|
|
<h4>{editingMilestone ? 'Edit Milestone' : 'New Milestone'}</h4>
|
|
<input
|
|
type="text"
|
|
placeholder="Title"
|
|
value={newMilestone.title}
|
|
onChange={(e) => setNewMilestone({ ...newMilestone, title: e.target.value })}
|
|
/>
|
|
<input
|
|
type="text"
|
|
placeholder="Description"
|
|
value={newMilestone.description}
|
|
onChange={(e) => setNewMilestone({ ...newMilestone, description: e.target.value })}
|
|
/>
|
|
<input
|
|
type="date"
|
|
value={newMilestone.date}
|
|
onChange={(e) => setNewMilestone({ ...newMilestone, date: e.target.value })}
|
|
/>
|
|
<input
|
|
type="number"
|
|
placeholder="Progress (%)"
|
|
value={newMilestone.progress === 0 ? '' : newMilestone.progress}
|
|
onChange={(e) =>
|
|
setNewMilestone((prev) => ({
|
|
...prev,
|
|
progress: parseInt(e.target.value || '0', 10)
|
|
}))
|
|
}
|
|
/>
|
|
|
|
{/* If “Financial” => show impacts */}
|
|
{activeView === 'Financial' && (
|
|
<div style={{ border: '1px solid #ccc', padding: '1rem', marginTop: '1rem' }}>
|
|
<h5>Financial Impacts</h5>
|
|
{newMilestone.impacts.map((imp, idx) => (
|
|
<div
|
|
key={idx}
|
|
style={{ border: '1px solid #bbb', margin: '0.5rem', padding: '0.5rem' }}
|
|
>
|
|
{imp.id && <p style={{ fontSize: '0.8rem' }}>ID: {imp.id}</p>}
|
|
<div>
|
|
<label>Type: </label>
|
|
<select
|
|
value={imp.impact_type}
|
|
onChange={(e) => updateImpact(idx, 'impact_type', e.target.value)}
|
|
>
|
|
<option value="ONE_TIME">One-Time</option>
|
|
<option value="MONTHLY">Monthly</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label>Direction: </label>
|
|
<select
|
|
value={imp.direction}
|
|
onChange={(e) => updateImpact(idx, 'direction', e.target.value)}
|
|
>
|
|
<option value="add">Add (Income)</option>
|
|
<option value="subtract">Subtract (Expense)</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label>Amount: </label>
|
|
<input
|
|
type="number"
|
|
value={imp.amount}
|
|
onChange={(e) => updateImpact(idx, 'amount', e.target.value)}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label>Start Date: </label>
|
|
<input
|
|
type="date"
|
|
value={imp.start_date || ''}
|
|
onChange={(e) => updateImpact(idx, 'start_date', e.target.value)}
|
|
/>
|
|
</div>
|
|
{imp.impact_type === 'MONTHLY' && (
|
|
<div>
|
|
<label>End Date: </label>
|
|
<input
|
|
type="date"
|
|
value={imp.end_date || ''}
|
|
onChange={(e) => updateImpact(idx, 'end_date', e.target.value)}
|
|
/>
|
|
</div>
|
|
)}
|
|
<Button
|
|
style={{ marginLeft: '0.5rem', color: 'red' }}
|
|
onClick={() => removeImpact(idx)}
|
|
>
|
|
Remove
|
|
</Button>
|
|
</div>
|
|
))}
|
|
<Button onClick={addNewImpact}>+ Add Impact</Button>
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ marginTop: '1rem' }}>
|
|
<label>
|
|
<input
|
|
type="checkbox"
|
|
checked={!!newMilestone.isUniversal}
|
|
onChange={(e) =>
|
|
setNewMilestone((prev) => ({
|
|
...prev,
|
|
isUniversal: e.target.checked ? 1 : 0
|
|
}))
|
|
}
|
|
/>{' '}
|
|
Apply this milestone to all scenarios
|
|
</label>
|
|
</div>
|
|
|
|
<div style={{ marginTop: '1rem' }}>
|
|
<Button onClick={saveMilestone}>
|
|
{editingMilestone ? 'Update' : 'Add'} Milestone
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Render the (Career + Financial) milestones in a simple vertical list */}
|
|
{Object.keys(milestones).map((typeKey) =>
|
|
milestones[typeKey].map((m) => {
|
|
const tasks = m.tasks || [];
|
|
return (
|
|
<div
|
|
key={m.id}
|
|
style={{ border: '1px solid #ccc', marginTop: '1rem', padding: '0.5rem' }}
|
|
>
|
|
<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})` : ''}{' '}
|
|
{/* EDIT & DELETE Task buttons */}
|
|
<Button
|
|
onClick={() => handleEditTask(m.id, t)}
|
|
style={{ marginLeft: '0.5rem' }}
|
|
>
|
|
Edit
|
|
</Button>
|
|
<Button
|
|
onClick={() => deleteTask(t.id)}
|
|
style={{ marginLeft: '0.5rem', color: 'red' }}
|
|
>
|
|
Delete
|
|
</Button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
|
|
{/* Add or edit a task */}
|
|
<Button
|
|
onClick={() => {
|
|
// if we are already showing the form for this milestone => Cancel
|
|
if (showTaskForm === m.id) {
|
|
setShowTaskForm(null);
|
|
setNewTask({ id: null, title: '', description: '', due_date: '' });
|
|
} else {
|
|
handleAddTask(m.id);
|
|
}
|
|
}}
|
|
style={{ marginRight: '0.5rem' }}
|
|
>
|
|
{showTaskForm === m.id ? 'Cancel Task' : '+ Task'}
|
|
</Button>
|
|
|
|
<Button onClick={() => handleEditMilestone(m)}>Edit</Button>
|
|
<Button
|
|
style={{ marginLeft: '0.5rem' }}
|
|
onClick={() => setCopyWizardMilestone(m)}
|
|
>
|
|
Copy
|
|
</Button>
|
|
<Button
|
|
style={{ marginLeft: '0.5rem', background: 'red', color: 'black' }}
|
|
onClick={() => handleDeleteMilestone(m)}
|
|
>
|
|
Delete
|
|
</Button>
|
|
|
|
{/* If this is the milestone whose tasks we're editing => show the form */}
|
|
{showTaskForm === m.id && (
|
|
<div
|
|
style={{
|
|
marginTop: '0.5rem',
|
|
border: '1px solid #aaa',
|
|
padding: '0.5rem'
|
|
}}
|
|
>
|
|
<h5>{newTask.id ? 'Edit Task' : 'New Task'}</h5>
|
|
<input
|
|
type="text"
|
|
placeholder="Task Title"
|
|
value={newTask.title}
|
|
onChange={(e) =>
|
|
setNewTask((prev) => ({ ...prev, title: e.target.value }))
|
|
}
|
|
/>
|
|
<input
|
|
type="text"
|
|
placeholder="Task Description"
|
|
value={newTask.description}
|
|
onChange={(e) =>
|
|
setNewTask((prev) => ({ ...prev, description: e.target.value }))
|
|
}
|
|
/>
|
|
<input
|
|
type="date"
|
|
value={newTask.due_date || ''}
|
|
onChange={(e) =>
|
|
setNewTask((prev) => ({ ...prev, due_date: e.target.value }))
|
|
}
|
|
/>
|
|
<Button onClick={() => saveTask(m.id)}>
|
|
{newTask.id ? 'Update' : 'Add'} Task
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
|
|
{/* Copy wizard if open */}
|
|
{copyWizardMilestone && (
|
|
<CopyMilestoneWizard
|
|
milestone={copyWizardMilestone}
|
|
scenarios={scenarios}
|
|
onClose={() => setCopyWizardMilestone(null)}
|
|
authFetch={authFetch}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|