Added tasks.

This commit is contained in:
Josh 2025-04-14 17:18:05 +00:00
parent a04ab21d02
commit 6f01c1c9ae
3 changed files with 271 additions and 55 deletions

View File

@ -169,6 +169,84 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => { app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => {
try { try {
const body = req.body;
// CASE 1: If client sent { milestones: [ ... ] }, do a bulk insert
if (Array.isArray(body.milestones)) {
const createdMilestones = [];
for (const m of body.milestones) {
const {
milestone_type,
title,
description,
date,
career_path_id,
progress,
status,
new_salary
} = m;
// Validate some required fields
if (!milestone_type || !title || !date || !career_path_id) {
// Optionally handle partial errors, but let's do a quick check
return res.status(400).json({
error: 'One or more milestones missing required fields',
details: m
});
}
const id = uuidv4();
const now = new Date().toISOString();
await db.run(`
INSERT INTO milestones (
id,
user_id,
career_path_id,
milestone_type,
title,
description,
date,
progress,
status,
new_salary,
created_at,
updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
id,
req.userId,
career_path_id,
milestone_type,
title,
description || '',
date,
progress || 0,
status || 'planned',
new_salary || null,
now,
now
]);
createdMilestones.push({
id,
user_id: req.userId,
career_path_id,
milestone_type,
title,
description: description || '',
date,
progress: progress || 0,
status: status || 'planned',
new_salary: new_salary || null,
tasks: []
});
}
// Return array of created milestones
return res.status(201).json(createdMilestones);
}
// CASE 2: Handle single milestone (the old logic)
const { const {
milestone_type, milestone_type,
title, title,
@ -178,7 +256,7 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
progress, progress,
status, status,
new_salary new_salary
} = req.body; } = body;
if (!milestone_type || !title || !date || !career_path_id) { if (!milestone_type || !title || !date || !career_path_id) {
return res.status(400).json({ return res.status(400).json({
@ -201,7 +279,7 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
date, date,
progress, progress,
status, status,
new_salary, -- store the full new salary if provided new_salary,
created_at, created_at,
updated_at updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
@ -220,8 +298,7 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
now now
]); ]);
// Return the newly created milestone object // Return the newly created single milestone object
// (No tasks initially, so tasks = [])
const newMilestone = { const newMilestone = {
id, id,
user_id: req.userId, user_id: req.userId,
@ -238,8 +315,8 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
res.status(201).json(newMilestone); res.status(201).json(newMilestone);
} catch (err) { } catch (err) {
console.error('Error creating milestone:', err); console.error('Error creating milestone(s):', err);
res.status(500).json({ error: 'Failed to create milestone.' }); res.status(500).json({ error: 'Failed to create milestone(s).' });
} }
}); });
@ -675,16 +752,71 @@ app.get('/api/premium/financial-projection/:careerPathId', authenticatePremiumUs
// POST create a new task // POST create a new task
app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => { app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => {
const { try {
milestone_id, // which milestone this belongs to const { milestone_id, title, description, due_date } = req.body;
user_id, // might come from token or from body
title,
description,
due_date
} = req.body;
// Insert into tasks table // Ensure required fields
// Return the new task in JSON if (!milestone_id || !title) {
return res.status(400).json({
error: 'Missing required fields',
details: { milestone_id, title }
});
}
// Confirm milestone is owned by this user
const milestone = await db.get(`
SELECT user_id
FROM milestones
WHERE id = ?
`, [milestone_id]);
if (!milestone || milestone.user_id !== req.userId) {
return res.status(403).json({ error: 'Milestone not found or not yours.' });
}
const taskId = uuidv4();
const now = new Date().toISOString();
// Insert the new task
await db.run(`
INSERT INTO tasks (
id,
milestone_id,
user_id,
title,
description,
due_date,
status,
created_at,
updated_at
) VALUES (?, ?, ?, ?, ?, ?, 'not_started', ?, ?)
`, [
taskId,
milestone_id,
req.userId,
title,
description || '',
due_date || null,
now,
now
]);
// Return the newly created task as JSON
const newTask = {
id: taskId,
milestone_id,
user_id: req.userId,
title,
description: description || '',
due_date: due_date || null,
status: 'not_started'
};
res.status(201).json(newTask);
} catch (err) {
console.error('Error creating task:', err);
res.status(500).json({ error: 'Failed to create task.' });
}
}); });
// GET tasks for a milestone // GET tasks for a milestone

View File

@ -9,19 +9,20 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
title: '', title: '',
description: '', description: '',
date: '', date: '',
progress: 0, progress: '',
newSalary: '' newSalary: ''
}); });
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
const [editingMilestone, setEditingMilestone] = useState(null); const [editingMilestone, setEditingMilestone] = useState(null);
const [showTaskForm, setShowTaskForm] = useState(null); // store milestoneId or null
const [newTask, setNewTask] = useState({ title: '', description: '', due_date: '' });
/** /**
* Fetch all milestones (and their tasks) for this careerPathId * Fetch all milestones (and their tasks) for this careerPathId
* Then categorize them by milestone_type: 'Career' or 'Financial'. * Then categorize them by milestone_type: 'Career' or 'Financial'.
*/ */
const fetchMilestones = useCallback(async () => { const fetchMilestones = useCallback(async () => {
if (!careerPathId) return; if (!careerPathId) return;
try { try {
const res = await authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`); const res = await authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`);
if (!res.ok) { if (!res.ok) {
@ -79,7 +80,6 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
career_path_id: careerPathId, career_path_id: careerPathId,
progress: newMilestone.progress, progress: newMilestone.progress,
status: newMilestone.progress >= 100 ? 'completed' : 'planned', status: newMilestone.progress >= 100 ? 'completed' : 'planned',
// Only include new_salary if it's a Financial milestone
new_salary: activeView === 'Financial' && newMilestone.newSalary new_salary: activeView === 'Financial' && newMilestone.newSalary
? parseFloat(newMilestone.newSalary) ? parseFloat(newMilestone.newSalary)
: null : null
@ -106,7 +106,6 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
// Update local state so we don't have to refetch everything // Update local state so we don't have to refetch everything
setMilestones((prev) => { setMilestones((prev) => {
const updated = { ...prev }; const updated = { ...prev };
// If editing, replace existing; else push new
if (editingMilestone) { if (editingMilestone) {
updated[activeView] = updated[activeView].map((m) => updated[activeView] = updated[activeView].map((m) =>
m.id === editingMilestone.id ? savedMilestone : m m.id === editingMilestone.id ? savedMilestone : m
@ -132,6 +131,57 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
} }
}; };
const addTask = async (milestoneId) => {
try {
const taskPayload = {
milestone_id: milestoneId,
title: newTask.title,
description: newTask.description,
due_date: newTask.due_date
};
console.log('Creating new task:', taskPayload);
const res = await authFetch('/api/premium/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(taskPayload)
});
if (!res.ok) {
const errorData = await res.json();
console.error('Failed to create task:', errorData);
alert(errorData.error || 'Error creating task');
return;
}
const createdTask = await res.json();
console.log('Task created:', createdTask);
// Update the milestone's tasks in local state
setMilestones((prev) => {
// We need to find which classification this milestone belongs to
const newState = { ...prev };
// Could be Career or Financial
['Career', 'Financial'].forEach((category) => {
newState[category] = newState[category].map((m) => {
if (m.id === milestoneId) {
return {
...m,
tasks: [...m.tasks, createdTask]
};
}
return m;
});
});
return newState;
});
// Reset the addTask form
setNewTask({ title: '', description: '', due_date: '' });
setShowTaskForm(null); // close the task form
} catch (err) {
console.error('Error adding task:', err);
}
};
/** /**
* Figure out the timeline's "end" date by scanning all milestones. * Figure out the timeline's "end" date by scanning all milestones.
*/ */
@ -161,7 +211,7 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
return ( return (
<div className="milestone-timeline"> <div className="milestone-timeline">
{/* View selector buttons */} {/* View selector */}
<div className="view-selector"> <div className="view-selector">
{['Career', 'Financial'].map((view) => ( {['Career', 'Financial'].map((view) => (
<button <button
@ -180,13 +230,7 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
// Cancel form // Cancel form
setShowForm(false); setShowForm(false);
setEditingMilestone(null); setEditingMilestone(null);
setNewMilestone({ setNewMilestone({ title: '', description: '', date: '', progress: 0, newSalary: '' });
title: '',
description: '',
date: '',
progress: 0,
newSalary: ''
});
} else { } else {
// Show form // Show form
setShowForm(true); setShowForm(true);
@ -211,20 +255,24 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
onChange={(e) => setNewMilestone({ ...newMilestone, description: e.target.value })} onChange={(e) => setNewMilestone({ ...newMilestone, description: e.target.value })}
/> />
<input <input
type="date" type="text"
placeholder="mm/dd/yyyy"
value={newMilestone.date} value={newMilestone.date}
onChange={(e) => setNewMilestone({ ...newMilestone, date: e.target.value })} onChange={(e) =>
setNewMilestone((prev) => ({
...prev,
date: e.target.value
}))
}
/> />
<input <input
type="number" type="number"
placeholder="Progress (%)" placeholder="Progress (%)"
value={newMilestone.progress} value={newMilestone.progress === 0 ? '' : newMilestone.progress}
onChange={(e) => onChange={(e) => {
setNewMilestone({ const val = e.target.value === '' ? 0 : parseInt(e.target.value, 10);
...newMilestone, setNewMilestone((prev) => ({ ...prev, progress: val }));
progress: parseInt(e.target.value, 10) }}
})
}
/> />
{activeView === 'Financial' && ( {activeView === 'Financial' && (
<div> <div>
@ -232,9 +280,7 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
type="number" type="number"
placeholder="Full New Salary (e.g., 70000)" placeholder="Full New Salary (e.g., 70000)"
value={newMilestone.newSalary} value={newMilestone.newSalary}
onChange={(e) => onChange={(e) => setNewMilestone({ ...newMilestone, newSalary: e.target.value })}
setNewMilestone({ ...newMilestone, newSalary: e.target.value })
}
/> />
<p>Enter the full new salary (not just the increase) after the milestone occurs.</p> <p>Enter the full new salary (not just the increase) after the milestone occurs.</p>
</div> </div>
@ -255,20 +301,22 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
key={m.id} key={m.id}
className="milestone-timeline-post" className="milestone-timeline-post"
style={{ left: `${leftPos}%` }} style={{ left: `${leftPos}%` }}
onClick={() => {
// Clicking a milestone => edit it
setEditingMilestone(m);
setNewMilestone({
title: m.title,
description: m.description,
date: m.date,
progress: m.progress,
newSalary: m.new_salary || ''
});
setShowForm(true);
}}
> >
<div className="milestone-timeline-dot" /> <div
className="milestone-timeline-dot"
onClick={() => {
// Clicking a milestone => edit it
setEditingMilestone(m);
setNewMilestone({
title: m.title,
description: m.description,
date: m.date,
progress: m.progress,
newSalary: m.new_salary || ''
});
setShowForm(true);
}}
/>
<div className="milestone-content"> <div className="milestone-content">
<div className="title">{m.title}</div> <div className="title">{m.title}</div>
{m.description && <p>{m.description}</p>} {m.description && <p>{m.description}</p>}
@ -277,14 +325,50 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
</div> </div>
<div className="date">{m.date}</div> <div className="date">{m.date}</div>
{/* If the milestone has tasks */} {/* Existing tasks */}
{m.tasks && m.tasks.length > 0 && ( {m.tasks && m.tasks.length > 0 && (
<ul> <ul>
{m.tasks.map((t) => ( {m.tasks.map((t) => (
<li key={t.id}>{t.title}</li> <li key={t.id}>
<strong>{t.title}</strong>
{t.description ? ` - ${t.description}` : ''}
{t.due_date ? ` (Due: ${t.due_date})` : ''}
</li>
))} ))}
</ul> </ul>
)} )}
{/* Button to show/hide Add Task form */}
<button onClick={() => {
setShowTaskForm(showTaskForm === m.id ? null : m.id);
setNewTask({ title: '', description: '', due_date: '' });
}}>
{showTaskForm === m.id ? 'Cancel Task' : 'Add Task'}
</button>
{/* Conditionally render the Add Task form for this milestone */}
{showTaskForm === m.id && (
<div className="task-form">
<input
type="text"
placeholder="Task Title"
value={newTask.title}
onChange={(e) => setNewTask({ ...newTask, title: e.target.value })}
/>
<input
type="text"
placeholder="Task Description"
value={newTask.description}
onChange={(e) => setNewTask({ ...newTask, description: e.target.value })}
/>
<input
type="date"
value={newTask.due_date}
onChange={(e) => setNewTask({ ...newTask, due_date: e.target.value })}
/>
<button onClick={() => addTask(m.id)}>Save Task</button>
</div>
)}
</div> </div>
</div> </div>
); );

Binary file not shown.