import { useState, useEffect, useContext, useRef } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { ArrowLeft, Plus, Check, Trash2, LayoutGrid, List, GanttChart, Settings, Calendar, Clock, MessageCircle, X, Image as ImageIcon } from 'lucide-react' import { format, differenceInDays, startOfDay, addDays, isAfter, isBefore } from 'date-fns' import { AppContext } from '../App' import { api, PRIORITY_CONFIG } from '../utils/api' import { useAuth } from '../contexts/AuthContext' import StatusBadge from '../components/StatusBadge' import BrandBadge from '../components/BrandBadge' import Modal from '../components/Modal' import CommentsSection from '../components/CommentsSection' import ProjectEditPanel from '../components/ProjectEditPanel' import TaskDetailPanel from '../components/TaskDetailPanel' const TASK_COLUMNS = [ { id: 'todo', label: 'To Do', color: 'bg-gray-400' }, { id: 'in_progress', label: 'In Progress', color: 'bg-blue-400' }, { id: 'done', label: 'Done', color: 'bg-emerald-400' }, ] export default function ProjectDetail() { const { id } = useParams() const navigate = useNavigate() const { teamMembers, brands } = useContext(AppContext) const { permissions, canEditResource, canDeleteResource } = useAuth() const canManageProject = permissions?.canEditProjects const [project, setProject] = useState(null) const [tasks, setTasks] = useState([]) const [loading, setLoading] = useState(true) const [assignableUsers, setAssignableUsers] = useState([]) const [view, setView] = useState('kanban') const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) const [taskToDelete, setTaskToDelete] = useState(null) const [showDiscussion, setShowDiscussion] = useState(false) const [thumbnailUploading, setThumbnailUploading] = useState(false) const thumbnailInputRef = useRef(null) // Panel state const [panelProject, setPanelProject] = useState(null) const [panelTask, setPanelTask] = useState(null) // Drag state for kanban const [draggedTask, setDraggedTask] = useState(null) const [dragOverCol, setDragOverCol] = useState(null) useEffect(() => { loadProject() }, [id]) useEffect(() => { api.get('/users/assignable').then(res => setAssignableUsers(Array.isArray(res) ? res : [])).catch(() => {}) }, []) const loadProject = async () => { try { const proj = await api.get(`/projects/${id}`) setProject(proj) const tasksRes = await api.get(`/tasks?project_id=${id}`) setTasks(Array.isArray(tasksRes) ? tasksRes : []) } catch (err) { console.error('Failed to load project:', err) } finally { setLoading(false) } } const handleTaskStatusChange = async (taskId, newStatus) => { try { await api.patch(`/tasks/${taskId}`, { status: newStatus }) loadProject() } catch (err) { console.error('Status change failed:', err) } } const handleDeleteTask = async (taskId) => { setTaskToDelete(taskId) setShowDeleteConfirm(true) } const confirmDeleteTask = async () => { if (!taskToDelete) return try { await api.delete(`/tasks/${taskToDelete}`) loadProject() setTaskToDelete(null) } catch (err) { console.error('Delete failed:', err) } } // Panel handlers const handleProjectPanelSave = async (projectId, data) => { await api.patch(`/projects/${projectId}`, data) loadProject() } const handleProjectPanelDelete = async (projectId) => { await api.delete(`/projects/${projectId}`) navigate('/projects') } const handleTaskPanelSave = async (taskId, data) => { if (taskId) { await api.patch(`/tasks/${taskId}`, data) } else { await api.post('/tasks', { ...data, project_id: Number(id) }) } setPanelTask(null) loadProject() } const handleTaskPanelDelete = async (taskId) => { await api.delete(`/tasks/${taskId}`) setPanelTask(null) loadProject() } const openEditTask = (task) => { setPanelTask(task) } const openNewTask = () => { setPanelTask({ title: '', status: 'todo', priority: 'medium', project_id: Number(id) }) } const openEditProject = () => { if (!project) return setPanelProject(project) } const handleThumbnailUpload = async (file) => { if (!file) return setThumbnailUploading(true) try { const fd = new FormData() fd.append('file', file) await api.upload(`/projects/${id}/thumbnail`, fd) loadProject() } catch (err) { console.error('Thumbnail upload failed:', err) } finally { setThumbnailUploading(false) } } const handleThumbnailRemove = async () => { try { await api.delete(`/projects/${id}/thumbnail`) loadProject() } catch (err) { console.error('Thumbnail remove failed:', err) } } // Drag handlers const handleDragStart = (e, task) => { setDraggedTask(task) e.dataTransfer.effectAllowed = 'move' setTimeout(() => { e.target.style.opacity = '0.4' }, 0) } const handleDragEnd = (e) => { e.target.style.opacity = '1' setDraggedTask(null) setDragOverCol(null) } const handleDragOver = (e, colId) => { e.preventDefault() e.dataTransfer.dropEffect = 'move' setDragOverCol(colId) } const handleDragLeave = (e) => { if (!e.currentTarget.contains(e.relatedTarget)) setDragOverCol(null) } const handleDrop = (e, colId) => { e.preventDefault() setDragOverCol(null) if (draggedTask && draggedTask.status !== colId) { handleTaskStatusChange(draggedTask._id, colId) } setDraggedTask(null) } if (loading) { return (
) } if (!project) { return (

Project not found

) } const canEditProject = canEditResource('project', project) const completedTasks = tasks.filter(t => t.status === 'done').length const progress = tasks.length > 0 ? Math.round((completedTasks / tasks.length) * 100) : 0 const ownerName = project.ownerName || project.owner_name const brandName = project.brandName || project.brand_name return (
{/* Main content */}
{/* Back button */} {/* Project header */}
{/* Thumbnail banner */} {(project.thumbnail_url || project.thumbnailUrl) && (
{canEditProject && (
)}
)} { handleThumbnailUpload(e.target.files[0]); e.target.value = '' }} />

{project.name}

{brandName && } {ownerName && ( Owned by {ownerName} )} {project.dueDate && ( Due {format(new Date(project.dueDate), 'MMMM d, yyyy')} )}
{canEditProject && !project.thumbnail_url && !project.thumbnailUrl && ( )} {canEditProject && ( )}
{project.description && (

{project.description}

)} {/* Progress */}
Progress {progress}%

{completedTasks} of {tasks.length} tasks completed

{/* end p-6 wrapper */}
{/* end project header card */} {/* View switcher + Add Task */}
{[ { id: 'kanban', icon: LayoutGrid, label: 'Board' }, { id: 'list', icon: List, label: 'List' }, { id: 'gantt', icon: GanttChart, label: 'Timeline' }, ].map(v => ( ))}
{/* ─── KANBAN VIEW ─── */} {view === 'kanban' && (
{TASK_COLUMNS.map(col => { const colTasks = tasks.filter(t => t.status === col.id) const isOver = dragOverCol === col.id && draggedTask?.status !== col.id return (

{col.label}

{colTasks.length}
handleDragOver(e, col.id)} onDragLeave={handleDragLeave} onDrop={(e) => handleDrop(e, col.id)} > {colTasks.length === 0 ? (
{isOver ? 'Drop here' : 'No tasks'}
) : ( colTasks.map(task => ( openEditTask(task)} onDelete={() => handleDeleteTask(task._id)} onStatusChange={handleTaskStatusChange} onDragStart={handleDragStart} onDragEnd={handleDragEnd} /> )) )}
) })}
)} {/* ─── LIST VIEW ─── */} {view === 'list' && (
{tasks.length === 0 ? ( ) : ( tasks.map(task => { const prio = PRIORITY_CONFIG[task.priority] || PRIORITY_CONFIG.medium const assigneeName = task.assignedName || task.assigned_name const isOverdue = task.dueDate && new Date(task.dueDate) < new Date() && task.status !== 'done' return ( openEditTask(task)} className="hover:bg-surface-secondary cursor-pointer transition-colors"> ) }) )}
Task Status Priority Assignee Due
{t('tasks.noTasks')}
{task.title} {task.description &&

{task.description}

}
{prio.label} {assigneeName || '—'} {task.dueDate ? format(new Date(task.dueDate), 'MMM d, yyyy') : '—'}
)} {/* ─── GANTT / TIMELINE VIEW ─── */} {view === 'gantt' && { try { await api.patch(`/tasks/${taskId}`, { color: color || '' }) loadProject() } catch (err) { console.error('Task color update failed:', err) } }} />}
{/* end main content */} {/* ─── DISCUSSION SIDEBAR ─── */} {showDiscussion && (

Discussion

)} {/* ─── DELETE TASK CONFIRMATION ─── */} { setShowDeleteConfirm(false); setTaskToDelete(null) }} title="Delete Task?" isConfirm danger confirmText="Delete Task" onConfirm={confirmDeleteTask} > Are you sure you want to delete this task? This action cannot be undone. {/* Project Edit Panel */} {panelProject && ( setPanelProject(null)} onSave={handleProjectPanelSave} onDelete={handleProjectPanelDelete} brands={brands} teamMembers={teamMembers} /> )} {/* Task Detail Panel */} {panelTask && ( setPanelTask(null)} onSave={handleTaskPanelSave} onDelete={handleTaskPanelDelete} projects={project ? [project] : []} users={assignableUsers} brands={brands} /> )}
) } // ─── Task Kanban Card ─────────────────────────────── function TaskKanbanCard({ task, canEdit, canDelete, onClick, onDelete, onStatusChange, onDragStart, onDragEnd }) { const priority = PRIORITY_CONFIG[task.priority] || PRIORITY_CONFIG.medium const assigneeName = task.assignedName || task.assigned_name const isOverdue = task.dueDate && new Date(task.dueDate) < new Date() && task.status !== 'done' return (
canEdit && onDragStart(e, task)} onDragEnd={onDragEnd} onClick={onClick} className={`bg-surface rounded-lg border border-border p-3 group hover:border-brand-primary/30 hover:shadow-sm transition-all cursor-pointer ${canEdit ? 'active:cursor-grabbing' : ''}`} >
{task.title}
{assigneeName && ( {assigneeName} )} {task.dueDate && ( {format(new Date(task.dueDate), 'MMM d')} )}
{/* Actions on hover */} {(canEdit || canDelete) && (
{canEdit && task.status !== 'done' && ( )} {canDelete && ( )}
)}
) } // ─── Gantt / Timeline View ────────────────────────── const GANTT_ZOOM = [ { key: 'month', label: 'Month', pxPerDay: 8 }, { key: 'week', label: 'Week', pxPerDay: 20 }, { key: 'day', label: 'Day', pxPerDay: 48 }, ] const GANTT_COLOR_PALETTE = [ '#6366f1', '#ec4899', '#10b981', '#f59e0b', '#8b5cf6', '#06b6d4', '#f43f5e', '#14b8a6', '#3b82f6', '#ef4444', '#84cc16', '#a855f7', ] function GanttView({ tasks, project, onEditTask, onTaskColorChange }) { const [zoomIdx, setZoomIdx] = useState(0) const ganttRef = useRef(null) const [colorPicker, setColorPicker] = useState(null) const colorPickerRef = useRef(null) useEffect(() => { if (!colorPicker) return const handleClick = (e) => { if (colorPickerRef.current && !colorPickerRef.current.contains(e.target)) { setColorPicker(null) } } document.addEventListener('mousedown', handleClick) return () => document.removeEventListener('mousedown', handleClick) }, [colorPicker]) if (tasks.length === 0) { return (

No tasks to display

Add tasks with due dates to see the timeline

) } const today = startOfDay(new Date()) // Calculate range let earliest = addDays(today, -7) let latest = addDays(today, 30) tasks.forEach(t => { const created = t.createdAt ? startOfDay(new Date(t.createdAt)) : today const start = t.startDate || t.start_date ? startOfDay(new Date(t.startDate || t.start_date)) : created const due = t.dueDate ? startOfDay(new Date(t.dueDate)) : null if (isBefore(start, earliest)) earliest = addDays(start, -3) if (isBefore(created, earliest)) earliest = addDays(created, -3) if (due && isAfter(due, latest)) latest = addDays(due, 7) }) if (project.dueDate) { const pd = startOfDay(new Date(project.dueDate)) if (isAfter(pd, latest)) latest = addDays(pd, 7) } const totalDays = differenceInDays(latest, earliest) + 1 // Generate day headers const days = [] for (let i = 0; i < totalDays; i++) { days.push(addDays(earliest, i)) } const dayWidth = GANTT_ZOOM[zoomIdx].pxPerDay const getBarStyle = (task) => { const start = task.startDate || task.start_date ? startOfDay(new Date(task.startDate || task.start_date)) : task.createdAt ? startOfDay(new Date(task.createdAt)) : today const end = task.dueDate ? startOfDay(new Date(task.dueDate)) : addDays(start, 3) const left = differenceInDays(start, earliest) * dayWidth const width = Math.max(dayWidth, (differenceInDays(end, start) + 1) * dayWidth) return { left: `${left}px`, width: `${width}px` } } const statusColors = { todo: 'bg-gray-300', in_progress: 'bg-blue-400', done: 'bg-emerald-400', } return (
{/* Zoom toolbar */}
{GANTT_ZOOM.map((z, i) => ( ))}
{/* Day headers */}
Task
{days.map((day, i) => { const isToday = differenceInDays(day, today) === 0 const isWeekend = day.getDay() === 0 || day.getDay() === 6 const isMonthStart = day.getDate() === 1 const isWeekStart = day.getDay() === 1 return (
{dayWidth >= 30 &&
{format(day, 'd')}
} {dayWidth >= 40 &&
{format(day, 'EEE')}
} {dayWidth >= 15 && dayWidth < 30 && day.getDate() % 7 === 1 &&
{format(day, 'd')}
} {dayWidth < 15 && isMonthStart && (
{format(day, 'MMM')}
)} {dayWidth < 15 && !isMonthStart && isWeekStart && day.getDate() % 14 <= 7 && (
{format(day, 'd')}
)}
) })}
{/* Task rows */} {tasks.map(task => { const prio = PRIORITY_CONFIG[task.priority] || PRIORITY_CONFIG.medium const barStyle = getBarStyle(task) return (
{onTaskColorChange && (
{/* Today line */} {differenceInDays(today, earliest) >= 0 && (
)} {/* Bar */}
onEditTask(task)} title={`${task.title}${task.dueDate ? ` — Due ${format(new Date(task.dueDate), 'MMM d')}` : ''}`} />
) })}
{/* Color Picker Popover */} {colorPicker && onTaskColorChange && (
{GANTT_COLOR_PALETTE.map(c => (
)}
) }