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) => {
|
||||
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 {
|
||||
milestone_type,
|
||||
title,
|
||||
@ -178,7 +184,79 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
|
||||
progress,
|
||||
status,
|
||||
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) {
|
||||
return res.status(400).json({
|
||||
@ -201,7 +279,7 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
|
||||
date,
|
||||
progress,
|
||||
status,
|
||||
new_salary, -- store the full new salary if provided
|
||||
new_salary,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
@ -220,8 +298,7 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
|
||||
now
|
||||
]);
|
||||
|
||||
// Return the newly created milestone object
|
||||
// (No tasks initially, so tasks = [])
|
||||
// Return the newly created single milestone object
|
||||
const newMilestone = {
|
||||
id,
|
||||
user_id: req.userId,
|
||||
@ -238,8 +315,8 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
|
||||
|
||||
res.status(201).json(newMilestone);
|
||||
} catch (err) {
|
||||
console.error('Error creating milestone:', err);
|
||||
res.status(500).json({ error: 'Failed to create milestone.' });
|
||||
console.error('Error creating milestone(s):', err);
|
||||
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
|
||||
app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => {
|
||||
const {
|
||||
milestone_id, // which milestone this belongs to
|
||||
user_id, // might come from token or from body
|
||||
try {
|
||||
const { milestone_id, title, description, due_date } = req.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,
|
||||
description,
|
||||
due_date
|
||||
} = req.body;
|
||||
due_date,
|
||||
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 new task in JSON
|
||||
// Return the newly created task as 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
|
||||
|
@ -9,11 +9,13 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
||||
title: '',
|
||||
description: '',
|
||||
date: '',
|
||||
progress: 0,
|
||||
progress: '',
|
||||
newSalary: ''
|
||||
});
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
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
|
||||
@ -21,7 +23,6 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
||||
*/
|
||||
const fetchMilestones = useCallback(async () => {
|
||||
if (!careerPathId) return;
|
||||
|
||||
try {
|
||||
const res = await authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`);
|
||||
if (!res.ok) {
|
||||
@ -79,7 +80,6 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
||||
career_path_id: careerPathId,
|
||||
progress: newMilestone.progress,
|
||||
status: newMilestone.progress >= 100 ? 'completed' : 'planned',
|
||||
// Only include new_salary if it's a Financial milestone
|
||||
new_salary: activeView === 'Financial' && newMilestone.newSalary
|
||||
? parseFloat(newMilestone.newSalary)
|
||||
: null
|
||||
@ -106,7 +106,6 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
||||
// Update local state so we don't have to refetch everything
|
||||
setMilestones((prev) => {
|
||||
const updated = { ...prev };
|
||||
// If editing, replace existing; else push new
|
||||
if (editingMilestone) {
|
||||
updated[activeView] = updated[activeView].map((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.
|
||||
*/
|
||||
@ -161,7 +211,7 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
||||
|
||||
return (
|
||||
<div className="milestone-timeline">
|
||||
{/* View selector buttons */}
|
||||
{/* View selector */}
|
||||
<div className="view-selector">
|
||||
{['Career', 'Financial'].map((view) => (
|
||||
<button
|
||||
@ -180,13 +230,7 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
||||
// Cancel form
|
||||
setShowForm(false);
|
||||
setEditingMilestone(null);
|
||||
setNewMilestone({
|
||||
title: '',
|
||||
description: '',
|
||||
date: '',
|
||||
progress: 0,
|
||||
newSalary: ''
|
||||
});
|
||||
setNewMilestone({ title: '', description: '', date: '', progress: 0, newSalary: '' });
|
||||
} else {
|
||||
// Show form
|
||||
setShowForm(true);
|
||||
@ -211,20 +255,24 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
||||
onChange={(e) => setNewMilestone({ ...newMilestone, description: e.target.value })}
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
type="text"
|
||||
placeholder="mm/dd/yyyy"
|
||||
value={newMilestone.date}
|
||||
onChange={(e) => setNewMilestone({ ...newMilestone, date: e.target.value })}
|
||||
onChange={(e) =>
|
||||
setNewMilestone((prev) => ({
|
||||
...prev,
|
||||
date: e.target.value
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Progress (%)"
|
||||
value={newMilestone.progress}
|
||||
onChange={(e) =>
|
||||
setNewMilestone({
|
||||
...newMilestone,
|
||||
progress: parseInt(e.target.value, 10)
|
||||
})
|
||||
}
|
||||
value={newMilestone.progress === 0 ? '' : newMilestone.progress}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value === '' ? 0 : parseInt(e.target.value, 10);
|
||||
setNewMilestone((prev) => ({ ...prev, progress: val }));
|
||||
}}
|
||||
/>
|
||||
{activeView === 'Financial' && (
|
||||
<div>
|
||||
@ -232,9 +280,7 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
||||
type="number"
|
||||
placeholder="Full New Salary (e.g., 70000)"
|
||||
value={newMilestone.newSalary}
|
||||
onChange={(e) =>
|
||||
setNewMilestone({ ...newMilestone, newSalary: e.target.value })
|
||||
}
|
||||
onChange={(e) => setNewMilestone({ ...newMilestone, newSalary: e.target.value })}
|
||||
/>
|
||||
<p>Enter the full new salary (not just the increase) after the milestone occurs.</p>
|
||||
</div>
|
||||
@ -255,6 +301,9 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
||||
key={m.id}
|
||||
className="milestone-timeline-post"
|
||||
style={{ left: `${leftPos}%` }}
|
||||
>
|
||||
<div
|
||||
className="milestone-timeline-dot"
|
||||
onClick={() => {
|
||||
// Clicking a milestone => edit it
|
||||
setEditingMilestone(m);
|
||||
@ -267,8 +316,7 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
||||
});
|
||||
setShowForm(true);
|
||||
}}
|
||||
>
|
||||
<div className="milestone-timeline-dot" />
|
||||
/>
|
||||
<div className="milestone-content">
|
||||
<div className="title">{m.title}</div>
|
||||
{m.description && <p>{m.description}</p>}
|
||||
@ -277,14 +325,50 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
||||
</div>
|
||||
<div className="date">{m.date}</div>
|
||||
|
||||
{/* If the milestone has tasks */}
|
||||
{/* Existing tasks */}
|
||||
{m.tasks && m.tasks.length > 0 && (
|
||||
<ul>
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user