From 22232060c2c49554cec21d3173b0d9c02047ac49 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 31 Mar 2025 13:51:21 +0000 Subject: [PATCH] AI suggested Milestone functionality, clean up add/edit milestone database interaction --- MilestoneTimeline.js | 24 ++++ backend/server3.js | 143 ++++++++++++++++------ src/components/AISuggestedMilestones.js | 79 +++++++++--- src/components/CareerSelectDropdown.js | 54 ++++++-- src/components/MilestoneTimeline.css | 54 ++++++++ src/components/MilestoneTimeline.js | 156 ++++++++++++++++++------ src/components/MilestoneTracker.js | 24 ++-- user_profile.db | Bin 57344 -> 57344 bytes 8 files changed, 417 insertions(+), 117 deletions(-) create mode 100644 src/components/MilestoneTimeline.css diff --git a/MilestoneTimeline.js b/MilestoneTimeline.js index e69de29..c19e5ff 100644 --- a/MilestoneTimeline.js +++ b/MilestoneTimeline.js @@ -0,0 +1,24 @@ +import React, { useState } from 'react'; + +const MilestoneTimeline = () => { + const [showInputFields, setShowInputFields] = useState(false); + + const toggleInputFields = () => { + setShowInputFields((prev) => !prev); + }; + + return ( +
+ + {showInputFields && ( +
+ + + +
+ )} +
+ ); +}; + +export default MilestoneTimeline; \ No newline at end of file diff --git a/backend/server3.js b/backend/server3.js index 8b70f2e..968677e 100644 --- a/backend/server3.js +++ b/backend/server3.js @@ -127,41 +127,91 @@ app.post('/api/premium/planned-path', authenticatePremiumUser, async (req, res) } }); - - // Save a new milestone -app.post('/api/premium/milestones', authenticatePremiumUser, async (req, res) => { - const { - milestone_type, - title, - description, - date, - career_path_id, - salary_increase, - status = 'planned', - date_completed = null, - context_snapshot = null - } = req.body; +app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => { + const rawMilestones = Array.isArray(req.body.milestones) ? req.body.milestones : [req.body]; - if (!milestone_type || !title || !description || !date) { - return res.status(400).json({ error: 'Missing required fields' }); + const errors = []; + const validMilestones = []; + + for (const [index, m] of rawMilestones.entries()) { + const { + milestone_type, + title, + description, + date, + career_path_id, + salary_increase, + status = 'planned', + date_completed = null, + context_snapshot = null, + progress = 0, + } = m; + + // Validate required fields + if (!milestone_type || !title || !description || !date || !career_path_id) { + errors.push({ + index, + error: 'Missing required fields', + title, // <-- Add the title for identification + date, + details: { + milestone_type: !milestone_type ? 'Required' : undefined, + title: !title ? 'Required' : undefined, + description: !description ? 'Required' : undefined, + date: !date ? 'Required' : undefined, + career_path_id: !career_path_id ? 'Required' : undefined, + } + }); + continue; + } + + validMilestones.push({ + id: uuidv4(), // ✅ assign UUID for unique milestone ID + user_id: req.userId, + milestone_type, + title, + description, + date, + career_path_id, + salary_increase: salary_increase || null, + status, + date_completed, + context_snapshot, + progress + }); + } + + if (errors.length) { + console.warn('❗ Some milestones failed validation. Logging malformed records...'); + console.warn(JSON.stringify(errors, null, 2)); + + return res.status(400).json({ + error: 'Some milestones are invalid', + errors + }); } try { - await db.run( - `INSERT INTO milestones ( - user_id, milestone_type, title, description, date, career_path_id, - salary_increase, status, date_completed, context_snapshot, progress, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`, - [ - req.userId, milestone_type, title, description, date, career_path_id, - salary_increase || null, status, date_completed, context_snapshot - ] + const insertPromises = validMilestones.map(m => + db.run( + `INSERT INTO milestones ( + id, user_id, milestone_type, title, description, date, career_path_id, + salary_increase, status, date_completed, context_snapshot, progress, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`, + [ + m.id, m.user_id, m.milestone_type, m.title, m.description, m.date, m.career_path_id, + m.salary_increase, m.status, m.date_completed, m.context_snapshot, m.progress + ] + ) ); - res.status(201).json({ message: 'Milestone saved successfully' }); + + await Promise.all(insertPromises); + + res.status(201).json({ message: 'Milestones saved successfully', count: validMilestones.length }); } catch (error) { - console.error('Error saving milestone:', error); - res.status(500).json({ error: 'Failed to save milestone' }); + console.error('Error saving milestones:', error); + res.status(500).json({ error: 'Failed to save milestones' }); } }); @@ -170,19 +220,16 @@ app.post('/api/premium/milestones', authenticatePremiumUser, async (req, res) => // Get all milestones app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => { try { - const milestones = await db.all( - `SELECT * FROM milestones WHERE user_id = ? ORDER BY date ASC`, - [req.userId] - ); + const { careerPathId } = req.query; - const mapped = milestones.map(m => ({ - title: m.title, - description: m.description, - date: m.date, - type: m.milestone_type, - progress: m.progress || 0, - career_path_id: m.career_path_id - })); + if (!careerPathId) { + return res.status(400).json({ error: 'careerPathId is required' }); + } + + const milestones = await db.all( + `SELECT * FROM milestones WHERE user_id = ? AND career_path_id = ? ORDER BY date ASC`, + [req.userId, careerPathId] + ); res.json({ milestones }); } catch (error) { @@ -191,6 +238,7 @@ app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => } }); + /// Update an existing milestone app.put('/api/premium/milestones/:id', authenticatePremiumUser, async (req, res) => { try { @@ -212,6 +260,21 @@ app.put('/api/premium/milestones/:id', authenticatePremiumUser, async (req, res) salary_increase, context_snapshot, } = req.body; + + // Explicit required field validation + if (!milestone_type || !title || !description || !date || progress === undefined) { + return res.status(400).json({ + error: 'Missing required fields', + details: { + milestone_type: !milestone_type ? 'Required' : undefined, + title: !title ? 'Required' : undefined, + description: !description ? 'Required' : undefined, + date: !date ? 'Required' : undefined, + progress: progress === undefined ? 'Required' : undefined, + } + }); + } + console.log('Updating milestone with:', { milestone_type, diff --git a/src/components/AISuggestedMilestones.js b/src/components/AISuggestedMilestones.js index 751b15d..c13b608 100644 --- a/src/components/AISuggestedMilestones.js +++ b/src/components/AISuggestedMilestones.js @@ -1,8 +1,11 @@ // src/components/AISuggestedMilestones.js import React, { useEffect, useState } from 'react'; -const AISuggestedMilestones = ({ career, careerPathId, authFetch }) => { + +const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, activeView }) => { const [suggestedMilestones, setSuggestedMilestones] = useState([]); + const [selected, setSelected] = useState([]); + const [loading, setLoading] = useState(false); useEffect(() => { if (!career) return; @@ -11,33 +14,69 @@ const AISuggestedMilestones = ({ career, careerPathId, authFetch }) => { { title: `Mid-Level ${career}`, date: '2027-01-01', progress: 0 }, { title: `Senior-Level ${career}`, date: '2030-01-01', progress: 0 }, ]); + setSelected([]); }, [career]); - - const confirmMilestones = async () => { - for (const milestone of suggestedMilestones) { - await authFetch(`/api/premium/milestones`, { - method: 'POST', - body: JSON.stringify({ - milestone_type: 'Career', - title: milestone.title, - description: milestone.title, - date: milestone.date, - career_path_id: careerPathId, - progress: milestone.progress, - status: 'planned', - }), - }); - } - setSuggestedMilestones([]); + + const toggleSelect = (index) => { + setSelected(prev => + prev.includes(index) ? prev.filter(i => i !== index) : [...prev, index] + ); }; + const confirmSelectedMilestones = async () => { + const milestonesToSend = selected.map(index => { + const m = suggestedMilestones[index]; + return { + title: m.title, + description: m.title, + date: m.date, + progress: m.progress, + milestone_type: activeView || 'Career', + career_path_id: careerPathId, + }; + }); + + try { + setLoading(true); + const res = await authFetch(`/api/premium/milestone`, { + method: 'POST', + body: JSON.stringify({ milestones: milestonesToSend }), + headers: { 'Content-Type': 'application/json' }, + }); + + if (!res.ok) throw new Error('Failed to save selected milestones'); + const data = await res.json(); + console.log('Confirmed milestones:', data); + setSelected([]); // Clear selection + window.location.reload(); + } catch (error) { + console.error('Error saving selected milestones:', error); + } finally { + setLoading(false); + } + }; + + if (!suggestedMilestones.length) return null; return (

AI-Suggested Milestones

- - + +
); }; diff --git a/src/components/CareerSelectDropdown.js b/src/components/CareerSelectDropdown.js index b7abb6d..93f238e 100644 --- a/src/components/CareerSelectDropdown.js +++ b/src/components/CareerSelectDropdown.js @@ -1,7 +1,29 @@ // src/components/CareerSelectDropdown.js -import React from 'react'; +import React, { useEffect } from 'react'; + +const CareerSelectDropdown = ({ existingCareerPaths, selectedCareer, onChange, loading, authFetch }) => { + const fetchMilestones = (careerPathId) => { + authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`) + .then((response) => response.json()) + .then((data) => { + console.log('Milestones:', data); + // Handle milestones data as needed + }) + .catch((error) => { + console.error('Error fetching milestones:', error); + }); + }; + + const handleChange = (selected) => { + onChange(selected); // selected is the full career object + if (selected?.id) { + fetchMilestones(selected.id); // 🔥 Correct: use the id from the object + } else { + console.warn('No career ID found for selected object:', selected); + } + }; + -const CareerSelectDropdown = ({ existingCareerPaths, selectedCareer, onChange, loading }) => { return (
@@ -9,20 +31,28 @@ const CareerSelectDropdown = ({ existingCareerPaths, selectedCareer, onChange, l

Loading career paths...

) : ( + + + - - {existingCareerPaths.map((path) => ( - - ))} - )}
); + }; export default CareerSelectDropdown; diff --git a/src/components/MilestoneTimeline.css b/src/components/MilestoneTimeline.css new file mode 100644 index 0000000..266daba --- /dev/null +++ b/src/components/MilestoneTimeline.css @@ -0,0 +1,54 @@ +.milestone-timeline-container { + position: relative; + margin-top: 40px; + height: 120px; +} + +.milestone-timeline-line { + position: absolute; + top: 50%; + left: 0; + right: 0; + height: 4px; + background-color: #ccc; + transform: translateY(-50%); +} + +.milestone-timeline-post { + position: absolute; + transform: translateX(-50%); + cursor: pointer; +} + +.milestone-timeline-dot { + width: 12px; + height: 12px; + border-radius: 50%; + background-color: #007bff; + margin: 0 auto; +} + +.milestone-content { + margin-top: 10px; + text-align: center; + width: 160px; + background: white; + border: 1px solid #ddd; + padding: 6px; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +.progress-bar { + height: 6px; + background-color: #e0e0e0; + border-radius: 3px; + margin-top: 5px; + overflow: hidden; +} + +.progress { + height: 100%; + background-color: #28a745; +} + diff --git a/src/components/MilestoneTimeline.js b/src/components/MilestoneTimeline.js index 9d28ffe..5f6c150 100644 --- a/src/components/MilestoneTimeline.js +++ b/src/components/MilestoneTimeline.js @@ -3,29 +3,54 @@ import React, { useEffect, useState, useCallback } from 'react'; const today = new Date(); -const MilestoneTimeline = ({ careerPathId, authFetch }) => { +const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView }) => { + const [milestones, setMilestones] = useState({ Career: [], Financial: [], Retirement: [] }); - const [activeView, setActiveView] = useState('Career'); - const [newMilestone, setNewMilestone] = useState({ title: '', date: '', progress: 0 }); + const [newMilestone, setNewMilestone] = useState({ title: '', date: '', description: '', progress: 0 }); const [showForm, setShowForm] = useState(false); const [editingMilestone, setEditingMilestone] = useState(null); const fetchMilestones = useCallback(async () => { - if (!careerPathId) return; + if (!careerPathId) { + console.warn('No careerPathId provided.'); + return; + } - const res = await authFetch(`api/premium/milestones`); - if (!res) return; + const res = await authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`); + if (!res) { + console.error('Failed to fetch milestones.'); + return; + } const data = await res.json(); + + const raw = Array.isArray(data.milestones[0]) + ? data.milestones.flat() + : data.milestones.milestones || data.milestones; + +const flatMilestones = Array.isArray(data.milestones[0]) +? data.milestones.flat() +: data.milestones; + +const filteredMilestones = raw.filter( + (m) => m.career_path_id === careerPathId +); + const categorized = { Career: [], Financial: [], Retirement: [] }; - data.milestones.forEach((m) => { - if (m.career_path_id === careerPathId && categorized[m.milestone_type]) { - categorized[m.milestone_type].push(m); - } - }); + filteredMilestones.forEach((m) => { + const type = m.milestone_type; + if (categorized[type]) { + categorized[type].push(m); + } else { + console.warn(`Unknown milestone type: ${type}`); + } +}); + setMilestones(categorized); + console.log('Milestones set for view:', categorized); + }, [careerPathId, authFetch]); // ✅ useEffect simply calls the function @@ -34,24 +59,67 @@ const MilestoneTimeline = ({ careerPathId, authFetch }) => { }, [fetchMilestones]); const saveMilestone = async () => { - const url = editingMilestone ? `/api/premium/milestones/${editingMilestone.id}` : `/api/premium/milestones`; + const url = editingMilestone + ? `/api/premium/milestones/${editingMilestone.id}` + : `/api/premium/milestone`; const method = editingMilestone ? 'PUT' : 'POST'; const payload = { milestone_type: activeView, title: newMilestone.title, - description: newMilestone.title, + description: newMilestone.description, date: newMilestone.date, career_path_id: careerPathId, progress: newMilestone.progress, status: newMilestone.progress === 100 ? 'completed' : 'planned', }; - const res = await authFetch(url, { method, body: JSON.stringify(payload) }); - if (res && res.ok) { - fetchMilestones(); + try { + console.log('Sending request to:', url); + console.log('HTTP Method:', method); + console.log('Payload:', payload); + + const res = await authFetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + if (!res.ok) { + const errorData = await res.json(); + console.error('Failed to save milestone:', errorData); + + let message = 'An error occurred while saving the milestone.'; + if (errorData?.error === 'Missing required fields') { + message = 'Please complete all required fields before saving.'; + console.warn('Missing fields:', errorData.details); + } + + alert(message); // Replace with your preferred UI messaging + return; + } + + const savedMilestone = await res.json(); + + // Update state locally instead of fetching all milestones + setMilestones((prevMilestones) => { + const updatedMilestones = { ...prevMilestones }; + if (editingMilestone) { + // Update the existing milestone + updatedMilestones[activeView] = updatedMilestones[activeView].map((m) => + m.id === editingMilestone.id ? savedMilestone : m + ); + } else { + // Add the new milestone + updatedMilestones[activeView].push(savedMilestone); + } + return updatedMilestones; + }); + setShowForm(false); setEditingMilestone(null); - setNewMilestone({ title: '', date: '', progress: 0 }); + setNewMilestone({ title: '', description: '', date: '', progress: 0 }); + } catch (error) { + console.error('Error saving milestone:', error); } }; @@ -69,47 +137,61 @@ const MilestoneTimeline = ({ careerPathId, authFetch }) => { return Math.min(Math.max(position, 0), 100); }; + console.log('Rendering view:', activeView, milestones?.[activeView]); + + if (!activeView || !milestones?.[activeView]) { + return ( +
+

Loading milestones...

+
+ ); + } + return (
{['Career', 'Financial', 'Retirement'].map((view) => ( - - ))} + +))}
-
- {milestones[activeView]?.map((m) => ( -
-

{m.title}

-

{m.description}

-

Date: {m.date}

-

Progress: {m.progress}%

-
- ))} -
- - + {showForm && (
setNewMilestone({ ...newMilestone, title: e.target.value })} /> + setNewMilestone({ ...newMilestone, description: e.target.value })} /> setNewMilestone({ ...newMilestone, date: e.target.value })} /> setNewMilestone({ ...newMilestone, progress: parseInt(e.target.value, 10) })} />
)} -
-
+
+
{milestones[activeView]?.map((m) => ( -
{ +
{ setEditingMilestone(m); setNewMilestone({ title: m.title, date: m.date, progress: m.progress }); setShowForm(true); }}> -
+
{m.title}
diff --git a/src/components/MilestoneTracker.js b/src/components/MilestoneTracker.js index ce947ae..0d31bff 100644 --- a/src/components/MilestoneTracker.js +++ b/src/components/MilestoneTracker.js @@ -7,6 +7,7 @@ import CareerSearch from './CareerSearch.js'; import MilestoneTimeline from './MilestoneTimeline.js'; import AISuggestedMilestones from './AISuggestedMilestones.js'; import './MilestoneTracker.css'; +import './MilestoneTimeline.css'; // Ensure this file contains styles for timeline-line and milestone-dot const MilestoneTracker = ({ selectedCareer: initialCareer }) => { const location = useLocation(); @@ -17,6 +18,8 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { const [existingCareerPaths, setExistingCareerPaths] = useState([]); const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false); const [pendingCareerForModal, setPendingCareerForModal] = useState(null); + const [activeView, setActiveView] = useState("Career"); + const apiURL = process.env.REACT_APP_API_URL; @@ -72,11 +75,12 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { fetchCareerPaths(); }, []); - const handleCareerChange = (careerName) => { - const match = existingCareerPaths.find(p => p.career_name === careerName); - if (match) { - setSelectedCareer(match); - setCareerPathId(match.career_path_id); + const handleCareerChange = (selected) => { + if (selected && selected.id && selected.career_name) { + setSelectedCareer(selected); + setCareerPathId(selected.id); + } else { + console.warn('Invalid career object received in handleCareerChange:', selected); } }; @@ -103,17 +107,21 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { - + + {console.log('Passing careerPathId to MilestoneTimeline:', careerPathId)} - + setPendingCareerForModal(careerName)} + setPendingCareerForModal={setPendingCareerForModal} + authFetch={authFetch} /> {pendingCareerForModal && ( diff --git a/user_profile.db b/user_profile.db index 326912b91ae039c06ee483ac6bfd41d82cac48c9..505218f0996d41564d9fa9b04d1d60dc7b36270f 100644 GIT binary patch delta 1538 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 q);qEW`a<