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
This commit is contained in:
acx10
2026-01-29 00:21:33 -07:00
parent bbea391c9c
commit 0c56b7b720
3 changed files with 73 additions and 0 deletions

View File

@@ -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<List<ReadingSessionHeatmapResponse>> getHeatmapForMonth(
@RequestParam int year,
@RequestParam int month) {
List<ReadingSessionHeatmapResponse> 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<CompletionTimelineResponse> 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<List<BookCompletionHeatmapResponse>> getBookCompletionHeatmap() {
List<BookCompletionHeatmapResponse> heatmapData = readingSessionService.getBookCompletionHeatmap();
return ResponseEntity.ok(heatmapData);
}
}

View File

@@ -25,6 +25,20 @@ public interface ReadingSessionRepository extends JpaRepository<ReadingSessionEn
""")
List<ReadingSessionCountDto> 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<ReadingSessionCountDto> findSessionCountsByUserAndYearAndMonth(
@Param("userId") Long userId,
@Param("year") int year,
@Param("month") int month);
@Query("""
SELECT
b.id as bookId,

View File

@@ -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<ReadingSessionHeatmapResponse> 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<ReadingSessionTimelineResponse> 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<BookCompletionHeatmapResponse> 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());
}
}