Pagination + filtering
This commit is contained in:
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
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;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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, 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