From 119d896a5971a73564f216a475a74f471ec4cb11 Mon Sep 17 00:00:00 2001 From: Gabriel Santana de Menezes Date: Wed, 20 May 2026 15:51:40 -0400 Subject: [PATCH 1/2] feat: add "Mostrar apenas as minhas" checkbox to task filters Adds a pre-checked checkbox next to the employee filter that automatically filters tasks to the logged-in user on page load. Unchecking restores the free-select dropdown behavior. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/TaskFilterBar.tsx | 19 ++++++++++- frontend/src/pages/TaskBoardPage.tsx | 41 +++++++++++++++++++---- frontend/src/pages/TaskListPage.tsx | 35 ++++++++++++++----- 3 files changed, 79 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/TaskFilterBar.tsx b/frontend/src/components/TaskFilterBar.tsx index 7684be2..e02ab44 100644 --- a/frontend/src/components/TaskFilterBar.tsx +++ b/frontend/src/components/TaskFilterBar.tsx @@ -22,6 +22,7 @@ interface TaskFilterBarProps { projects: Project[] categories: Category[] assignees: TaskAssignee[] + currentUserId?: number isLoading?: boolean onRefresh?: () => void } @@ -32,6 +33,7 @@ export function TaskFilterBar({ projects, categories, assignees, + currentUserId, isLoading, onRefresh, }: TaskFilterBarProps) { @@ -98,7 +100,22 @@ export function TaskFilterBar({ {assignees.length > 0 && ( -
+
+ {currentUserId !== undefined && ( + + )} ([]) const [assignees, setAssignees] = useState([]) const [filters, setFilters] = useState(DEFAULT_TASK_FILTERS) + const [currentUserAssigneeId, setCurrentUserAssigneeId] = useState(null) const navigate = useNavigate() const { activeOrg, user } = useAuth() const debounceRef = useRef | null>(null) @@ -95,12 +97,38 @@ export function TaskBoardPage() { useEffect(() => { if (!activeOrg) return - setFilters(DEFAULT_TASK_FILTERS) - setProjects([]) - setAssignees([]) - setCategories([]) - void loadTasks(DEFAULT_TASK_FILTERS) - categoryService.listByOrganization(activeOrg.id).then(setCategories).catch(() => {}) + + 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) => { @@ -182,6 +210,7 @@ export function TaskBoardPage() { projects={projects} categories={categories} assignees={assignees} + currentUserId={currentUserAssigneeId ?? undefined} isLoading={isLoading} onRefresh={() => void loadTasks(filters)} /> diff --git a/frontend/src/pages/TaskListPage.tsx b/frontend/src/pages/TaskListPage.tsx index c50c812..177b58d 100644 --- a/frontend/src/pages/TaskListPage.tsx +++ b/frontend/src/pages/TaskListPage.tsx @@ -94,6 +94,7 @@ export function TaskListPage() { const [categories, setCategories] = useState([]) const [assignees, setAssignees] = useState([]) const [filters, setFilters] = useState(DEFAULT_TASK_FILTERS) + const [currentUserAssigneeId, setCurrentUserAssigneeId] = useState(null) const navigate = useNavigate() const { activeOrg, user } = useAuth() const debounceRef = useRef | null>(null) @@ -126,22 +127,37 @@ export function TaskListPage() { useEffect(() => { if (!activeOrg) return - setFilters(DEFAULT_TASK_FILTERS) - setCategories([]) - void loadTasks(DEFAULT_TASK_FILTERS) - categoryService.listByOrganization(activeOrg.id).then(setCategories).catch(() => {}) + const init = async () => { + setFilters(DEFAULT_TASK_FILTERS) + setCategories([]) - projectService.listByOrganization(activeOrg.id).then(setProjects).catch(() => {}) + const [members] = await Promise.all([ + organizationService.listMembers(activeOrg.id), + categoryService.listByOrganization(activeOrg.id).then(setCategories).catch(() => {}), + projectService.listByOrganization(activeOrg.id).then(setProjects).catch(() => {}), + ]) - organizationService.listMembers(activeOrg.id).then((members) => { - setAssignees(members.map((m) => ({ + const assigneesData = members.map((m) => ({ id: m.userId, name: m.userName, email: m.userEmail, photo: m.userPhoto ?? null, - }))) - }).catch(() => {}) + })) + 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) => { @@ -193,6 +209,7 @@ export function TaskListPage() { projects={projects} categories={categories} assignees={assignees} + currentUserId={currentUserAssigneeId ?? undefined} isLoading={isLoading} onRefresh={() => void loadTasks(filters)} /> From 5a71aba13fe09a3c6f68c4d9c59ba598148f1574 Mon Sep 17 00:00:00 2001 From: Gabriel Santana de Menezes Date: Thu, 21 May 2026 10:41:07 -0400 Subject: [PATCH 2/2] 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 --- frontend/src/hooks/useTaskPage.ts | 152 +++++++++++++++++++++++++++ frontend/src/pages/TaskBoardPage.tsx | 129 +++-------------------- frontend/src/pages/TaskListPage.tsx | 131 +++-------------------- 3 files changed, 182 insertions(+), 230 deletions(-) create mode 100644 frontend/src/hooks/useTaskPage.ts diff --git a/frontend/src/hooks/useTaskPage.ts b/frontend/src/hooks/useTaskPage.ts new file mode 100644 index 0000000..73d98b5 --- /dev/null +++ b/frontend/src/hooks/useTaskPage.ts @@ -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 + 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() + + 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([]) + const [isLoading, setIsLoading] = useState(true) + const [projects, setProjects] = useState([]) + const [categories, setCategories] = useState([]) + const [assignees, setAssignees] = useState([]) + const [filters, setFilters] = useState(DEFAULT_TASK_FILTERS) + const [currentUserAssigneeId, setCurrentUserAssigneeId] = useState(null) + const { activeOrg, user } = useAuth() + const debounceRef = useRef | 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[] = [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, + } +} diff --git a/frontend/src/pages/TaskBoardPage.tsx b/frontend/src/pages/TaskBoardPage.tsx index 35249b7..05ec2b7 100644 --- a/frontend/src/pages/TaskBoardPage.tsx +++ b/frontend/src/pages/TaskBoardPage.tsx @@ -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() - - 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([]) - const [isLoading, setIsLoading] = useState(true) - const [projects, setProjects] = useState([]) - const [categories, setCategories] = useState([]) - const [assignees, setAssignees] = useState([]) - const [filters, setFilters] = useState(DEFAULT_TASK_FILTERS) - const [currentUserAssigneeId, setCurrentUserAssigneeId] = useState(null) const navigate = useNavigate() - const { activeOrg, user } = useAuth() - const debounceRef = useRef | 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) diff --git a/frontend/src/pages/TaskListPage.tsx b/frontend/src/pages/TaskListPage.tsx index 177b58d..d2cba88 100644 --- a/frontend/src/pages/TaskListPage.tsx +++ b/frontend/src/pages/TaskListPage.tsx @@ -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() - - 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([]) - const [isLoading, setIsLoading] = useState(true) - const [projects, setProjects] = useState([]) - const [categories, setCategories] = useState([]) - const [assignees, setAssignees] = useState([]) - const [filters, setFilters] = useState(DEFAULT_TASK_FILTERS) - const [currentUserAssigneeId, setCurrentUserAssigneeId] = useState(null) const navigate = useNavigate() - const { activeOrg, user } = useAuth() - const debounceRef = useRef | 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 (