Added tasks.
This commit is contained in:
parent
a04ab21d02
commit
6f01c1c9ae
@ -169,6 +169,12 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
|
|||||||
|
|
||||||
app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => {
|
app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const body = req.body;
|
||||||
|
|
||||||
|
// CASE 1: If client sent { milestones: [ ... ] }, do a bulk insert
|
||||||
|
if (Array.isArray(body.milestones)) {
|
||||||
|
const createdMilestones = [];
|
||||||
|
for (const m of body.milestones) {
|
||||||
const {
|
const {
|
||||||
milestone_type,
|
milestone_type,
|
||||||
title,
|
title,
|
||||||
@ -178,7 +184,79 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
|
|||||||
progress,
|
progress,
|
||||||
status,
|
status,
|
||||||
new_salary
|
new_salary
|
||||||
} = req.body;
|
} = m;
|
||||||
|
|
||||||
|
// Validate some required fields
|
||||||
|
if (!milestone_type || !title || !date || !career_path_id) {
|
||||||
|
// Optionally handle partial errors, but let's do a quick check
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'One or more milestones missing required fields',
|
||||||
|
details: m
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = uuidv4();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
await db.run(`
|
||||||
|
INSERT INTO milestones (
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
career_path_id,
|
||||||
|
milestone_type,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
date,
|
||||||
|
progress,
|
||||||
|
status,
|
||||||
|
new_salary,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`, [
|
||||||
|
id,
|
||||||
|
req.userId,
|
||||||
|
career_path_id,
|
||||||
|
milestone_type,
|
||||||
|
title,
|
||||||
|
description || '',
|
||||||
|
date,
|
||||||
|
progress || 0,
|
||||||
|
status || 'planned',
|
||||||
|
new_salary || null,
|
||||||
|
now,
|
||||||
|
now
|
||||||
|
]);
|
||||||
|
|
||||||
|
createdMilestones.push({
|
||||||
|
id,
|
||||||
|
user_id: req.userId,
|
||||||
|
career_path_id,
|
||||||
|
milestone_type,
|
||||||
|
title,
|
||||||
|
description: description || '',
|
||||||
|
date,
|
||||||
|
progress: progress || 0,
|
||||||
|
status: status || 'planned',
|
||||||
|
new_salary: new_salary || null,
|
||||||
|
tasks: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Return array of created milestones
|
||||||
|
return res.status(201).json(createdMilestones);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CASE 2: Handle single milestone (the old logic)
|
||||||
|
const {
|
||||||
|
milestone_type,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
date,
|
||||||
|
career_path_id,
|
||||||
|
progress,
|
||||||
|
status,
|
||||||
|
new_salary
|
||||||
|
} = body;
|
||||||
|
|
||||||
if (!milestone_type || !title || !date || !career_path_id) {
|
if (!milestone_type || !title || !date || !career_path_id) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@ -201,7 +279,7 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
|
|||||||
date,
|
date,
|
||||||
progress,
|
progress,
|
||||||
status,
|
status,
|
||||||
new_salary, -- store the full new salary if provided
|
new_salary,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
@ -220,8 +298,7 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
|
|||||||
now
|
now
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Return the newly created milestone object
|
// Return the newly created single milestone object
|
||||||
// (No tasks initially, so tasks = [])
|
|
||||||
const newMilestone = {
|
const newMilestone = {
|
||||||
id,
|
id,
|
||||||
user_id: req.userId,
|
user_id: req.userId,
|
||||||
@ -238,8 +315,8 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
|
|||||||
|
|
||||||
res.status(201).json(newMilestone);
|
res.status(201).json(newMilestone);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error creating milestone:', err);
|
console.error('Error creating milestone(s):', err);
|
||||||
res.status(500).json({ error: 'Failed to create milestone.' });
|
res.status(500).json({ error: 'Failed to create milestone(s).' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -675,16 +752,71 @@ app.get('/api/premium/financial-projection/:careerPathId', authenticatePremiumUs
|
|||||||
|
|
||||||
// POST create a new task
|
// POST create a new task
|
||||||
app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => {
|
app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => {
|
||||||
const {
|
try {
|
||||||
milestone_id, // which milestone this belongs to
|
const { milestone_id, title, description, due_date } = req.body;
|
||||||
user_id, // might come from token or from body
|
|
||||||
|
// Ensure required fields
|
||||||
|
if (!milestone_id || !title) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Missing required fields',
|
||||||
|
details: { milestone_id, title }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm milestone is owned by this user
|
||||||
|
const milestone = await db.get(`
|
||||||
|
SELECT user_id
|
||||||
|
FROM milestones
|
||||||
|
WHERE id = ?
|
||||||
|
`, [milestone_id]);
|
||||||
|
|
||||||
|
if (!milestone || milestone.user_id !== req.userId) {
|
||||||
|
return res.status(403).json({ error: 'Milestone not found or not yours.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskId = uuidv4();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
// Insert the new task
|
||||||
|
await db.run(`
|
||||||
|
INSERT INTO tasks (
|
||||||
|
id,
|
||||||
|
milestone_id,
|
||||||
|
user_id,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
due_date
|
due_date,
|
||||||
} = req.body;
|
status,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, 'not_started', ?, ?)
|
||||||
|
`, [
|
||||||
|
taskId,
|
||||||
|
milestone_id,
|
||||||
|
req.userId,
|
||||||
|
title,
|
||||||
|
description || '',
|
||||||
|
due_date || null,
|
||||||
|
now,
|
||||||
|
now
|
||||||
|
]);
|
||||||
|
|
||||||
// Insert into tasks table
|
// Return the newly created task as JSON
|
||||||
// Return the new task in JSON
|
const newTask = {
|
||||||
|
id: taskId,
|
||||||
|
milestone_id,
|
||||||
|
user_id: req.userId,
|
||||||
|
title,
|
||||||
|
description: description || '',
|
||||||
|
due_date: due_date || null,
|
||||||
|
status: 'not_started'
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(201).json(newTask);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error creating task:', err);
|
||||||
|
res.status(500).json({ error: 'Failed to create task.' });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET tasks for a milestone
|
// GET tasks for a milestone
|
||||||
|
@ -9,11 +9,13 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
|||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
date: '',
|
date: '',
|
||||||
progress: 0,
|
progress: '',
|
||||||
newSalary: ''
|
newSalary: ''
|
||||||
});
|
});
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
const [editingMilestone, setEditingMilestone] = useState(null);
|
const [editingMilestone, setEditingMilestone] = useState(null);
|
||||||
|
const [showTaskForm, setShowTaskForm] = useState(null); // store milestoneId or null
|
||||||
|
const [newTask, setNewTask] = useState({ title: '', description: '', due_date: '' });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all milestones (and their tasks) for this careerPathId
|
* Fetch all milestones (and their tasks) for this careerPathId
|
||||||
@ -21,7 +23,6 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
|||||||
*/
|
*/
|
||||||
const fetchMilestones = useCallback(async () => {
|
const fetchMilestones = useCallback(async () => {
|
||||||
if (!careerPathId) return;
|
if (!careerPathId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`);
|
const res = await authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@ -79,7 +80,6 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
|||||||
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',
|
||||||
// Only include new_salary if it's a Financial milestone
|
|
||||||
new_salary: activeView === 'Financial' && newMilestone.newSalary
|
new_salary: activeView === 'Financial' && newMilestone.newSalary
|
||||||
? parseFloat(newMilestone.newSalary)
|
? parseFloat(newMilestone.newSalary)
|
||||||
: null
|
: null
|
||||||
@ -106,7 +106,6 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
|||||||
// Update local state so we don't have to refetch everything
|
// Update local state so we don't have to refetch everything
|
||||||
setMilestones((prev) => {
|
setMilestones((prev) => {
|
||||||
const updated = { ...prev };
|
const updated = { ...prev };
|
||||||
// If editing, replace existing; else push new
|
|
||||||
if (editingMilestone) {
|
if (editingMilestone) {
|
||||||
updated[activeView] = updated[activeView].map((m) =>
|
updated[activeView] = updated[activeView].map((m) =>
|
||||||
m.id === editingMilestone.id ? savedMilestone : m
|
m.id === editingMilestone.id ? savedMilestone : m
|
||||||
@ -132,6 +131,57 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addTask = async (milestoneId) => {
|
||||||
|
try {
|
||||||
|
const taskPayload = {
|
||||||
|
milestone_id: milestoneId,
|
||||||
|
title: newTask.title,
|
||||||
|
description: newTask.description,
|
||||||
|
due_date: newTask.due_date
|
||||||
|
};
|
||||||
|
console.log('Creating new task:', taskPayload);
|
||||||
|
|
||||||
|
const res = await authFetch('/api/premium/tasks', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(taskPayload)
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorData = await res.json();
|
||||||
|
console.error('Failed to create task:', errorData);
|
||||||
|
alert(errorData.error || 'Error creating task');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const createdTask = await res.json();
|
||||||
|
console.log('Task created:', createdTask);
|
||||||
|
|
||||||
|
// Update the milestone's tasks in local state
|
||||||
|
setMilestones((prev) => {
|
||||||
|
// We need to find which classification this milestone belongs to
|
||||||
|
const newState = { ...prev };
|
||||||
|
// Could be Career or Financial
|
||||||
|
['Career', 'Financial'].forEach((category) => {
|
||||||
|
newState[category] = newState[category].map((m) => {
|
||||||
|
if (m.id === milestoneId) {
|
||||||
|
return {
|
||||||
|
...m,
|
||||||
|
tasks: [...m.tasks, createdTask]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset the addTask form
|
||||||
|
setNewTask({ title: '', description: '', due_date: '' });
|
||||||
|
setShowTaskForm(null); // close the task form
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error adding task:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Figure out the timeline's "end" date by scanning all milestones.
|
* Figure out the timeline's "end" date by scanning all milestones.
|
||||||
*/
|
*/
|
||||||
@ -161,7 +211,7 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="milestone-timeline">
|
<div className="milestone-timeline">
|
||||||
{/* View selector buttons */}
|
{/* View selector */}
|
||||||
<div className="view-selector">
|
<div className="view-selector">
|
||||||
{['Career', 'Financial'].map((view) => (
|
{['Career', 'Financial'].map((view) => (
|
||||||
<button
|
<button
|
||||||
@ -180,13 +230,7 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
|||||||
// Cancel form
|
// Cancel form
|
||||||
setShowForm(false);
|
setShowForm(false);
|
||||||
setEditingMilestone(null);
|
setEditingMilestone(null);
|
||||||
setNewMilestone({
|
setNewMilestone({ title: '', description: '', date: '', progress: 0, newSalary: '' });
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
date: '',
|
|
||||||
progress: 0,
|
|
||||||
newSalary: ''
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// Show form
|
// Show form
|
||||||
setShowForm(true);
|
setShowForm(true);
|
||||||
@ -211,20 +255,24 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
|||||||
onChange={(e) => setNewMilestone({ ...newMilestone, description: e.target.value })}
|
onChange={(e) => setNewMilestone({ ...newMilestone, description: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="text"
|
||||||
|
placeholder="mm/dd/yyyy"
|
||||||
value={newMilestone.date}
|
value={newMilestone.date}
|
||||||
onChange={(e) => setNewMilestone({ ...newMilestone, date: e.target.value })}
|
onChange={(e) =>
|
||||||
|
setNewMilestone((prev) => ({
|
||||||
|
...prev,
|
||||||
|
date: e.target.value
|
||||||
|
}))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Progress (%)"
|
placeholder="Progress (%)"
|
||||||
value={newMilestone.progress}
|
value={newMilestone.progress === 0 ? '' : newMilestone.progress}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
setNewMilestone({
|
const val = e.target.value === '' ? 0 : parseInt(e.target.value, 10);
|
||||||
...newMilestone,
|
setNewMilestone((prev) => ({ ...prev, progress: val }));
|
||||||
progress: parseInt(e.target.value, 10)
|
}}
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
{activeView === 'Financial' && (
|
{activeView === 'Financial' && (
|
||||||
<div>
|
<div>
|
||||||
@ -232,9 +280,7 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
|||||||
type="number"
|
type="number"
|
||||||
placeholder="Full New Salary (e.g., 70000)"
|
placeholder="Full New Salary (e.g., 70000)"
|
||||||
value={newMilestone.newSalary}
|
value={newMilestone.newSalary}
|
||||||
onChange={(e) =>
|
onChange={(e) => setNewMilestone({ ...newMilestone, newSalary: e.target.value })}
|
||||||
setNewMilestone({ ...newMilestone, newSalary: e.target.value })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<p>Enter the full new salary (not just the increase) after the milestone occurs.</p>
|
<p>Enter the full new salary (not just the increase) after the milestone occurs.</p>
|
||||||
</div>
|
</div>
|
||||||
@ -255,6 +301,9 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
|||||||
key={m.id}
|
key={m.id}
|
||||||
className="milestone-timeline-post"
|
className="milestone-timeline-post"
|
||||||
style={{ left: `${leftPos}%` }}
|
style={{ left: `${leftPos}%` }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="milestone-timeline-dot"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Clicking a milestone => edit it
|
// Clicking a milestone => edit it
|
||||||
setEditingMilestone(m);
|
setEditingMilestone(m);
|
||||||
@ -267,8 +316,7 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
|||||||
});
|
});
|
||||||
setShowForm(true);
|
setShowForm(true);
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<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>
|
||||||
{m.description && <p>{m.description}</p>}
|
{m.description && <p>{m.description}</p>}
|
||||||
@ -277,14 +325,50 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
|||||||
</div>
|
</div>
|
||||||
<div className="date">{m.date}</div>
|
<div className="date">{m.date}</div>
|
||||||
|
|
||||||
{/* If the milestone has tasks */}
|
{/* Existing tasks */}
|
||||||
{m.tasks && m.tasks.length > 0 && (
|
{m.tasks && m.tasks.length > 0 && (
|
||||||
<ul>
|
<ul>
|
||||||
{m.tasks.map((t) => (
|
{m.tasks.map((t) => (
|
||||||
<li key={t.id}>{t.title}</li>
|
<li key={t.id}>
|
||||||
|
<strong>{t.title}</strong>
|
||||||
|
{t.description ? ` - ${t.description}` : ''}
|
||||||
|
{t.due_date ? ` (Due: ${t.due_date})` : ''}
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Button to show/hide Add Task form */}
|
||||||
|
<button onClick={() => {
|
||||||
|
setShowTaskForm(showTaskForm === m.id ? null : m.id);
|
||||||
|
setNewTask({ title: '', description: '', due_date: '' });
|
||||||
|
}}>
|
||||||
|
{showTaskForm === m.id ? 'Cancel Task' : 'Add Task'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Conditionally render the Add Task form for this milestone */}
|
||||||
|
{showTaskForm === m.id && (
|
||||||
|
<div className="task-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Task Title"
|
||||||
|
value={newTask.title}
|
||||||
|
onChange={(e) => setNewTask({ ...newTask, title: e.target.value })}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Task Description"
|
||||||
|
value={newTask.description}
|
||||||
|
onChange={(e) => setNewTask({ ...newTask, description: e.target.value })}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={newTask.due_date}
|
||||||
|
onChange={(e) => setNewTask({ ...newTask, due_date: e.target.value })}
|
||||||
|
/>
|
||||||
|
<button onClick={() => addTask(m.id)}>Save Task</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user