import { useState, useEffect, useContext, useMemo } from 'react' import { Plus, CheckSquare, Trash2, Search, LayoutGrid, List, CalendarDays, X, SlidersHorizontal } from 'lucide-react' import { AppContext } from '../App' import { useAuth } from '../contexts/AuthContext' import { useLanguage } from '../i18n/LanguageContext' import { api, PRIORITY_CONFIG, getBrandColor } from '../utils/api' import TaskCard from '../components/TaskCard' import TaskDetailPanel from '../components/TaskDetailPanel' import TaskCalendarView from '../components/TaskCalendarView' import DatePresetPicker from '../components/DatePresetPicker' import EmptyState from '../components/EmptyState' import { useToast } from '../components/ToastContainer' import { format } from 'date-fns' const VIEW_MODES = ['board', 'list', 'calendar'] const VIEW_ICONS = { board: LayoutGrid, list: List, calendar: CalendarDays } export default function Tasks() { const { t } = useLanguage() const { currentUser, brands } = useContext(AppContext) const { user: authUser, canEditResource, canDeleteResource } = useAuth() const toast = useToast() // Data const [tasks, setTasks] = useState([]) const [projects, setProjects] = useState([]) const [loading, setLoading] = useState(true) // UI state const [viewMode, setViewMode] = useState('board') const [selectedTask, setSelectedTask] = useState(null) const [draggedTask, setDraggedTask] = useState(null) const [dragOverCol, setDragOverCol] = useState(null) // Filters const [searchQuery, setSearchQuery] = useState('') const [filterProject, setFilterProject] = useState('') const [filterBrand, setFilterBrand] = useState('') const [filterStatus, setFilterStatus] = useState([]) // empty = all const [filterPriority, setFilterPriority] = useState('') const [filterAssignee, setFilterAssignee] = useState('') const [filterCreator, setFilterCreator] = useState('') const [filterDateFrom, setFilterDateFrom] = useState('') const [filterDateTo, setFilterDateTo] = useState('') const [filterOverdue, setFilterOverdue] = useState(false) const [activePreset, setActivePreset] = useState('') const [showFilters, setShowFilters] = useState(false) // Assignable users & team const [assignableUsers, setAssignableUsers] = useState([]) const [users, setUsers] = useState([]) const isSuperadmin = authUser?.role === 'superadmin' useEffect(() => { loadTasks() }, [currentUser]) useEffect(() => { api.get('/users/assignable').then(res => setAssignableUsers(res.data || res || [])).catch(() => {}) api.get('/projects').then(res => setProjects(res.data || res || [])).catch(() => {}) if (isSuperadmin) { api.get('/team').then(res => setUsers(res.data || res || [])).catch(() => {}) } }, [isSuperadmin]) const loadTasks = async () => { try { const res = await api.get('/tasks') setTasks(res.data || res || []) } catch (err) { console.error('Failed to load tasks:', err) } finally { setLoading(false) } } // Determine if any filter is active const hasActiveFilters = searchQuery || filterProject || filterBrand || filterStatus.length > 0 || filterPriority || filterAssignee || filterCreator || filterDateFrom || filterDateTo || filterOverdue const clearFilters = () => { setSearchQuery('') setFilterProject('') setFilterBrand('') setFilterStatus([]) setFilterPriority('') setFilterAssignee('') setFilterCreator('') setFilterDateFrom('') setFilterDateTo('') setFilterOverdue(false) } // Client-side filtering const filteredTasks = useMemo(() => { return tasks.filter(task => { // Search if (searchQuery) { const q = searchQuery.toLowerCase() if (!(task.title || '').toLowerCase().includes(q) && !(task.description || '').toLowerCase().includes(q)) return false } // Project if (filterProject && String(task.project_id || task.projectId || '') !== String(filterProject)) return false // Brand if (filterBrand && String(task.brand_id || task.brandId || '') !== String(filterBrand)) return false // Status if (filterStatus.length > 0 && !filterStatus.includes(task.status)) return false // Priority if (filterPriority && task.priority !== filterPriority) return false // Assignee if (filterAssignee) { const assignee = task.assigned_to || task.assignedTo || task.assigned_to_id || task.assignedToId if (String(assignee || '') !== String(filterAssignee)) return false } // Creator if (filterCreator) { const creator = task.created_by_user_id || task.createdByUserId if (String(creator || '') !== String(filterCreator)) return false } // Date range if (filterDateFrom) { const dd = task.due_date || task.dueDate if (!dd || new Date(dd) < new Date(filterDateFrom)) return false } if (filterDateTo) { const dd = task.due_date || task.dueDate if (!dd || new Date(dd) > new Date(filterDateTo + 'T23:59:59')) return false } // Overdue if (filterOverdue) { const dd = task.due_date || task.dueDate if (!dd || new Date(dd) >= new Date() || task.status === 'done') return false } return true }) }, [tasks, searchQuery, filterProject, filterBrand, filterStatus, filterPriority, filterAssignee, filterCreator, filterDateFrom, filterDateTo, filterOverdue]) // ─── CRUD ────────────────────────────────────────── const handlePanelSave = async (taskId, data, files = []) => { try { if (taskId) { // Edit mode await api.patch(`/tasks/${taskId}`, data) toast.success(t('tasks.updated')) loadTasks() const updated = { ...selectedTask, ...data } setSelectedTask(updated) } else { // Create mode — create task then upload any pending files const newTask = await api.post('/tasks', { ...data, is_personal: false }) const newId = newTask.Id || newTask.id || newTask._id for (const file of files) { const fd = new FormData() fd.append('file', file) await api.upload(`/tasks/${newId}/attachments`, fd) } toast.success(t('tasks.created')) setSelectedTask(null) loadTasks() } } catch (err) { console.error('Save failed:', err) if (err.message?.includes('403')) { toast.error(t('tasks.canOnlyEditOwn')) } else { toast.error(t('common.saveFailed')) } } } const handlePanelDelete = async (taskId) => { try { await api.delete(`/tasks/${taskId}`) toast.success(t('tasks.deleted')) setSelectedTask(null) loadTasks() } catch (err) { console.error('Delete failed:', err) toast.error(t('common.deleteFailed')) } } const handleMove = async (taskId, newStatus) => { try { await api.patch(`/tasks/${taskId}`, { status: newStatus }) toast.success(t('tasks.statusUpdated')) loadTasks() } catch (err) { if (err.message?.includes('403')) toast.error(t('tasks.canOnlyEditOwn')) else toast.error(t('common.updateFailed')) } } const openTask = (task) => { setSelectedTask(task) } // ─── Drag and drop (Kanban) ───────────────────────── const handleDragStart = (e, task) => { setDraggedTask(task) e.dataTransfer.effectAllowed = 'move' setTimeout(() => { if (e.target) e.target.style.opacity = '0.4' }, 0) } const handleDragEnd = (e) => { e.target.style.opacity = '1' setDraggedTask(null) setDragOverCol(null) } const handleDragOver = (e, colStatus) => { e.preventDefault() e.dataTransfer.dropEffect = 'move' setDragOverCol(colStatus) } const handleDragLeave = (e) => { if (!e.currentTarget.contains(e.relatedTarget)) setDragOverCol(null) } const handleDrop = (e, colStatus) => { e.preventDefault() setDragOverCol(null) if (draggedTask && draggedTask.status !== colStatus) { handleMove(draggedTask._id || draggedTask.id, colStatus) } setDraggedTask(null) } // ─── Kanban columns ────────────────────────────────── const todoTasks = filteredTasks.filter(t => t.status === 'todo') const inProgressTasks = filteredTasks.filter(t => t.status === 'in_progress') const doneTasks = filteredTasks.filter(t => t.status === 'done') const columns = [ { label: t('tasks.todo'), status: 'todo', items: todoTasks, color: 'bg-gray-400' }, { label: t('tasks.in_progress'), status: 'in_progress', items: inProgressTasks, color: 'bg-blue-400' }, { label: t('tasks.done'), status: 'done', items: doneTasks, color: 'bg-emerald-400' }, ] // ─── List view sorting ──────────────────────────────── const [sortBy, setSortBy] = useState('due_date') const [sortDir, setSortDir] = useState('asc') const sortedListTasks = useMemo(() => { const priorityOrder = { urgent: 0, high: 1, medium: 2, low: 3 } const statusOrder = { todo: 0, in_progress: 1, done: 2 } return [...filteredTasks].sort((a, b) => { let cmp = 0 if (sortBy === 'due_date') { const da = a.due_date || a.dueDate || '' const db = b.due_date || b.dueDate || '' if (!da && !db) cmp = 0 else if (!da) cmp = 1 else if (!db) cmp = -1 else cmp = da.localeCompare(db) } else if (sortBy === 'priority') { cmp = (priorityOrder[a.priority] ?? 2) - (priorityOrder[b.priority] ?? 2) } else if (sortBy === 'status') { cmp = (statusOrder[a.status] ?? 0) - (statusOrder[b.status] ?? 0) } else if (sortBy === 'title') { cmp = (a.title || '').localeCompare(b.title || '') } return sortDir === 'asc' ? cmp : -cmp }) }, [filteredTasks, sortBy, sortDir]) const toggleSort = (col) => { if (sortBy === col) setSortDir(d => d === 'asc' ? 'desc' : 'asc') else { setSortBy(col); setSortDir('asc') } } // ─── Unique brands from tasks ───────────────────────── const taskBrands = useMemo(() => { const map = new Map() for (const t of tasks) { const bid = t.brand_id || t.brandId const bname = t.brand_name || t.brandName if (bid && bname) map.set(String(bid), bname) } return Array.from(map, ([id, name]) => ({ id, name })) }, [tasks]) // ─── Unique projects from tasks ─────────────────────── const taskProjects = useMemo(() => { const map = new Map() for (const t of tasks) { const pid = t.project_id || t.projectId const pname = t.project_name || t.projectName if (pid && pname) map.set(String(pid), pname) } return Array.from(map, ([id, name]) => ({ id, name })) }, [tasks]) if (loading) { return (
{[...Array(3)].map((_, i) =>
)}
) } return (
{/* ─── Toolbar ──────────────────────────────── */}
{/* Search */}
setSearchQuery(e.target.value)} placeholder={t('tasks.search')} className="w-full pl-9 pr-3 py-1.5 text-sm border border-border rounded-lg bg-surface-primary text-text-primary focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary" /> {searchQuery && ( )}
{/* View switcher */}
{VIEW_MODES.map(mode => { const Icon = VIEW_ICONS[mode] return ( ) })}
{/* Filter toggle */} {/* Task count */} {filteredTasks.length}{hasActiveFilters ? ` ${t('tasks.of')} ${tasks.length}` : ''} {filteredTasks.length !== 1 ? t('tasks.tasks') : t('tasks.task')}
{/* ─── Filter Bar ───────────────────────────── */} {showFilters && (
{/* Project */} {/* Brand */} {/* Status chips */}
{['todo', 'in_progress', 'done'].map(s => { const active = filterStatus.length === 0 || filterStatus.includes(s) return ( ) })}
{/* Priority */} {/* Assignee */} {/* Creator (superadmin only) */} {isSuperadmin && ( )} {/* Date presets */} { setFilterDateFrom(from); setFilterDateTo(to); setActivePreset(key) }} onClear={() => { setFilterDateFrom(''); setFilterDateTo(''); setActivePreset('') }} /> {/* Date range */}
{ setFilterDateFrom(e.target.value); setActivePreset('') }} className="px-2 py-1.5 text-xs border border-border rounded-lg bg-white text-text-primary focus:outline-none focus:ring-1 focus:ring-brand-primary/20" title={t('posts.periodFrom')} /> - { setFilterDateTo(e.target.value); setActivePreset('') }} className="px-2 py-1.5 text-xs border border-border rounded-lg bg-white text-text-primary focus:outline-none focus:ring-1 focus:ring-brand-primary/20" title={t('posts.periodTo')} />
{/* Overdue toggle */} {/* Clear all */} {hasActiveFilters && ( )}
)} {/* ─── Views ────────────────────────────────── */} {filteredTasks.length === 0 ? ( { setSelectedTask({ title: '', description: '', priority: 'medium', start_date: '', due_date: '', status: 'todo', assigned_to: '', project_id: '' }) } : null} secondaryActionLabel={hasActiveFilters ? t('tasks.clearFilters') : null} onSecondaryAction={clearFilters} /> ) : ( <> {/* ─── Board View ──────────────────────── */} {viewMode === 'board' && (
{columns.map(col => { const isOver = dragOverCol === col.status && draggedTask?.status !== col.status return (

{col.label}

{col.items.length}
handleDragOver(e, col.status)} onDragLeave={handleDragLeave} onDrop={(e) => handleDrop(e, col.status)} > {col.items.length === 0 ? (
{isOver ? t('posts.dropHere') : t('tasks.noTasks')}
) : ( col.items.map(task => { const canEdit = canEditResource('task', task) const canDelete = canDeleteResource('task', task) return (
canEdit && handleDragStart(e, task)} onDragEnd={handleDragEnd} className={canEdit ? 'cursor-grab active:cursor-grabbing' : ''} >
openTask(task)}> {canDelete && (
)}
) }) )}
) })}
)} {/* ─── List View ───────────────────────── */} {viewMode === 'list' && (
{sortedListTasks.map(task => { const priority = PRIORITY_CONFIG[task.priority] || PRIORITY_CONFIG.medium const dueDate = task.due_date || task.dueDate const isOverdue = dueDate && new Date(dueDate) < new Date() && task.status !== 'done' const projectName = task.project_name || task.projectName const brandName = task.brand_name || task.brandName const assignedName = task.assigned_name || task.assignedName const statusLabels = { todo: t('tasks.todo'), in_progress: t('tasks.in_progress'), done: t('tasks.done') } const statusColors = { todo: 'bg-gray-100 text-gray-600', in_progress: 'bg-blue-100 text-blue-700', done: 'bg-emerald-100 text-emerald-700' } return ( openTask(task)} className="border-b border-border-light hover:bg-surface-secondary/30 cursor-pointer transition-colors group" > ) })}
toggleSort('title')} > {t('tasks.taskTitle')} {sortBy === 'title' && (sortDir === 'asc' ? '↑' : '↓')} {t('tasks.project')} {t('tasks.brand')} toggleSort('status')} > {t('tasks.status')} {sortBy === 'status' && (sortDir === 'asc' ? '↑' : '↓')} {t('tasks.assignee')} toggleSort('due_date')} > {t('tasks.dueDate')} {sortBy === 'due_date' && (sortDir === 'asc' ? '↑' : '↓')} toggleSort('priority')} > {t('tasks.priority')} {sortBy === 'priority' && (sortDir === 'asc' ? '↑' : '↓')}
{task.title} {(task.comment_count || task.commentCount) > 0 && ( 💬 {task.comment_count || task.commentCount} )} {projectName || '—'} {brandName ? ( {brandName} ) : } {statusLabels[task.status] || task.status} {assignedName || t('common.unassigned')} {dueDate ? ( {format(new Date(dueDate), 'MMM d, yyyy')} ) : } {priority.label}
)} {/* ─── Calendar View ───────────────────── */} {viewMode === 'calendar' && ( )} )} {/* ─── Task Detail Side Panel ──────────────── */} {selectedTask && ( setSelectedTask(null)} onSave={handlePanelSave} onDelete={canDeleteResource('task', selectedTask) ? handlePanelDelete : undefined} projects={projects} users={assignableUsers} brands={brands} /> )}
) }