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 (
| 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} |