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 326912b..505218f 100644 Binary files a/user_profile.db and b/user_profile.db differ