import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Task } from '../entities/Task'; import { Project } from '../entities/Project'; import { updateTask } from '../utils/tasksService'; import { ChevronLeftIcon, ChevronRightIcon, CalendarIcon, XMarkIcon, ArrowTopRightOnSquareIcon, } from '@heroicons/react/24/outline'; import { format, addWeeks, addDays } from 'date-fns'; import { el, enUS, es, ja, uk, de } from 'date-fns/locale'; import CalendarMonthView from './Calendar/CalendarMonthView'; import CalendarWeekView from './Calendar/CalendarWeekView'; import CalendarDayView from './Calendar/CalendarDayView'; import { getApiPath } from '../config/paths'; import { Link, useNavigate } from 'react-router-dom'; import { parseDateString } from '../utils/dateUtils'; const getLocale = (language: string) => { switch (language) { case 'el': return el; case 'es': return es; case 'jp': return ja; case 'ua': return uk; case 'de': return de; default: return enUS; } }; interface CalendarEvent { id: string; title: string; start: Date; end: Date; type: 'task' | 'event'; color?: string; } const Calendar: React.FC = () => { const { t, i18n } = useTranslation(); const navigate = useNavigate(); const [currentDate, setCurrentDate] = useState(new Date()); const [view, setView] = useState<'month' | 'week' | 'day'>('month'); const [events, setEvents] = useState([]); const [isLoadingTasks, setIsLoadingTasks] = useState(false); const [selectedTask, setSelectedTask] = useState(null); const [allTasks, setAllTasks] = useState([]); const [, setProjects] = useState([]); const [isEventDetailModalOpen, setIsEventDetailModalOpen] = useState(false); // Dispatch global modal events const locale = getLocale(i18n.language); // Load tasks and projects on component mount useEffect(() => { loadTasks(); loadProjects(); }, []); const loadTasks = async () => { setIsLoadingTasks(true); try { const response = await fetch(getApiPath('tasks'), { credentials: 'include', }); if (response.ok) { const data = await response.json(); // Handle different API response formats let tasks; if (Array.isArray(data)) { tasks = data; } else if (data && Array.isArray(data.tasks)) { tasks = data.tasks; } else if (data && data.data && Array.isArray(data.data)) { tasks = data.data; } else { console.error('Unexpected API response format:', data); tasks = []; } // Store the original tasks for later reference setAllTasks(tasks); const taskEvents = convertTasksToEvents(tasks); setEvents(taskEvents); } else { console.error('Failed to load tasks, status:', response.status); } } catch (error) { console.error('Error loading tasks:', error); } finally { setIsLoadingTasks(false); } }; const convertTasksToEvents = (tasks: any[]): CalendarEvent[] => { const taskEvents: CalendarEvent[] = []; if (!Array.isArray(tasks)) { console.error('convertTasksToEvents received non-array:', tasks); return []; } tasks.forEach((task) => { // Add deferred tasks with defer_until dates if (task.defer_until) { const deferDate = new Date(task.defer_until); const taskEvent = { id: `task-defer-${task.id}`, title: `⏰ ${task.name || task.title || `Task ${task.id}`}`, start: deferDate, end: new Date(deferDate.getTime() + 60 * 60 * 1000), // 1 hour duration type: 'task' as const, color: task.completed_at ? '#22c55e' : '#f59e0b', // Green if completed, amber if deferred }; taskEvents.push(taskEvent); } // Add tasks with due dates if (task.due_date) { const dueDate = parseDateString(task.due_date); if (dueDate) { const taskEvent = { id: `task-${task.id}`, title: task.name || task.title || `Task ${task.id}`, start: dueDate, end: new Date(dueDate.getTime() + 60 * 60 * 1000), // 1 hour duration type: 'task' as const, color: task.completed_at ? '#22c55e' : '#3b82f6', // Green if completed, blue if not }; taskEvents.push(taskEvent); } } // Add tasks scheduled for today (if they don't have defer_until or due_date) if (!task.defer_until && !task.due_date && task.created_at) { const createdDate = new Date(task.created_at); const today = new Date(); // Show tasks created today on the calendar if (createdDate.toDateString() === today.toDateString()) { const taskEvent = { id: `task-created-${task.id}`, title: `📝 ${task.name || task.title || `Task ${task.id}`}`, start: createdDate, end: new Date(createdDate.getTime() + 30 * 60 * 1000), // 30 min duration type: 'task' as const, color: task.completed_at ? '#22c55e' : '#3b82f6', // Green if completed, blue if not }; taskEvents.push(taskEvent); } } // Always add tasks to calendar for easier debugging (only if no defer_until, due_date, or created_at) if (!task.defer_until && !task.due_date && !task.created_at) { const taskEvent = { id: `task-fallback-${task.id}`, title: `📌 ${task.name || task.title || `Task ${task.id}`}`, start: new Date(), // Today end: new Date(Date.now() + 30 * 60 * 1000), // 30 min duration type: 'task' as const, color: task.completed_at ? '#22c55e' : '#8b5cf6', // Green if completed, purple if not }; taskEvents.push(taskEvent); } }); return taskEvents; }; const loadProjects = async () => { try { const response = await fetch(getApiPath('projects'), { credentials: 'include', }); if (response.ok) { const projectsData = await response.json(); setProjects(Array.isArray(projectsData) ? projectsData : []); } } catch (error) { console.error('Error loading projects:', error); } }; const navigateView = (direction: 'prev' | 'next') => { setCurrentDate((prev) => { if (view === 'month') { const newDate = new Date(prev); if (direction === 'prev') { newDate.setMonth(prev.getMonth() - 1); } else { newDate.setMonth(prev.getMonth() + 1); } return newDate; } else if (view === 'week') { return direction === 'prev' ? addWeeks(prev, -1) : addWeeks(prev, 1); } else { // day return direction === 'prev' ? addDays(prev, -1) : addDays(prev, 1); } }); }; const goToToday = () => { setCurrentDate(new Date()); }; const handleDateClick = () => { // Date click handler - can be used for future functionality }; const handleEventClick = (event: CalendarEvent) => { // Handle task events if (event.type === 'task') { // Extract task ID from event ID (handles task-, task-defer-, task-created-, task-fallback-) const taskId = event.id.replace( /^task(-defer|-created|-fallback)?-/, '' ); const task = allTasks.find((t) => t.id.toString() === taskId); if (task) { // Normalize task shape before opening TaskDetails const taskEntity: Task = { ...task, name: task.name || task.title || `Task ${task.id}`, // Ensure all required Task properties are present priority: task.priority || 'low', status: task.status || 'not_started', tags: task.tags || [], note: task.note || task.description || '', due_date: task.due_date, created_at: task.created_at, completed_at: task.completed_at, project_id: task.project_id, }; setSelectedTask(taskEntity); setIsEventDetailModalOpen(true); } } }; const handleTimeSlotClick = () => { // Time slot click handler - can be used for future functionality }; const handleEditTask = () => { if (selectedTask?.uid) { setIsEventDetailModalOpen(false); const targetUid = selectedTask.uid; setSelectedTask(null); navigate(`/task/${targetUid}`); } }; const handleEventDrop = async ( eventId: string, newDate: Date, newHour?: number ) => { console.log('Event drop:', { eventId, newDate, newHour }); // Extract task ID from event ID const taskId = eventId.replace( /^task(-defer|-created|-fallback)?-/, '' ); const task = allTasks.find((t) => t.id.toString() === taskId); if (!task) { console.error('Task not found:', taskId); return; } if (!task.uid) { console.error('Task has no uid:', task); return; } console.log('Found task:', task); // Calculate new date/time const newDateTime = new Date(newDate); if (newHour !== undefined) { newDateTime.setHours(newHour, 0, 0, 0); } else { // If no hour specified (month view), keep the original time or set to start of day if (task.due_date) { const originalTime = parseDateString(task.due_date); if (originalTime) { newDateTime.setHours( originalTime.getHours(), originalTime.getMinutes(), 0, 0 ); } else { newDateTime.setHours(0, 0, 0, 0); } } else { newDateTime.setHours(0, 0, 0, 0); } } // Determine which field to update based on event type const isDeferEvent = eventId.startsWith('task-defer-'); const fieldToUpdate = isDeferEvent ? 'defer_until' : 'due_date'; console.log('Updating task:', { uid: task.uid, field: fieldToUpdate, newDateTime: newDateTime.toISOString(), }); // Optimistically update the UI first const updatedTask = { ...task, [fieldToUpdate]: newDateTime.toISOString(), }; // Update local tasks state setAllTasks((prev) => prev.map((t) => (t.id === task.id ? updatedTask : t)) ); // Update events state setEvents((prevEvents) => prevEvents.map((event) => { if (event.id === eventId) { return { ...event, start: newDateTime, end: new Date(newDateTime.getTime() + 60 * 60 * 1000), }; } return event; }) ); // Update in background try { await updateTask(task.uid, { [fieldToUpdate]: newDateTime.toISOString(), }); console.log('Task updated successfully'); } catch (error) { console.error('Error updating task:', error); // Revert on error await loadTasks(); } }; return (
{/* Header */}

{t('sidebar.calendar')}

{format(currentDate, 'MMMM yyyy', { locale })}
{/* View selector */}
{['month', 'week', 'day'].map((viewType) => ( ))}
{/* Navigation */}
{/* Loading indicator */} {isLoadingTasks && (
{t('calendar.loadingTasks')}
)} {/* Calendar view */}
{view === 'month' && ( )} {view === 'week' && ( )} {view === 'day' && ( )}
{/* Event Details Modal */} {selectedTask && ( { setIsEventDetailModalOpen(false); setSelectedTask(null); }} task={selectedTask} onEditTask={handleEditTask} /> )} {/* Full Task Edit Modal */}
); }; // Simple Task Event Details Modal Component interface TaskEventModalProps { isOpen: boolean; task: Task; onClose: () => void; onEditTask: () => void; } const TaskEventModal: React.FC = ({ isOpen, task, onClose, onEditTask, }) => { const { t, i18n } = useTranslation(); const locale = getLocale(i18n.language); if (!isOpen) return null; return (

📋 {t('calendar.taskDetails')}

{/* Task Title */}

{task.name || `Task ${task.id}`}

{/* Task Status */}
{task.completed_at ? `✅ ${t('calendar.completed')}` : `⏳ ${t('calendar.pending')}`}
{/* Due Date */} {task.due_date && (

{parseDateString(task.due_date) && format( parseDateString(task.due_date) as Date, 'PPP', { locale: locale, } )}

)} {/* Priority */} {task.priority && (
{t(`calendar.${task.priority}`)}
)} {/* Project */} {task.Project?.name && (

{task.Project.name}

)} {/* Area - Note: Area relationship not in Task entity, removing this section */} {/* Note */} {task.note && (

{task.note}

)} {/* Created Date */} {task.created_at && (

{format(new Date(task.created_at), 'PPp', { locale: locale, })}

)}
{/* Action Buttons */}
{t('calendar.goToTasks')}
); }; export default Calendar;