// 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 (

Loading or no milestones in this view...

); } return (
{/* View selector buttons */}
{['Career', 'Financial'].map((view) => ( ))}
{showForm && (
setNewMilestone({ ...newMilestone, title: e.target.value })} /> setNewMilestone({ ...newMilestone, description: e.target.value })} /> setNewMilestone({ ...newMilestone, date: e.target.value })} /> setNewMilestone({ ...newMilestone, progress: parseInt(e.target.value, 10) }) } /> {activeView === 'Financial' && (
setNewMilestone({ ...newMilestone, newSalary: e.target.value }) } />

Enter the full new salary (not just the increase) after the milestone occurs.

)}
)} {/* Timeline rendering */}
{milestones[activeView].map((m) => { const leftPos = calcPosition(m.date); return (
{ // 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); }} >
{m.title}
{m.description &&

{m.description}

}
{m.date}
{/* If the milestone has tasks */} {m.tasks && m.tasks.length > 0 && (
    {m.tasks.map((t) => (
  • {t.title}
  • ))}
)}
); })}
); }; export default MilestoneTimeline;