import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react' import { useSearchParams } from 'react-router-dom' import { LayoutGrid, List as ListIcon, Plus, Search, Filter, RefreshCw, Folder, Users, User, CheckCircle2, Clock, Sparkles, AlertCircle, CircleDot, Ban, Eye, EyeOff, SlidersHorizontal, } from 'lucide-react' import { useAuth } from '../contexts/AuthContext' import { useTimer } from '../contexts/TimerContext' import { taskService } from '../services/taskService' import { projectService } from '../services/projectService' import type { Task, TaskStatus, Project } from '../types/task' import { TaskKanbanView, COLUMNS } from '../components/tasks/TaskKanbanView' import { TaskListView } from '../components/tasks/TaskListView' import { CreateTaskModal } from '../components/tasks/CreateTaskModal' import { CustomSelect } from '../components/ui/Select' import { getLocalPreferences, syncPreferencesFromBackend, savePreferencesToBackend, } from '../lib/userPreferences' export const TasksPage: React.FC = () => { const { activeOrg, user } = useAuth() const { activeTask, activeSeconds, startTimer, stopTimer, syncWithTasks } = useTimer() const syncWithTasksRef = useRef(syncWithTasks) useEffect(() => { syncWithTasksRef.current = syncWithTasks }, [syncWithTasks]) const [tasks, setTasks] = useState([]) const [projects, setProjects] = useState([]) const [loading, setLoading] = useState(true) const [refreshing, setRefreshing] = useState(false) const [viewMode, setViewMode] = useState<'kanban' | 'list'>('kanban') // Kanban column visibility preferences const [hiddenColumns, setHiddenColumns] = useState([]) const [showAllColumns, setShowAllColumns] = useState(false) const [showColumnSettings, setShowColumnSettings] = useState(false) // Filters const [searchQuery, setSearchQuery] = useState('') const [selectedStatus, setSelectedStatus] = useState('ALL') const [selectedProjectId, setSelectedProjectId] = useState('ALL') // Scope Toggle State: 'mine' (Individual) vs 'team' (Equipe) const [searchParams, setSearchParams] = useSearchParams() const initialScope = (searchParams.get('scope') as 'mine' | 'team') || (localStorage.getItem('beetask_tasks_scope') as 'mine' | 'team') || 'mine' const [viewScope, setViewScope] = useState<'mine' | 'team'>(initialScope) const handleScopeChange = (newScope: 'mine' | 'team') => { setViewScope(newScope) try { localStorage.setItem('beetask_tasks_scope', newScope) } catch (_) {} setSearchParams( (prev) => { const next = new URLSearchParams(prev) next.set('scope', newScope) return next }, { replace: true } ) } // Modal state const [isModalOpen, setIsModalOpen] = useState(false) const [modalInitialStatus, setModalInitialStatus] = useState('NOT_STARTED') const [modalInitialTab, setModalInitialTab] = useState<'details' | 'history'>('details') const [editingTask, setEditingTask] = useState(null) const [actionError, setActionError] = useState(null) // Load user preferences for hidden columns from backend useEffect(() => { if (!user?.email) return const local = getLocalPreferences(user.email) if (local.kanban?.hiddenColumns) { setHiddenColumns(local.kanban.hiddenColumns) } syncPreferencesFromBackend(user.email) .then((prefs) => { if (prefs.kanban?.hiddenColumns) { setHiddenColumns(prefs.kanban.hiddenColumns) } }) .catch((err) => console.warn('Could not load kanban preferences', err)) }, [user?.email]) // Save hidden columns to backend const updateHiddenColumns = useCallback( (newHidden: TaskStatus[]) => { setHiddenColumns(newHidden) if (!user?.email) return const current = getLocalPreferences(user.email) const updated = { ...current, kanban: { ...current.kanban, hiddenColumns: newHidden, }, } savePreferencesToBackend(user.email, updated).catch((err) => console.error('Failed to save hidden columns to backend', err) ) }, [user?.email] ) const handleHideColumn = useCallback( (status: TaskStatus) => { if (!hiddenColumns.includes(status)) { updateHiddenColumns([...hiddenColumns, status]) } }, [hiddenColumns, updateHiddenColumns] ) const handleUnhideColumn = useCallback( (status: TaskStatus) => { updateHiddenColumns(hiddenColumns.filter((s) => s !== status)) }, [hiddenColumns, updateHiddenColumns] ) const handleUnhideAllColumns = useCallback(() => { updateHiddenColumns([]) }, [updateHiddenColumns]) const activeOrgId = activeOrg?.id // Load tasks & projects for current organization const loadData = useCallback(async () => { if (!activeOrgId) return try { setRefreshing(true) const [tasksRes, projsRes] = await Promise.all([ taskService.listTasks({ organizationId: activeOrgId }), projectService.listProjects(activeOrgId).catch(() => []), ]) setTasks(tasksRes || []) setProjects(projsRes || []) syncWithTasksRef.current(tasksRes || []) } catch (err) { console.error('Failed to load tasks', err) setActionError('Falha ao carregar tarefas. Tente atualizar a página.') } finally { setLoading(false) setRefreshing(false) } }, [activeOrgId]) useEffect(() => { loadData() }, [loadData]) // User's own tasks const myTasks = useMemo(() => { return tasks.filter((t) => { if (!user?.email) return false return Boolean( t.user?.email && t.user.email.toLowerCase() === user.email.toLowerCase() ) }) }, [tasks, user]) // Scope tasks based on viewScope const scopeTasks = viewScope === 'mine' ? myTasks : tasks // Filter tasks client-side for ultra-responsive search const filteredTasks = useMemo(() => { return scopeTasks.filter((task) => { // Status filter if (selectedStatus !== 'ALL' && task.status !== selectedStatus) { return false } // Project filter if (selectedProjectId !== 'ALL' && task.project?.id !== selectedProjectId) { return false } // Search text filter if (searchQuery.trim()) { const query = searchQuery.toLowerCase() const matchesTitle = task.title.toLowerCase().includes(query) const matchesDesc = task.description?.toLowerCase().includes(query) const matchesProject = task.project?.name.toLowerCase().includes(query) const matchesCategory = task.category?.name.toLowerCase().includes(query) if (!matchesTitle && !matchesDesc && !matchesProject && !matchesCategory) { return false } } return true }) }, [scopeTasks, selectedStatus, selectedProjectId, searchQuery]) // Timer play/pause handler const handleToggleTimer = async (task: Task) => { setActionError(null) const isCurrentlyRunning = activeTask?.id === task.id try { if (isCurrentlyRunning) { const stopped = await stopTimer(task) if (stopped) { setTasks((prev) => prev.map((t) => (t.id === stopped.id ? stopped : t))) } } else { const started = await startTimer(task) setTasks((prev) => prev.map((t) => (t.id === started.id ? started : t))) } } catch (err: unknown) { console.error('Timer action failed', err) setActionError( err instanceof Error ? err.message : 'Falha ao acionar cronômetro da tarefa.' ) } } // Status change handler const handleStatusChange = async (task: Task, newStatus: TaskStatus) => { setActionError(null) try { // Optimistic update setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, status: newStatus } : t)) ) const updated = await taskService.updateStatus(task.id, newStatus) setTasks((prev) => prev.map((t) => (t.id === updated.id ? updated : t))) } catch (err: unknown) { console.error('Failed to update status', err) setActionError('Falha ao atualizar status da tarefa.') loadData() // Revert to server state } } // Create or Update task modal submit const handleModalSubmit = async (taskData: { title: string description: string status: TaskStatus projectId?: number categoryId?: number assigneeUserId?: number history?: import('../types/task').TaskHistoryItemInput[] externalUrl?: string }) => { if (!activeOrgId) return setActionError(null) if (editingTask) { const updated = await taskService.updateTask(editingTask.id, { title: taskData.title, description: taskData.description, status: taskData.status, projectId: taskData.projectId, categoryId: taskData.categoryId, assigneeUserId: taskData.assigneeUserId, organizationId: activeOrgId, history: taskData.history, externalUrl: taskData.externalUrl, }) setTasks((prev) => prev.map((t) => (t.id === updated.id ? updated : t))) syncWithTasks([updated]) setEditingTask(null) } else { const created = await taskService.createTask({ title: taskData.title, description: taskData.description, status: taskData.status, projectId: taskData.projectId, categoryId: taskData.categoryId, assigneeUserId: taskData.assigneeUserId, organizationId: activeOrgId, externalUrl: taskData.externalUrl, }) setTasks((prev) => [created, ...prev]) } } const handleOpenCreateModal = (initialStatus: TaskStatus = 'NOT_STARTED') => { setEditingTask(null) setModalInitialStatus(initialStatus) setModalInitialTab('details') setIsModalOpen(true) } const handleOpenEditModal = useCallback((task: Task) => { setEditingTask(task) setModalInitialTab('details') setIsModalOpen(true) }, []) const handleOpenHistoryModal = useCallback((task: Task) => { setEditingTask(task) setModalInitialTab('history') setIsModalOpen(true) }, []) const handleDeleteTask = useCallback( async (task: Task) => { setActionError(null) try { if (activeTask?.id === task.id) { await stopTimer(task) } await taskService.deleteTask(task.id) setTasks((prev) => prev.filter((t) => t.id !== task.id)) if (editingTask?.id === task.id) { setEditingTask(null) setIsModalOpen(false) } } catch (err: unknown) { console.error('Failed to delete task', err) setActionError( err instanceof Error ? err.message : 'Falha ao excluir a tarefa.' ) } }, [activeTask, stopTimer, editingTask] ) // Summary counts based on active scope const notStartedCount = scopeTasks.filter((t) => t.status === 'NOT_STARTED').length const inProgressCount = scopeTasks.filter((t) => t.status === 'IN_PROGRESS').length const completedCount = scopeTasks.filter((t) => t.status === 'COMPLETED').length const canceledCount = scopeTasks.filter((t) => t.status === 'CANCELED').length return (
{/* Page Header */}

Quadro de Tarefas

{activeOrg?.name || 'Workspace'}

Planeje, execute e rastreie o tempo investido em cada entrega da equipe.

{/* View Switcher & New Task Button */}
{/* Refresh */} {/* Kanban / List Toggle */}
{/* Kanban Columns Visibility Control */} {viewMode === 'kanban' && (
{showColumnSettings && ( <>
setShowColumnSettings(false)} />
Visibilidade de Colunas {hiddenColumns.length > 0 && ( )}
{/* Toggle show all columns temporarily */}
{showAllColumns ? ( ) : ( )} Mostrar ocultas
setShowAllColumns(e.target.checked)} className="w-4 h-4 rounded text-brand border-border focus:ring-brand" />
{/* Individual columns list */}
{COLUMNS.map((col) => { const isHidden = hiddenColumns.includes(col.status) return ( ) })}
)}
)} {/* New Task Button */}
{/* Error alert if any */} {actionError && (
{actionError}
)} {/* Scope Selector: Individual (Minhas Tarefas) vs Equipe (Workspace) */}
Modo de Visualização:
{viewScope === 'mine' ? 'Exibindo tarefas atribuídas diretamente a você' : 'Exibindo tarefas de todos os integrantes do workspace'}
{/* Filter and Search Bar with embedded status stats */}
{/* Left side: Search Input & Quick Status Counts */}
{/* Search Input */}
setSearchQuery(e.target.value)} placeholder="Buscar por título, projeto, categoria..." className="w-full pl-9 pr-3.5 py-2 rounded-xl border border-border-soft bg-surface-raised/80 text-xs text-foreground placeholder-muted focus:outline-none focus:ring-2 focus:ring-brand/40 focus:border-brand transition-all" /> {searchQuery && ( )}
{/* Status Statistics (Icon + Number) */}
{/* Total / All */} {/* A Fazer (NOT_STARTED) */} {/* Em Andamento (IN_PROGRESS) */} {/* Concluído (COMPLETED) */} {/* Cancelado (CANCELED) */}
{/* Filters Group */}
{/* Project Filter */} {projects.length > 0 && (
setSelectedProjectId(val)} leftIcon={} options={[ { value: 'ALL', label: 'Todos os Projetos' }, ...projects.map((p) => ({ value: p.id, label: p.name, color: p.color || '#0f766e', })), ]} />
)} {/* Status Filter */}
setSelectedStatus(val as TaskStatus | 'ALL')} options={[ { value: 'ALL', label: 'Todos os Status' }, { value: 'NOT_STARTED', label: 'A Fazer', color: '#71717a' }, { value: 'IN_PROGRESS', label: 'Em Andamento', color: '#14b8a6' }, { value: 'COMPLETED', label: 'Concluído', color: '#10b981' }, { value: 'CANCELED', label: 'Cancelado', color: '#f43f5e' }, ]} />
{/* Main Content Area */} {loading ? (
{[1, 2, 3, 4].map((i) => (
))}
) : filteredTasks.length === 0 ? (

Nenhuma tarefa encontrada

{searchQuery || selectedStatus !== 'ALL' || selectedProjectId !== 'ALL' ? 'Nenhum resultado corresponde aos filtros selecionados. Tente limpar os filtros para ver mais.' : 'Você ainda não possui tarefas criadas nesta organização. Comece agora mesmo organizando sua primeira entrega!'}

) : viewMode === 'kanban' ? ( ) : ( )} {/* Task Creation & Edit Modal */} {activeOrgId && ( { setIsModalOpen(false) setEditingTask(null) }} onSubmit={handleModalSubmit} onDelete={handleDeleteTask} organizationId={activeOrgId} initialStatus={modalInitialStatus} initialTask={editingTask} initialTab={modalInitialTab} /> )}
) }