import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { TaskEvent } from '../../entities/TaskEvent'; import { getTaskTimeline, getEventTypeLabel, getStatusLabel, getPriorityLabel, } from '../../utils/taskEventService'; import { ClockIcon, CheckCircleIcon, ExclamationTriangleIcon, PencilIcon, TagIcon, CalendarIcon, FolderIcon, PlayIcon, ArchiveBoxIcon, SparklesIcon, AdjustmentsHorizontalIcon, } from '@heroicons/react/24/outline'; interface TaskTimelineProps { taskId: number | undefined; } const TaskTimeline: React.FC = ({ taskId }) => { const { t } = useTranslation(); const [events, setEvents] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchTimeline = async () => { if (!taskId || taskId === undefined) { setLoading(false); setEvents([]); return; } setLoading(true); setError(null); try { const timeline = await getTaskTimeline(taskId); setEvents(timeline); } catch (err) { console.error('Error fetching task timeline:', err); setError(t('timeline.failedToLoad', 'Failed to load timeline')); } finally { setLoading(false); } }; fetchTimeline(); }, [taskId]); const getEventIcon = (eventType: string, newValue?: any) => { const iconClass = 'h-3.5 w-3.5'; switch (eventType) { case 'created': return ( ); case 'status_changed': if (newValue?.status === 1) return ( ); if (newValue?.status === 2) return ( ); if (newValue?.status === 3) return ( ); return ( ); case 'completed': return ( ); case 'priority_changed': return ( ); case 'due_date_changed': return ( ); case 'project_changed': return ( ); case 'name_changed': case 'description_changed': case 'note_changed': return ; case 'tags_changed': return ; case 'archived': return ( ); case 'today_changed': return ( ); default: return ; } }; const getEventDescription = (event: TaskEvent) => { const { event_type, old_value, new_value } = event; switch (event_type) { case 'created': return t('timeline.events.taskCreated'); case 'status_changed': case 'completed': { const oldStatus = old_value?.status; const newStatus = new_value?.status; if (oldStatus !== undefined && newStatus !== undefined) { return `${t('timeline.events.status')}: ${getStatusLabel(oldStatus)} → ${getStatusLabel(newStatus)}`; } return t('timeline.events.statusChanged'); } case 'priority_changed': { const oldPriority = old_value?.priority; const newPriority = new_value?.priority; if (oldPriority !== undefined && newPriority !== undefined) { return `${t('timeline.events.priority')}: ${getPriorityLabel(oldPriority)} → ${getPriorityLabel(newPriority)}`; } return t('timeline.events.priorityChanged'); } case 'due_date_changed': { const oldDate = old_value?.due_date; const newDate = new_value?.due_date; if (oldDate || newDate) { return `${t('timeline.events.dueDate')}: ${oldDate || t('timeline.events.none')} → ${newDate || t('timeline.events.none')}`; } return t('timeline.events.dueDateChanged'); } case 'name_changed': return t('timeline.events.nameUpdated'); case 'description_changed': return t('timeline.events.descriptionUpdated'); case 'note_changed': return t('timeline.events.noteUpdated'); case 'project_changed': return t('timeline.events.projectChanged'); case 'tags_changed': return t('timeline.events.tagsUpdated'); case 'archived': return t('timeline.events.taskArchived'); case 'today_changed': return t('timeline.events.todayFlagChanged'); default: return getEventTypeLabel(event_type); } }; const formatTimeAgo = (dateString: string) => { const date = new Date(dateString); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMinutes = Math.floor(diffMs / (1000 * 60)); const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffMinutes < 1) return 'Just now'; if (diffMinutes < 60) return `${diffMinutes}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString(); }; if (loading) { return (
Loading timeline...
); } if (error) { return (
{error}
); } if (!taskId) { return (
Timeline will appear after saving
); } if (events.length === 0) { return (
No activity yet
); } return (
{events.map((event) => (
{/* Event item */}
{/* Icon */}
{getEventIcon( event.event_type, event.new_value )}
{/* Content */}
{getEventDescription(event)}
{formatTimeAgo(event.created_at)}
{/* Additional details for certain events */} {event.event_type === 'tags_changed' && event.new_value && (
{Array.isArray(event.new_value) && event.new_value.map( ( tag: any, tagIndex: number ) => ( {tag.name || tag} ) )}
)}
))}
); }; export default TaskTimeline;