diff --git a/backend/server3.js b/backend/server3.js index 4af2713..6ffb7ef 100644 --- a/backend/server3.js +++ b/backend/server3.js @@ -169,6 +169,84 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => { 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 { milestone_type, title, @@ -178,7 +256,7 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => progress, status, new_salary - } = req.body; + } = body; if (!milestone_type || !title || !date || !career_path_id) { return res.status(400).json({ @@ -201,7 +279,7 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => date, progress, status, - new_salary, -- store the full new salary if provided + new_salary, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) @@ -220,8 +298,7 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => now ]); - // Return the newly created milestone object - // (No tasks initially, so tasks = []) + // Return the newly created single milestone object const newMilestone = { id, user_id: req.userId, @@ -238,8 +315,8 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => res.status(201).json(newMilestone); } catch (err) { - console.error('Error creating milestone:', err); - res.status(500).json({ error: 'Failed to create milestone.' }); + console.error('Error creating milestone(s):', err); + 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 app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => { - const { - milestone_id, // which milestone this belongs to - user_id, // might come from token or from body - title, - description, - due_date - } = req.body; + try { + const { milestone_id, title, description, due_date } = req.body; - // Insert into tasks table - // Return the new task in JSON + // Ensure required fields + 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 diff --git a/src/components/MilestoneTimeline.js b/src/components/MilestoneTimeline.js index 60c92fd..caac548 100644 --- a/src/components/MilestoneTimeline.js +++ b/src/components/MilestoneTimeline.js @@ -9,19 +9,20 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView title: '', description: '', date: '', - progress: 0, + progress: '', newSalary: '' }); const [showForm, setShowForm] = useState(false); 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 * Then categorize them by milestone_type: 'Career' or 'Financial'. */ const fetchMilestones = useCallback(async () => { if (!careerPathId) return; - try { const res = await authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`); if (!res.ok) { @@ -79,7 +80,6 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView career_path_id: careerPathId, progress: newMilestone.progress, status: newMilestone.progress >= 100 ? 'completed' : 'planned', - // Only include new_salary if it's a Financial milestone new_salary: activeView === 'Financial' && newMilestone.newSalary ? parseFloat(newMilestone.newSalary) : null @@ -106,7 +106,6 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView // Update local state so we don't have to refetch everything setMilestones((prev) => { const updated = { ...prev }; - // If editing, replace existing; else push new if (editingMilestone) { updated[activeView] = updated[activeView].map((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. */ @@ -161,7 +211,7 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView return (
- {/* View selector buttons */} + {/* View selector */}
{['Career', 'Financial'].map((view) => ( + + {/* Conditionally render the Add Task form for this milestone */} + {showTaskForm === m.id && ( +
+ setNewTask({ ...newTask, title: e.target.value })} + /> + setNewTask({ ...newTask, description: e.target.value })} + /> + setNewTask({ ...newTask, due_date: e.target.value })} + /> + +
+ )}
); diff --git a/user_profile.db b/user_profile.db index bf11784..15489de 100644 Binary files a/user_profile.db and b/user_profile.db differ