dev1/src/components/MilestoneTimeline.js

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;