beetask/frontend/src/pages/TasksPage.tsx

832 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Task[]>([])
const [projects, setProjects] = useState<Project[]>([])
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<TaskStatus[]>([])
const [showAllColumns, setShowAllColumns] = useState(false)
const [showColumnSettings, setShowColumnSettings] = useState(false)
// Filters
const [searchQuery, setSearchQuery] = useState('')
const [selectedStatus, setSelectedStatus] = useState<TaskStatus | 'ALL'>('ALL')
const [selectedProjectId, setSelectedProjectId] = useState<number | 'ALL'>('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<TaskStatus>('NOT_STARTED')
const [modalInitialTab, setModalInitialTab] = useState<'details' | 'history'>('details')
const [editingTask, setEditingTask] = useState<Task | null>(null)
const [actionError, setActionError] = useState<string | null>(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 (
<div className="space-y-6 max-w-7xl mx-auto">
{/* Page Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<div className="flex items-center gap-2 mb-1.5 flex-wrap">
<h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight bg-gradient-to-r from-brand via-brand-strong to-accent bg-clip-text text-transparent">
Quadro de Tarefas
</h1>
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-semibold bg-brand/15 text-brand border border-brand/25 shadow-xs">
<span className="w-1.5 h-1.5 rounded-full bg-brand animate-pulse" />
{activeOrg?.name || 'Workspace'}
</span>
</div>
<p className="text-xs text-muted">
Planeje, execute e rastreie o tempo investido em cada entrega da equipe.
</p>
</div>
{/* View Switcher & New Task Button */}
<div className="flex items-center gap-2.5 flex-wrap">
{/* Refresh */}
<button
type="button"
onClick={loadData}
disabled={refreshing}
title="Recarregar tarefas"
className="p-2.5 rounded-xl border border-border-soft bg-surface/90 backdrop-blur-md text-muted hover:text-foreground hover:bg-surface-raised transition-all shadow-panel disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 ${refreshing ? 'animate-spin text-brand' : ''}`} />
</button>
{/* Kanban / List Toggle */}
<div className="flex items-center p-1 rounded-xl border border-border-soft bg-surface/90 backdrop-blur-md shadow-panel">
<button
type="button"
onClick={() => setViewMode('kanban')}
className={`flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg text-xs font-semibold transition-all ${
viewMode === 'kanban'
? 'bg-brand text-white shadow-md shadow-brand/20'
: 'text-muted hover:text-foreground'
}`}
>
<LayoutGrid className="w-3.5 h-3.5" />
<span>Kanban</span>
</button>
<button
type="button"
onClick={() => setViewMode('list')}
className={`flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg text-xs font-semibold transition-all ${
viewMode === 'list'
? 'bg-brand text-white shadow-md shadow-brand/20'
: 'text-muted hover:text-foreground'
}`}
>
<ListIcon className="w-3.5 h-3.5" />
<span>Lista</span>
</button>
</div>
{/* Kanban Columns Visibility Control */}
{viewMode === 'kanban' && (
<div className="relative">
<button
type="button"
onClick={() => setShowColumnSettings(!showColumnSettings)}
title="Gerenciar colunas do Kanban"
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl border text-xs font-medium transition-all shadow-panel ${
hiddenColumns.length > 0
? 'border-brand/40 bg-brand/10 text-brand'
: 'border-border-soft bg-surface/90 text-muted hover:text-foreground hover:bg-surface-raised'
}`}
>
<SlidersHorizontal className="w-3.5 h-3.5" />
<span>Colunas</span>
{hiddenColumns.length > 0 && (
<span className="px-1.5 py-0.2 rounded-full text-[10px] bg-brand text-white font-bold">
{hiddenColumns.length} oculta{hiddenColumns.length > 1 ? 's' : ''}
</span>
)}
</button>
{showColumnSettings && (
<>
<div
className="fixed inset-0 z-40"
onClick={() => setShowColumnSettings(false)}
/>
<div className="absolute right-0 top-11 z-50 w-64 rounded-2xl border border-border bg-surface-raised p-3 shadow-2xl space-y-2.5">
<div className="flex items-center justify-between pb-2 border-b border-border">
<span className="text-xs font-bold text-foreground">Visibilidade de Colunas</span>
{hiddenColumns.length > 0 && (
<button
type="button"
onClick={handleUnhideAllColumns}
className="text-[11px] text-brand hover:underline font-medium"
>
Restaurar todas
</button>
)}
</div>
{/* Toggle show all columns temporarily */}
<div className="flex items-center justify-between p-2 rounded-xl bg-surface border border-border-soft">
<div className="flex items-center gap-2">
{showAllColumns ? (
<Eye className="w-3.5 h-3.5 text-brand" />
) : (
<EyeOff className="w-3.5 h-3.5 text-muted" />
)}
<span className="text-xs font-medium text-foreground">Mostrar ocultas</span>
</div>
<input
type="checkbox"
checked={showAllColumns}
onChange={(e) => setShowAllColumns(e.target.checked)}
className="w-4 h-4 rounded text-brand border-border focus:ring-brand"
/>
</div>
{/* Individual columns list */}
<div className="space-y-1">
{COLUMNS.map((col) => {
const isHidden = hiddenColumns.includes(col.status)
return (
<button
key={col.status}
type="button"
onClick={() => {
if (isHidden) {
handleUnhideColumn(col.status)
} else {
handleHideColumn(col.status)
}
}}
className={`w-full flex items-center justify-between p-2 rounded-xl text-xs transition-colors text-left ${
isHidden
? 'bg-surface/50 text-muted hover:bg-surface'
: 'bg-surface hover:bg-surface-sunken text-foreground'
}`}
>
<div className="flex items-center gap-2">
<span
className={`w-2 h-2 rounded-full ${
isHidden ? 'bg-zinc-500' : 'bg-brand'
}`}
/>
<span>{col.label}</span>
</div>
{isHidden ? (
<span className="flex items-center gap-1 text-[11px] text-amber-500 font-medium">
<EyeOff className="w-3.5 h-3.5" />
<span>Oculta</span>
</span>
) : (
<span className="flex items-center gap-1 text-[11px] text-brand font-medium">
<Eye className="w-3.5 h-3.5" />
<span>Visível</span>
</span>
)}
</button>
)
})}
</div>
</div>
</>
)}
</div>
)}
{/* New Task Button */}
<button
type="button"
onClick={() => handleOpenCreateModal('NOT_STARTED')}
className="flex items-center gap-1.5 px-4 py-2.5 rounded-xl bg-brand text-white font-semibold text-xs hover:bg-brand-hover shadow-md shadow-brand/25 transition-all hover:-translate-y-0.5 active:translate-y-0"
>
<Plus className="w-4 h-4" />
<span>Nova Tarefa</span>
</button>
</div>
</div>
{/* Error alert if any */}
{actionError && (
<div className="flex items-center justify-between p-4 rounded-2xl bg-rose-500/10 border border-rose-500/25 text-rose-300 text-xs shadow-panel backdrop-blur-md">
<div className="flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-rose-400" />
<span>{actionError}</span>
</div>
<button
type="button"
onClick={() => setActionError(null)}
className="text-rose-400 hover:text-rose-200 text-xs font-bold"
>
Fechar
</button>
</div>
)}
{/* Scope Selector: Individual (Minhas Tarefas) vs Equipe (Workspace) */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 p-3 sm:p-4 rounded-3xl bg-surface/90 border border-border-soft backdrop-blur-md shadow-panel">
<div className="flex items-center gap-3">
<span className="text-xs font-bold text-muted uppercase tracking-wider hidden sm:inline">
Modo de Visualização:
</span>
<div className="inline-flex p-1 rounded-xl bg-surface-sunken border border-border-soft shadow-inner">
<button
type="button"
onClick={() => handleScopeChange('mine')}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-xs font-bold transition-all ${
viewScope === 'mine'
? 'bg-brand text-white shadow-md shadow-brand/25'
: 'text-muted hover:text-foreground'
}`}
>
<User className="w-3.5 h-3.5" />
<span>Individual (Minhas Tarefas)</span>
<span
className={`px-1.5 py-0.2 rounded-full text-[10px] font-bold ${
viewScope === 'mine' ? 'bg-white/20 text-white' : 'bg-surface text-muted'
}`}
>
{myTasks.length}
</span>
</button>
<button
type="button"
onClick={() => handleScopeChange('team')}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-xs font-bold transition-all ${
viewScope === 'team'
? 'bg-brand text-white shadow-md shadow-brand/25'
: 'text-muted hover:text-foreground'
}`}
>
<Users className="w-3.5 h-3.5" />
<span>Equipe (Workspace Completo)</span>
<span
className={`px-1.5 py-0.2 rounded-full text-[10px] font-bold ${
viewScope === 'team' ? 'bg-white/20 text-white' : 'bg-surface text-muted'
}`}
>
{tasks.length}
</span>
</button>
</div>
</div>
<div className="flex items-center gap-2 text-xs text-muted font-medium">
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse shrink-0" />
<span className="text-[11px] truncate">
{viewScope === 'mine'
? 'Exibindo tarefas atribuídas diretamente a você'
: 'Exibindo tarefas de todos os integrantes do workspace'}
</span>
</div>
</div>
{/* Filter and Search Bar with embedded status stats */}
<div className="p-3 sm:p-4 rounded-3xl border border-border-soft bg-surface/90 backdrop-blur-md flex flex-col lg:flex-row items-stretch lg:items-center justify-between gap-3 shadow-panel">
{/* Left side: Search Input & Quick Status Counts */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 flex-1 min-w-0">
{/* Search Input */}
<div className="relative w-full sm:w-72 lg:w-80 shrink-0">
<Search className="w-4 h-4 text-muted absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none" />
<input
type="text"
value={searchQuery}
onChange={(e) => 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 && (
<button
type="button"
onClick={() => setSearchQuery('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted hover:text-foreground text-xs"
>
×
</button>
)}
</div>
{/* Status Statistics (Icon + Number) */}
<div className="flex items-center gap-1.5 sm:gap-2 flex-wrap py-1">
{/* Total / All */}
<button
type="button"
onClick={() => setSelectedStatus('ALL')}
title="Todas as tarefas do escopo ativo"
className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-xl text-xs font-semibold border transition-all cursor-pointer ${
selectedStatus === 'ALL'
? 'bg-surface-sunken border-border text-foreground shadow-xs'
: 'bg-surface-raised/60 border-border-soft text-muted hover:text-foreground hover:bg-surface-raised'
}`}
>
<Filter className="w-3.5 h-3.5 text-muted shrink-0" />
<span className="font-mono text-xs">{scopeTasks.length}</span>
</button>
{/* A Fazer (NOT_STARTED) */}
<button
type="button"
onClick={() => setSelectedStatus(selectedStatus === 'NOT_STARTED' ? 'ALL' : 'NOT_STARTED')}
title="A Fazer"
className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-xl text-xs font-semibold border transition-all cursor-pointer ${
selectedStatus === 'NOT_STARTED'
? 'bg-zinc-500/15 border-zinc-500/40 text-foreground ring-1 ring-zinc-500/30'
: 'bg-surface-raised/60 border-border-soft text-muted hover:text-foreground hover:bg-surface-raised'
}`}
>
<CircleDot className="w-3.5 h-3.5 text-muted shrink-0" />
<span className="font-mono text-xs text-foreground">{notStartedCount}</span>
</button>
{/* Em Andamento (IN_PROGRESS) */}
<button
type="button"
onClick={() => setSelectedStatus(selectedStatus === 'IN_PROGRESS' ? 'ALL' : 'IN_PROGRESS')}
title="Em Andamento"
className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-xl text-xs font-semibold border transition-all cursor-pointer ${
selectedStatus === 'IN_PROGRESS'
? 'bg-teal-500/20 border-teal-500/50 text-teal-700 dark:text-teal-300 ring-1 ring-teal-500/30'
: 'bg-surface-raised/60 border-border-soft text-teal-700 dark:text-teal-400 hover:bg-surface-raised'
}`}
>
<Clock className="w-3.5 h-3.5 text-teal-700 dark:text-teal-400 shrink-0" />
<span className="font-mono text-xs">{inProgressCount}</span>
</button>
{/* Concluído (COMPLETED) */}
<button
type="button"
onClick={() => setSelectedStatus(selectedStatus === 'COMPLETED' ? 'ALL' : 'COMPLETED')}
title="Concluído"
className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-xl text-xs font-semibold border transition-all cursor-pointer ${
selectedStatus === 'COMPLETED'
? 'bg-emerald-500/20 border-emerald-500/50 text-emerald-700 dark:text-emerald-300 ring-1 ring-emerald-500/30'
: 'bg-surface-raised/60 border-border-soft text-emerald-700 dark:text-emerald-400 hover:bg-surface-raised'
}`}
>
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-700 dark:text-emerald-400 shrink-0" />
<span className="font-mono text-xs">{completedCount}</span>
</button>
{/* Cancelado (CANCELED) */}
<button
type="button"
onClick={() => setSelectedStatus(selectedStatus === 'CANCELED' ? 'ALL' : 'CANCELED')}
title="Cancelado"
className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-xl text-xs font-semibold border transition-all cursor-pointer ${
selectedStatus === 'CANCELED'
? 'bg-rose-500/20 border-rose-500/50 text-rose-700 dark:text-rose-300 ring-1 ring-rose-500/30'
: 'bg-surface-raised/60 border-border-soft text-rose-700 dark:text-rose-400 hover:bg-surface-raised'
}`}
>
<Ban className="w-3.5 h-3.5 text-rose-700 dark:text-rose-400 shrink-0" />
<span className="font-mono text-xs">{canceledCount}</span>
</button>
</div>
</div>
{/* Filters Group */}
<div className="flex items-center gap-2.5 flex-wrap w-full md:w-auto justify-end">
{/* Project Filter */}
{projects.length > 0 && (
<div className="w-48">
<CustomSelect
value={selectedProjectId}
onChange={(val) => setSelectedProjectId(val)}
leftIcon={<Folder className="w-3.5 h-3.5 text-brand" />}
options={[
{ value: 'ALL', label: 'Todos os Projetos' },
...projects.map((p) => ({
value: p.id,
label: p.name,
color: p.color || '#0f766e',
})),
]}
/>
</div>
)}
{/* Status Filter */}
<div className="w-44">
<CustomSelect
value={selectedStatus}
onChange={(val) => 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' },
]}
/>
</div>
</div>
</div>
{/* Main Content Area */}
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 py-8">
{[1, 2, 3, 4].map((i) => (
<div
key={i}
className="h-96 rounded-2xl border border-border bg-surface-sunken/40 animate-pulse p-4 space-y-3"
>
<div className="h-4 bg-surface-raised rounded-md w-1/3" />
<div className="h-28 bg-surface-raised rounded-xl" />
<div className="h-28 bg-surface-raised rounded-xl" />
</div>
))}
</div>
) : filteredTasks.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 px-4 text-center rounded-2xl border border-dashed border-border bg-surface/50">
<div className="w-14 h-14 rounded-2xl bg-brand/10 border border-brand/20 flex items-center justify-center text-brand mb-4">
<Sparkles className="w-7 h-7" />
</div>
<h3 className="text-base font-bold text-foreground mb-1">
Nenhuma tarefa encontrada
</h3>
<p className="text-xs text-muted max-w-sm mb-5 leading-relaxed">
{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!'}
</p>
<button
type="button"
onClick={() => handleOpenCreateModal('NOT_STARTED')}
className="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-brand text-white text-xs font-semibold hover:bg-brand-hover shadow-md shadow-brand/20 transition-all"
>
<Plus className="w-4 h-4" />
<span>Criar Primeira Tarefa</span>
</button>
</div>
) : viewMode === 'kanban' ? (
<TaskKanbanView
tasks={filteredTasks}
onToggleTimer={handleToggleTimer}
onStatusChange={handleStatusChange}
onEditTask={handleOpenEditModal}
onOpenHistory={handleOpenHistoryModal}
onDeleteTask={handleDeleteTask}
onAddTaskToStatus={handleOpenCreateModal}
activeTaskId={activeTask?.id}
activeTaskSeconds={activeSeconds}
hiddenColumns={hiddenColumns}
showAllColumns={showAllColumns}
onHideColumn={handleHideColumn}
onUnhideColumn={handleUnhideColumn}
/>
) : (
<TaskListView
tasks={filteredTasks}
onToggleTimer={handleToggleTimer}
onStatusChange={handleStatusChange}
onEditTask={handleOpenEditModal}
onOpenHistory={handleOpenHistoryModal}
onDeleteTask={handleDeleteTask}
activeTaskId={activeTask?.id}
activeTaskSeconds={activeSeconds}
/>
)}
{/* Task Creation & Edit Modal */}
{activeOrgId && (
<CreateTaskModal
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false)
setEditingTask(null)
}}
onSubmit={handleModalSubmit}
onDelete={handleDeleteTask}
organizationId={activeOrgId}
initialStatus={modalInitialStatus}
initialTask={editingTask}
initialTab={modalInitialTab}
/>
)}
</div>
)
}