From db463551e6feed35f89d548547276969951d4b56 Mon Sep 17 00:00:00 2001
From: Gabriel Santana de Menezes
Date: Thu, 14 May 2026 22:12:05 -0400
Subject: [PATCH] feat: filtros de busca nas listagens de tasks (#26)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Backend: adiciona ListMyTasksInput, ListMyTasksRequest (@BeanParam) com
parâmetros text, projectId e status; novo método findTasksByUserFiltered
no repositório com query dinâmica via Panache; mapper atualizado;
CLAUDE.md documenta regra de DTO Request para 3+ parâmetros.
Frontend: componente reutilizável TaskFilterBar (texto, projeto, status)
integrado em TaskListPage e TaskBoardPage; getMyTasks() aceita filtros
repassados como query params ao backend.
closes #26
Co-Authored-By: Claude Sonnet 4.6
---
CLAUDE.md | 8 +
.../repository/TaskRepositoryImpl.java | 30 ++++
.../controllers/task/ListMyTasksRequest.java | 16 ++
.../controllers/task/TaskController.java | 8 +-
.../task/TaskControllerMapper.java | 5 +
.../usecase/repository/TaskRepository.java | 2 +
.../task/listall/ListMyTasksInput.java | 5 +
.../task/listall/ListMyTasksUseCase.java | 7 +-
frontend/src/components/TaskFilterBar.tsx | 101 +++++++++++++
frontend/src/pages/TaskBoardPage.tsx | 119 ++++++++-------
frontend/src/pages/TaskListPage.tsx | 139 ++++++------------
frontend/src/services/taskService.ts | 9 +-
12 files changed, 295 insertions(+), 154 deletions(-)
create mode 100644 backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/ListMyTasksRequest.java
create mode 100644 backend/src/main/java/net/beetechgroup/beetask/usecase/task/listall/ListMyTasksInput.java
create mode 100644 frontend/src/components/TaskFilterBar.tsx
diff --git a/CLAUDE.md b/CLAUDE.md
index 62c4899..2b8553d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -56,6 +56,14 @@ A new use case `usecase///` contains exactly:
Then **wire it** via `@Produces` in the matching `frameworks/config/UseCaseConfig.java`. Use cases are NOT annotated with CDI scopes themselves — the producer is what makes them injectable.
+### Query parameter DTOs
+
+When a controller endpoint receives **3 or more query/request parameters**, create a dedicated DTO class in the controller package and a mapping method in the `XxxControllerMapper`:
+
+- For **query parameters** (GET): use a plain Java class with public fields annotated with `@QueryParam`, and receive it via `@BeanParam XxxRequest` in the controller method.
+- For **request body** parameters (POST/PUT): use a `record XxxRequest(...)` as usual.
+- Add `toXxxInput(XxxRequest request, ...)` to `XxxControllerMapper` — never inline the mapping in the controller.
+
### Controller anatomy
`interfaceadapters/controllers//`:
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 22fe044..da4d98a 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
@@ -1,5 +1,6 @@
package net.beetechgroup.beetask.frameworks.persistence.repository;
+import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.time.LocalDateTime;
@@ -50,6 +51,35 @@ public class TaskRepositoryImpl implements TaskRepository, PanacheRepository findTasksByUserFiltered(String email, String text, Long projectId, TaskStatus status) {
+ StringBuilder query = new StringBuilder("user.email = ?1");
+ List params = new ArrayList<>();
+ params.add(email);
+
+ int idx = 2;
+ if (Objects.nonNull(text) && !text.isBlank()) {
+ query.append(" and (lower(title) like ?").append(idx).append(" or lower(description) like ?").append(idx + 1).append(")");
+ String pattern = "%" + text.toLowerCase() + "%";
+ params.add(pattern);
+ params.add(pattern);
+ idx += 2;
+ }
+ if (Objects.nonNull(projectId)) {
+ query.append(" and project.id = ?").append(idx);
+ params.add(projectId);
+ idx++;
+ }
+ if (Objects.nonNull(status)) {
+ query.append(" and status = ?").append(idx);
+ params.add(status);
+ }
+
+ List 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, status=%s)", tasks.size(), email, text, projectId, status);
+ return tasks;
+ }
+
@Override
public List findTasksWorkedByUserInPeriod(String email, LocalDateTime start, LocalDateTime end) {
return find("user.email = ?1", email).list().stream()
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
new file mode 100644
index 0000000..111c1d7
--- /dev/null
+++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/ListMyTasksRequest.java
@@ -0,0 +1,16 @@
+package net.beetechgroup.beetask.interfaceadapters.controllers.task;
+
+import jakarta.ws.rs.QueryParam;
+import net.beetechgroup.beetask.entities.task.TaskStatus;
+
+public class ListMyTasksRequest {
+
+ @QueryParam("text")
+ public String text;
+
+ @QueryParam("projectId")
+ public Long projectId;
+
+ @QueryParam("status")
+ public TaskStatus status;
+}
diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskController.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskController.java
index 8de8895..3b11b84 100644
--- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskController.java
+++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskController.java
@@ -1,6 +1,7 @@
package net.beetechgroup.beetask.interfaceadapters.controllers.task;
import jakarta.inject.Inject;
+import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PATCH;
@@ -144,11 +145,12 @@ public class TaskController {
@GET
@Path("/mine")
- @Operation(summary = "List my tasks", description = "Lists all tasks belonging to the authenticated user")
+ @Operation(summary = "List my tasks", description = "Lists all tasks belonging to the authenticated user, with optional filters")
@APIResponse(responseCode = "200", description = "Tasks listed successfully")
- public List listMyTasks() {
+ public List listMyTasks(@BeanParam ListMyTasksRequest request) {
String email = securityIdentity.getPrincipal().getName();
- List tasks = listMyTasksUseCase.execute(email).stream().map(TaskControllerMapper::toCreateTaskResponse).toList();
+ List tasks = listMyTasksUseCase.execute(TaskControllerMapper.toListMyTasksInput(request, email))
+ .stream().map(TaskControllerMapper::toCreateTaskResponse).toList();
LOGGER.infof("Listed %d tasks for user %s", tasks.size(), email);
return tasks;
}
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 ded52f9..925cc76 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
@@ -3,6 +3,7 @@ package net.beetechgroup.beetask.interfaceadapters.controllers.task;
import java.util.Objects;
import net.beetechgroup.beetask.usecase.task.create.CreateTaskInput;
import net.beetechgroup.beetask.usecase.task.create.CreateTaskOutput;
+import net.beetechgroup.beetask.usecase.task.listall.ListMyTasksInput;
import net.beetechgroup.beetask.usecase.task.start.StartTaskInput;
import net.beetechgroup.beetask.usecase.task.stop.StopTaskInput;
import net.beetechgroup.beetask.usecase.task.update.TaskHistoryItemInput;
@@ -10,6 +11,10 @@ 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.status);
+ }
+
public static CreateTaskInput toCreateTaskInput(CreateTaskRequest request, String userEmail) {
return new CreateTaskInput(
request.title(),
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 90c58b6..a921518 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
@@ -4,6 +4,7 @@ import java.util.List;
import java.time.LocalDateTime;
import net.beetechgroup.beetask.entities.task.Task;
+import net.beetechgroup.beetask.entities.task.TaskStatus;
public interface TaskRepository {
Task saveTask(Task task);
@@ -12,6 +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, TaskStatus status);
List findTasksWorkedByOrgInPeriod(Long orgId, LocalDateTime start, LocalDateTime end);
List findTasksFinishedByOrgInPeriod(Long orgId, LocalDateTime start, LocalDateTime end);
List findTasksWorkedByUserIdInPeriod(Long userId, LocalDateTime start, LocalDateTime end);
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
new file mode 100644
index 0000000..e597801
--- /dev/null
+++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/listall/ListMyTasksInput.java
@@ -0,0 +1,5 @@
+package net.beetechgroup.beetask.usecase.task.listall;
+
+import net.beetechgroup.beetask.entities.task.TaskStatus;
+
+public record ListMyTasksInput(String email, String text, Long projectId, TaskStatus status) {}
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 b0934a6..a5119c5 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
@@ -16,9 +16,10 @@ public class ListMyTasksUseCase {
this.taskRepository = taskRepository;
}
- public List execute(String email) {
- List tasks = taskRepository.findTasksByUser(email).stream().map(CreateTaskMapper::toCreateTaskOutput).toList();
- LOGGER.infof("Loaded %d tasks for user %s", tasks.size(), email);
+ public List execute(ListMyTasksInput input) {
+ List tasks = taskRepository.findTasksByUserFiltered(input.email(), input.text(), input.projectId(), input.status())
+ .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/TaskFilterBar.tsx b/frontend/src/components/TaskFilterBar.tsx
new file mode 100644
index 0000000..9a6df2a
--- /dev/null
+++ b/frontend/src/components/TaskFilterBar.tsx
@@ -0,0 +1,101 @@
+import { Filter, RefreshCw, Search } from 'lucide-react'
+import { cn } from '../lib/utils'
+import type { TaskStatus } from '../types/task'
+import { TASK_STATUS_LABELS, TASK_STATUS_OPTIONS } from '../types/task'
+import type { Project } from '../services/projectService'
+
+export interface TaskFilters {
+ searchQuery: string
+ statusFilter: TaskStatus | 'ALL'
+ projectId: number | 'ALL'
+}
+
+export const DEFAULT_TASK_FILTERS: TaskFilters = {
+ searchQuery: '',
+ statusFilter: 'ALL',
+ projectId: 'ALL',
+}
+
+interface TaskFilterBarProps {
+ filters: TaskFilters
+ onFiltersChange: (filters: TaskFilters) => void
+ projects: Project[]
+ isLoading?: boolean
+ onRefresh?: () => void
+}
+
+export function TaskFilterBar({
+ filters,
+ onFiltersChange,
+ projects,
+ isLoading,
+ onRefresh,
+}: TaskFilterBarProps) {
+ const update = (partial: Partial) =>
+ onFiltersChange({ ...filters, ...partial })
+
+ return (
+
+
+
+ update({ searchQuery: e.target.value })}
+ className="h-11 w-full rounded-xl border border-border-soft bg-app-bg pl-10 pr-4 text-sm outline-none transition-all focus:border-brand focus:ring-2 focus:ring-brand/10"
+ />
+
+
+ {projects.length > 0 && (
+
+ update({
+ projectId: e.target.value === 'ALL' ? 'ALL' : Number(e.target.value),
+ })
+ }
+ className="h-11 rounded-xl border border-border-soft bg-app-bg px-3 text-sm text-text-main outline-none transition-all focus:border-brand focus:ring-2 focus:ring-brand/10"
+ >
+ Todos os projetos
+ {projects.map((p) => (
+
+ {p.name}
+
+ ))}
+
+ )}
+
+
+
+ {(['ALL', ...TASK_STATUS_OPTIONS] as const).map((status) => (
+ update({ statusFilter: status })}
+ className={cn(
+ 'whitespace-nowrap rounded-lg px-4 py-2 text-xs font-bold transition-all',
+ filters.statusFilter === status
+ ? 'bg-brand text-white'
+ : 'bg-app-bg text-text-muted hover:bg-surface-muted hover:text-text-main',
+ )}
+ >
+ {status === 'ALL' ? 'Todos' : TASK_STATUS_LABELS[status]}
+
+ ))}
+
+
+ {onRefresh && (
+
+
+
+ )}
+
+ )
+}
diff --git a/frontend/src/pages/TaskBoardPage.tsx b/frontend/src/pages/TaskBoardPage.tsx
index e837ad5..e5eed2c 100644
--- a/frontend/src/pages/TaskBoardPage.tsx
+++ b/frontend/src/pages/TaskBoardPage.tsx
@@ -4,69 +4,71 @@ import {
AlertCircle,
CheckCircle2,
Clock,
+ List,
MoreVertical,
Plus,
- RefreshCw,
- Tag,
XCircle,
} from 'lucide-react'
-import { useEffect, useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { taskService } from '../services/taskService'
+import { projectService } from '../services/projectService'
+import type { Project } from '../services/projectService'
import { cn } from '../lib/utils'
import type { TaskResponse, TaskStatus } from '../types/task'
+import { TaskTimer } from '../components/TaskTimer'
+import { TaskFilterBar, DEFAULT_TASK_FILTERS } from '../components/TaskFilterBar'
+import type { TaskFilters } from '../components/TaskFilterBar'
+import { useAuth } from '../contexts/AuthContext'
const COLUMNS: { id: TaskStatus; label: string; icon: any; color: string }[] = [
- {
- id: 'NOT_STARTED',
- label: 'Pendente',
- icon: AlertCircle,
- color: 'text-text-muted',
- },
- {
- id: 'IN_PROGRESS',
- label: 'Em Andamento',
- icon: Clock,
- color: 'text-warning',
- },
- {
- id: 'COMPLETED',
- label: 'Concluída',
- icon: CheckCircle2,
- color: 'text-success',
- },
- {
- id: 'CANCELED',
- label: 'Cancelada',
- icon: XCircle,
- color: 'text-danger',
- },
+ { id: 'NOT_STARTED', label: 'Pendente', icon: AlertCircle, color: 'text-text-muted' },
+ { id: 'IN_PROGRESS', label: 'Em Andamento', icon: Clock, color: 'text-warning' },
+ { id: 'COMPLETED', label: 'Concluída', icon: CheckCircle2, color: 'text-success' },
+ { id: 'CANCELED', label: 'Cancelada', icon: XCircle, color: 'text-danger' },
]
-import { TaskTimer } from '../components/TaskTimer'
-
export function TaskBoardPage() {
const [tasks, setTasks] = useState([])
const [isLoading, setIsLoading] = useState(true)
+ const [projects, setProjects] = useState([])
+ const [filters, setFilters] = useState(DEFAULT_TASK_FILTERS)
const navigate = useNavigate()
+ const { activeOrg } = useAuth()
+ const debounceRef = useRef | null>(null)
- const loadTasks = async (silent = false) => {
+ const loadTasks = async (currentFilters: TaskFilters, silent = false) => {
if (!silent) setIsLoading(true)
try {
- const response = await taskService.getMyTasks()
+ const response = await taskService.getMyTasks({
+ text: currentFilters.searchQuery || undefined,
+ projectId: currentFilters.projectId !== 'ALL' ? currentFilters.projectId : undefined,
+ status: currentFilters.statusFilter !== 'ALL' ? currentFilters.statusFilter : undefined,
+ })
setTasks(response)
} catch (error) {
- console.error('Failed to load tasks', error)
+ console.error('Erro ao carregar tarefas', error)
} finally {
setIsLoading(false)
}
}
useEffect(() => {
- void loadTasks()
+ void loadTasks(filters)
+ if (activeOrg) {
+ projectService.listByOrganization(activeOrg.id).then(setProjects).catch(() => {})
+ }
}, [])
+ const handleFiltersChange = (newFilters: TaskFilters) => {
+ setFilters(newFilters)
+ if (debounceRef.current) clearTimeout(debounceRef.current)
+ debounceRef.current = setTimeout(() => {
+ void loadTasks(newFilters)
+ }, 300)
+ }
+
const onDragEnd = async (result: DropResult) => {
const { destination, source, draggableId } = result
@@ -81,18 +83,16 @@ export function TaskBoardPage() {
const taskId = Number.parseInt(draggableId)
const newStatus = destination.droppableId as TaskStatus
- // Optimistic update
const updatedTasks = tasks.map((t) =>
t.id === taskId ? { ...t, status: newStatus } : t,
)
setTasks(updatedTasks)
try {
- // Use the new generic status update endpoint
await taskService.updateTaskStatus(taskId, newStatus)
} catch (error) {
- console.error('Failed to update task status', error)
- void loadTasks(true) // Revert on error
+ console.error('Erro ao atualizar status da tarefa', error)
+ void loadTasks(filters, true)
}
}
@@ -111,12 +111,19 @@ export function TaskBoardPage() {
-
void loadTasks()}
- className="flex h-11 w-11 items-center justify-center rounded-xl bg-surface border border-border-soft text-text-muted transition-all hover:bg-surface-muted hover:text-brand"
- >
-
-
+
+
+
+ Lista
+
+
+
+ Quadro
+
+
+ void loadTasks(filters)}
+ />
+
{COLUMNS.map((column) => (
@@ -135,13 +150,13 @@ export function TaskBoardPage() {
- {column.label}
+ {column.label}
{tasksByStatus(column.id).length}
-
+
@@ -154,7 +169,7 @@ export function TaskBoardPage() {
className={cn(
'flex min-h-[500px] flex-col gap-3 rounded-[2rem] border-2 border-transparent p-3 transition-colors',
snapshot.isDraggingOver
- ? 'bg-brand/5 border-dashed border-brand/20'
+ ? 'border-dashed border-brand/20 bg-brand/5'
: 'bg-surface/50',
)}
>
@@ -172,26 +187,28 @@ export function TaskBoardPage() {
onClick={() => navigate(`/edit/${task.id}`)}
className={cn(
'group cursor-pointer rounded-2xl border border-border-soft bg-surface p-4 shadow-sm transition-all',
- snapshot.isDragging ? 'shadow-2xl ring-2 ring-brand/20' : 'hover:border-brand/30',
+ snapshot.isDragging
+ ? 'shadow-2xl ring-2 ring-brand/20'
+ : 'hover:border-brand/30',
)}
>
-
+
{task.title}
{task.description && (
-
+
{task.description}
)}
-
loadTasks(true)} />
+ loadTasks(filters, true)} />
-
+
{task.project?.name || 'Geral'}
diff --git a/frontend/src/pages/TaskListPage.tsx b/frontend/src/pages/TaskListPage.tsx
index 3aa6b86..8a0499d 100644
--- a/frontend/src/pages/TaskListPage.tsx
+++ b/frontend/src/pages/TaskListPage.tsx
@@ -5,21 +5,24 @@ import {
CheckCircle2,
Clock,
Columns,
- Filter,
List,
Plus,
- RefreshCw,
Search,
Tag,
XCircle,
} from 'lucide-react'
-import { useEffect, useMemo, useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { taskService } from '../services/taskService'
+import { projectService } from '../services/projectService'
+import type { Project } from '../services/projectService'
import { cn } from '../lib/utils'
-import { TASK_STATUS_LABELS, TASK_STATUS_OPTIONS } from '../types/task'
-import type { TaskResponse, TaskStatus } from '../types/task'
+import type { TaskResponse } from '../types/task'
+import { TaskTimer } from '../components/TaskTimer'
+import { TaskFilterBar, DEFAULT_TASK_FILTERS } from '../components/TaskFilterBar'
+import type { TaskFilters } from '../components/TaskFilterBar'
+import { useAuth } from '../contexts/AuthContext'
function getStatusConfig(status: TaskResponse['status']) {
switch (status) {
@@ -50,56 +53,48 @@ function getStatusConfig(status: TaskResponse['status']) {
}
}
-import { TaskTimer } from '../components/TaskTimer'
-
export function TaskListPage() {
const [tasks, setTasks] = useState([])
const [isLoading, setIsLoading] = useState(true)
- const [_errorMessage, setErrorMessage] = useState(null)
+ const [projects, setProjects] = useState([])
+ const [filters, setFilters] = useState(DEFAULT_TASK_FILTERS)
const navigate = useNavigate()
- const [searchQuery, setSearchQuery] = useState('')
- const [statusFilter, setStatusFilter] = useState('ALL')
+ const { activeOrg } = useAuth()
+ const debounceRef = useRef | null>(null)
- const loadTasks = async (silent = false) => {
+ const loadTasks = async (currentFilters: TaskFilters, silent = false) => {
if (!silent) setIsLoading(true)
- setErrorMessage(null)
-
try {
- const response = await taskService.getMyTasks()
+ const response = await taskService.getMyTasks({
+ text: currentFilters.searchQuery || undefined,
+ projectId: currentFilters.projectId !== 'ALL' ? currentFilters.projectId : undefined,
+ status: currentFilters.statusFilter !== 'ALL' ? currentFilters.statusFilter : undefined,
+ })
setTasks(response)
} catch (error) {
- setErrorMessage(
- error instanceof Error
- ? error.message
- : 'Não foi possível carregar as tarefas.',
- )
+ console.error('Erro ao carregar tarefas', error)
} finally {
setIsLoading(false)
}
}
useEffect(() => {
- void loadTasks()
+ void loadTasks(filters)
+ if (activeOrg) {
+ projectService.listByOrganization(activeOrg.id).then(setProjects).catch(() => {})
+ }
}, [])
- const filteredTasks = useMemo(() => {
- return tasks.filter((task) => {
- const matchesSearch =
- task.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
- task.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
- task.project?.name?.toLowerCase().includes(searchQuery.toLowerCase())
-
- const matchesStatus = statusFilter === 'ALL' || task.status === statusFilter
-
- return matchesSearch && matchesStatus
- })
- }, [tasks, searchQuery, statusFilter])
-
-
+ const handleFiltersChange = (newFilters: TaskFilters) => {
+ setFilters(newFilters)
+ if (debounceRef.current) clearTimeout(debounceRef.current)
+ debounceRef.current = setTimeout(() => {
+ void loadTasks(newFilters)
+ }, 300)
+ }
return (
- {/* Header & Action */}
@@ -110,7 +105,6 @@ export function TaskListPage() {
- {/* View toggle */}
@@ -134,57 +128,17 @@ export function TaskListPage() {
+
void loadTasks(filters)}
+ />
-
- {/* Toolbar */}
-
-
-
- setSearchQuery(e.target.value)}
- className="h-11 w-full rounded-xl border border-border-soft bg-app-bg pl-10 pr-4 text-sm outline-none transition-all focus:border-brand focus:ring-2 focus:ring-brand/10"
- />
-
-
-
-
- {(['ALL', ...TASK_STATUS_OPTIONS] as const).map(
- (status) => (
- setStatusFilter(status)}
- className={cn(
- 'whitespace-nowrap rounded-lg px-4 py-2 text-xs font-bold transition-all',
- statusFilter === status
- ? 'bg-brand text-white'
- : 'bg-app-bg text-text-muted hover:bg-surface-muted hover:text-text-main',
- )}
- >
- {status === 'ALL' ? 'Todos' : TASK_STATUS_LABELS[status]}
-
- ),
- )}
-
-
-
void loadTasks()}
- className="flex h-11 w-11 items-center justify-center rounded-xl bg-app-bg text-text-muted transition-all hover:bg-surface-muted hover:text-brand"
- title="Recarregar"
- >
-
-
-
-
- {/* Task List */}
- {filteredTasks.map((task, i) => {
+ {tasks.map((task, i) => {
const config = getStatusConfig(task.status)
const StatusIcon = config.icon
@@ -209,21 +163,21 @@ export function TaskListPage() {
{config.label}
-
+
#{task.id}
-
+
{task.title}
-
+
{task.description || 'Sem descrição informada.'}
-
loadTasks(true)} />
+ loadTasks(filters, true)} />
@@ -243,8 +197,7 @@ export function TaskListPage() {
- {/* Empty States */}
- {!isLoading && filteredTasks.length === 0 && (
+ {!isLoading && tasks.length === 0 && (
{
- setSearchQuery('')
- setStatusFilter('ALL')
- }}
+ onClick={() => handleFiltersChange(DEFAULT_TASK_FILTERS)}
className="mt-6 text-sm font-bold text-brand hover:underline"
>
Limpar todos os filtros
@@ -272,7 +222,6 @@ export function TaskListPage() {
)}
- {/* Loading State */}
{isLoading && tasks.length === 0 && (
{Array.from({ length: 6 }).map((_, i) => (
diff --git a/frontend/src/services/taskService.ts b/frontend/src/services/taskService.ts
index f6ac60d..e086712 100644
--- a/frontend/src/services/taskService.ts
+++ b/frontend/src/services/taskService.ts
@@ -6,8 +6,13 @@ export const taskService = {
return apiFetch('/tasks')
},
- async getMyTasks(): Promise {
- return apiFetch('/tasks/mine')
+ async getMyTasks(filters?: { text?: string; projectId?: number; status?: TaskStatus }): Promise {
+ const params = new URLSearchParams()
+ if (filters?.text) params.set('text', filters.text)
+ if (filters?.projectId) params.set('projectId', String(filters.projectId))
+ if (filters?.status) params.set('status', filters.status)
+ const query = params.toString()
+ return apiFetch(`/tasks/mine${query ? `?${query}` : ''}`)
},
async createTask(task: { title: string; description?: string; projectId?: number | null; status?: TaskStatus }): Promise {