AI suggested Milestone functionality, clean up add/edit milestone database interaction
This commit is contained in:
parent
b13a7f1299
commit
13b102bfe2
@ -0,0 +1,24 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
const MilestoneTimeline = () => {
|
||||||
|
const [showInputFields, setShowInputFields] = useState(false);
|
||||||
|
|
||||||
|
const toggleInputFields = () => {
|
||||||
|
setShowInputFields((prev) => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={toggleInputFields}>+ New Milestone</button>
|
||||||
|
{showInputFields && (
|
||||||
|
<div>
|
||||||
|
<input type="text" placeholder="Milestone Name" />
|
||||||
|
<input type="date" placeholder="Due Date" />
|
||||||
|
<button>Save</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MilestoneTimeline;
|
@ -127,41 +127,91 @@ app.post('/api/premium/planned-path', authenticatePremiumUser, async (req, res)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Save a new milestone
|
// Save a new milestone
|
||||||
app.post('/api/premium/milestones', authenticatePremiumUser, async (req, res) => {
|
app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => {
|
||||||
const {
|
const rawMilestones = Array.isArray(req.body.milestones) ? req.body.milestones : [req.body];
|
||||||
milestone_type,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
date,
|
|
||||||
career_path_id,
|
|
||||||
salary_increase,
|
|
||||||
status = 'planned',
|
|
||||||
date_completed = null,
|
|
||||||
context_snapshot = null
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
if (!milestone_type || !title || !description || !date) {
|
const errors = [];
|
||||||
return res.status(400).json({ error: 'Missing required fields' });
|
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 {
|
try {
|
||||||
await db.run(
|
const insertPromises = validMilestones.map(m =>
|
||||||
`INSERT INTO milestones (
|
db.run(
|
||||||
user_id, milestone_type, title, description, date, career_path_id,
|
`INSERT INTO milestones (
|
||||||
salary_increase, status, date_completed, context_snapshot, progress, updated_at
|
id, user_id, milestone_type, title, description, date, career_path_id,
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
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
|
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) {
|
} catch (error) {
|
||||||
console.error('Error saving milestone:', error);
|
console.error('Error saving milestones:', error);
|
||||||
res.status(500).json({ error: 'Failed to save milestone' });
|
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
|
// Get all milestones
|
||||||
app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => {
|
app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const milestones = await db.all(
|
const { careerPathId } = req.query;
|
||||||
`SELECT * FROM milestones WHERE user_id = ? ORDER BY date ASC`,
|
|
||||||
[req.userId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const mapped = milestones.map(m => ({
|
if (!careerPathId) {
|
||||||
title: m.title,
|
return res.status(400).json({ error: 'careerPathId is required' });
|
||||||
description: m.description,
|
}
|
||||||
date: m.date,
|
|
||||||
type: m.milestone_type,
|
const milestones = await db.all(
|
||||||
progress: m.progress || 0,
|
`SELECT * FROM milestones WHERE user_id = ? AND career_path_id = ? ORDER BY date ASC`,
|
||||||
career_path_id: m.career_path_id
|
[req.userId, careerPathId]
|
||||||
}));
|
);
|
||||||
|
|
||||||
res.json({ milestones });
|
res.json({ milestones });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -191,6 +238,7 @@ app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/// Update an existing milestone
|
/// Update an existing milestone
|
||||||
app.put('/api/premium/milestones/:id', authenticatePremiumUser, async (req, res) => {
|
app.put('/api/premium/milestones/:id', authenticatePremiumUser, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@ -212,6 +260,21 @@ app.put('/api/premium/milestones/:id', authenticatePremiumUser, async (req, res)
|
|||||||
salary_increase,
|
salary_increase,
|
||||||
context_snapshot,
|
context_snapshot,
|
||||||
} = req.body;
|
} = 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:', {
|
console.log('Updating milestone with:', {
|
||||||
milestone_type,
|
milestone_type,
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
// src/components/AISuggestedMilestones.js
|
// src/components/AISuggestedMilestones.js
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
const AISuggestedMilestones = ({ career, careerPathId, authFetch }) => {
|
|
||||||
|
const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, activeView }) => {
|
||||||
const [suggestedMilestones, setSuggestedMilestones] = useState([]);
|
const [suggestedMilestones, setSuggestedMilestones] = useState([]);
|
||||||
|
const [selected, setSelected] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!career) return;
|
if (!career) return;
|
||||||
@ -11,33 +14,69 @@ const AISuggestedMilestones = ({ career, careerPathId, authFetch }) => {
|
|||||||
{ title: `Mid-Level ${career}`, date: '2027-01-01', progress: 0 },
|
{ title: `Mid-Level ${career}`, date: '2027-01-01', progress: 0 },
|
||||||
{ title: `Senior-Level ${career}`, date: '2030-01-01', progress: 0 },
|
{ title: `Senior-Level ${career}`, date: '2030-01-01', progress: 0 },
|
||||||
]);
|
]);
|
||||||
|
setSelected([]);
|
||||||
}, [career]);
|
}, [career]);
|
||||||
|
|
||||||
const confirmMilestones = async () => {
|
const toggleSelect = (index) => {
|
||||||
for (const milestone of suggestedMilestones) {
|
setSelected(prev =>
|
||||||
await authFetch(`/api/premium/milestones`, {
|
prev.includes(index) ? prev.filter(i => i !== index) : [...prev, index]
|
||||||
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 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;
|
if (!suggestedMilestones.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="suggested-milestones">
|
<div className="suggested-milestones">
|
||||||
<h4>AI-Suggested Milestones</h4>
|
<h4>AI-Suggested Milestones</h4>
|
||||||
<ul>{suggestedMilestones.map((m, i) => <li key={i}>{m.title} - {m.date}</li>)}</ul>
|
<ul>
|
||||||
<button onClick={confirmMilestones}>Confirm Milestones</button>
|
{suggestedMilestones.map((m, i) => (
|
||||||
|
<li key={i}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selected.includes(i)}
|
||||||
|
onChange={() => toggleSelect(i)}
|
||||||
|
/>
|
||||||
|
{m.title} – {m.date}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<button onClick={confirmSelectedMilestones} disabled={loading || selected.length === 0}>
|
||||||
|
{loading ? 'Saving...' : 'Confirm Selected'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,29 @@
|
|||||||
// src/components/CareerSelectDropdown.js
|
// 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 (
|
return (
|
||||||
<div className="career-select-dropdown">
|
<div className="career-select-dropdown">
|
||||||
<label>Select Career Path:</label>
|
<label>Select Career Path:</label>
|
||||||
@ -9,20 +31,28 @@ const CareerSelectDropdown = ({ existingCareerPaths, selectedCareer, onChange, l
|
|||||||
<p>Loading career paths...</p>
|
<p>Loading career paths...</p>
|
||||||
) : (
|
) : (
|
||||||
<select
|
<select
|
||||||
value={selectedCareer || ''}
|
value={selectedCareer?.id || ''}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => {
|
||||||
>
|
const selectedId = e.target.value;
|
||||||
|
const selected = existingCareerPaths.find(path => path.id === selectedId);
|
||||||
|
handleChange(selected); // ✅ Pass the full object
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="" disabled>Select career path...</option>
|
||||||
|
{existingCareerPaths.map((path) => (
|
||||||
|
<option key={path.id} value={path.id}>
|
||||||
|
{path.career_name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<option value="" disabled>Select career path...</option>
|
|
||||||
{existingCareerPaths.map((path) => (
|
|
||||||
<option key={path.career_path_id} value={path.career_name}>
|
|
||||||
{path.career_name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CareerSelectDropdown;
|
export default CareerSelectDropdown;
|
||||||
|
54
src/components/MilestoneTimeline.css
Normal file
54
src/components/MilestoneTimeline.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -3,29 +3,54 @@ import React, { useEffect, useState, useCallback } from 'react';
|
|||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
|
|
||||||
const MilestoneTimeline = ({ careerPathId, authFetch }) => {
|
const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView }) => {
|
||||||
|
|
||||||
const [milestones, setMilestones] = useState({ Career: [], Financial: [], Retirement: [] });
|
const [milestones, setMilestones] = useState({ Career: [], Financial: [], Retirement: [] });
|
||||||
const [activeView, setActiveView] = useState('Career');
|
const [newMilestone, setNewMilestone] = useState({ title: '', date: '', description: '', progress: 0 });
|
||||||
const [newMilestone, setNewMilestone] = useState({ title: '', date: '', progress: 0 });
|
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
const [editingMilestone, setEditingMilestone] = useState(null);
|
const [editingMilestone, setEditingMilestone] = useState(null);
|
||||||
|
|
||||||
const fetchMilestones = useCallback(async () => {
|
const fetchMilestones = useCallback(async () => {
|
||||||
if (!careerPathId) return;
|
if (!careerPathId) {
|
||||||
|
console.warn('No careerPathId provided.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const res = await authFetch(`api/premium/milestones`);
|
const res = await authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`);
|
||||||
if (!res) return;
|
if (!res) {
|
||||||
|
console.error('Failed to fetch milestones.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
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: [] };
|
const categorized = { Career: [], Financial: [], Retirement: [] };
|
||||||
|
|
||||||
data.milestones.forEach((m) => {
|
filteredMilestones.forEach((m) => {
|
||||||
if (m.career_path_id === careerPathId && categorized[m.milestone_type]) {
|
const type = m.milestone_type;
|
||||||
categorized[m.milestone_type].push(m);
|
if (categorized[type]) {
|
||||||
}
|
categorized[type].push(m);
|
||||||
});
|
} else {
|
||||||
|
console.warn(`Unknown milestone type: ${type}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
setMilestones(categorized);
|
setMilestones(categorized);
|
||||||
|
console.log('Milestones set for view:', categorized);
|
||||||
|
|
||||||
}, [careerPathId, authFetch]);
|
}, [careerPathId, authFetch]);
|
||||||
|
|
||||||
// ✅ useEffect simply calls the function
|
// ✅ useEffect simply calls the function
|
||||||
@ -34,24 +59,67 @@ const MilestoneTimeline = ({ careerPathId, authFetch }) => {
|
|||||||
}, [fetchMilestones]);
|
}, [fetchMilestones]);
|
||||||
|
|
||||||
const saveMilestone = async () => {
|
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 method = editingMilestone ? 'PUT' : 'POST';
|
||||||
const payload = {
|
const payload = {
|
||||||
milestone_type: activeView,
|
milestone_type: activeView,
|
||||||
title: newMilestone.title,
|
title: newMilestone.title,
|
||||||
description: newMilestone.title,
|
description: newMilestone.description,
|
||||||
date: newMilestone.date,
|
date: newMilestone.date,
|
||||||
career_path_id: careerPathId,
|
career_path_id: careerPathId,
|
||||||
progress: newMilestone.progress,
|
progress: newMilestone.progress,
|
||||||
status: newMilestone.progress === 100 ? 'completed' : 'planned',
|
status: newMilestone.progress === 100 ? 'completed' : 'planned',
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await authFetch(url, { method, body: JSON.stringify(payload) });
|
try {
|
||||||
if (res && res.ok) {
|
console.log('Sending request to:', url);
|
||||||
fetchMilestones();
|
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);
|
setShowForm(false);
|
||||||
setEditingMilestone(null);
|
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);
|
return Math.min(Math.max(position, 0), 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('Rendering view:', activeView, milestones?.[activeView]);
|
||||||
|
|
||||||
|
if (!activeView || !milestones?.[activeView]) {
|
||||||
|
return (
|
||||||
|
<div className="milestone-timeline">
|
||||||
|
<p>Loading milestones...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="milestone-timeline">
|
<div className="milestone-timeline">
|
||||||
<div className="view-selector">
|
<div className="view-selector">
|
||||||
{['Career', 'Financial', 'Retirement'].map((view) => (
|
{['Career', 'Financial', 'Retirement'].map((view) => (
|
||||||
<button key={view} className={activeView === view ? 'active' : ''} onClick={() => setActiveView(view)}>
|
<button
|
||||||
{view}
|
key={view}
|
||||||
</button>
|
className={activeView === view ? 'active' : ''}
|
||||||
))}
|
onClick={() => setActiveView(view)}
|
||||||
|
>
|
||||||
|
{view}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="timeline">
|
<button onClick={() => {
|
||||||
{milestones[activeView]?.map((m) => (
|
if (showForm) {
|
||||||
<div key={m.id} className="milestone-entry">
|
setShowForm(false);
|
||||||
<h4>{m.title}</h4>
|
setEditingMilestone(null);
|
||||||
<p>{m.description}</p>
|
setNewMilestone({ title: '', date: '', progress: 0 });
|
||||||
<p>Date: {m.date}</p>
|
} else {
|
||||||
<p>Progress: {m.progress}%</p>
|
setShowForm(true);
|
||||||
</div>
|
}
|
||||||
))}
|
}}>
|
||||||
</div>
|
{showForm ? 'Cancel' : '+ New Milestone'}
|
||||||
|
</button>
|
||||||
<button onClick={() => setShowForm(true)}>+ New Milestone</button>
|
|
||||||
|
|
||||||
{showForm && (
|
{showForm && (
|
||||||
<div className="form">
|
<div className="form">
|
||||||
<input type="text" placeholder="Title" value={newMilestone.title} onChange={(e) => setNewMilestone({ ...newMilestone, title: e.target.value })} />
|
<input type="text" placeholder="Title" value={newMilestone.title} onChange={(e) => setNewMilestone({ ...newMilestone, title: e.target.value })} />
|
||||||
|
<input type="text" placeholder="Description" value={newMilestone.description} onChange={(e) => setNewMilestone({ ...newMilestone, description: e.target.value })} />
|
||||||
<input type="date" value={newMilestone.date} onChange={(e) => setNewMilestone({ ...newMilestone, date: e.target.value })} />
|
<input type="date" value={newMilestone.date} onChange={(e) => setNewMilestone({ ...newMilestone, date: e.target.value })} />
|
||||||
<input type="number" placeholder="Progress (%)" value={newMilestone.progress} onChange={(e) => setNewMilestone({ ...newMilestone, progress: parseInt(e.target.value, 10) })} />
|
<input type="number" placeholder="Progress (%)" value={newMilestone.progress} onChange={(e) => setNewMilestone({ ...newMilestone, progress: parseInt(e.target.value, 10) })} />
|
||||||
<button onClick={saveMilestone}>{editingMilestone ? 'Update' : 'Add'} Milestone</button>
|
<button onClick={saveMilestone}>{editingMilestone ? 'Update' : 'Add'} Milestone</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="timeline-container">
|
<div className="milestone-timeline-container">
|
||||||
<div className="timeline-line" />
|
<div className="milestone-timeline-line" />
|
||||||
{milestones[activeView]?.map((m) => (
|
{milestones[activeView]?.map((m) => (
|
||||||
<div key={m.id} className="milestone-post" style={{ left: `${calcPosition(m.date)}%` }} onClick={() => {
|
<div key={m.id} className="milestone-timeline-post" style={{ left: `${calcPosition(m.date)}%` }} onClick={() => {
|
||||||
setEditingMilestone(m);
|
setEditingMilestone(m);
|
||||||
setNewMilestone({ title: m.title, date: m.date, progress: m.progress });
|
setNewMilestone({ title: m.title, date: m.date, progress: m.progress });
|
||||||
setShowForm(true);
|
setShowForm(true);
|
||||||
}}>
|
}}>
|
||||||
<div className="milestone-dot" />
|
<div className="milestone-timeline-dot" />
|
||||||
<div className="milestone-content">
|
<div className="milestone-content">
|
||||||
<div className="title">{m.title}</div>
|
<div className="title">{m.title}</div>
|
||||||
<div className="progress-bar">
|
<div className="progress-bar">
|
||||||
|
@ -7,6 +7,7 @@ import CareerSearch from './CareerSearch.js';
|
|||||||
import MilestoneTimeline from './MilestoneTimeline.js';
|
import MilestoneTimeline from './MilestoneTimeline.js';
|
||||||
import AISuggestedMilestones from './AISuggestedMilestones.js';
|
import AISuggestedMilestones from './AISuggestedMilestones.js';
|
||||||
import './MilestoneTracker.css';
|
import './MilestoneTracker.css';
|
||||||
|
import './MilestoneTimeline.css'; // Ensure this file contains styles for timeline-line and milestone-dot
|
||||||
|
|
||||||
const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -17,6 +18,8 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
const [existingCareerPaths, setExistingCareerPaths] = useState([]);
|
const [existingCareerPaths, setExistingCareerPaths] = useState([]);
|
||||||
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false);
|
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false);
|
||||||
const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
|
const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
|
||||||
|
const [activeView, setActiveView] = useState("Career");
|
||||||
|
|
||||||
|
|
||||||
const apiURL = process.env.REACT_APP_API_URL;
|
const apiURL = process.env.REACT_APP_API_URL;
|
||||||
|
|
||||||
@ -72,11 +75,12 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
fetchCareerPaths();
|
fetchCareerPaths();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCareerChange = (careerName) => {
|
const handleCareerChange = (selected) => {
|
||||||
const match = existingCareerPaths.find(p => p.career_name === careerName);
|
if (selected && selected.id && selected.career_name) {
|
||||||
if (match) {
|
setSelectedCareer(selected);
|
||||||
setSelectedCareer(match);
|
setCareerPathId(selected.id);
|
||||||
setCareerPathId(match.career_path_id);
|
} else {
|
||||||
|
console.warn('Invalid career object received in handleCareerChange:', selected);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -103,17 +107,21 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
|
|
||||||
<CareerSelectDropdown
|
<CareerSelectDropdown
|
||||||
existingCareerPaths={existingCareerPaths}
|
existingCareerPaths={existingCareerPaths}
|
||||||
selectedCareer={selectedCareer?.career_name}
|
selectedCareer={selectedCareer}
|
||||||
onChange={handleCareerChange}
|
onChange={handleCareerChange}
|
||||||
loading={!existingCareerPaths.length}
|
loading={!existingCareerPaths.length}
|
||||||
|
authFetch={authFetch}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MilestoneTimeline careerPathId={careerPathId} authFetch={authFetch} />
|
<MilestoneTimeline careerPathId={careerPathId} authFetch={authFetch} activeView={activeView} setActiveView={setActiveView} />
|
||||||
|
{console.log('Passing careerPathId to MilestoneTimeline:', careerPathId)}
|
||||||
|
|
||||||
<AISuggestedMilestones career={selectedCareer?.career_name} careerPathId={careerPathId} authFetch={authFetch} />
|
<AISuggestedMilestones career={selectedCareer?.career_name} careerPathId={careerPathId} authFetch={authFetch} activeView={activeView}/>
|
||||||
|
|
||||||
<CareerSearch
|
<CareerSearch
|
||||||
onSelectCareer={(careerName) => setPendingCareerForModal(careerName)}
|
onSelectCareer={(careerName) => setPendingCareerForModal(careerName)}
|
||||||
|
setPendingCareerForModal={setPendingCareerForModal}
|
||||||
|
authFetch={authFetch}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{pendingCareerForModal && (
|
{pendingCareerForModal && (
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user