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) => {
|
||||
// ... 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
|
||||
// ... no changes ...
|
||||
app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (req, res) => {
|
||||
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)
|
||||
@ -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) {
|
||||
return res.status(404).json({ error: 'No planned path found for user' });
|
||||
// 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
|
||||
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 = {
|
||||
jobTitle: userCareer.career_name,
|
||||
salary: 80000,
|
||||
tuition: 50000,
|
||||
netGain: 80000 - 50000
|
||||
};
|
||||
// 3. Attach tasks to each milestone object
|
||||
const milestonesWithTasks = milestones.map(m => ({
|
||||
...m,
|
||||
tasks: tasksByMilestone[m.id] || []
|
||||
}));
|
||||
|
||||
res.json(roi);
|
||||
} catch (error) {
|
||||
console.error('Error calculating ROI:', error);
|
||||
res.status(500).json({ error: 'Failed to calculate ROI' });
|
||||
res.json({ milestones: milestonesWithTasks });
|
||||
} catch (err) {
|
||||
console.error('Error fetching milestones with tasks:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch milestones.' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
app.listen(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 MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView }) => {
|
||||
|
||||
const [milestones, setMilestones] = useState({ Career: [], Financial: [], Retirement: [] });
|
||||
const [newMilestone, setNewMilestone] = useState({ title: '', date: '', description: '', progress: 0 });
|
||||
const [milestones, setMilestones] = useState({ Career: [], Financial: [] });
|
||||
const [newMilestone, setNewMilestone] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
date: '',
|
||||
progress: 0,
|
||||
newSalary: ''
|
||||
});
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
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 () => {
|
||||
if (!careerPathId) {
|
||||
console.warn('No careerPathId provided.');
|
||||
return;
|
||||
if (!careerPathId) return;
|
||||
|
||||
try {
|
||||
const res = await authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`);
|
||||
if (!res.ok) {
|
||||
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;
|
||||
}
|
||||
|
||||
// data.milestones = [ { id, milestone_type, date, ..., tasks: [ ... ] }, ... ]
|
||||
console.log('Fetched milestones with tasks:', data.milestones);
|
||||
|
||||
// Categorize by milestone_type
|
||||
const categorized = { Career: [], Financial: [] };
|
||||
data.milestones.forEach((m) => {
|
||||
if (categorized[m.milestone_type]) {
|
||||
categorized[m.milestone_type].push(m);
|
||||
} else {
|
||||
console.warn(`Unknown milestone type: ${m.milestone_type}`);
|
||||
}
|
||||
});
|
||||
|
||||
setMilestones(categorized);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch milestones:', err);
|
||||
}
|
||||
|
||||
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: [] };
|
||||
|
||||
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
|
||||
// Run fetchMilestones on mount or when careerPathId changes
|
||||
useEffect(() => {
|
||||
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 () => {
|
||||
if (!activeView) return;
|
||||
|
||||
const url = editingMilestone
|
||||
? `/api/premium/milestones/${editingMilestone.id}`
|
||||
: `/api/premium/milestone`;
|
||||
const method = editingMilestone ? 'PUT' : 'POST';
|
||||
|
||||
const payload = {
|
||||
milestone_type: activeView,
|
||||
title: newMilestone.title,
|
||||
@ -70,148 +78,217 @@ const filteredMilestones = raw.filter(
|
||||
date: newMilestone.date,
|
||||
career_path_id: careerPathId,
|
||||
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 {
|
||||
console.log('Sending request to:', url);
|
||||
console.log('HTTP Method:', method);
|
||||
console.log('Payload:', payload);
|
||||
|
||||
console.log('Sending request:', method, url, payload);
|
||||
const res = await authFetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
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
|
||||
alert(errorData.error || 'Error saving milestone');
|
||||
return;
|
||||
}
|
||||
|
||||
const savedMilestone = await res.json();
|
||||
|
||||
// Update state locally instead of fetching all milestones
|
||||
setMilestones((prevMilestones) => {
|
||||
const updatedMilestones = { ...prevMilestones };
|
||||
const savedMilestone = await res.json();
|
||||
console.log('Milestone saved/updated:', savedMilestone);
|
||||
|
||||
// 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) {
|
||||
// Update the existing milestone
|
||||
updatedMilestones[activeView] = updatedMilestones[activeView].map((m) =>
|
||||
updated[activeView] = updated[activeView].map((m) =>
|
||||
m.id === editingMilestone.id ? savedMilestone : m
|
||||
);
|
||||
} else {
|
||||
// Add the new milestone
|
||||
updatedMilestones[activeView].push(savedMilestone);
|
||||
updated[activeView].push(savedMilestone);
|
||||
}
|
||||
return updatedMilestones;
|
||||
return updated;
|
||||
});
|
||||
|
||||
// Reset form
|
||||
setShowForm(false);
|
||||
setEditingMilestone(null);
|
||||
setNewMilestone({ title: '', description: '', date: '', progress: 0 });
|
||||
} catch (error) {
|
||||
console.error('Error saving milestone:', error);
|
||||
setNewMilestone({
|
||||
title: '',
|
||||
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];
|
||||
const lastDate = allMilestones.reduce(
|
||||
(latest, m) => (new Date(m.date) > latest ? new Date(m.date) : latest),
|
||||
today
|
||||
);
|
||||
/**
|
||||
* Figure out the timeline's "end" date by scanning all milestones.
|
||||
*/
|
||||
const allMilestonesCombined = [...milestones.Career, ...milestones.Financial];
|
||||
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 end = lastDate.getTime();
|
||||
const position = ((new Date(date).getTime() - start) / (end - start)) * 100;
|
||||
return Math.min(Math.max(position, 0), 100);
|
||||
const dateVal = new Date(dateString).getTime();
|
||||
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 || !milestones?.[activeView]) {
|
||||
// If activeView not set or the array is missing, show a loading or empty state
|
||||
if (!activeView || !milestones[activeView]) {
|
||||
return (
|
||||
<div className="milestone-timeline">
|
||||
<p>Loading milestones...</p>
|
||||
<p>Loading or no milestones in this view...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="milestone-timeline">
|
||||
{/* View selector buttons */}
|
||||
<div className="view-selector">
|
||||
{['Career', 'Financial', 'Retirement'].map((view) => (
|
||||
<button
|
||||
key={view}
|
||||
className={activeView === view ? 'active' : ''}
|
||||
onClick={() => setActiveView(view)}
|
||||
>
|
||||
{view}
|
||||
</button>
|
||||
))}
|
||||
{['Career', 'Financial'].map((view) => (
|
||||
<button
|
||||
key={view}
|
||||
className={activeView === view ? 'active' : ''}
|
||||
onClick={() => setActiveView(view)}
|
||||
>
|
||||
{view}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button onClick={() => {
|
||||
if (showForm) {
|
||||
setShowForm(false);
|
||||
setEditingMilestone(null);
|
||||
setNewMilestone({ title: '', date: '', progress: 0 });
|
||||
} else {
|
||||
setShowForm(true);
|
||||
}
|
||||
}}>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (showForm) {
|
||||
// Cancel form
|
||||
setShowForm(false);
|
||||
setEditingMilestone(null);
|
||||
setNewMilestone({
|
||||
title: '',
|
||||
description: '',
|
||||
date: '',
|
||||
progress: 0,
|
||||
newSalary: ''
|
||||
});
|
||||
} else {
|
||||
// Show form
|
||||
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) })} />
|
||||
<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)
|
||||
})
|
||||
}
|
||||
/>
|
||||
{activeView === 'Financial' && (
|
||||
<div>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Full New Salary (e.g., 70000)"
|
||||
value={newMilestone.newSalary || ''}
|
||||
onChange={(e) => setNewMilestone({ ...newMilestone, newSalary: parseFloat(e.target.value) })}
|
||||
value={newMilestone.newSalary}
|
||||
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>
|
||||
)}
|
||||
<button onClick={saveMilestone}>{editingMilestone ? 'Update' : 'Add'} Milestone</button>
|
||||
<button onClick={saveMilestone}>
|
||||
{editingMilestone ? 'Update' : 'Add'} Milestone
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timeline rendering */}
|
||||
<div className="milestone-timeline-container">
|
||||
<div className="milestone-timeline-line" />
|
||||
{milestones[activeView]?.map((m) => (
|
||||
<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-timeline-dot" />
|
||||
<div className="milestone-content">
|
||||
<div className="title">{m.title}</div>
|
||||
<div className="progress-bar">
|
||||
<div className="progress" style={{ width: `${m.progress}%` }} />
|
||||
{milestones[activeView].map((m) => {
|
||||
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);
|
||||
setNewMilestone({
|
||||
title: m.title,
|
||||
description: m.description,
|
||||
date: m.date,
|
||||
progress: m.progress,
|
||||
newSalary: m.new_salary || ''
|
||||
});
|
||||
setShowForm(true);
|
||||
}}
|
||||
>
|
||||
<div className="milestone-timeline-dot" />
|
||||
<div className="milestone-content">
|
||||
<div className="title">{m.title}</div>
|
||||
{m.description && <p>{m.description}</p>}
|
||||
<div className="progress-bar">
|
||||
<div className="progress" style={{ width: `${m.progress}%` }} />
|
||||
</div>
|
||||
<div className="date">{m.date}</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 className="date">{m.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -26,8 +26,8 @@ export function simulateFinancialProjection(userProfile) {
|
||||
inCollege = false,
|
||||
programType,
|
||||
hoursCompleted = 0,
|
||||
creditHoursPerYear = 30,
|
||||
calculatedTuition = 10000, // e.g. annual tuition
|
||||
creditHoursPerYear,
|
||||
calculatedTuition, // e.g. annual tuition
|
||||
gradDate, // known graduation date, or null
|
||||
startDate, // when sim starts
|
||||
academicCalendar = 'monthly', // new
|
||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user