Added tasks concept, but no UI. Adjusted server3 for new milestones endpoints.
This commit is contained in:
parent
b98f93d442
commit
a04ab21d02
@ -167,15 +167,203 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
|
||||||
MILESTONES (same as before)
|
|
||||||
------------------------------------------------------------------ */
|
|
||||||
app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => {
|
app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => {
|
||||||
// ... no changes, same logic ...
|
try {
|
||||||
|
const {
|
||||||
|
milestone_type,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
date,
|
||||||
|
career_path_id,
|
||||||
|
progress,
|
||||||
|
status,
|
||||||
|
new_salary
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!milestone_type || !title || !date || !career_path_id) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Missing required fields',
|
||||||
|
details: { milestone_type, title, date, career_path_id }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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, -- store the full new salary if provided
|
||||||
|
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
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Return the newly created milestone object
|
||||||
|
// (No tasks initially, so tasks = [])
|
||||||
|
const newMilestone = {
|
||||||
|
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: []
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(201).json(newMilestone);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error creating milestone:', err);
|
||||||
|
res.status(500).json({ error: 'Failed to create milestone.' });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET, PUT, DELETE milestones
|
app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (req, res) => {
|
||||||
// ... no changes ...
|
try {
|
||||||
|
const { milestoneId } = req.params;
|
||||||
|
const {
|
||||||
|
milestone_type,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
date,
|
||||||
|
career_path_id,
|
||||||
|
progress,
|
||||||
|
status,
|
||||||
|
new_salary
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// Check if milestone exists and belongs to user
|
||||||
|
const existing = await db.get(`
|
||||||
|
SELECT *
|
||||||
|
FROM milestones
|
||||||
|
WHERE id = ?
|
||||||
|
AND user_id = ?
|
||||||
|
`, [milestoneId, req.userId]);
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
return res.status(404).json({ error: 'Milestone not found or not yours.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
await db.run(`
|
||||||
|
UPDATE milestones
|
||||||
|
SET
|
||||||
|
milestone_type = ?,
|
||||||
|
title = ?,
|
||||||
|
description = ?,
|
||||||
|
date = ?,
|
||||||
|
career_path_id = ?,
|
||||||
|
progress = ?,
|
||||||
|
status = ?,
|
||||||
|
new_salary = ?,
|
||||||
|
updated_at = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`, [
|
||||||
|
milestone_type || existing.milestone_type,
|
||||||
|
title || existing.title,
|
||||||
|
description || existing.description,
|
||||||
|
date || existing.date,
|
||||||
|
career_path_id || existing.career_path_id,
|
||||||
|
progress != null ? progress : existing.progress,
|
||||||
|
status || existing.status,
|
||||||
|
new_salary != null ? new_salary : existing.new_salary,
|
||||||
|
now,
|
||||||
|
milestoneId
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Return the updated record with tasks
|
||||||
|
const updatedMilestoneRow = await db.get(`
|
||||||
|
SELECT *
|
||||||
|
FROM milestones
|
||||||
|
WHERE id = ?
|
||||||
|
`, [milestoneId]);
|
||||||
|
|
||||||
|
// Fetch tasks for this milestone
|
||||||
|
const tasks = await db.all(`
|
||||||
|
SELECT *
|
||||||
|
FROM tasks
|
||||||
|
WHERE milestone_id = ?
|
||||||
|
`, [milestoneId]);
|
||||||
|
|
||||||
|
const updatedMilestone = {
|
||||||
|
...updatedMilestoneRow,
|
||||||
|
tasks: tasks || []
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json(updatedMilestone);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error updating milestone:', err);
|
||||||
|
res.status(500).json({ error: 'Failed to update milestone.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => {
|
||||||
|
const { careerPathId } = req.query;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Fetch the milestones for this user + path
|
||||||
|
const milestones = await db.all(`
|
||||||
|
SELECT *
|
||||||
|
FROM milestones
|
||||||
|
WHERE user_id = ?
|
||||||
|
AND career_path_id = ?
|
||||||
|
`, [req.userId, careerPathId]);
|
||||||
|
|
||||||
|
// 2. For each milestone, fetch tasks
|
||||||
|
const milestoneIds = milestones.map(m => m.id);
|
||||||
|
let tasksByMilestone = {};
|
||||||
|
if (milestoneIds.length > 0) {
|
||||||
|
const tasks = await db.all(`
|
||||||
|
SELECT *
|
||||||
|
FROM tasks
|
||||||
|
WHERE milestone_id IN (${milestoneIds.map(() => '?').join(',')})
|
||||||
|
`, milestoneIds);
|
||||||
|
|
||||||
|
tasksByMilestone = tasks.reduce((acc, t) => {
|
||||||
|
if (!acc[t.milestone_id]) acc[t.milestone_id] = [];
|
||||||
|
acc[t.milestone_id].push(t);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Attach tasks to each milestone object
|
||||||
|
const milestonesWithTasks = milestones.map(m => ({
|
||||||
|
...m,
|
||||||
|
tasks: tasksByMilestone[m.id] || []
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({ milestones: milestonesWithTasks });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching milestones with tasks:', err);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch milestones.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
FINANCIAL PROFILES (Renamed emergency_contribution)
|
FINANCIAL PROFILES (Renamed emergency_contribution)
|
||||||
@ -484,36 +672,67 @@ app.get('/api/premium/financial-projection/:careerPathId', authenticatePremiumUs
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
|
||||||
ROI ANALYSIS (placeholder)
|
|
||||||
------------------------------------------------------------------ */
|
|
||||||
app.get('/api/premium/roi-analysis', authenticatePremiumUser, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const userCareer = await db.get(`
|
|
||||||
SELECT * FROM career_paths
|
|
||||||
WHERE user_id = ?
|
|
||||||
ORDER BY start_date DESC
|
|
||||||
LIMIT 1
|
|
||||||
`, [req.userId]);
|
|
||||||
|
|
||||||
if (!userCareer) {
|
// POST create a new task
|
||||||
return res.status(404).json({ error: 'No planned path found for user' });
|
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
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
due_date
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// Insert into tasks table
|
||||||
|
// Return the new task in JSON
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET tasks for a milestone
|
||||||
|
app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => {
|
||||||
|
const { careerPathId } = req.query;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Fetch the milestones for this user + path
|
||||||
|
const milestones = await db.all(`
|
||||||
|
SELECT *
|
||||||
|
FROM milestones
|
||||||
|
WHERE user_id = ?
|
||||||
|
AND career_path_id = ?
|
||||||
|
`, [req.userId, careerPathId]);
|
||||||
|
|
||||||
|
// 2. For each milestone, fetch tasks (or do a single join—see note below)
|
||||||
|
// We'll do it in Node code for clarity:
|
||||||
|
const milestoneIds = milestones.map(m => m.id);
|
||||||
|
let tasksByMilestone = {};
|
||||||
|
if (milestoneIds.length > 0) {
|
||||||
|
const tasks = await db.all(`
|
||||||
|
SELECT *
|
||||||
|
FROM tasks
|
||||||
|
WHERE milestone_id IN (${milestoneIds.map(() => '?').join(',')})
|
||||||
|
`, milestoneIds);
|
||||||
|
|
||||||
|
// Group tasks by milestone_id
|
||||||
|
tasksByMilestone = tasks.reduce((acc, t) => {
|
||||||
|
if (!acc[t.milestone_id]) acc[t.milestone_id] = [];
|
||||||
|
acc[t.milestone_id].push(t);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
const roi = {
|
// 3. Attach tasks to each milestone object
|
||||||
jobTitle: userCareer.career_name,
|
const milestonesWithTasks = milestones.map(m => ({
|
||||||
salary: 80000,
|
...m,
|
||||||
tuition: 50000,
|
tasks: tasksByMilestone[m.id] || []
|
||||||
netGain: 80000 - 50000
|
}));
|
||||||
};
|
|
||||||
|
|
||||||
res.json(roi);
|
res.json({ milestones: milestonesWithTasks });
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error('Error calculating ROI:', error);
|
console.error('Error fetching milestones with tasks:', err);
|
||||||
res.status(500).json({ error: 'Failed to calculate ROI' });
|
res.status(500).json({ error: 'Failed to fetch milestones.' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Premium server running on http://localhost:${PORT}`);
|
console.log(`Premium server running on http://localhost:${PORT}`);
|
||||||
});
|
});
|
||||||
|
@ -4,65 +4,73 @@ import React, { useEffect, useState, useCallback } from 'react';
|
|||||||
const today = new Date();
|
const today = new Date();
|
||||||
|
|
||||||
const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView }) => {
|
const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView }) => {
|
||||||
|
const [milestones, setMilestones] = useState({ Career: [], Financial: [] });
|
||||||
const [milestones, setMilestones] = useState({ Career: [], Financial: [], Retirement: [] });
|
const [newMilestone, setNewMilestone] = useState({
|
||||||
const [newMilestone, setNewMilestone] = useState({ title: '', date: '', description: '', progress: 0 });
|
title: '',
|
||||||
|
description: '',
|
||||||
|
date: '',
|
||||||
|
progress: 0,
|
||||||
|
newSalary: ''
|
||||||
|
});
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
const [editingMilestone, setEditingMilestone] = useState(null);
|
const [editingMilestone, setEditingMilestone] = useState(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all milestones (and their tasks) for this careerPathId
|
||||||
|
* Then categorize them by milestone_type: 'Career' or 'Financial'.
|
||||||
|
*/
|
||||||
const fetchMilestones = useCallback(async () => {
|
const fetchMilestones = useCallback(async () => {
|
||||||
if (!careerPathId) {
|
if (!careerPathId) return;
|
||||||
console.warn('No careerPathId provided.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
try {
|
||||||
const res = await authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`);
|
const res = await authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`);
|
||||||
if (!res) {
|
if (!res.ok) {
|
||||||
console.error('Failed to fetch milestones.');
|
console.error('Failed to fetch milestones. Status:', res.status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.milestones) {
|
||||||
|
console.warn('No milestones field in response:', data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
// data.milestones = [ { id, milestone_type, date, ..., tasks: [ ... ] }, ... ]
|
||||||
|
console.log('Fetched milestones with tasks:', data.milestones);
|
||||||
|
|
||||||
const raw = Array.isArray(data.milestones[0])
|
// Categorize by milestone_type
|
||||||
? data.milestones.flat()
|
const categorized = { Career: [], Financial: [] };
|
||||||
: data.milestones.milestones || data.milestones;
|
data.milestones.forEach((m) => {
|
||||||
|
if (categorized[m.milestone_type]) {
|
||||||
const flatMilestones = Array.isArray(data.milestones[0])
|
categorized[m.milestone_type].push(m);
|
||||||
? data.milestones.flat()
|
|
||||||
: data.milestones;
|
|
||||||
|
|
||||||
const filteredMilestones = raw.filter(
|
|
||||||
(m) => m.career_path_id === careerPathId
|
|
||||||
);
|
|
||||||
|
|
||||||
const categorized = { Career: [], Financial: [], Retirement: [] };
|
|
||||||
|
|
||||||
filteredMilestones.forEach((m) => {
|
|
||||||
const type = m.milestone_type;
|
|
||||||
if (categorized[type]) {
|
|
||||||
categorized[type].push(m);
|
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Unknown milestone type: ${type}`);
|
console.warn(`Unknown milestone type: ${m.milestone_type}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
setMilestones(categorized);
|
setMilestones(categorized);
|
||||||
console.log('Milestones set for view:', categorized);
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch milestones:', err);
|
||||||
|
}
|
||||||
}, [careerPathId, authFetch]);
|
}, [careerPathId, authFetch]);
|
||||||
|
|
||||||
// ✅ useEffect simply calls the function
|
// Run fetchMilestones on mount or when careerPathId changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMilestones();
|
fetchMilestones();
|
||||||
}, [fetchMilestones]);
|
}, [fetchMilestones]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or update a milestone.
|
||||||
|
* If editingMilestone is set, we do PUT -> /api/premium/milestones/:id
|
||||||
|
* Else we do POST -> /api/premium/milestone
|
||||||
|
*/
|
||||||
const saveMilestone = async () => {
|
const saveMilestone = async () => {
|
||||||
|
if (!activeView) return;
|
||||||
|
|
||||||
const url = editingMilestone
|
const url = editingMilestone
|
||||||
? `/api/premium/milestones/${editingMilestone.id}`
|
? `/api/premium/milestones/${editingMilestone.id}`
|
||||||
: `/api/premium/milestone`;
|
: `/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,
|
||||||
@ -70,87 +78,92 @@ const filteredMilestones = raw.filter(
|
|||||||
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',
|
||||||
|
// Only include new_salary if it's a Financial milestone
|
||||||
|
new_salary: activeView === 'Financial' && newMilestone.newSalary
|
||||||
|
? parseFloat(newMilestone.newSalary)
|
||||||
|
: null
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Sending request to:', url);
|
console.log('Sending request:', method, url, payload);
|
||||||
console.log('HTTP Method:', method);
|
|
||||||
console.log('Payload:', payload);
|
|
||||||
|
|
||||||
const res = await authFetch(url, {
|
const res = await authFetch(url, {
|
||||||
method,
|
method,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errorData = await res.json();
|
const errorData = await res.json();
|
||||||
console.error('Failed to save milestone:', errorData);
|
console.error('Failed to save milestone:', errorData);
|
||||||
|
alert(errorData.error || 'Error saving milestone');
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedMilestone = await res.json();
|
const savedMilestone = await res.json();
|
||||||
|
console.log('Milestone saved/updated:', savedMilestone);
|
||||||
|
|
||||||
// Update state locally instead of fetching all milestones
|
// Update local state so we don't have to refetch everything
|
||||||
setMilestones((prevMilestones) => {
|
setMilestones((prev) => {
|
||||||
const updatedMilestones = { ...prevMilestones };
|
const updated = { ...prev };
|
||||||
|
// If editing, replace existing; else push new
|
||||||
if (editingMilestone) {
|
if (editingMilestone) {
|
||||||
// Update the existing milestone
|
updated[activeView] = updated[activeView].map((m) =>
|
||||||
updatedMilestones[activeView] = updatedMilestones[activeView].map((m) =>
|
|
||||||
m.id === editingMilestone.id ? savedMilestone : m
|
m.id === editingMilestone.id ? savedMilestone : m
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Add the new milestone
|
updated[activeView].push(savedMilestone);
|
||||||
updatedMilestones[activeView].push(savedMilestone);
|
|
||||||
}
|
}
|
||||||
return updatedMilestones;
|
return updated;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reset form
|
||||||
setShowForm(false);
|
setShowForm(false);
|
||||||
setEditingMilestone(null);
|
setEditingMilestone(null);
|
||||||
setNewMilestone({ title: '', description: '', date: '', progress: 0 });
|
setNewMilestone({
|
||||||
} catch (error) {
|
title: '',
|
||||||
console.error('Error saving milestone:', error);
|
description: '',
|
||||||
|
date: '',
|
||||||
|
progress: 0,
|
||||||
|
newSalary: ''
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error saving milestone:', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate last milestone date properly by combining all arrays
|
/**
|
||||||
const allMilestones = [...milestones.Career, ...milestones.Financial, ...milestones.Retirement];
|
* Figure out the timeline's "end" date by scanning all milestones.
|
||||||
const lastDate = allMilestones.reduce(
|
*/
|
||||||
(latest, m) => (new Date(m.date) > latest ? new Date(m.date) : latest),
|
const allMilestonesCombined = [...milestones.Career, ...milestones.Financial];
|
||||||
today
|
const lastDate = allMilestonesCombined.reduce((latest, m) => {
|
||||||
);
|
const d = new Date(m.date);
|
||||||
|
return d > latest ? d : latest;
|
||||||
|
}, today);
|
||||||
|
|
||||||
const calcPosition = (date) => {
|
const calcPosition = (dateString) => {
|
||||||
const start = today.getTime();
|
const start = today.getTime();
|
||||||
const end = lastDate.getTime();
|
const end = lastDate.getTime();
|
||||||
const position = ((new Date(date).getTime() - start) / (end - start)) * 100;
|
const dateVal = new Date(dateString).getTime();
|
||||||
return Math.min(Math.max(position, 0), 100);
|
if (end === start) return 0; // edge case if only one date
|
||||||
|
const ratio = (dateVal - start) / (end - start);
|
||||||
|
return Math.min(Math.max(ratio * 100, 0), 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Rendering view:', activeView, milestones?.[activeView]);
|
// If activeView not set or the array is missing, show a loading or empty state
|
||||||
|
if (!activeView || !milestones[activeView]) {
|
||||||
if (!activeView || !milestones?.[activeView]) {
|
|
||||||
return (
|
return (
|
||||||
<div className="milestone-timeline">
|
<div className="milestone-timeline">
|
||||||
<p>Loading milestones...</p>
|
<p>Loading or no milestones in this view...</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="milestone-timeline">
|
<div className="milestone-timeline">
|
||||||
|
{/* View selector buttons */}
|
||||||
<div className="view-selector">
|
<div className="view-selector">
|
||||||
{['Career', 'Financial', 'Retirement'].map((view) => (
|
{['Career', 'Financial'].map((view) => (
|
||||||
<button
|
<button
|
||||||
key={view}
|
key={view}
|
||||||
className={activeView === view ? 'active' : ''}
|
className={activeView === view ? 'active' : ''}
|
||||||
@ -158,60 +171,124 @@ const filteredMilestones = raw.filter(
|
|||||||
>
|
>
|
||||||
{view}
|
{view}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button onClick={() => {
|
<button
|
||||||
|
onClick={() => {
|
||||||
if (showForm) {
|
if (showForm) {
|
||||||
|
// Cancel form
|
||||||
setShowForm(false);
|
setShowForm(false);
|
||||||
setEditingMilestone(null);
|
setEditingMilestone(null);
|
||||||
setNewMilestone({ title: '', date: '', progress: 0 });
|
setNewMilestone({
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
date: '',
|
||||||
|
progress: 0,
|
||||||
|
newSalary: ''
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
|
// Show form
|
||||||
setShowForm(true);
|
setShowForm(true);
|
||||||
}
|
}
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{showForm ? 'Cancel' : '+ New Milestone'}
|
{showForm ? 'Cancel' : '+ New Milestone'}
|
||||||
</button>
|
</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
|
||||||
<input type="text" placeholder="Description" value={newMilestone.description} onChange={(e) => setNewMilestone({ ...newMilestone, description: e.target.value })} />
|
type="text"
|
||||||
<input type="date" value={newMilestone.date} onChange={(e) => setNewMilestone({ ...newMilestone, date: e.target.value })} />
|
placeholder="Title"
|
||||||
<input type="number" placeholder="Progress (%)" value={newMilestone.progress} onChange={(e) => setNewMilestone({ ...newMilestone, progress: parseInt(e.target.value, 10) })} />
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
{activeView === 'Financial' && (
|
{activeView === 'Financial' && (
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
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) => setNewMilestone({ ...newMilestone, newSalary: parseFloat(e.target.value) })}
|
onChange={(e) =>
|
||||||
|
setNewMilestone({ ...newMilestone, newSalary: e.target.value })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<p>Enter the full new salary (not just the change) after the milestone has taken place.</p>
|
<p>Enter the full new salary (not just the increase) after the milestone occurs.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button onClick={saveMilestone}>{editingMilestone ? 'Update' : 'Add'} Milestone</button>
|
<button onClick={saveMilestone}>
|
||||||
|
{editingMilestone ? 'Update' : 'Add'} Milestone
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Timeline rendering */}
|
||||||
<div className="milestone-timeline-container">
|
<div className="milestone-timeline-container">
|
||||||
<div className="milestone-timeline-line" />
|
<div className="milestone-timeline-line" />
|
||||||
{milestones[activeView]?.map((m) => (
|
{milestones[activeView].map((m) => {
|
||||||
<div key={m.id} className="milestone-timeline-post" style={{ left: `${calcPosition(m.date)}%` }} onClick={() => {
|
const leftPos = calcPosition(m.date);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={m.id}
|
||||||
|
className="milestone-timeline-post"
|
||||||
|
style={{ left: `${leftPos}%` }}
|
||||||
|
onClick={() => {
|
||||||
|
// Clicking a milestone => edit it
|
||||||
setEditingMilestone(m);
|
setEditingMilestone(m);
|
||||||
setNewMilestone({ title: m.title, date: m.date, progress: m.progress });
|
setNewMilestone({
|
||||||
|
title: m.title,
|
||||||
|
description: m.description,
|
||||||
|
date: m.date,
|
||||||
|
progress: m.progress,
|
||||||
|
newSalary: m.new_salary || ''
|
||||||
|
});
|
||||||
setShowForm(true);
|
setShowForm(true);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<div className="milestone-timeline-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>
|
||||||
|
{m.description && <p>{m.description}</p>}
|
||||||
<div className="progress-bar">
|
<div className="progress-bar">
|
||||||
<div className="progress" style={{ width: `${m.progress}%` }} />
|
<div className="progress" style={{ width: `${m.progress}%` }} />
|
||||||
</div>
|
</div>
|
||||||
<div className="date">{m.date}</div>
|
<div className="date">{m.date}</div>
|
||||||
</div>
|
|
||||||
</div>
|
{/* If the milestone has tasks */}
|
||||||
|
{m.tasks && m.tasks.length > 0 && (
|
||||||
|
<ul>
|
||||||
|
{m.tasks.map((t) => (
|
||||||
|
<li key={t.id}>{t.title}</li>
|
||||||
))}
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -26,8 +26,8 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
inCollege = false,
|
inCollege = false,
|
||||||
programType,
|
programType,
|
||||||
hoursCompleted = 0,
|
hoursCompleted = 0,
|
||||||
creditHoursPerYear = 30,
|
creditHoursPerYear,
|
||||||
calculatedTuition = 10000, // e.g. annual tuition
|
calculatedTuition, // e.g. annual tuition
|
||||||
gradDate, // known graduation date, or null
|
gradDate, // known graduation date, or null
|
||||||
startDate, // when sim starts
|
startDate, // when sim starts
|
||||||
academicCalendar = 'monthly', // new
|
academicCalendar = 'monthly', // new
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user