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
|
||||
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,
|
||||
|
@ -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 (
|
||||
<div className="suggested-milestones">
|
||||
<h4>AI-Suggested Milestones</h4>
|
||||
<ul>{suggestedMilestones.map((m, i) => <li key={i}>{m.title} - {m.date}</li>)}</ul>
|
||||
<button onClick={confirmMilestones}>Confirm Milestones</button>
|
||||
<ul>
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
@ -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 (
|
||||
<div className="career-select-dropdown">
|
||||
<label>Select Career Path:</label>
|
||||
@ -9,20 +31,28 @@ const CareerSelectDropdown = ({ existingCareerPaths, selectedCareer, onChange, l
|
||||
<p>Loading career paths...</p>
|
||||
) : (
|
||||
<select
|
||||
value={selectedCareer || ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
>
|
||||
value={selectedCareer?.id || ''}
|
||||
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>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
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 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 (
|
||||
<div className="milestone-timeline">
|
||||
<p>Loading milestones...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="milestone-timeline">
|
||||
<div className="view-selector">
|
||||
{['Career', 'Financial', 'Retirement'].map((view) => (
|
||||
<button key={view} className={activeView === view ? 'active' : ''} onClick={() => setActiveView(view)}>
|
||||
{view}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
key={view}
|
||||
className={activeView === view ? 'active' : ''}
|
||||
onClick={() => setActiveView(view)}
|
||||
>
|
||||
{view}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="timeline">
|
||||
{milestones[activeView]?.map((m) => (
|
||||
<div key={m.id} className="milestone-entry">
|
||||
<h4>{m.title}</h4>
|
||||
<p>{m.description}</p>
|
||||
<p>Date: {m.date}</p>
|
||||
<p>Progress: {m.progress}%</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button onClick={() => setShowForm(true)}>+ New Milestone</button>
|
||||
<button onClick={() => {
|
||||
if (showForm) {
|
||||
setShowForm(false);
|
||||
setEditingMilestone(null);
|
||||
setNewMilestone({ title: '', date: '', progress: 0 });
|
||||
} else {
|
||||
setShowForm(true);
|
||||
}
|
||||
}}>
|
||||
{showForm ? 'Cancel' : '+ New Milestone'}
|
||||
</button>
|
||||
|
||||
{showForm && (
|
||||
<div className="form">
|
||||
<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="number" placeholder="Progress (%)" value={newMilestone.progress} onChange={(e) => setNewMilestone({ ...newMilestone, progress: parseInt(e.target.value, 10) })} />
|
||||
<button onClick={saveMilestone}>{editingMilestone ? 'Update' : 'Add'} Milestone</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="timeline-container">
|
||||
<div className="timeline-line" />
|
||||
<div className="milestone-timeline-container">
|
||||
<div className="milestone-timeline-line" />
|
||||
{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);
|
||||
setNewMilestone({ title: m.title, date: m.date, progress: m.progress });
|
||||
setShowForm(true);
|
||||
}}>
|
||||
<div className="milestone-dot" />
|
||||
<div className="milestone-timeline-dot" />
|
||||
<div className="milestone-content">
|
||||
<div className="title">{m.title}</div>
|
||||
<div className="progress-bar">
|
||||
|
@ -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 }) => {
|
||||
|
||||
<CareerSelectDropdown
|
||||
existingCareerPaths={existingCareerPaths}
|
||||
selectedCareer={selectedCareer?.career_name}
|
||||
selectedCareer={selectedCareer}
|
||||
onChange={handleCareerChange}
|
||||
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
|
||||
onSelectCareer={(careerName) => setPendingCareerForModal(careerName)}
|
||||
setPendingCareerForModal={setPendingCareerForModal}
|
||||
authFetch={authFetch}
|
||||
/>
|
||||
|
||||
{pendingCareerForModal && (
|
||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user