From aec449cc8122ee2f2f29383157f782d9a3817fc3 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Thu, 27 Aug 2026 14:24:07 +0200 Subject: [PATCH] feat: add admin occupancy and reservation history reports --- .../report/AdminReportController.java | 42 +++++++++++++++ .../slotline/report/AdminReportService.java | 54 +++++++++++++++++++ .../slotline/report/dto/OccupancyReport.java | 14 +++++ .../report/dto/ReservationHistoryEntry.java | 27 ++++++++++ 4 files changed, 137 insertions(+) create mode 100644 src/main/java/com/slotline/report/AdminReportController.java create mode 100644 src/main/java/com/slotline/report/AdminReportService.java create mode 100644 src/main/java/com/slotline/report/dto/OccupancyReport.java create mode 100644 src/main/java/com/slotline/report/dto/ReservationHistoryEntry.java diff --git a/src/main/java/com/slotline/report/AdminReportController.java b/src/main/java/com/slotline/report/AdminReportController.java new file mode 100644 index 0000000..9f2ebac --- /dev/null +++ b/src/main/java/com/slotline/report/AdminReportController.java @@ -0,0 +1,42 @@ +package com.slotline.report; + +import com.slotline.report.dto.OccupancyReport; +import com.slotline.report.dto.ReservationHistoryEntry; +import com.slotline.security.AuthenticatedUser; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.Instant; +import java.util.List; + +@RestController +@RequestMapping("/api/admin/reports") +@Tag(name = "Admin reports") +public class AdminReportController { + + private final AdminReportService reportService; + + public AdminReportController(AdminReportService reportService) { + this.reportService = reportService; + } + + @GetMapping("/resources/{resourceId}/occupancy") + @Operation(summary = "Occupancy of a resource over a time window (ADMIN only)") + public OccupancyReport occupancy(@AuthenticationPrincipal AuthenticatedUser actor, @PathVariable Long resourceId, + @RequestParam String from, @RequestParam String until) { + return reportService.occupancy(actor.organizationId(), resourceId, Instant.parse(from), Instant.parse(until)); + } + + @GetMapping("/resources/{resourceId}/history") + @Operation(summary = "Full reservation history of a resource (ADMIN only)") + public List history(@AuthenticationPrincipal AuthenticatedUser actor, + @PathVariable Long resourceId) { + return reportService.history(actor.organizationId(), resourceId); + } +} diff --git a/src/main/java/com/slotline/report/AdminReportService.java b/src/main/java/com/slotline/report/AdminReportService.java new file mode 100644 index 0000000..f3170c9 --- /dev/null +++ b/src/main/java/com/slotline/report/AdminReportService.java @@ -0,0 +1,54 @@ +package com.slotline.report; + +import com.slotline.report.dto.OccupancyReport; +import com.slotline.report.dto.ReservationHistoryEntry; +import com.slotline.reservation.Reservation; +import com.slotline.reservation.ReservationRepository; +import com.slotline.resource.Resource; +import com.slotline.resource.ResourceService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.time.Instant; +import java.util.List; + +@Service +public class AdminReportService { + + private final ReservationRepository reservations; + private final ResourceService resources; + + public AdminReportService(ReservationRepository reservations, ResourceService resources) { + this.reservations = reservations; + this.resources = resources; + } + + @Transactional(readOnly = true) + public OccupancyReport occupancy(Long organizationId, Long resourceId, Instant from, Instant until) { + Resource resource = resources.require(organizationId, resourceId); + List active = reservations.findActiveForResourceBetween(resource.getId(), from, until); + long bookedMinutes = active.stream() + .mapToLong(reservation -> clampedMinutes(reservation, from, until)) + .sum(); + long windowMinutes = Duration.between(from, until).toMinutes(); + double utilization = windowMinutes > 0 ? (bookedMinutes * 100.0) / windowMinutes : 0.0; + return new OccupancyReport(resource.getId(), resource.getName(), from, until, active.size(), bookedMinutes, + windowMinutes, Math.round(utilization * 100.0) / 100.0); + } + + @Transactional(readOnly = true) + public List history(Long organizationId, Long resourceId) { + Resource resource = resources.require(organizationId, resourceId); + return reservations.findByResourceIdOrderByStartTimeAsc(resource.getId()).stream() + .map(ReservationHistoryEntry::from) + .toList(); + } + + private long clampedMinutes(Reservation reservation, Instant from, Instant until) { + Instant start = reservation.getStartTime().isBefore(from) ? from : reservation.getStartTime(); + Instant end = reservation.getEndTime().isAfter(until) ? until : reservation.getEndTime(); + long minutes = Duration.between(start, end).toMinutes(); + return Math.max(minutes, 0); + } +} diff --git a/src/main/java/com/slotline/report/dto/OccupancyReport.java b/src/main/java/com/slotline/report/dto/OccupancyReport.java new file mode 100644 index 0000000..80f66ad --- /dev/null +++ b/src/main/java/com/slotline/report/dto/OccupancyReport.java @@ -0,0 +1,14 @@ +package com.slotline.report.dto; + +import java.time.Instant; + +public record OccupancyReport( + Long resourceId, + String resourceName, + Instant from, + Instant until, + int reservationCount, + long bookedMinutes, + long windowMinutes, + double utilizationPercent) { +} diff --git a/src/main/java/com/slotline/report/dto/ReservationHistoryEntry.java b/src/main/java/com/slotline/report/dto/ReservationHistoryEntry.java new file mode 100644 index 0000000..c41b19e --- /dev/null +++ b/src/main/java/com/slotline/report/dto/ReservationHistoryEntry.java @@ -0,0 +1,27 @@ +package com.slotline.report.dto; + +import com.slotline.reservation.Reservation; +import com.slotline.reservation.ReservationStatus; + +import java.time.Instant; + +public record ReservationHistoryEntry( + Long id, + Long userId, + Instant startTime, + Instant endTime, + ReservationStatus status, + String seriesId, + Instant createdAt) { + + public static ReservationHistoryEntry from(Reservation reservation) { + return new ReservationHistoryEntry( + reservation.getId(), + reservation.getUserId(), + reservation.getStartTime(), + reservation.getEndTime(), + reservation.getStatus(), + reservation.getSeriesId(), + reservation.getCreatedAt()); + } +}