From 6f01c1c9ae6f18b634ff8a2abdb3341bee05e8ec Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 14 Apr 2025 17:18:05 +0000 Subject: [PATCH] Added tasks. --- backend/server3.js | 162 ++++++++++++++++++++++++--- src/components/MilestoneTimeline.js | 164 +++++++++++++++++++++------- user_profile.db | Bin 106496 -> 106496 bytes 3 files changed, 271 insertions(+), 55 deletions(-) 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 bf117846b82aa6ec1e8fbfaa1ad5f719fb96e5b7..15489de883da37701732979ca752c9c8d95edd0d 100644 GIT binary patch delta 1427 zcma)+-EZ4e7{+~^G+(RhR_)eLiIZ+(L+YvgmDmmhv^8lJsqG3RV(JCq*gjsPrg3C@ zl-{(nkc!3yT3eyqaDgfWmn@q^u7-Mvgg|iD3xwbf_!kBe$6Z?Jrb)SYCHpx?*6)4Z z=k?vA*xjSp{mCK3Fw6w43$$Xi=J#ggKE%wrhi&UK=PLlg;NgS8hxdmEljHQ5qm_6x z_4yxs4DbbpFSLUkzZ2*nRjII-)%4|j-cUu=RIrI`T@a9>5gTDUtDvlANP?w{wxKAh zh6O<~Riw)q4Jn41MVciGNGGyFbeULoModnZiRZ&I_ST_zqwbjN<{GXwV6ox5ThOPn zBuE(~C@3##4acu~K6ZU#ovNZz7S)`X$w}g@E@`VT(ocHaKZ??Zjv4;=(a_3|{I#o* zO0t(m9{vX4WW2;A*==Uvqk(1a@BSzK-=!W$xx@s!&2E1>1%8UgOJKhpd;&V#EciYe zP0GyAd%+K#FOpy!L>UHr+zxg@=Wz-w^o=FidrZuTy^{Ke?G@#({SrB4sNvmVgbjUsa1cg*#Z$T+>$SX znU}FoV7Yj!Jd=X3={mPv;(4&NTrMsY--3nW{PoIW849iSi(q`GS;vhAv95-98)UQU z;X0->C=};POV<}e)x<6#ZncU1wQ9{e`!GUq+1Z5Hvf!3;$Ax}vgTmsCrUy4`{u;E1 zjqlWbFBg8oC6vjkVmfomb;dAPY5fvXe;WZ8qTxyF_X#l7DU5(yY-AYBwSzZ6OrDO7 zE~H!OF)-G;9S4{CMiTQ(qBA-M{)`WVp^b1=hJR_`HLltY-sI+k)d*K%$4jMjVv?!k z^BK*Q6iv5LR?uXm$Qcvij4mKU&8oJfU~Cv9IX!pMazB>+)uqc zS>|)7YX=>N>;J#rMWwqgisx;8{W)7DVOCJZ=TWa^13ubc5Q+vMEIltx!dFsP2^Y**B_WKs;Wk2I^nNYkN6d}q8qNMA;nU4q!Ydf)6pu(aCKL< z38C1qkNBJ3$T75wj7-b|Y{C;tEj;};-r_=<#(Kv#4Rcx5DX``}AHKePUpo&XX zq{+$U7^a2sB)AD)9sNh{JGAHZ2PdsY<>fDltW;JNBQn8(+@oW?T`X&^Vpk9@+gxv| zV^lF^2Vr7UjW|@Hj`mFN6E%HKx%KDVD+lmDj2GbS0zU<3xh?Kp{NhjPOL4zc5T$#9 z^r>`D`d<2N8a1WSwR9Vt<{XGg;%PQqI!Qdu&rMxTw`W&QqiKE*jMJq(asTP%rQBb_ zTxwgG|7`wt@t?wz!cX}}+3jpYEY5!Q`9<*Sbmswxw^QI3F{KqI`{#hVQv7&0mp!+< z4DMq_ET48gpN6d8qv5l++~nIc;GLBlc?gf49kvxv*s9-cC2r<1|L|+_<^p(qR9kD+ z6Tf}X9XbJFn;p+NOos@;-yhzGtxYe4-C@X}by&)uHW`K#dhBWl32e6;H9kg2d@|VT zcs+YS36Iw5)kbseRx*6Z2pfjS6b@muWB0qkC7&|tz+;MjkI@fVC+v|R-0ZW-dkbLk zQZm3WNNSu8VX)O`*6*~|nj87+d{cb48+{XB$bm%=Kgxskne$n(CS0A7k#iBy&s1q@LFoR^CO6BXTjZ6`du)dMjwHA={)!&8_xvb$>q2dfSJ