298 lines
9.1 KiB
JavaScript
298 lines
9.1 KiB
JavaScript
// src/components/MilestoneTimeline.js
|
|
import React, { useEffect, useState, useCallback } from 'react';
|
|
|
|
const today = new Date();
|
|
|
|
const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView }) => {
|
|
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) 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);
|
|
}
|
|
}, [careerPathId, authFetch]);
|
|
|
|
// 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,
|
|
description: newMilestone.description,
|
|
date: newMilestone.date,
|
|
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
|
|
};
|
|
|
|
try {
|
|
console.log('Sending request:', method, url, 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);
|
|
alert(errorData.error || 'Error saving milestone');
|
|
return;
|
|
}
|
|
|
|
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) {
|
|
updated[activeView] = updated[activeView].map((m) =>
|
|
m.id === editingMilestone.id ? savedMilestone : m
|
|
);
|
|
} else {
|
|
updated[activeView].push(savedMilestone);
|
|
}
|
|
return updated;
|
|
});
|
|
|
|
// Reset form
|
|
setShowForm(false);
|
|
setEditingMilestone(null);
|
|
setNewMilestone({
|
|
title: '',
|
|
description: '',
|
|
date: '',
|
|
progress: 0,
|
|
newSalary: ''
|
|
});
|
|
} catch (err) {
|
|
console.error('Error saving milestone:', err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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 = (dateString) => {
|
|
const start = today.getTime();
|
|
const end = lastDate.getTime();
|
|
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);
|
|
};
|
|
|
|
// 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 or no milestones in this view...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="milestone-timeline">
|
|
{/* View selector buttons */}
|
|
<div className="view-selector">
|
|
{['Career', 'Financial'].map((view) => (
|
|
<button
|
|
key={view}
|
|
className={activeView === view ? 'active' : ''}
|
|
onClick={() => setActiveView(view)}
|
|
>
|
|
{view}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<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)
|
|
})
|
|
}
|
|
/>
|
|
{activeView === 'Financial' && (
|
|
<div>
|
|
<input
|
|
type="number"
|
|
placeholder="Full New Salary (e.g., 70000)"
|
|
value={newMilestone.newSalary}
|
|
onChange={(e) =>
|
|
setNewMilestone({ ...newMilestone, newSalary: e.target.value })
|
|
}
|
|
/>
|
|
<p>Enter the full new salary (not just the increase) after the milestone occurs.</p>
|
|
</div>
|
|
)}
|
|
<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) => {
|
|
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>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MilestoneTimeline;
|