From ea7ffaaffdf4bdcef5308ec5413db1cc21764b19 Mon Sep 17 00:00:00 2001 From: Gabriel Santana de Menezes Date: Sun, 17 May 2026 19:22:44 -0400 Subject: [PATCH] Add multi-select task filters for projects and statuses --- .../repository/TaskRepositoryImpl.java | 12 +- .../controllers/task/ListMyTasksRequest.java | 2 +- .../task/TaskControllerMapper.java | 2 +- .../usecase/repository/TaskRepository.java | 4 +- .../task/listall/ListMyTasksInput.java | 2 +- .../task/listall/ListMyTasksUseCase.java | 2 +- .../src/components/MultiSelectDropdown.tsx | 132 ++++++++++++++++++ frontend/src/components/TaskFilterBar.tsx | 62 +++----- frontend/src/components/taskFilters.ts | 15 ++ frontend/src/pages/TaskBoardPage.tsx | 6 +- frontend/src/pages/TaskListPage.tsx | 6 +- frontend/src/services/taskService.ts | 4 +- 12 files changed, 190 insertions(+), 59 deletions(-) create mode 100644 frontend/src/components/MultiSelectDropdown.tsx create mode 100644 frontend/src/components/taskFilters.ts diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/TaskRepositoryImpl.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/TaskRepositoryImpl.java index 93cf234..a108c03 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/TaskRepositoryImpl.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/TaskRepositoryImpl.java @@ -53,7 +53,7 @@ public class TaskRepositoryImpl implements TaskRepository, PanacheRepository findTasksByUserFiltered(String email, String text, Long projectId, + public List findTasksByUserFiltered(String email, String text, List projectIds, List statuses, List categoryIds) { StringBuilder query = new StringBuilder("user.email = ?1"); List params = new ArrayList<>(); @@ -67,9 +67,9 @@ public class TaskRepositoryImpl implements TaskRepository, PanacheRepository tasks = find(query.toString(), params.toArray()).list().stream().map(TaskEntityMapper::toDomain).toList(); - LOGGER.infof("Loaded %d tasks for user %s with filters (text=%s, projectId=%s, statuses=%s, categoryIds=%s)", - tasks.size(), email, text, projectId, statuses, categoryIds); + LOGGER.infof("Loaded %d tasks for user %s with filters (text=%s, projectIds=%s, statuses=%s, categoryIds=%s)", + tasks.size(), email, text, projectIds, statuses, categoryIds); return tasks; } diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/ListMyTasksRequest.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/ListMyTasksRequest.java index 7475e5b..024dcf4 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/ListMyTasksRequest.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/ListMyTasksRequest.java @@ -11,7 +11,7 @@ public class ListMyTasksRequest { public String text; @QueryParam("projectId") - public Long projectId; + public List projectIds; @QueryParam("status") public List statuses; diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskControllerMapper.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskControllerMapper.java index c2291bb..5eb4650 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskControllerMapper.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskControllerMapper.java @@ -12,7 +12,7 @@ import net.beetechgroup.beetask.usecase.task.update.UpdateTaskInput; public class TaskControllerMapper { public static ListMyTasksInput toListMyTasksInput(ListMyTasksRequest request, String email) { - return new ListMyTasksInput(email, request.text, request.projectId, request.statuses, request.categoryIds); + return new ListMyTasksInput(email, request.text, request.projectIds, request.statuses, request.categoryIds); } public static CreateTaskInput toCreateTaskInput(CreateTaskRequest request, String userEmail) { diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/TaskRepository.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/TaskRepository.java index 4478b5d..45f0833 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/TaskRepository.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/TaskRepository.java @@ -13,7 +13,7 @@ public interface TaskRepository { List findTasksWorkedByUserInPeriod(String email, LocalDateTime start, LocalDateTime end); List findTasksFinishedByUserInPeriod(String email, LocalDateTime start, LocalDateTime end); List findTasksByUser(String email); - List findTasksByUserFiltered(String email, String text, Long projectId, + List findTasksByUserFiltered(String email, String text, List projectIds, List statuses, List categoryIds); List findTasksWorkedByOrgInPeriod(Long orgId, LocalDateTime start, LocalDateTime end); List findTasksFinishedByOrgInPeriod(Long orgId, LocalDateTime start, LocalDateTime end); @@ -23,4 +23,4 @@ public interface TaskRepository { List findTasksFinishedByUserEmailsInPeriod(List emails, LocalDateTime start, LocalDateTime end); long countByStatus(TaskStatus status); long sumAllCompletedTrackedMinutes(); -} \ No newline at end of file +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/listall/ListMyTasksInput.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/listall/ListMyTasksInput.java index c6dfbdc..1e1b9f4 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/listall/ListMyTasksInput.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/listall/ListMyTasksInput.java @@ -4,5 +4,5 @@ import java.util.List; import net.beetechgroup.beetask.entities.task.TaskStatus; -public record ListMyTasksInput(String email, String text, Long projectId, +public record ListMyTasksInput(String email, String text, List projectIds, List statuses, List categoryIds) {} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/listall/ListMyTasksUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/listall/ListMyTasksUseCase.java index cd413d7..ce7f75a 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/listall/ListMyTasksUseCase.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/listall/ListMyTasksUseCase.java @@ -18,7 +18,7 @@ public class ListMyTasksUseCase { public List execute(ListMyTasksInput input) { List tasks = taskRepository.findTasksByUserFiltered( - input.email(), input.text(), input.projectId(), input.statuses(), input.categoryIds()) + input.email(), input.text(), input.projectIds(), input.statuses(), input.categoryIds()) .stream().map(CreateTaskMapper::toCreateTaskOutput).toList(); LOGGER.infof("Loaded %d tasks for user %s", tasks.size(), input.email()); return tasks; diff --git a/frontend/src/components/MultiSelectDropdown.tsx b/frontend/src/components/MultiSelectDropdown.tsx new file mode 100644 index 0000000..27b274c --- /dev/null +++ b/frontend/src/components/MultiSelectDropdown.tsx @@ -0,0 +1,132 @@ +import { Check, ChevronDown, X } from 'lucide-react' +import { useEffect, useRef, useState, type ReactNode } from 'react' + +import { cn } from '../lib/utils' + +export interface MultiSelectDropdownOption { + value: T + label: string + icon?: ReactNode +} + +interface MultiSelectDropdownProps { + label: string + allLabel: string + options: MultiSelectDropdownOption[] + selected: T[] + onChange: (selected: T[]) => void + className?: string +} + +export function MultiSelectDropdown({ + label, + allLabel, + options, + selected, + onChange, + className, +}: MultiSelectDropdownProps) { + const [isOpen, setIsOpen] = useState(false) + const containerRef = useRef(null) + + useEffect(() => { + if (!isOpen) return + + const handlePointerDown = (event: MouseEvent) => { + if (!containerRef.current?.contains(event.target as Node)) { + setIsOpen(false) + } + } + + document.addEventListener('mousedown', handlePointerDown) + return () => document.removeEventListener('mousedown', handlePointerDown) + }, [isOpen]) + + const toggleValue = (value: T) => { + if (selected.includes(value)) { + onChange(selected.filter((item) => item !== value)) + return + } + + onChange([...selected, value]) + } + + const getSummaryLabel = () => { + if (selected.length === 0) return allLabel + if (selected.length === 1) { + return options.find((option) => option.value === selected[0])?.label ?? allLabel + } + return `${selected.length} selecionados` + } + + return ( +
+ + {label} + + + + + {isOpen && ( +
+ + +
+ {options.map((option) => { + const isSelected = selected.includes(option.value) + + return ( + + ) + })} +
+
+ )} +
+ ) +} diff --git a/frontend/src/components/TaskFilterBar.tsx b/frontend/src/components/TaskFilterBar.tsx index 735a682..6b3ef13 100644 --- a/frontend/src/components/TaskFilterBar.tsx +++ b/frontend/src/components/TaskFilterBar.tsx @@ -1,24 +1,11 @@ import { Filter, RefreshCw, Search } from 'lucide-react' -import type { TaskStatus } from '../types/task' import { TASK_STATUS_LABELS, TASK_STATUS_OPTIONS } from '../types/task' import type { Project } from '../services/projectService' import type { Category } from '../types/category' import { MultiSelectChips } from './MultiSelectChips' import { CategoryIcon } from './CategoryIcon' - -export interface TaskFilters { - searchQuery: string - statuses: TaskStatus[] - categoryIds: number[] - projectId: number | 'ALL' -} - -export const DEFAULT_TASK_FILTERS: TaskFilters = { - searchQuery: '', - statuses: [], - categoryIds: [], - projectId: 'ALL', -} +import { MultiSelectDropdown } from './MultiSelectDropdown' +import type { TaskFilters } from './taskFilters' interface TaskFilterBarProps { filters: TaskFilters @@ -57,25 +44,6 @@ export function TaskFilterBar({ /> - {projects.length > 0 && ( - - )} - {onRefresh && (