refactor: extract shared task page logic into useTaskPage hook

Eliminates code duplication between TaskListPage and TaskBoardPage by
extracting common state, effects, and handlers into a custom hook. Also
fixes bug where TaskBoardPage's mergeProjects was omitting color/icon.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gabriel Santana de Menezes 2026-05-21 10:41:07 -04:00
parent 119d896a59
commit 5a71aba13f
3 changed files with 182 additions and 230 deletions

View file

@ -0,0 +1,152 @@
import { useEffect, useRef, useState } from 'react'
import { taskService } from '../services/taskService'
import type { Project } from '../services/projectService'
import { projectService } from '../services/projectService'
import { categoryService } from '../services/categoryService'
import { organizationService } from '../services/organizationService'
import type { Category } from '../types/category'
import type { TaskAssignee, TaskResponse } from '../types/task'
import { DEFAULT_TASK_FILTERS, type TaskFilters } from '../components/taskFilters'
import { useAuth } from '../contexts/AuthContext'
interface UseTaskPageOptions {
fetchProjects?: boolean
}
interface UseTaskPageResult {
tasks: TaskResponse[]
isLoading: boolean
projects: Project[]
categories: Category[]
assignees: TaskAssignee[]
filters: TaskFilters
currentUserAssigneeId: number | null
loadTasks: (filters: TaskFilters, silent?: boolean) => Promise<void>
handleFiltersChange: (newFilters: TaskFilters) => void
}
function mergeProjects(existing: Project[], tasks: TaskResponse[]) {
const byId = new Map(existing.map((project) => [project.id, project]))
tasks.forEach((task) => {
if (task.project) {
byId.set(task.project.id, {
id: task.project.id,
name: task.project.name,
color: task.project.color,
icon: task.project.icon,
})
}
})
return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name))
}
function extractAssignees(tasks: TaskResponse[]) {
const byId = new Map<number, TaskAssignee>()
tasks.forEach((task) => {
if (task.user) {
byId.set(task.user.id, task.user)
}
})
return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name))
}
export function useTaskPage({ fetchProjects = false }: UseTaskPageOptions = {}): UseTaskPageResult {
const [tasks, setTasks] = useState<TaskResponse[]>([])
const [isLoading, setIsLoading] = useState(true)
const [projects, setProjects] = useState<Project[]>([])
const [categories, setCategories] = useState<Category[]>([])
const [assignees, setAssignees] = useState<TaskAssignee[]>([])
const [filters, setFilters] = useState<TaskFilters>(DEFAULT_TASK_FILTERS)
const [currentUserAssigneeId, setCurrentUserAssigneeId] = useState<number | null>(null)
const { activeOrg, user } = useAuth()
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const loadTasks = async (currentFilters: TaskFilters, silent = false) => {
if (!activeOrg) return
if (!silent) setIsLoading(true)
try {
const response = await taskService.getTasks({
organizationId: activeOrg.id,
text: currentFilters.searchQuery || undefined,
projectIds: currentFilters.projectIds.length > 0 ? currentFilters.projectIds : undefined,
statuses: currentFilters.statuses.length > 0 ? currentFilters.statuses : undefined,
categoryIds: currentFilters.categoryIds.length > 0 ? currentFilters.categoryIds : undefined,
userIds: currentFilters.userIds.length > 0 ? currentFilters.userIds : undefined,
})
setTasks(response)
setProjects((current) => mergeProjects(current, response))
setAssignees((current) => {
const byId = new Map(current.map((assignee) => [assignee.id, assignee]))
extractAssignees(response).forEach((assignee) => byId.set(assignee.id, assignee))
return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name))
})
} catch (error) {
console.error('Erro ao carregar tarefas', error)
} finally {
setIsLoading(false)
}
}
useEffect(() => {
if (!activeOrg) return
const init = async () => {
setFilters(DEFAULT_TASK_FILTERS)
setCategories([])
const promises: Promise<any>[] = [organizationService.listMembers(activeOrg.id)]
promises.push(categoryService.listByOrganization(activeOrg.id).then(setCategories).catch(() => {}))
if (fetchProjects) {
promises.push(projectService.listByOrganization(activeOrg.id).then(setProjects).catch(() => {}))
}
const [members] = await Promise.all(promises)
const assigneesData = members.map((m: any) => ({
id: m.userId,
name: m.userName,
email: m.userEmail,
photo: m.userPhoto ?? null,
}))
setAssignees(assigneesData)
const currentMember = members.find((m: any) => m.userEmail === user?.email)
const userId = currentMember?.userId ?? null
setCurrentUserAssigneeId(userId)
const initialFilters = userId
? { ...DEFAULT_TASK_FILTERS, userIds: [userId] }
: DEFAULT_TASK_FILTERS
setFilters(initialFilters)
void loadTasks(initialFilters)
}
void init()
}, [activeOrg, user?.email, fetchProjects])
const handleFiltersChange = (newFilters: TaskFilters) => {
setFilters(newFilters)
if (debounceRef.current) clearTimeout(debounceRef.current)
debounceRef.current = setTimeout(() => {
void loadTasks(newFilters)
}, 300)
}
return {
tasks,
isLoading,
projects,
categories,
assignees,
filters,
currentUserAssigneeId,
loadTasks,
handleFiltersChange,
}
}

View file

@ -10,21 +10,16 @@ import {
Plus,
XCircle,
} from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { taskService } from '../services/taskService'
import type { Project } from '../services/projectService'
import { categoryService } from '../services/categoryService'
import { organizationService } from '../services/organizationService'
import type { Category } from '../types/category'
import type { TaskStatus } from '../types/task'
import { cn } from '../lib/utils'
import type { TaskAssignee, TaskResponse, TaskStatus } from '../types/task'
import { TaskTimer } from '../components/TaskTimer'
import { TaskFilterBar } from '../components/TaskFilterBar'
import { DEFAULT_TASK_FILTERS, type TaskFilters } from '../components/taskFilters'
import { CategoryBadge } from '../components/CategoryBadge'
import { useAuth } from '../contexts/AuthContext'
import { useTaskPage } from '../hooks/useTaskPage'
const COLUMNS: { id: TaskStatus; label: string; icon: LucideIcon; color: string }[] = [
{ id: 'NOT_STARTED', label: 'Pendente', icon: AlertCircle, color: 'text-text-muted' },
@ -33,111 +28,21 @@ const COLUMNS: { id: TaskStatus; label: string; icon: LucideIcon; color: string
{ id: 'CANCELED', label: 'Cancelada', icon: XCircle, color: 'text-danger' },
]
function mergeProjects(existing: Project[], tasks: TaskResponse[]) {
const byId = new Map(existing.map((project) => [project.id, project]))
tasks.forEach((task) => {
if (task.project) {
byId.set(task.project.id, { id: task.project.id, name: task.project.name })
}
})
return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name))
}
function extractAssignees(tasks: TaskResponse[]) {
const byId = new Map<number, TaskAssignee>()
tasks.forEach((task) => {
if (task.user) {
byId.set(task.user.id, task.user)
}
})
return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name))
}
export function TaskBoardPage() {
const [tasks, setTasks] = useState<TaskResponse[]>([])
const [isLoading, setIsLoading] = useState(true)
const [projects, setProjects] = useState<Project[]>([])
const [categories, setCategories] = useState<Category[]>([])
const [assignees, setAssignees] = useState<TaskAssignee[]>([])
const [filters, setFilters] = useState<TaskFilters>(DEFAULT_TASK_FILTERS)
const [currentUserAssigneeId, setCurrentUserAssigneeId] = useState<number | null>(null)
const navigate = useNavigate()
const { activeOrg, user } = useAuth()
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const loadTasks = async (currentFilters: TaskFilters, silent = false) => {
if (!activeOrg) return
if (!silent) setIsLoading(true)
try {
const response = await taskService.getTasks({
organizationId: activeOrg.id,
text: currentFilters.searchQuery || undefined,
projectIds: currentFilters.projectIds.length > 0 ? currentFilters.projectIds : undefined,
statuses: currentFilters.statuses.length > 0 ? currentFilters.statuses : undefined,
categoryIds: currentFilters.categoryIds.length > 0 ? currentFilters.categoryIds : undefined,
userIds: currentFilters.userIds.length > 0 ? currentFilters.userIds : undefined,
})
setTasks(response)
setProjects((current) => mergeProjects(current, response))
setAssignees((current) => {
const byId = new Map(current.map((assignee) => [assignee.id, assignee]))
extractAssignees(response).forEach((assignee) => byId.set(assignee.id, assignee))
return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name))
})
} catch (error) {
console.error('Erro ao carregar tarefas', error)
} finally {
setIsLoading(false)
}
}
useEffect(() => {
if (!activeOrg) return
const init = async () => {
setFilters(DEFAULT_TASK_FILTERS)
setProjects([])
setAssignees([])
setCategories([])
const [members] = await Promise.all([
organizationService.listMembers(activeOrg.id),
categoryService.listByOrganization(activeOrg.id).then(setCategories).catch(() => {}),
])
const assigneesData = members.map((m) => ({
id: m.userId,
name: m.userName,
email: m.userEmail,
photo: m.userPhoto ?? null,
}))
setAssignees(assigneesData)
const currentMember = members.find((m) => m.userEmail === user?.email)
const userId = currentMember?.userId ?? null
setCurrentUserAssigneeId(userId)
const initialFilters = userId
? { ...DEFAULT_TASK_FILTERS, userIds: [userId] }
: DEFAULT_TASK_FILTERS
setFilters(initialFilters)
void loadTasks(initialFilters)
}
void init()
}, [activeOrg])
const handleFiltersChange = (newFilters: TaskFilters) => {
setFilters(newFilters)
if (debounceRef.current) clearTimeout(debounceRef.current)
debounceRef.current = setTimeout(() => {
void loadTasks(newFilters)
}, 300)
}
const { user } = useAuth()
const {
tasks,
isLoading,
projects,
categories,
assignees,
filters,
currentUserAssigneeId,
loadTasks,
handleFiltersChange,
} = useTaskPage()
const onDragEnd = async (result: DropResult) => {
const { destination, source, draggableId } = result
@ -153,13 +58,9 @@ export function TaskBoardPage() {
const taskId = Number.parseInt(draggableId)
const newStatus = destination.droppableId as TaskStatus
const updatedTasks = tasks.map((t) =>
t.id === taskId ? { ...t, status: newStatus } : t,
)
setTasks(updatedTasks)
try {
await taskService.updateTaskStatus(taskId, newStatus)
await loadTasks(filters, true)
} catch (error) {
console.error('Erro ao atualizar status da tarefa', error)
void loadTasks(filters, true)

View file

@ -11,23 +11,17 @@ import {
Tag,
XCircle,
} from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { taskService } from '../services/taskService'
import type { Project } from '../services/projectService'
import { projectService } from '../services/projectService'
import { categoryService } from '../services/categoryService'
import { organizationService } from '../services/organizationService'
import type { Category } from '../types/category'
import type { TaskResponse } from '../types/task'
import { cn } from '../lib/utils'
import type { TaskAssignee, TaskResponse } from '../types/task'
import { TaskTimer } from '../components/TaskTimer'
import { TaskFilterBar } from '../components/TaskFilterBar'
import { DEFAULT_TASK_FILTERS, type TaskFilters } from '../components/taskFilters'
import { DEFAULT_TASK_FILTERS } from '../components/taskFilters'
import { CategoryBadge } from '../components/CategoryBadge'
import { UserAvatar } from '../components/UserAvatar'
import { useAuth } from '../contexts/AuthContext'
import { useTaskPage } from '../hooks/useTaskPage'
function getStatusConfig(status: TaskResponse['status']) {
switch (status) {
@ -58,115 +52,20 @@ function getStatusConfig(status: TaskResponse['status']) {
}
}
function mergeProjects(existing: Project[], tasks: TaskResponse[]) {
const byId = new Map(existing.map((project) => [project.id, project]))
tasks.forEach((task) => {
if (task.project) {
byId.set(task.project.id, {
id: task.project.id,
name: task.project.name,
color: task.project.color,
icon: task.project.icon,
})
}
})
return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name))
}
function extractAssignees(tasks: TaskResponse[]) {
const byId = new Map<number, TaskAssignee>()
tasks.forEach((task) => {
if (task.user) {
byId.set(task.user.id, task.user)
}
})
return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name))
}
export function TaskListPage() {
const [tasks, setTasks] = useState<TaskResponse[]>([])
const [isLoading, setIsLoading] = useState(true)
const [projects, setProjects] = useState<Project[]>([])
const [categories, setCategories] = useState<Category[]>([])
const [assignees, setAssignees] = useState<TaskAssignee[]>([])
const [filters, setFilters] = useState<TaskFilters>(DEFAULT_TASK_FILTERS)
const [currentUserAssigneeId, setCurrentUserAssigneeId] = useState<number | null>(null)
const navigate = useNavigate()
const { activeOrg, user } = useAuth()
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const loadTasks = async (currentFilters: TaskFilters, silent = false) => {
if (!activeOrg) return
if (!silent) setIsLoading(true)
try {
const response = await taskService.getTasks({
organizationId: activeOrg.id,
text: currentFilters.searchQuery || undefined,
projectIds: currentFilters.projectIds.length > 0 ? currentFilters.projectIds : undefined,
statuses: currentFilters.statuses.length > 0 ? currentFilters.statuses : undefined,
categoryIds: currentFilters.categoryIds.length > 0 ? currentFilters.categoryIds : undefined,
userIds: currentFilters.userIds.length > 0 ? currentFilters.userIds : undefined,
})
setTasks(response)
setProjects((current) => mergeProjects(current, response))
setAssignees((current) => {
const byId = new Map(current.map((assignee) => [assignee.id, assignee]))
extractAssignees(response).forEach((assignee) => byId.set(assignee.id, assignee))
return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name))
})
} catch (error) {
console.error('Erro ao carregar tarefas', error)
} finally {
setIsLoading(false)
}
}
useEffect(() => {
if (!activeOrg) return
const init = async () => {
setFilters(DEFAULT_TASK_FILTERS)
setCategories([])
const [members] = await Promise.all([
organizationService.listMembers(activeOrg.id),
categoryService.listByOrganization(activeOrg.id).then(setCategories).catch(() => {}),
projectService.listByOrganization(activeOrg.id).then(setProjects).catch(() => {}),
])
const assigneesData = members.map((m) => ({
id: m.userId,
name: m.userName,
email: m.userEmail,
photo: m.userPhoto ?? null,
}))
setAssignees(assigneesData)
const currentMember = members.find((m) => m.userEmail === user?.email)
const userId = currentMember?.userId ?? null
setCurrentUserAssigneeId(userId)
const initialFilters = userId
? { ...DEFAULT_TASK_FILTERS, userIds: [userId] }
: DEFAULT_TASK_FILTERS
setFilters(initialFilters)
void loadTasks(initialFilters)
}
void init()
}, [activeOrg])
const handleFiltersChange = (newFilters: TaskFilters) => {
setFilters(newFilters)
if (debounceRef.current) clearTimeout(debounceRef.current)
debounceRef.current = setTimeout(() => {
void loadTasks(newFilters)
}, 300)
}
const { user } = useAuth()
const {
tasks,
isLoading,
projects,
categories,
assignees,
filters,
currentUserAssigneeId,
loadTasks,
handleFiltersChange,
} = useTaskPage({ fetchProjects: true })
return (
<div className="space-y-8 pb-12">