feat: slide panels, task calendar, team management, project editing, collapsible sections

- Add SlidePanel, TaskDetailPanel, PostDetailPanel, TeamPanel, TeamMemberPanel
- Add ProjectEditPanel, CollapsibleSection, DatePresetPicker, TaskCalendarView
- Update App, AuthContext, i18n (ar/en), PostProduction, ProjectDetail, Projects
- Update Settings, Tasks, Team pages
- Update InteractiveTimeline, MemberCard, ProjectCard, TaskCard components
- Update server API utilities
- Remove tracked server/node_modules (now properly gitignored)
This commit is contained in:
fahed
2026-02-19 11:35:42 +03:00
parent e76be78498
commit 4522edeea8
2207 changed files with 3767 additions and 831225 deletions
+141 -270
View File
@@ -1,8 +1,9 @@
import { useState, useEffect, useContext } from 'react'
import { useState, useEffect, useContext, useRef } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import {
ArrowLeft, Plus, Check, Trash2, Edit3, LayoutGrid, List,
GanttChart, Settings, Calendar, Clock, MessageCircle, X
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'
@@ -12,6 +13,8 @@ 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' },
@@ -30,19 +33,16 @@ export default function ProjectDetail() {
const [loading, setLoading] = useState(true)
const [assignableUsers, setAssignableUsers] = useState([])
const [view, setView] = useState('kanban')
const [showTaskModal, setShowTaskModal] = useState(false)
const [showProjectModal, setShowProjectModal] = useState(false)
const [editingTask, setEditingTask] = useState(null)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [taskToDelete, setTaskToDelete] = useState(null)
const [taskForm, setTaskForm] = useState({
title: '', description: '', priority: 'medium', assigned_to: '', start_date: '', due_date: '', status: 'todo'
})
const [projectForm, setProjectForm] = useState({
name: '', description: '', brand_id: '', owner_id: '', status: 'active', start_date: '', due_date: ''
})
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)
@@ -66,32 +66,6 @@ export default function ProjectDetail() {
}
}
const handleTaskSave = async () => {
try {
const data = {
title: taskForm.title,
description: taskForm.description,
priority: taskForm.priority,
assigned_to: taskForm.assigned_to ? Number(taskForm.assigned_to) : null,
start_date: taskForm.start_date || null,
due_date: taskForm.due_date || null,
status: taskForm.status,
project_id: Number(id),
}
if (editingTask) {
await api.patch(`/tasks/${editingTask._id}`, data)
} else {
await api.post('/tasks', data)
}
setShowTaskModal(false)
setEditingTask(null)
setTaskForm({ title: '', description: '', priority: 'medium', assigned_to: '', start_date: '', due_date: '', status: 'todo' })
loadProject()
} catch (err) {
console.error('Task save failed:', err)
}
}
const handleTaskStatusChange = async (taskId, newStatus) => {
try {
await api.patch(`/tasks/${taskId}`, { status: newStatus })
@@ -117,55 +91,67 @@ export default function ProjectDetail() {
}
}
// 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) => {
setEditingTask(task)
setTaskForm({
title: task.title || '',
description: task.description || '',
priority: task.priority || 'medium',
assigned_to: task.assignedTo || task.assigned_to || '',
start_date: task.startDate || task.start_date ? new Date(task.startDate || task.start_date).toISOString().slice(0, 10) : '',
due_date: task.dueDate ? new Date(task.dueDate).toISOString().slice(0, 10) : '',
status: task.status || 'todo',
})
setShowTaskModal(true)
setPanelTask(task)
}
const openNewTask = () => {
setEditingTask(null)
setTaskForm({ title: '', description: '', priority: 'medium', assigned_to: '', start_date: '', due_date: '', status: 'todo' })
setShowTaskModal(true)
setPanelTask({ title: '', status: 'todo', priority: 'medium', project_id: Number(id) })
}
const openEditProject = () => {
if (!project) return
setProjectForm({
name: project.name || '',
description: project.description || '',
brand_id: project.brandId || project.brand_id || '',
owner_id: project.ownerId || project.owner_id || '',
status: project.status || 'active',
start_date: project.startDate || project.start_date ? new Date(project.startDate || project.start_date).toISOString().slice(0, 10) : '',
due_date: project.dueDate ? new Date(project.dueDate).toISOString().slice(0, 10) : '',
})
setShowProjectModal(true)
setPanelProject(project)
}
const handleProjectSave = async () => {
const handleThumbnailUpload = async (file) => {
if (!file) return
setThumbnailUploading(true)
try {
await api.patch(`/projects/${id}`, {
name: projectForm.name,
description: projectForm.description,
brand_id: projectForm.brand_id ? Number(projectForm.brand_id) : null,
owner_id: projectForm.owner_id ? Number(projectForm.owner_id) : null,
status: projectForm.status,
start_date: projectForm.start_date || null,
due_date: projectForm.due_date || null,
})
setShowProjectModal(false)
const fd = new FormData()
fd.append('file', file)
await api.upload(`/projects/${id}/thumbnail`, fd)
loadProject()
} catch (err) {
console.error('Project save failed:', 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)
}
}
@@ -237,7 +223,38 @@ export default function ProjectDetail() {
</button>
{/* Project header */}
<div className="bg-white rounded-xl border border-border p-6">
<div className="bg-white rounded-xl border border-border overflow-hidden">
{/* Thumbnail banner */}
{(project.thumbnail_url || project.thumbnailUrl) && (
<div className="relative w-full h-40 overflow-hidden">
<img src={project.thumbnail_url || project.thumbnailUrl} alt="" className="w-full h-full object-cover" />
<div className="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent" />
{canEditProject && (
<div className="absolute top-2 right-2 flex items-center gap-1">
<button
onClick={() => thumbnailInputRef.current?.click()}
className="px-2 py-1 text-xs bg-black/40 hover:bg-black/60 rounded text-white transition-colors"
>
Change
</button>
<button
onClick={handleThumbnailRemove}
className="p-1 bg-black/40 hover:bg-red-500/80 rounded text-white transition-colors"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
)}
</div>
)}
<input
ref={thumbnailInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={e => { handleThumbnailUpload(e.target.files[0]); e.target.value = '' }}
/>
<div className="p-6">
<div className="flex items-start justify-between gap-4 mb-4">
<div>
<div className="flex items-center gap-3 mb-2">
@@ -260,6 +277,16 @@ export default function ProjectDetail() {
</div>
</div>
<div className="flex items-center gap-2">
{canEditProject && !project.thumbnail_url && !project.thumbnailUrl && (
<button
onClick={() => thumbnailInputRef.current?.click()}
disabled={thumbnailUploading}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-text-secondary hover:text-text-primary hover:bg-surface-tertiary rounded-lg transition-colors"
>
<ImageIcon className="w-4 h-4" />
{thumbnailUploading ? 'Uploading...' : 'Thumbnail'}
</button>
)}
<button
onClick={() => setShowDiscussion(prev => !prev)}
className={`flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
@@ -299,7 +326,8 @@ export default function ProjectDetail() {
</div>
<p className="text-xs text-text-tertiary mt-1">{completedTasks} of {tasks.length} tasks completed</p>
</div>
</div>
</div>{/* end p-6 wrapper */}
</div>{/* end project header card */}
{/* View switcher + Add Task */}
<div className="flex items-center justify-between">
@@ -366,7 +394,7 @@ export default function ProjectDetail() {
task={task}
canEdit={canEditResource('task', task)}
canDelete={canDeleteResource('task', task)}
onEdit={() => openEditTask(task)}
onClick={() => openEditTask(task)}
onDelete={() => handleDeleteTask(task._id)}
onStatusChange={handleTaskStatusChange}
onDragStart={handleDragStart}
@@ -393,26 +421,25 @@ export default function ProjectDetail() {
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">Priority</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">Assignee</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">Due</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider w-16"></th>
</tr>
</thead>
<tbody className="divide-y divide-border-light">
{tasks.length === 0 ? (
<tr><td colSpan={7} className="py-12 text-center text-sm text-text-tertiary">No tasks yet</td></tr>
<tr><td colSpan={6} className="py-12 text-center text-sm text-text-tertiary">No tasks yet</td></tr>
) : (
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 (
<tr key={task._id} className="hover:bg-surface-secondary group">
<tr key={task._id} onClick={() => openEditTask(task)} className="hover:bg-surface-secondary cursor-pointer transition-colors">
<td className="px-4 py-3">
<div className={`w-2.5 h-2.5 rounded-full ${prio.color}`} />
</td>
<td className="px-4 py-3">
<button onClick={() => openEditTask(task)} className="text-sm font-medium text-text-primary hover:text-brand-primary text-left">
<span className="text-sm font-medium text-text-primary">
{task.title}
</button>
</span>
{task.description && <p className="text-xs text-text-tertiary line-clamp-1 mt-0.5">{task.description}</p>}
</td>
<td className="px-4 py-3"><StatusBadge status={task.status} size="xs" /></td>
@@ -421,20 +448,6 @@ export default function ProjectDetail() {
<td className={`px-4 py-3 text-xs ${isOverdue ? 'text-red-500 font-medium' : 'text-text-tertiary'}`}>
{task.dueDate ? format(new Date(task.dueDate), 'MMM d, yyyy') : '—'}
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
{canEditResource('task', task) && (
<button onClick={() => openEditTask(task)} className="p-1 rounded hover:bg-surface-tertiary text-text-tertiary">
<Edit3 className="w-3.5 h-3.5" />
</button>
)}
{canDeleteResource('task', task) && (
<button onClick={() => handleDeleteTask(task._id)} className="p-1 rounded hover:bg-red-50 text-red-400">
<Trash2 className="w-3.5 h-3.5" />
</button>
)}
</div>
</td>
</tr>
)
})
@@ -466,167 +479,6 @@ export default function ProjectDetail() {
</div>
)}
{/* ─── TASK MODAL ─── */}
<Modal
isOpen={showTaskModal}
onClose={() => { setShowTaskModal(false); setEditingTask(null) }}
title={editingTask ? 'Edit Task' : 'Add Task'}
size="md"
>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Title *</label>
<input
type="text"
value={taskForm.title}
onChange={e => setTaskForm(f => ({ ...f, title: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
placeholder="Task title"
/>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Description</label>
<textarea
value={taskForm.description}
onChange={e => setTaskForm(f => ({ ...f, description: e.target.value }))}
rows={2}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary resize-none"
placeholder="Optional description"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Priority</label>
<select value={taskForm.priority} onChange={e => setTaskForm(f => ({ ...f, priority: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Status</label>
<select value={taskForm.status} onChange={e => setTaskForm(f => ({ ...f, status: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary">
<option value="todo">To Do</option>
<option value="in_progress">In Progress</option>
<option value="done">Done</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Assign To</label>
<select value={taskForm.assigned_to} onChange={e => setTaskForm(f => ({ ...f, assigned_to: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary">
<option value="">Unassigned</option>
{assignableUsers.map(m => <option key={m._id || m.id} value={m._id || m.id}>{m.name}</option>)}
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Start Date</label>
<input type="date" value={taskForm.start_date} onChange={e => setTaskForm(f => ({ ...f, start_date: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary" />
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Due Date</label>
<input type="date" value={taskForm.due_date} onChange={e => setTaskForm(f => ({ ...f, due_date: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary" />
</div>
</div>
<div className="flex items-center justify-end gap-3 pt-4 border-t border-border">
{editingTask && canDeleteResource('task', editingTask) && (
<button onClick={() => handleDeleteTask(editingTask._id)}
className="px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-50 rounded-lg mr-auto">
Delete
</button>
)}
<button onClick={() => { setShowTaskModal(false); setEditingTask(null) }}
className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-surface-tertiary rounded-lg">
Cancel
</button>
<button onClick={handleTaskSave} disabled={!taskForm.title}
className="px-5 py-2 bg-brand-primary text-white rounded-lg text-sm font-medium hover:bg-brand-primary-light disabled:opacity-50 disabled:cursor-not-allowed shadow-sm">
{editingTask ? 'Save Changes' : 'Add Task'}
</button>
</div>
</div>
</Modal>
{/* ─── PROJECT EDIT MODAL ─── */}
<Modal
isOpen={showProjectModal}
onClose={() => setShowProjectModal(false)}
title="Edit Project"
size="md"
>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Name *</label>
<input type="text" value={projectForm.name} onChange={e => setProjectForm(f => ({ ...f, name: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
placeholder="Project name" />
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Description</label>
<textarea value={projectForm.description} onChange={e => setProjectForm(f => ({ ...f, description: e.target.value }))}
rows={3} className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary resize-none"
placeholder="Project description..." />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Brand</label>
<select value={projectForm.brand_id} onChange={e => setProjectForm(f => ({ ...f, brand_id: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary">
<option value="">Select brand</option>
{brands.map(b => <option key={b._id} value={b._id}>{b.icon} {b.name}</option>)}
</select>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Status</label>
<select value={projectForm.status} onChange={e => setProjectForm(f => ({ ...f, status: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary">
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Owner</label>
<select value={projectForm.owner_id} onChange={e => setProjectForm(f => ({ ...f, owner_id: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary">
<option value="">Unassigned</option>
{teamMembers.map(m => <option key={m._id} value={m._id}>{m.name}</option>)}
</select>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Start Date</label>
<input type="date" value={projectForm.start_date} onChange={e => setProjectForm(f => ({ ...f, start_date: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary" />
</div>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Due Date</label>
<input type="date" value={projectForm.due_date} onChange={e => setProjectForm(f => ({ ...f, due_date: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary" />
</div>
<div className="flex items-center justify-end gap-3 pt-4 border-t border-border">
<button onClick={() => setShowProjectModal(false)}
className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-surface-tertiary rounded-lg">
Cancel
</button>
<button onClick={handleProjectSave} disabled={!projectForm.name}
className="px-5 py-2 bg-brand-primary text-white rounded-lg text-sm font-medium hover:bg-brand-primary-light disabled:opacity-50 disabled:cursor-not-allowed shadow-sm">
Save Changes
</button>
</div>
</div>
</Modal>
{/* ─── DELETE TASK CONFIRMATION ─── */}
<Modal
isOpen={showDeleteConfirm}
@@ -639,22 +491,48 @@ export default function ProjectDetail() {
>
Are you sure you want to delete this task? This action cannot be undone.
</Modal>
{/* Project Edit Panel */}
{panelProject && (
<ProjectEditPanel
project={panelProject}
onClose={() => setPanelProject(null)}
onSave={handleProjectPanelSave}
onDelete={handleProjectPanelDelete}
brands={brands}
teamMembers={teamMembers}
/>
)}
{/* Task Detail Panel */}
{panelTask && (
<TaskDetailPanel
task={panelTask}
onClose={() => setPanelTask(null)}
onSave={handleTaskPanelSave}
onDelete={handleTaskPanelDelete}
projects={project ? [project] : []}
users={assignableUsers}
brands={brands}
/>
)}
</div>
)
}
// ─── Task Kanban Card ───────────────────────────────
function TaskKanbanCard({ task, canEdit, canDelete, onEdit, onDelete, onStatusChange, onDragStart, onDragEnd }) {
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 (
<div
draggable
onDragStart={(e) => onDragStart(e, task)}
draggable={canEdit}
onDragStart={(e) => canEdit && onDragStart(e, task)}
onDragEnd={onDragEnd}
className="bg-white rounded-lg border border-border p-3 group hover:border-brand-primary/30 hover:shadow-sm transition-all cursor-grab active:cursor-grabbing"
onClick={onClick}
className={`bg-white rounded-lg border border-border p-3 group hover:border-brand-primary/30 hover:shadow-sm transition-all cursor-pointer ${canEdit ? 'active:cursor-grabbing' : ''}`}
>
<div className="flex items-start gap-2">
<div className={`w-2 h-2 rounded-full ${priority.color} mt-1.5 shrink-0`} title={priority.label} />
@@ -679,20 +557,14 @@ function TaskKanbanCard({ task, canEdit, canDelete, onEdit, onDelete, onStatusCh
{(canEdit || canDelete) && (
<div className="flex items-center gap-1 mt-2 pt-2 border-t border-border-light opacity-0 group-hover:opacity-100 transition-opacity">
{canEdit && task.status !== 'done' && (
<button onClick={() => onStatusChange(task._id, task.status === 'todo' ? 'in_progress' : 'done')}
<button onClick={(e) => { e.stopPropagation(); onStatusChange(task._id, task.status === 'todo' ? 'in_progress' : 'done') }}
className="text-[10px] text-brand-primary hover:bg-brand-primary/10 px-2 py-0.5 rounded-full flex items-center gap-1">
<Check className="w-3 h-3" />
{task.status === 'todo' ? 'Start' : 'Complete'}
</button>
)}
{canEdit && (
<button onClick={onEdit}
className="text-[10px] text-text-tertiary hover:bg-surface-tertiary px-2 py-0.5 rounded-full flex items-center gap-1">
<Edit3 className="w-3 h-3" /> Edit
</button>
)}
{canDelete && (
<button onClick={onDelete}
<button onClick={(e) => { e.stopPropagation(); onDelete() }}
className="text-[10px] text-red-400 hover:bg-red-50 px-2 py-0.5 rounded-full flex items-center gap-1 ml-auto">
<Trash2 className="w-3 h-3" />
</button>
@@ -823,4 +695,3 @@ function GanttView({ tasks, project, onEditTask }) {
</div>
)
}