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 (
+
+ );
+};
+
+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
-
{suggestedMilestones.map((m, i) => - {m.title} - {m.date}
)}
-
+
+
);
};
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