Pagination + filtering
This commit is contained in:
@ -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<Project, UUID> {
|
||||
List<Project> findByHouseholdId(UUID householdId);
|
||||
Page<Project> findByHouseholdId(UUID householdId, Pageable pageable);
|
||||
boolean existsByIdAndHouseholdId(UUID projectId, UUID householdId);
|
||||
}
|
||||
|
||||
@ -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<Task, UUID> {
|
||||
public interface TaskRepository extends JpaRepository<Task, UUID>, JpaSpecificationExecutor<Task> {
|
||||
|
||||
// Simple finders (used in existing flows)
|
||||
List<Task> findByProjectId(UUID projectId);
|
||||
boolean existsByIdAndProjectId(UUID taskId, UUID projectId);
|
||||
|
||||
// Household-level tasks (no project)
|
||||
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);
|
||||
|
||||
// Only tasks for households where the given user is a member (for the /me view)
|
||||
@Query("""
|
||||
select t
|
||||
from Task t
|
||||
|
||||
34
src/main/java/se/urmo/hemhub/repo/TaskSpecifications.java
Normal file
34
src/main/java/se/urmo/hemhub/repo/TaskSpecifications.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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<Project> listProjects(UUID householdId, String requesterSub) {
|
||||
public Page<Project> 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<Task> listTasks(UUID projectId, String requesterSub) {
|
||||
public Page<Task> 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<Task> listHouseholdTasks(UUID householdId, String requesterSub) {
|
||||
public Page<Task> 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<Task> 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<se.urmo.hemhub.domain.Task> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<ProjectResponse> 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<ProjectResponse> 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<TaskResponse> listTasks(@PathVariable UUID projectId, Authentication auth) {
|
||||
return svc.listTasks(projectId, sub(auth)).stream().map(this::toDto).collect(toList());
|
||||
public Page<TaskResponse> 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<TaskResponse> listHouseholdTasks(@PathVariable UUID householdId, Authentication auth) {
|
||||
return svc.listHouseholdTasks(householdId, sub(auth)).stream().map(this::toDto).collect(toList());
|
||||
public Page<TaskResponse> 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<TaskResponse> 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<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, 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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user