Pagination + filtering

This commit is contained in:
Urban Modig
2025-10-08 19:54:31 +02:00
parent 24c3b7a72c
commit 302078fbec
5 changed files with 113 additions and 35 deletions

View File

@ -1,10 +1,13 @@
package se.urmo.hemhub.repo; 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 org.springframework.data.jpa.repository.JpaRepository;
import se.urmo.hemhub.domain.Project; import se.urmo.hemhub.domain.Project;
import java.util.*; import java.util.*;
public interface ProjectRepository extends JpaRepository<Project, UUID> { public interface ProjectRepository extends JpaRepository<Project, UUID> {
List<Project> findByHouseholdId(UUID householdId); Page<Project> findByHouseholdId(UUID householdId, Pageable pageable);
boolean existsByIdAndHouseholdId(UUID projectId, UUID householdId); boolean existsByIdAndHouseholdId(UUID projectId, UUID householdId);
} }

View File

@ -1,25 +1,27 @@
package se.urmo.hemhub.repo; package se.urmo.hemhub.repo;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.domain.Page;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import se.urmo.hemhub.domain.Task; import se.urmo.hemhub.domain.Task;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.*; import java.util.*;
public interface TaskRepository extends JpaRepository<Task, UUID> { public interface TaskRepository extends JpaRepository<Task, UUID>, JpaSpecificationExecutor<Task> {
// Simple finders (used in existing flows)
List<Task> findByProjectId(UUID projectId); List<Task> findByProjectId(UUID projectId);
boolean existsByIdAndProjectId(UUID taskId, UUID projectId); boolean existsByIdAndProjectId(UUID taskId, UUID projectId);
// Household-level tasks (no project)
List<Task> findByHouseholdIdAndProjectIsNull(UUID householdId); List<Task> findByHouseholdIdAndProjectIsNull(UUID householdId);
// All tasks due on a given day, excluding a particular status (e.g., DONE) // Paging versions (used by new endpoints)
Page<Task> findAll(org.springframework.data.jpa.domain.Specification<Task> spec, Pageable pageable);
// For Iteration 4 service
List<Task> findByDueDateAndStatusNot(LocalDate date, Task.Status status); List<Task> findByDueDateAndStatusNot(LocalDate date, Task.Status status);
// Only tasks for households where the given user is a member (for the /me view)
@Query(""" @Query("""
select t select t
from Task t from Task t

View File

@ -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<Task> inProject(UUID projectId) {
return (root, q, cb) -> cb.equal(root.get("project").get("id"), projectId);
}
public static Specification<Task> inHousehold(UUID householdId) {
return (root, q, cb) -> cb.equal(root.get("household").get("id"), householdId);
}
public static Specification<Task> withStatus(Task.Status status) {
return (root, q, cb) -> cb.equal(root.get("status"), status);
}
public static Specification<Task> withPriority(Task.Priority prio) {
return (root, q, cb) -> cb.equal(root.get("priority"), prio);
}
public static Specification<Task> dueFrom(LocalDate from) {
return (root, q, cb) -> cb.greaterThanOrEqualTo(root.get("dueDate"), from);
}
public static Specification<Task> dueTo(LocalDate to) {
return (root, q, cb) -> cb.lessThanOrEqualTo(root.get("dueDate"), to);
}
}

View File

@ -1,15 +1,21 @@
package se.urmo.hemhub.service; package se.urmo.hemhub.service;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import se.urmo.hemhub.domain.*; import se.urmo.hemhub.domain.*;
import se.urmo.hemhub.dto.ProjectTaskDtos.TaskFilter;
import se.urmo.hemhub.repo.*; import se.urmo.hemhub.repo.*;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.*; import java.util.*;
import java.util.function.Consumer; import java.util.function.Consumer;
import static org.springframework.data.jpa.domain.Specification.where;
import static se.urmo.hemhub.repo.TaskSpecifications.*;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class ProjectTaskService { public class ProjectTaskService {
@ -42,16 +48,16 @@ public class ProjectTaskService {
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
public List<Project> listProjects(UUID householdId, String requesterSub) { public Page<Project> listProjects(UUID householdId, String requesterSub, Pageable pageable) {
ensureMember(householdId, requesterSub); ensureMember(householdId, requesterSub);
return projects.findByHouseholdId(householdId); return projects.findByHouseholdId(householdId, pageable);
} }
@Transactional @Transactional
public void deleteProject(UUID projectId, String requesterSub) { public void deleteProject(UUID projectId, String requesterSub) {
var p = projects.findById(projectId).orElseThrow(); var p = projects.findById(projectId).orElseThrow();
ensureOwner(p.getHousehold().getId(), requesterSub); ensureOwner(p.getHousehold().getId(), requesterSub);
projects.delete(p); // cascades tasks for that project projects.delete(p);
} }
// -------- Tasks (project-scoped) -------- // -------- Tasks (project-scoped) --------
@ -66,10 +72,18 @@ public class ProjectTaskService {
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
public List<Task> listTasks(UUID projectId, String requesterSub) { public Page<Task> listTasks(UUID projectId, String requesterSub, TaskFilter filter, Pageable pageable) {
var p = projects.findById(projectId).orElseThrow(); var p = projects.findById(projectId).orElseThrow();
ensureMember(p.getHousehold().getId(), requesterSub); 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) -------- // -------- Tasks (household-scoped, no project) --------
@ -85,9 +99,19 @@ public class ProjectTaskService {
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
public List<Task> listHouseholdTasks(UUID householdId, String requesterSub) { public Page<Task> listHouseholdTasks(UUID householdId, String requesterSub, TaskFilter filter, Pageable pageable) {
ensureMember(householdId, requesterSub); 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 -------- // -------- Common updates --------
@ -100,9 +124,10 @@ public class ProjectTaskService {
return tasks.save(t); return tasks.save(t);
} }
// -------- Iteration 4 helper --------
@Transactional(readOnly = true) @Transactional(readOnly = true)
public java.util.List<Task> tasksDueTomorrowForUser(String userSub) { public java.util.List<se.urmo.hemhub.domain.Task> tasksDueTomorrowForUser(String userSub) {
var tomorrow = LocalDate.now(java.time.Clock.systemDefaultZone()).plusDays(1); var tomorrow = java.time.LocalDate.now(java.time.Clock.systemDefaultZone()).plusDays(1);
return tasks.findDueByDateForUser(tomorrow, Task.Status.DONE, userSub); return tasks.findDueByDateForUser(tomorrow, se.urmo.hemhub.domain.Task.Status.DONE, userSub);
} }
} }

View File

@ -2,6 +2,8 @@ package se.urmo.hemhub.web;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt; 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.dto.ProjectTaskDtos.*;
import se.urmo.hemhub.service.ProjectTaskService; import se.urmo.hemhub.service.ProjectTaskService;
import java.time.LocalDate;
import java.util.*; import java.util.*;
import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toList;
@ -29,10 +32,9 @@ public class ProjectTaskController {
} }
@GetMapping("/households/{householdId}/projects") @GetMapping("/households/{householdId}/projects")
public List<ProjectResponse> listProjects(@PathVariable UUID householdId, Authentication auth) { public Page<ProjectResponse> listProjects(@PathVariable UUID householdId, Authentication auth, Pageable pageable) {
return svc.listProjects(householdId, sub(auth)).stream() return svc.listProjects(householdId, sub(auth), pageable)
.map(p -> new ProjectResponse(p.getId(), p.getName(), p.getDescription())) .map(p -> new ProjectResponse(p.getId(), p.getName(), p.getDescription()));
.collect(toList());
} }
@DeleteMapping("/projects/{projectId}") @DeleteMapping("/projects/{projectId}")
@ -54,8 +56,15 @@ public class ProjectTaskController {
} }
@GetMapping("/projects/{projectId}/tasks") @GetMapping("/projects/{projectId}/tasks")
public List<TaskResponse> listTasks(@PathVariable UUID projectId, Authentication auth) { public Page<TaskResponse> listTasks(@PathVariable UUID projectId,
return svc.listTasks(projectId, sub(auth)).stream().map(this::toDto).collect(toList()); @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) -------- // -------- Tasks (household-scoped) --------
@ -74,8 +83,15 @@ public class ProjectTaskController {
} }
@GetMapping("/households/{householdId}/tasks") @GetMapping("/households/{householdId}/tasks")
public List<TaskResponse> listHouseholdTasks(@PathVariable UUID householdId, Authentication auth) { public Page<TaskResponse> listHouseholdTasks(@PathVariable UUID householdId,
return svc.listHouseholdTasks(householdId, sub(auth)).stream().map(this::toDto).collect(toList()); @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) -------- // -------- Task update (common) --------
@ -95,17 +111,15 @@ public class ProjectTaskController {
return toDto(t); return toDto(t);
} }
// -------- Iteration 4 utility --------
@GetMapping("/tasks/due/tomorrow")
public List<TaskResponse> tasksDueTomorrow(Authentication auth) {
return svc.tasksDueTomorrowForUser(sub(auth)).stream().map(this::toDto).toList();
}
private TaskResponse toDto(Task t) { private TaskResponse toDto(Task t) {
return new TaskResponse(t.getId(), t.getTitle(), t.getDescription(), return new TaskResponse(t.getId(), t.getTitle(), t.getDescription(),
t.getPriority().name(), t.getStatus().name(), t.getDueDate(), t.getAssigneeSub()); t.getPriority().name(), t.getStatus().name(), t.getDueDate(), t.getAssigneeSub());
} }
@GetMapping("/tasks/due/tomorrow")
public List<TaskResponse> 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, well 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();
}
} }