210 lines
6.9 KiB
JavaScript
210 lines
6.9 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: [], Retirement: [] });
|
|
const [newMilestone, setNewMilestone] = useState({ title: '', date: '', description: '', progress: 0 });
|
|
const [showForm, setShowForm] = useState(false);
|
|
const [editingMilestone, setEditingMilestone] = useState(null);
|
|
|
|
const fetchMilestones = useCallback(async () => {
|
|
if (!careerPathId) {
|
|
console.warn('No careerPathId provided.');
|
|
return;
|
|
}
|
|
|
|
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
|
|
useEffect(() => {
|
|
fetchMilestones();
|
|
}, [fetchMilestones]);
|
|
|
|
const saveMilestone = async () => {
|
|
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',
|
|
};
|
|
|
|
try {
|
|
console.log('Sending request to:', url);
|
|
console.log('HTTP Method:', method);
|
|
console.log('Payload:', 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);
|
|
|
|
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;
|
|
}
|
|
|
|
const savedMilestone = await res.json();
|
|
|
|
// Update state locally instead of fetching all milestones
|
|
setMilestones((prevMilestones) => {
|
|
const updatedMilestones = { ...prevMilestones };
|
|
if (editingMilestone) {
|
|
// Update the existing milestone
|
|
updatedMilestones[activeView] = updatedMilestones[activeView].map((m) =>
|
|
m.id === editingMilestone.id ? savedMilestone : m
|
|
);
|
|
} else {
|
|
// Add the new milestone
|
|
updatedMilestones[activeView].push(savedMilestone);
|
|
}
|
|
return updatedMilestones;
|
|
});
|
|
|
|
setShowForm(false);
|
|
setEditingMilestone(null);
|
|
setNewMilestone({ title: '', description: '', date: '', progress: 0 });
|
|
} catch (error) {
|
|
console.error('Error saving milestone:', error);
|
|
}
|
|
};
|
|
|
|
// 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
|
|
);
|
|
|
|
const calcPosition = (date) => {
|
|
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);
|
|
};
|
|
|
|
console.log('Rendering view:', activeView, milestones?.[activeView]);
|
|
|
|
if (!activeView || !milestones?.[activeView]) {
|
|
return (
|
|
<div className="milestone-timeline">
|
|
<p>Loading milestones...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="milestone-timeline">
|
|
<div className="view-selector">
|
|
{['Career', 'Financial', 'Retirement'].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);
|
|
}
|
|
}}>
|
|
{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) })} />
|
|
<button onClick={saveMilestone}>{editingMilestone ? 'Update' : 'Add'} Milestone</button>
|
|
</div>
|
|
)}
|
|
|
|
<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}%` }} />
|
|
</div>
|
|
<div className="date">{m.date}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MilestoneTimeline;
|