From 302078fbece4069e8c517259ac3082cf151e3cb1 Mon Sep 17 00:00:00 2001 From: Urban Modig Date: Wed, 8 Oct 2025 19:54:31 +0200 Subject: [PATCH] Pagination + filtering --- .../urmo/hemhub/repo/ProjectRepository.java | 5 +- .../se/urmo/hemhub/repo/TaskRepository.java | 16 ++++--- .../urmo/hemhub/repo/TaskSpecifications.java | 34 +++++++++++++ .../hemhub/service/ProjectTaskService.java | 45 +++++++++++++---- .../hemhub/web/ProjectTaskController.java | 48 ++++++++++++------- 5 files changed, 113 insertions(+), 35 deletions(-) create mode 100644 src/main/java/se/urmo/hemhub/repo/TaskSpecifications.java diff --git a/src/main/java/se/urmo/hemhub/repo/ProjectRepository.java b/src/main/java/se/urmo/hemhub/repo/ProjectRepository.java index 3d29da7..44764d4 100644 --- a/src/main/java/se/urmo/hemhub/repo/ProjectRepository.java +++ b/src/main/java/se/urmo/hemhub/repo/ProjectRepository.java @@ -1,10 +1,13 @@ package se.urmo.hemhub.repo; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import se.urmo.hemhub.domain.Project; + import java.util.*; public interface ProjectRepository extends JpaRepository { - List findByHouseholdId(UUID householdId); + Page findByHouseholdId(UUID householdId, Pageable pageable); boolean existsByIdAndHouseholdId(UUID projectId, UUID householdId); } diff --git a/src/main/java/se/urmo/hemhub/repo/TaskRepository.java b/src/main/java/se/urmo/hemhub/repo/TaskRepository.java index ecf44d2..ecad74a 100644 --- a/src/main/java/se/urmo/hemhub/repo/TaskRepository.java +++ b/src/main/java/se/urmo/hemhub/repo/TaskRepository.java @@ -1,25 +1,27 @@ package se.urmo.hemhub.repo; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.*; import org.springframework.data.repository.query.Param; import se.urmo.hemhub.domain.Task; import java.time.LocalDate; import java.util.*; -public interface TaskRepository extends JpaRepository { +public interface TaskRepository extends JpaRepository, JpaSpecificationExecutor { + // Simple finders (used in existing flows) List findByProjectId(UUID projectId); boolean existsByIdAndProjectId(UUID taskId, UUID projectId); - - // Household-level tasks (no project) List findByHouseholdIdAndProjectIsNull(UUID householdId); - // All tasks due on a given day, excluding a particular status (e.g., DONE) + // Paging versions (used by new endpoints) + Page findAll(org.springframework.data.jpa.domain.Specification spec, Pageable pageable); + + // For Iteration 4 service List findByDueDateAndStatusNot(LocalDate date, Task.Status status); - // Only tasks for households where the given user is a member (for the /me view) @Query(""" select t from Task t diff --git a/src/main/java/se/urmo/hemhub/repo/TaskSpecifications.java b/src/main/java/se/urmo/hemhub/repo/TaskSpecifications.java new file mode 100644 index 0000000..2944406 --- /dev/null +++ b/src/main/java/se/urmo/hemhub/repo/TaskSpecifications.java @@ -0,0 +1,34 @@ +package se.urmo.hemhub.repo; + +import org.springframework.data.jpa.domain.Specification; +import se.urmo.hemhub.domain.Task; + +import java.time.LocalDate; +import java.util.UUID; + +public class TaskSpecifications { + + public static Specification inProject(UUID projectId) { + return (root, q, cb) -> cb.equal(root.get("project").get("id"), projectId); + } + + public static Specification inHousehold(UUID householdId) { + return (root, q, cb) -> cb.equal(root.get("household").get("id"), householdId); + } + + public static Specification withStatus(Task.Status status) { + return (root, q, cb) -> cb.equal(root.get("status"), status); + } + + public static Specification withPriority(Task.Priority prio) { + return (root, q, cb) -> cb.equal(root.get("priority"), prio); + } + + public static Specification dueFrom(LocalDate from) { + return (root, q, cb) -> cb.greaterThanOrEqualTo(root.get("dueDate"), from); + } + + public static Specification dueTo(LocalDate to) { + return (root, q, cb) -> cb.lessThanOrEqualTo(root.get("dueDate"), to); + } +} diff --git a/src/main/java/se/urmo/hemhub/service/ProjectTaskService.java b/src/main/java/se/urmo/hemhub/service/ProjectTaskService.java index 89f0312..d7522fa 100644 --- a/src/main/java/se/urmo/hemhub/service/ProjectTaskService.java +++ b/src/main/java/se/urmo/hemhub/service/ProjectTaskService.java @@ -1,15 +1,21 @@ package se.urmo.hemhub.service; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import se.urmo.hemhub.domain.*; +import se.urmo.hemhub.dto.ProjectTaskDtos.TaskFilter; import se.urmo.hemhub.repo.*; import java.time.LocalDate; import java.util.*; import java.util.function.Consumer; +import static org.springframework.data.jpa.domain.Specification.where; +import static se.urmo.hemhub.repo.TaskSpecifications.*; + @Service @RequiredArgsConstructor public class ProjectTaskService { @@ -42,16 +48,16 @@ public class ProjectTaskService { } @Transactional(readOnly = true) - public List listProjects(UUID householdId, String requesterSub) { + public Page listProjects(UUID householdId, String requesterSub, Pageable pageable) { ensureMember(householdId, requesterSub); - return projects.findByHouseholdId(householdId); + return projects.findByHouseholdId(householdId, pageable); } @Transactional public void deleteProject(UUID projectId, String requesterSub) { var p = projects.findById(projectId).orElseThrow(); ensureOwner(p.getHousehold().getId(), requesterSub); - projects.delete(p); // cascades tasks for that project + projects.delete(p); } // -------- Tasks (project-scoped) -------- @@ -66,10 +72,18 @@ public class ProjectTaskService { } @Transactional(readOnly = true) - public List listTasks(UUID projectId, String requesterSub) { + public Page listTasks(UUID projectId, String requesterSub, TaskFilter filter, Pageable pageable) { var p = projects.findById(projectId).orElseThrow(); ensureMember(p.getHousehold().getId(), requesterSub); - return tasks.findByProjectId(projectId); + + var spec = where(inProject(projectId)); + if (filter != null) { + if (filter.status() != null) spec = spec.and(withStatus(Task.Status.valueOf(filter.status()))); + if (filter.priority() != null) spec = spec.and(withPriority(Task.Priority.valueOf(filter.priority()))); + if (filter.dueFrom() != null) spec = spec.and(dueFrom(filter.dueFrom())); + if (filter.dueTo() != null) spec = spec.and(dueTo(filter.dueTo())); + } + return tasks.findAll(spec, pageable); } // -------- Tasks (household-scoped, no project) -------- @@ -85,9 +99,19 @@ public class ProjectTaskService { } @Transactional(readOnly = true) - public List listHouseholdTasks(UUID householdId, String requesterSub) { + public Page listHouseholdTasks(UUID householdId, String requesterSub, TaskFilter filter, Pageable pageable) { ensureMember(householdId, requesterSub); - return tasks.findByHouseholdIdAndProjectIsNull(householdId); + + var spec = where(inHousehold(householdId)); + // limit to non-project tasks? No—design choice: include both unless filter requires "projectless only". + // If you want ONLY household-level tasks (no project), add spec = spec.and((r,q,cb)->cb.isNull(r.get("project"))); + if (filter != null) { + if (filter.status() != null) spec = spec.and(withStatus(Task.Status.valueOf(filter.status()))); + if (filter.priority() != null) spec = spec.and(withPriority(Task.Priority.valueOf(filter.priority()))); + if (filter.dueFrom() != null) spec = spec.and(dueFrom(filter.dueFrom())); + if (filter.dueTo() != null) spec = spec.and(dueTo(filter.dueTo())); + } + return tasks.findAll(spec, pageable); } // -------- Common updates -------- @@ -100,9 +124,10 @@ public class ProjectTaskService { return tasks.save(t); } + // -------- Iteration 4 helper -------- @Transactional(readOnly = true) - public java.util.List tasksDueTomorrowForUser(String userSub) { - var tomorrow = LocalDate.now(java.time.Clock.systemDefaultZone()).plusDays(1); - return tasks.findDueByDateForUser(tomorrow, Task.Status.DONE, userSub); + public java.util.List tasksDueTomorrowForUser(String userSub) { + var tomorrow = java.time.LocalDate.now(java.time.Clock.systemDefaultZone()).plusDays(1); + return tasks.findDueByDateForUser(tomorrow, se.urmo.hemhub.domain.Task.Status.DONE, userSub); } } diff --git a/src/main/java/se/urmo/hemhub/web/ProjectTaskController.java b/src/main/java/se/urmo/hemhub/web/ProjectTaskController.java index 4c55489..001974e 100644 --- a/src/main/java/se/urmo/hemhub/web/ProjectTaskController.java +++ b/src/main/java/se/urmo/hemhub/web/ProjectTaskController.java @@ -2,6 +2,8 @@ package se.urmo.hemhub.web; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.*; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.jwt.Jwt; @@ -9,6 +11,7 @@ import se.urmo.hemhub.domain.Task; import se.urmo.hemhub.dto.ProjectTaskDtos.*; import se.urmo.hemhub.service.ProjectTaskService; +import java.time.LocalDate; import java.util.*; import static java.util.stream.Collectors.toList; @@ -29,10 +32,9 @@ public class ProjectTaskController { } @GetMapping("/households/{householdId}/projects") - public List listProjects(@PathVariable UUID householdId, Authentication auth) { - return svc.listProjects(householdId, sub(auth)).stream() - .map(p -> new ProjectResponse(p.getId(), p.getName(), p.getDescription())) - .collect(toList()); + public Page listProjects(@PathVariable UUID householdId, Authentication auth, Pageable pageable) { + return svc.listProjects(householdId, sub(auth), pageable) + .map(p -> new ProjectResponse(p.getId(), p.getName(), p.getDescription())); } @DeleteMapping("/projects/{projectId}") @@ -54,8 +56,15 @@ public class ProjectTaskController { } @GetMapping("/projects/{projectId}/tasks") - public List listTasks(@PathVariable UUID projectId, Authentication auth) { - return svc.listTasks(projectId, sub(auth)).stream().map(this::toDto).collect(toList()); + public Page listTasks(@PathVariable UUID projectId, + @RequestParam(required = false) String status, + @RequestParam(required = false) String priority, + @RequestParam(required = false) LocalDate dueFrom, + @RequestParam(required = false) LocalDate dueTo, + Authentication auth, + Pageable pageable) { + var filter = new TaskFilter(status, priority, dueFrom, dueTo); + return svc.listTasks(projectId, sub(auth), filter, pageable).map(this::toDto); } // -------- Tasks (household-scoped) -------- @@ -74,8 +83,15 @@ public class ProjectTaskController { } @GetMapping("/households/{householdId}/tasks") - public List listHouseholdTasks(@PathVariable UUID householdId, Authentication auth) { - return svc.listHouseholdTasks(householdId, sub(auth)).stream().map(this::toDto).collect(toList()); + public Page listHouseholdTasks(@PathVariable UUID householdId, + @RequestParam(required = false) String status, + @RequestParam(required = false) String priority, + @RequestParam(required = false) LocalDate dueFrom, + @RequestParam(required = false) LocalDate dueTo, + Authentication auth, + Pageable pageable) { + var filter = new TaskFilter(status, priority, dueFrom, dueTo); + return svc.listHouseholdTasks(householdId, sub(auth), filter, pageable).map(this::toDto); } // -------- Task update (common) -------- @@ -95,17 +111,15 @@ public class ProjectTaskController { return toDto(t); } + // -------- Iteration 4 utility -------- + + @GetMapping("/tasks/due/tomorrow") + public List tasksDueTomorrow(Authentication auth) { + return svc.tasksDueTomorrowForUser(sub(auth)).stream().map(this::toDto).toList(); + } + private TaskResponse toDto(Task t) { return new TaskResponse(t.getId(), t.getTitle(), t.getDescription(), t.getPriority().name(), t.getStatus().name(), t.getDueDate(), t.getAssigneeSub()); } - - @GetMapping("/tasks/due/tomorrow") - public List tasksDueTomorrow(Authentication auth) { - var todayPlus1 = java.time.LocalDate.now(); - // Service method not required; use repository through a lightweight orchestrator or reuse a new service - // To keep controllers thin, we’ll delegate to svc via a tiny method (add this method in ProjectTaskService if you prefer). - var list = svc.tasksDueTomorrowForUser(sub(auth)); - return list.stream().map(this::toDto).toList(); - } }