From 0c56b7b720f6cbf08ff9dd77fd0937b80493be20 Mon Sep 17 00:00:00 2001 From: acx10 Date: Thu, 29 Jan 2026 00:21:33 -0700 Subject: [PATCH] feat(user-stats): add monthly heatmap endpoint - Add GET /api/v1/user-stats/heatmap/monthly endpoint for month-based data - Keep original /heatmap endpoint unchanged for backward compatibility - Add findSessionCountsByUserAndYearAndMonth repository method - Add getSessionHeatmapForMonth service method --- .../controller/UserStatsController.java | 26 +++++++++++++++ .../repository/ReadingSessionRepository.java | 14 ++++++++ .../service/ReadingSessionService.java | 33 +++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/controller/UserStatsController.java b/booklore-api/src/main/java/com/adityachandel/booklore/controller/UserStatsController.java index c1501e799..3374075b4 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/controller/UserStatsController.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/controller/UserStatsController.java @@ -31,6 +31,20 @@ public class UserStatsController { return ResponseEntity.ok(heatmapData); } + @Operation(summary = "Get reading session heatmap for a month", description = "Returns daily reading session counts for the authenticated user for a specific year and month") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Heatmap data retrieved successfully"), + @ApiResponse(responseCode = "401", description = "Unauthorized") + }) + @GetMapping("/heatmap/monthly") + @PreAuthorize("@securityUtil.canAccessUserStats() or @securityUtil.isAdmin()") + public ResponseEntity> getHeatmapForMonth( + @RequestParam int year, + @RequestParam int month) { + List heatmapData = readingSessionService.getSessionHeatmapForMonth(year, month); + return ResponseEntity.ok(heatmapData); + } + @Operation(summary = "Get reading session timeline for a week", description = "Returns reading sessions grouped by book for calendar timeline view") @ApiResponses({ @ApiResponse(responseCode = "200", description = "Timeline data retrieved successfully"), @@ -109,4 +123,16 @@ public class UserStatsController { List timeline = readingSessionService.getCompletionTimeline(year); return ResponseEntity.ok(timeline); } + + @Operation(summary = "Get book completion heatmap", description = "Returns monthly book completion counts for the last 10 years for the authenticated user") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Book completion heatmap data retrieved successfully"), + @ApiResponse(responseCode = "401", description = "Unauthorized") + }) + @GetMapping("/book-completion-heatmap") + @PreAuthorize("@securityUtil.canAccessUserStats() or @securityUtil.isAdmin()") + public ResponseEntity> getBookCompletionHeatmap() { + List heatmapData = readingSessionService.getBookCompletionHeatmap(); + return ResponseEntity.ok(heatmapData); + } } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/repository/ReadingSessionRepository.java b/booklore-api/src/main/java/com/adityachandel/booklore/repository/ReadingSessionRepository.java index b95a680b9..c4efec662 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/repository/ReadingSessionRepository.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/repository/ReadingSessionRepository.java @@ -25,6 +25,20 @@ public interface ReadingSessionRepository extends JpaRepository findSessionCountsByUserAndYear(@Param("userId") Long userId, @Param("year") int year); + @Query(""" + SELECT CAST(rs.startTime AS LocalDate) as date, COUNT(rs) as count + FROM ReadingSessionEntity rs + WHERE rs.user.id = :userId + AND YEAR(rs.startTime) = :year + AND MONTH(rs.startTime) = :month + GROUP BY CAST(rs.startTime AS LocalDate) + ORDER BY date + """) + List findSessionCountsByUserAndYearAndMonth( + @Param("userId") Long userId, + @Param("year") int year, + @Param("month") int month); + @Query(""" SELECT b.id as bookId, diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/ReadingSessionService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/ReadingSessionService.java index a33bf9393..42f4a5442 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/ReadingSessionService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/ReadingSessionService.java @@ -4,6 +4,7 @@ import com.adityachandel.booklore.config.security.service.AuthenticationService; import com.adityachandel.booklore.exception.ApiError; import com.adityachandel.booklore.model.dto.BookLoreUser; import com.adityachandel.booklore.model.dto.request.ReadingSessionRequest; +import com.adityachandel.booklore.model.dto.response.BookCompletionHeatmapResponse; import com.adityachandel.booklore.model.dto.response.CompletionTimelineResponse; import com.adityachandel.booklore.model.dto.response.FavoriteReadingDaysResponse; import com.adityachandel.booklore.model.dto.response.GenreStatisticsResponse; @@ -94,6 +95,20 @@ public class ReadingSessionService { .collect(Collectors.toList()); } + @Transactional(readOnly = true) + public List getSessionHeatmapForMonth(int year, int month) { + BookLoreUser authenticatedUser = authenticationService.getAuthenticatedUser(); + Long userId = authenticatedUser.getId(); + + return readingSessionRepository.findSessionCountsByUserAndYearAndMonth(userId, year, month) + .stream() + .map(dto -> ReadingSessionHeatmapResponse.builder() + .date(dto.getDate()) + .count(dto.getCount()) + .build()) + .collect(Collectors.toList()); + } + @Transactional(readOnly = true) public List getSessionTimelineForWeek(int year, int week) { BookLoreUser authenticatedUser = authenticationService.getAuthenticatedUser(); @@ -254,4 +269,22 @@ public class ReadingSessionService { .createdAt(session.getCreatedAt()) .build()); } + + @Transactional(readOnly = true) + public List getBookCompletionHeatmap() { + BookLoreUser authenticatedUser = authenticationService.getAuthenticatedUser(); + Long userId = authenticatedUser.getId(); + + int currentYear = LocalDate.now().getYear(); + int startYear = currentYear - 9; + + return userBookProgressRepository.findBookCompletionHeatmap(userId, startYear, currentYear) + .stream() + .map(dto -> BookCompletionHeatmapResponse.builder() + .year(dto.getYear()) + .month(dto.getMonth()) + .count(dto.getCount()) + .build()) + .collect(Collectors.toList()); + } }