dev1/src/components/MilestoneTimeline.js

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;