diff --git a/pom.xml b/pom.xml index 9a40eeb..03b1c9b 100644 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,21 @@ com.fasterxml.jackson.core jackson-databind - + + + + org.apache.pdfbox + pdfbox + 2.0.30 + + + + + org.apache.poi + poi-ooxml + 5.2.3 + + org.springframework.boot spring-boot-starter-security diff --git a/src/main/java/edu/eci/cvds/prometeo/controller/UserController.java b/src/main/java/edu/eci/cvds/prometeo/controller/UserController.java index 9d2b40e..0edda18 100644 --- a/src/main/java/edu/eci/cvds/prometeo/controller/UserController.java +++ b/src/main/java/edu/eci/cvds/prometeo/controller/UserController.java @@ -1,17 +1,17 @@ package edu.eci.cvds.prometeo.controller; import edu.eci.cvds.prometeo.model.*; +import edu.eci.cvds.prometeo.model.enums.ReportFormat; import edu.eci.cvds.prometeo.repository.RoutineExerciseRepository; import edu.eci.cvds.prometeo.repository.RoutineRepository; import edu.eci.cvds.prometeo.service.*; import edu.eci.cvds.prometeo.dto.*; +import io.swagger.v3.oas.annotations.enums.ParameterIn; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -78,6 +78,9 @@ public class UserController { @Autowired private GoalService goalService; + @Autowired + private ReportService reportService; + // ----------------------------------------------------- // User profile endpoints // ----------------------------------------------------- @@ -930,21 +933,145 @@ public ResponseEntity> getAttendanceStatistics( } @GetMapping("/gym/sessions/{sessionId}") -@Operation(summary = "Get session by ID", description = "Retrieves details of a specific gym session") -@ApiResponse(responseCode = "200", description = "Session found") -@ApiResponse(responseCode = "404", description = "Session not found") -public ResponseEntity getSessionById( - @Parameter(description = "Session ID") @PathVariable UUID sessionId) { - - try { - Object session = gymSessionService.getSessionById(sessionId); - return ResponseEntity.ok(session); - } catch (Exception e) { - Map error = new HashMap<>(); - error.put("error", e.getMessage()); - return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + @Operation(summary = "Get session by ID", description = "Retrieves details of a specific gym session") + @ApiResponse(responseCode = "200", description = "Session found") + @ApiResponse(responseCode = "404", description = "Session not found") + public ResponseEntity getSessionById( + @Parameter(description = "Session ID") @PathVariable UUID sessionId) { + + try { + Object session = gymSessionService.getSessionById(sessionId); + return ResponseEntity.ok(session); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } } -} + + // ----------------------------------------------------- + // Reports and analysis endpoints + // ----------------------------------------------------- + + @GetMapping("/user-progress") + @Operation( + summary = "Generate user progress report", + description = "Returns a report with the user's physical progress over time (e.g., weight and goals).", + responses = { + @ApiResponse(responseCode = "200", description = "Report generated successfully", + content = @Content(mediaType = "application/octet-stream")), + @ApiResponse(responseCode = "400", description = "Invalid parameters", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", content = @Content) + } + ) + public ResponseEntity getUserProgressReport( + @Parameter(name = "userId", description = "UUID of the user", required = true, in = ParameterIn.QUERY) + @RequestParam UUID userId, + + @Parameter(name = "format", description = "Report format: PDF, XLSX, CSV, JSON", required = true, in = ParameterIn.QUERY) + @RequestParam ReportFormat format + ) { + byte[] report = reportService.generateUserProgressReport(userId, format); + return buildResponse(report, format, "user_progress_report"); + } + + @GetMapping("/gym-usage") + @Operation( + summary = "Generate gym usage report", + description = "Returns statistics about gym usage (reservations, capacity, duration) for a given date range.", + responses = { + @ApiResponse(responseCode = "200", description = "Report generated successfully", content = @Content), + @ApiResponse(responseCode = "400", description = "Invalid parameters", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", content = @Content) + } + ) + public ResponseEntity getGymUsageReport( + @Parameter(name = "startDate", description = "Start date in yyyy-MM-dd format", required = true) + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + + @Parameter(name = "endDate", description = "End date in yyyy-MM-dd format", required = true) + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, + + @Parameter(name = "format", description = "Report format: PDF, XLSX, CSV, JSON", required = true, in = ParameterIn.QUERY) + @RequestParam ReportFormat format + ) { + byte[] report = reportService.generateGymUsageReport(startDate, endDate, format); + return buildResponse(report, format, "gym_usage_report"); + } + + @GetMapping("/attendance") + @Operation( + summary = "Generate attendance report", + description = "Returns daily attendance statistics for the gym within a date range.", + responses = { + @ApiResponse(responseCode = "200", description = "Report generated successfully", content = @Content), + @ApiResponse(responseCode = "400", description = "Invalid parameters", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", content = @Content) + } + ) + public ResponseEntity getAttendanceReport( + @Parameter(name = "startDate", description = "Start date in yyyy-MM-dd format", required = true) + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + + @Parameter(name = "endDate", description = "End date in yyyy-MM-dd format", required = true) + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, + + @Parameter(name = "format", description = "Report format: PDF, XLSX, CSV, JSON", required = true, in = ParameterIn.QUERY) + @RequestParam ReportFormat format + ) { + byte[] report = reportService.getAttendanceStatistics(startDate, endDate, format); + return buildResponse(report, format, "attendance_report"); + } + + /** + * Builds an HTTP response with appropriate headers for file download, + * based on the specified report format. + * + *

This method sets the correct Content-Type and + * Content-Disposition headers to allow clients to download + * the report in the requested format (PDF, XLSX, CSV, JSON).

+ * + * @param content the byte array representing the report content + * @param format the format of the report (PDF, XLSX, CSV, JSON) + * @param filenameBase the base name for the file (without extension) + * @return a ResponseEntity with the file content and appropriate headers + */ + private ResponseEntity buildResponse(byte[] content, ReportFormat format, String filenameBase) { + String contentType; + String extension; + + switch (format) { + case PDF -> { + contentType = "application/pdf"; + extension = ".pdf"; + } + case XLSX -> { + contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + extension = ".xlsx"; + } + case CSV -> { + contentType = "text/csv"; + extension = ".csv"; + } + case JSON -> { + contentType = "application/json"; + extension = ".json"; + } + default -> { + contentType = "application/octet-stream"; + extension = ""; + } + } + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType(contentType)); + headers.setContentDisposition(ContentDisposition.attachment() + .filename(filenameBase + extension) + .build()); + + return new ResponseEntity<>(content, headers, HttpStatus.OK); + } + // // ------------------------------------------------------ // // Equipment reservations endpoints // // ----------------------------------------------------- @@ -974,77 +1101,6 @@ public ResponseEntity getSessionById( // @Parameter(description = "Equipment reservation ID") @PathVariable Long // equipmentReservationId); - // // ----------------------------------------------------- - // // Recommendations endpoints - // // ----------------------------------------------------- - - // @GetMapping("/{userId}/recommended-routines") - // @Operation(summary = "Get recommended routines", description = "Retrieves - // personalized routine recommendations for a user") - // public ResponseEntity> - // getRecommendedRoutines(@Parameter(description = "User ID") @PathVariable Long - // userId); - - // @GetMapping("/{userId}/recommended-classes") - // @Operation(summary = "Get recommended classes", description = "Retrieves - // personalized class recommendations for a user") - // public ResponseEntity> - // getRecommendedClasses(@Parameter(description = "User ID") @PathVariable Long - // userId); - - // // ----------------------------------------------------- - // // Reports and analysis endpoints - // // ----------------------------------------------------- - - // @GetMapping("/{userId}/reports/attendance") - // @Operation(summary = "Get attendance report", description = "Generates an - // attendance report for a user") - // public ResponseEntity getUserAttendanceReport( - // @Parameter(description = "User ID") @PathVariable Long userId, - // @Parameter(description = "Start date") @RequestParam(required = false) - // @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, - // @Parameter(description = "End date") @RequestParam(required = false) - // @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { - - // AttendanceReportDTO attendanceReport = - // reportService.generateAttendanceReport(userId, startDate, endDate); - - // return ResponseEntity.ok(attendanceReport); - // } - - // @GetMapping("/{userId}/reports/physical-evolution") - // @Operation(summary = "Get physical evolution report", description = - // "Generates a physical evolution report for a user") - // public ResponseEntity - // getUserPhysicalEvolutionReport( - // @Parameter(description = "User ID") @PathVariable Long userId, - // @Parameter(description = "Start date") @RequestParam(required = false) - // @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, - // @Parameter(description = "End date") @RequestParam(required = false) - // @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { - // PhysicalEvolutionReportDTO physicalEvolutionReport = - // reportService.generatePhysicalEvolutionReport(userId, startDate, endDate); - - // return ResponseEntity.ok(physicalEvolutionReport); - // } - - // @GetMapping("/{userId}/reports/routine-compliance") - // @Operation(summary = "Get routine compliance report", description = - // "Generates a routine compliance report for a user") - // public ResponseEntity - // getUserRoutineComplianceReport( - // @Parameter(description = "User ID") @PathVariable Long userId, - // @Parameter(description = "Start date") @RequestParam(required = false) - // @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, - // @Parameter(description = "End date") @RequestParam(required = false) - // @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { - - // RoutineComplianceReportDTO routineComplianceReport = - // reportService.generateRoutineComplianceReport(userId, startDate, endDate); - - // return ResponseEntity.ok(routineComplianceReport); - // } - // // ----------------------------------------------------- // // Admin/Trainer specific endpoints // // ----------------------------------------------------- diff --git a/src/main/java/edu/eci/cvds/prometeo/model/PhysicalProgress.java b/src/main/java/edu/eci/cvds/prometeo/model/PhysicalProgress.java index ec729bd..398e50b 100644 --- a/src/main/java/edu/eci/cvds/prometeo/model/PhysicalProgress.java +++ b/src/main/java/edu/eci/cvds/prometeo/model/PhysicalProgress.java @@ -26,8 +26,8 @@ public class PhysicalProgress extends AuditableEntity { private LocalDate recordDate; @ManyToOne(fetch = FetchType.LAZY) -@JoinColumn(name = "active_routine_id") -private Routine activeRoutine; + @JoinColumn(name = "active_routine_id") + private Routine activeRoutine; @Embedded private Weight weight; diff --git a/src/main/java/edu/eci/cvds/prometeo/model/enums/ReportFormat.java b/src/main/java/edu/eci/cvds/prometeo/model/enums/ReportFormat.java new file mode 100644 index 0000000..141acdf --- /dev/null +++ b/src/main/java/edu/eci/cvds/prometeo/model/enums/ReportFormat.java @@ -0,0 +1,8 @@ +package edu.eci.cvds.prometeo.model.enums; + +public enum ReportFormat { + CSV, + PDF, + XLSX, + JSON +} diff --git a/src/main/java/edu/eci/cvds/prometeo/service/GoalService.java b/src/main/java/edu/eci/cvds/prometeo/service/GoalService.java index f4f0619..bb2e5f3 100644 --- a/src/main/java/edu/eci/cvds/prometeo/service/GoalService.java +++ b/src/main/java/edu/eci/cvds/prometeo/service/GoalService.java @@ -6,9 +6,38 @@ import java.util.Map; import java.util.UUID; +/** + * Service for managing user goals. + * Provides methods to retrieve, add, update, and delete user-defined goals. + */ public interface GoalService { + /** + * Retrieves all goals associated with a specific user. + * + * @param userId The unique identifier of the user. + * @return A list of goals belonging to the user. + */ List getGoalsByUser(UUID userId); + + /** + * Adds new goals to the specified user. + * + * @param userId The unique identifier of the user. + * @param goals A list of goal descriptions to be added. + */ void addUserGoal(UUID userId, List goals); + + /** + * Updates the descriptions of existing goals. + * + * @param updatedGoals A map where the key is the goal ID and the value is the new goal description. + */ void updateUserGoal(Map updatedGoals); + + /** + * Deletes a goal by its unique identifier. + * + * @param goalId The unique identifier of the goal to be deleted. + */ void deleteGoal(UUID goalId); } diff --git a/src/main/java/edu/eci/cvds/prometeo/service/RecommendationService.java b/src/main/java/edu/eci/cvds/prometeo/service/RecommendationService.java index 867e435..c3083fe 100644 --- a/src/main/java/edu/eci/cvds/prometeo/service/RecommendationService.java +++ b/src/main/java/edu/eci/cvds/prometeo/service/RecommendationService.java @@ -12,17 +12,18 @@ public interface RecommendationService { /** - * Recommends routines for a user based on their profile and progress - * @param userId ID of the user - * @return List of recommended routines with compatibility scores + * Generates personalized routine recommendations for a specific user. + * + * @param userId the unique identifier of the user */ List> recommendRoutines(UUID userId); /** - * Finds routines from user - * @param userId ID of the user - * @return List of user IDs to similarity scores + * Retrieves the list of routines associated with a specific user. + * + * @param userId the unique identifier of the user + * @return a list of the user's routines */ List findUserRoutines(UUID userId); } \ No newline at end of file diff --git a/src/main/java/edu/eci/cvds/prometeo/service/ReportService.java b/src/main/java/edu/eci/cvds/prometeo/service/ReportService.java index 29cb63b..604ca8d 100644 --- a/src/main/java/edu/eci/cvds/prometeo/service/ReportService.java +++ b/src/main/java/edu/eci/cvds/prometeo/service/ReportService.java @@ -1,5 +1,7 @@ package edu.eci.cvds.prometeo.service; +import edu.eci.cvds.prometeo.model.enums.ReportFormat; + import java.time.LocalDate; import java.util.List; import java.util.Map; @@ -12,65 +14,35 @@ public interface ReportService { /** - * Generates a user progress report - * @param userId ID of the user - * @param startDate Start date - * @param endDate End date - * @param format Format of the report - * @return Report data as a JSON-compatible map - */ - // Map generateUserProgressReport(UUID userId, LocalDate startDate, LocalDate endDate, String format); - - /** - * Generates a gym usage report - * @param startDate Start date - * @param endDate End date - * @param groupBy How to group data (day, week, month) - * @param format Format of the report - * @return List of JSON-compatible maps with usage data - */ - List> generateGymUsageReport(LocalDate startDate, LocalDate endDate, String groupBy, String format); - - /** - * Generates a trainer performance report - * @param trainerId Optional trainer ID (null for all trainers) - * @param startDate Start date - * @param endDate End date - * @param format Format of the report - * @return List of JSON-compatible maps with trainer data - */ - // List> generateTrainerReport(Optional trainerId, LocalDate startDate, LocalDate endDate, String format); - - /** - * Gets attendance statistics - * @param startDate Start date - * @param endDate End date - * @return Map of statistics - */ - Map getAttendanceStatistics(LocalDate startDate, LocalDate endDate); - - /** - * Gets routine usage statistics - * @param startDate Start date - * @param endDate End date - * @return Map of routine IDs to usage counts + * Generates a user progress report. + * This report includes the user's physical progress data such as weight and goal. + * + * @param userId ID of the user whose progress data is to be reported. + * @param format Format in which the report will be generated (e.g., PDF, XLSX, CSV, JSON). + * @return A byte array containing the generated report data in the requested format. */ - // Map getRoutineUsageStatistics(LocalDate startDate, LocalDate endDate); + byte[] generateUserProgressReport(UUID userId, ReportFormat format); /** - * Gets progress statistics for a user - * @param userId ID of the user - * @param months Number of months to analyze - * @return Map of statistics + * Generates a gym usage report. + * This report provides details about gym session usage, such as total capacity, reserved spots, and utilization rate, + * for a given date range. + * + * @param startDate The start date of the period for the report. + * @param endDate The end date of the period for the report. + * @param format Format in which the report will be generated (e.g., PDF, XLSX, CSV, JSON). + * @return A byte array containing the generated gym usage report in the requested format. */ - // Map getUserProgressStatistics(UUID userId, int months); + byte[] generateGymUsageReport(LocalDate startDate, LocalDate endDate, ReportFormat format); /** - * Gets gym capacity utilization - * @param startDate Start date - * @param endDate End date - * @param groupBy How to group data (hour, day, week) - * @return Map of time periods to utilization percentages + * Gets attendance statistics for gym sessions within a specific date range. + * This includes data such as the number of attendees for each session. + * + * @param startDate The start date of the period for the statistics. + * @param endDate The end date of the period for the statistics. + * @param format Format in which the statistics will be generated (e.g., PDF, XLSX, CSV, JSON). + * @return A byte array containing the attendance statistics in the requested format. */ - Map getCapacityUtilization(LocalDate startDate, LocalDate endDate, String groupBy); + byte[] getAttendanceStatistics(LocalDate startDate, LocalDate endDate, ReportFormat format); } \ No newline at end of file diff --git a/src/main/java/edu/eci/cvds/prometeo/service/impl/GoalServiceImpl.java b/src/main/java/edu/eci/cvds/prometeo/service/impl/GoalServiceImpl.java index 6b8c12d..95f0b81 100644 --- a/src/main/java/edu/eci/cvds/prometeo/service/impl/GoalServiceImpl.java +++ b/src/main/java/edu/eci/cvds/prometeo/service/impl/GoalServiceImpl.java @@ -3,7 +3,6 @@ import edu.eci.cvds.prometeo.PrometeoExceptions; import edu.eci.cvds.prometeo.model.Goal; import edu.eci.cvds.prometeo.model.Recommendation; -import edu.eci.cvds.prometeo.model.User; import edu.eci.cvds.prometeo.repository.GoalRepository; import edu.eci.cvds.prometeo.repository.RecommendationRepository; import edu.eci.cvds.prometeo.repository.UserRepository; @@ -17,6 +16,12 @@ import java.util.Map; import java.util.UUID; + +/** + * Implementation of the {@link GoalService} interface. + * Handles the creation, update, retrieval, and soft deletion of user goals, + * and manages the regeneration of routine recommendations accordingly. + */ @Service public class GoalServiceImpl implements GoalService { @Autowired @@ -31,11 +36,24 @@ public class GoalServiceImpl implements GoalService { @Autowired private RecommendationService recommendationService; + /** + * Retrieves all active goals for a specific user. + * + * @param userId The UUID of the user. + * @return A list of the user's active goals. + */ @Override public List getGoalsByUser(UUID userId) { return goalRepository.findByUserIdAndActive(userId, true); } + /** + * Adds new goals to the specified user and regenerates recommendations. + * Existing recommendations are deactivated before new ones are generated. + * + * @param userId The UUID of the user. + * @param goals A list of goal descriptions to add. + */ @Override public void addUserGoal(UUID userId, List goals) { userRepository.findById(userId) @@ -56,7 +74,12 @@ public void addUserGoal(UUID userId, List goals) { recommendationService.recommendRoutines(userId); } - + /** + * Updates the text of existing user goals and regenerates recommendations. + * All current recommendations are deactivated and refreshed. + * + * @param updatedGoals A map of goal IDs and their new descriptions. + */ @Transactional @Override public void updateUserGoal(Map updatedGoals) { @@ -84,6 +107,12 @@ public void updateUserGoal(Map updatedGoals) { recommendationService.recommendRoutines(userId); } + /** + * Soft deletes a goal by setting its active flag to false. + * Also deactivates existing recommendations and generates new ones. + * + * @param goalId The UUID of the goal to delete. + */ @Override public void deleteGoal(UUID goalId) { Goal goal = goalRepository.findById(goalId) diff --git a/src/main/java/edu/eci/cvds/prometeo/service/impl/RecommendationServiceImpl.java b/src/main/java/edu/eci/cvds/prometeo/service/impl/RecommendationServiceImpl.java index 9dc252e..938c90b 100644 --- a/src/main/java/edu/eci/cvds/prometeo/service/impl/RecommendationServiceImpl.java +++ b/src/main/java/edu/eci/cvds/prometeo/service/impl/RecommendationServiceImpl.java @@ -5,7 +5,6 @@ import edu.eci.cvds.prometeo.openai.OpenAiClient; import edu.eci.cvds.prometeo.repository.*; import edu.eci.cvds.prometeo.service.RecommendationService; -import edu.eci.cvds.prometeo.huggingface.HuggingFaceClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -14,14 +13,14 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.time.LocalDate; -import java.time.LocalTime; import java.util.*; import java.util.stream.Collectors; + +/** + * Implementation of the {@link RecommendationService} interface. + * This service uses OpenAI to generate personalized routine recommendations for users based on their goals. + */ @Service public class RecommendationServiceImpl implements RecommendationService { @@ -41,6 +40,11 @@ public class RecommendationServiceImpl implements RecommendationService { @Autowired private OpenAiClient openAiClient; + /** + * Generates and saves routine recommendations for a user using their goals and available routines. + * + * @param userId The UUID of the user for whom recommendations are to be generated. + */ @Override public List> recommendRoutines(UUID userId) { User user = userRepository.findById(userId) @@ -60,7 +64,13 @@ public List> recommendRoutines(UUID userId) { return new ArrayList<>(); } } - + /* + * Builds a natural language prompt to send to OpenAI based on user goals and available routines. + * + * @param goals The list of active goals for the user. + * @param allRoutines All available routines in the system. + * @return A formatted string prompt describing goals and routines. + */ private String buildPrompt(List goals, List allRoutines) { StringBuilder prompt = new StringBuilder(); prompt.append("Las metas del usuario son:\n"); @@ -81,35 +91,48 @@ private String buildPrompt(List goals, List allRoutines) { return prompt.toString(); } -private List parseUUIDList(String response) { - List result = new ArrayList<>(); - try { - // Extraer la respuesta del formato JSON de OpenAI - JsonNode responseJson = new ObjectMapper().readTree(response); - String content = responseJson.path("choices").path(0).path("message").path("content").asText(""); - - // Buscar texto que parezca un UUID en la respuesta - Pattern uuidPattern = Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", - Pattern.CASE_INSENSITIVE); - Matcher matcher = uuidPattern.matcher(content); - - // Añadir todos los UUIDs encontrados - while (matcher.find() && result.size() < 10) { - try { - UUID uuid = UUID.fromString(matcher.group()); - result.add(uuid); - } catch (IllegalArgumentException e) { - // Ignora los formatos UUID inválidos + /* + * Extracts UUIDs from OpenAI response by parsing the JSON and searching for valid UUID patterns. + * + * @param response The raw JSON response from the OpenAI model. + * @return A list of up to 10 UUIDs extracted from the response. + */ + private List parseUUIDList(String response) { + List result = new ArrayList<>(); + try { + // Extraer la respuesta del formato JSON de OpenAI + JsonNode responseJson = new ObjectMapper().readTree(response); + String content = responseJson.path("choices").path(0).path("message").path("content").asText(""); + + // Buscar texto que parezca un UUID en la respuesta + Pattern uuidPattern = Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", + Pattern.CASE_INSENSITIVE); + Matcher matcher = uuidPattern.matcher(content); + + // Añadir todos los UUIDs encontrados + while (matcher.find() && result.size() < 10) { + try { + UUID uuid = UUID.fromString(matcher.group()); + result.add(uuid); + } catch (IllegalArgumentException e) { + // Ignora los formatos UUID inválidos + } } + } catch (Exception e) { + // Log the error + System.err.println("Error parsing OpenAI response: " + e.getMessage()); } - } catch (Exception e) { - // Log the error - System.err.println("Error parsing OpenAI response: " + e.getMessage()); + + return result; } - - return result; -} + /* + * Creates or updates recommendation entities for the user based on routine IDs. + * + * @param routineIds The list of routine UUIDs recommended by the AI. + * @param user The user receiving the recommendations. + * @return A list of maps associating each recommended routine with its weight. + */ private List> buildRecommendations(List routineIds, User user) { List> recommendedRoutines = new ArrayList<>(); for (int i = 0; i < routineIds.size(); i++) { @@ -142,6 +165,12 @@ private List> buildRecommendations(List routineIds, return recommendedRoutines; } + /** + * Retrieves all active recommended routines for a specific user. + * + * @param userId The UUID of the user. + * @return A list of routines recommended to the user. + */ @Override public List findUserRoutines(UUID userId) { userRepository.findById(userId) diff --git a/src/main/java/edu/eci/cvds/prometeo/service/impl/ReportServiceImpl.java b/src/main/java/edu/eci/cvds/prometeo/service/impl/ReportServiceImpl.java index 9cc3aa1..d649f57 100644 --- a/src/main/java/edu/eci/cvds/prometeo/service/impl/ReportServiceImpl.java +++ b/src/main/java/edu/eci/cvds/prometeo/service/impl/ReportServiceImpl.java @@ -1,172 +1,201 @@ -// package edu.eci.cvds.prometeo.service.impl; - -// import edu.eci.cvds.prometeo.service.ReportService; -// import edu.eci.cvds.prometeo.repository.ReservationRepository; -// import edu.eci.cvds.prometeo.repository.UserRoutineRepository; -// import edu.eci.cvds.prometeo.repository.UserRepository; -// import edu.eci.cvds.prometeo.repository.RoutineRepository; -// import edu.eci.cvds.prometeo.model.Reservation; -// import edu.eci.cvds.prometeo.model.UserRoutine; -// import edu.eci.cvds.prometeo.model.Routine; - -// import org.springframework.beans.factory.annotation.Autowired; -// import org.springframework.stereotype.Service; - -// import java.time.LocalDate; -// import java.time.format.DateTimeFormatter; -// import java.util.*; -// import java.util.stream.Collectors; -// import java.util.Optional; -// import java.util.UUID; - -// @Service -// public class ReportServiceImpl implements ReportService { - -// private final ReservationRepository reservationRepository; -// private final UserRoutineRepository userRoutineRepository; -// private final UserRepository userRepository; -// private final RoutineRepository routineRepository; - -// @Autowired -// public ReportServiceImpl( -// ReservationRepository reservationRepository, -// UserRoutineRepository userRoutineRepository, -// UserRepository userRepository, -// RoutineRepository routineRepository -// ) { -// this.reservationRepository = reservationRepository; -// this.userRoutineRepository = userRoutineRepository; -// this.userRepository = userRepository; -// this.routineRepository = routineRepository; -// } - -// // @Override -// // public Map generateUserProgressReport(UUID userId, LocalDate startDate, LocalDate endDate, String format) { -// // // Ejemplo sencillo: solo cuenta rutinas asignadas y reservas hechas en el periodo -// // Map report = new HashMap<>(); -// // List userRoutines = userRoutineRepository.findByUserIdAndAssignmentDateBetween(userId, startDate, endDate); -// // List reservations = reservationRepository.findByUserIdAndDateBetween(userId, startDate, endDate); - -// // report.put("userId", userId); -// // report.put("routinesAssigned", userRoutines.size()); -// // report.put("reservations", reservations.size()); -// // report.put("period", Map.of("start", startDate, "end", endDate)); -// // return report; -// // } - -// @Override -// public List> generateGymUsageReport(LocalDate startDate, LocalDate endDate, String groupBy, String format) { -// List reservations = reservationRepository.findByDateBetween(startDate, endDate); -// Map grouped; -// DateTimeFormatter formatter; -// if ("week".equalsIgnoreCase(groupBy)) { -// formatter = DateTimeFormatter.ofPattern("YYYY-'W'ww"); -// grouped = reservations.stream().collect(Collectors.groupingBy( -// r -> r.getDate().format(formatter), Collectors.counting())); -// } else if ("month".equalsIgnoreCase(groupBy)) { -// formatter = DateTimeFormatter.ofPattern("yyyy-MM"); -// grouped = reservations.stream().collect(Collectors.groupingBy( -// r -> r.getDate().format(formatter), Collectors.counting())); -// } else { -// formatter = DateTimeFormatter.ISO_DATE; -// grouped = reservations.stream().collect(Collectors.groupingBy( -// r -> r.getDate().format(formatter), Collectors.counting())); -// } -// List> report = new ArrayList<>(); -// for (Map.Entry entry : grouped.entrySet()) { -// Map item = new HashMap<>(); -// item.put("period", entry.getKey()); -// item.put("reservations", entry.getValue()); -// report.add(item); -// } -// return report; -// } - -// // @Override -// // public List> generateTrainerReport(Optional trainerId, LocalDate startDate, LocalDate endDate, String format) { -// // List reservations; -// // if (trainerId.isPresent()) { -// // reservations = reservationRepository.findByTrainerIdAndDateBetween(trainerId.get(), startDate, endDate); -// // } else { -// // reservations = reservationRepository.findByDateBetween(startDate, endDate); -// // } -// // List> report = new ArrayList<>(); -// // for (Reservation r : reservations) { -// // Map item = new HashMap<>(); -// // item.put("date", r.getDate()); -// // item.put("userId", r.getUserId()); -// // item.put("trainerId", r.getTrainerId()); -// // item.put("status", r.getStatus()); -// // report.add(item); -// // } -// // return report; -// // } - -// @Override -// public Map getAttendanceStatistics(LocalDate startDate, LocalDate endDate) { -// List reservations = reservationRepository.findByDateBetween(startDate, endDate); -// int attended = 0; -// int missed = 0; -// for (Reservation r : reservations) { -// if (Boolean.TRUE.equals(r.getAttended())) { -// attended++; -// } else { -// missed++; -// } -// } -// Map stats = new HashMap<>(); -// stats.put("attended", attended); -// stats.put("missed", missed); -// stats.put("total", reservations.size()); -// return stats; -// } - -// // @Override -// // public Map getRoutineUsageStatistics(LocalDate startDate, LocalDate endDate) { -// // List userRoutines = userRoutineRepository.findByAssignmentDateBetween(startDate, endDate); -// // Map usage = new HashMap<>(); -// // for (UserRoutine ur : userRoutines) { -// // usage.put(ur.getRoutineId(), usage.getOrDefault(ur.getRoutineId(), 0) + 1); -// // } -// // return usage; -// // } - -// // @Override -// // public Map getUserProgressStatistics(UUID userId, int months) { -// // LocalDate now = LocalDate.now(); -// // LocalDate from = now.minusMonths(months); -// // List userRoutines = userRoutineRepository.findByUserIdAndAssignmentDateBetween(userId, from, now); -// // Map stats = new HashMap<>(); -// // stats.put("routinesAssigned", userRoutines.size()); -// // stats.put("period", Map.of("start", from, "end", now)); -// // return stats; -// // } - -// @Override -// public Map getCapacityUtilization(LocalDate startDate, LocalDate endDate, String groupBy) { -// List reservations = reservationRepository.findByDateBetween(startDate, endDate); -// Map countByGroup = new HashMap<>(); -// Map capacityByGroup = new HashMap<>(); -// DateTimeFormatter formatter; -// if ("day".equalsIgnoreCase(groupBy)) { -// formatter = DateTimeFormatter.ISO_DATE; -// } else if ("week".equalsIgnoreCase(groupBy)) { -// formatter = DateTimeFormatter.ofPattern("YYYY-'W'ww"); -// } else { -// formatter = DateTimeFormatter.ofPattern("YYYY-MM"); -// } -// for (Reservation r : reservations) { -// String key = r.getDate().format(formatter); -// countByGroup.put(key, countByGroup.getOrDefault(key, 0) + 1); -// // Para demo, capacidad fija de 10 por grupo -// capacityByGroup.put(key, 10); -// } -// Map utilization = new HashMap<>(); -// for (String key : countByGroup.keySet()) { -// int used = countByGroup.get(key); -// int cap = capacityByGroup.getOrDefault(key, 10); -// utilization.put(key, cap == 0 ? 0.0 : (used * 100.0 / cap)); -// } -// return utilization; -// } -// } \ No newline at end of file +package edu.eci.cvds.prometeo.service.impl; + +import edu.eci.cvds.prometeo.model.GymSession; +import edu.eci.cvds.prometeo.model.PhysicalProgress; +import edu.eci.cvds.prometeo.repository.*; +import edu.eci.cvds.prometeo.service.ReportService; +import edu.eci.cvds.prometeo.model.enums.ReportFormat; + +import edu.eci.cvds.prometeo.service.report.ReportGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.time.LocalDate; +import java.util.*; +import java.util.function.Function; + +/** + * Implementation of the ReportService interface. + * This service generates various reports including user progress, gym usage, and attendance statistics. + */ +@Service +public class ReportServiceImpl implements ReportService { + @Autowired + private PhysicalProgressRepository physicalProgressRepository; + @Autowired + private GymSessionRepository gymSessionRepository; + + private final ReportGenerator reportGenerator = new ReportGenerator(); + + /** + * Generates a report on user progress (weight and goal data). + * + * @param userId ID of the user whose progress data is to be reported. + * @param format The desired format for the report (e.g., PDF, XLSX, CSV, JSON). + * @return A byte array containing the report data in the requested format. + */ + @Override + public byte[] generateUserProgressReport(UUID userId, ReportFormat format) { + List data = physicalProgressRepository.findByUserIdOrderByRecordDateDesc(userId); + + List headers = List.of("Fecha", "Peso", "Meta"); + Function> rowMapper = p -> List.of( + p.getRecordDate().toString(), + p.getWeight() != null ? String.valueOf(p.getWeight().getValue()) : "N/A", + p.getPhysicalGoal() != null ? p.getPhysicalGoal() : "N/A" + ); + + Function lineMapper = p -> + "Fecha: " + p.getRecordDate() + + " | Peso: " + (p.getWeight() != null ? p.getWeight().getValue() + "kg" : "N/A") + + " | Meta: " + (p.getPhysicalGoal() != null ? p.getPhysicalGoal() : "N/A"); + + try { + return switch (format) { + case PDF -> reportGenerator.generatePDF(data, "Reporte de Progreso Físico", lineMapper); + case XLSX -> reportGenerator.generateXLSX(data, headers, rowMapper); + case CSV -> reportGenerator.generateCSV(data, headers, rowMapper); + case JSON -> reportGenerator.generateJSON(data); + }; + } catch (IOException e) { + throw new RuntimeException("Error generando reporte en formato: " + format, e); + } + } + + /** + * Generates a gym usage report. + * + * @param startDate The start date of the period for the gym usage report. + * @param endDate The end date of the period for the gym usage report. + * @param format The desired format for the report (e.g., PDF, XLSX, CSV, JSON). + * @return A byte array containing the gym usage report data in the requested format. + */ + @Override + public byte[] generateGymUsageReport(LocalDate startDate, LocalDate endDate, ReportFormat format) { + List sessions = gymSessionRepository.findBySessionDateBetween(startDate, endDate); + + Map metrics = generateMetrics(sessions, startDate, endDate); + List> reportData = List.of(metrics); + List headers = List.of("Fecha", "Capacidad Total", "Reservas Totales", "Tasa de Utilización", "Utilización Promedio", "Duración Promedio"); + + Function> rowMapper = this::mapRow; + Function lineMapper = this::mapLine; + + try { + return switch (format) { + case PDF -> reportGenerator.generatePDF(sessions, "Reporte de Uso del Gimnasio", lineMapper); + case XLSX -> reportGenerator.generateXLSX(sessions, headers, rowMapper); + case CSV -> reportGenerator.generateCSV(sessions, headers, rowMapper); + case JSON -> reportGenerator.generateJSON(reportData); + }; + } catch (IOException e) { + throw new RuntimeException("Error generando reporte en formato: " + format, e); + } + } + + /* + * Generates metrics for the gym usage report. + * + * @param sessions List of gym sessions to generate metrics from. + * @param startDate The start date of the period for the metrics. + * @param endDate The end date of the period for the metrics. + * @return A map containing key metrics (total sessions, total capacity, total reserved spots, etc.). + */ + private Map generateMetrics(List sessions, LocalDate startDate, LocalDate endDate) { + long totalSessions = sessions.size(); + int totalCapacity = sessions.stream().mapToInt(GymSession::getCapacity).sum(); + int totalReserved = sessions.stream().mapToInt(GymSession::getReservedSpots).sum(); + double utilizationRate = totalCapacity > 0 ? (totalReserved * 100.0 / totalCapacity) : 0; + double avgUtilization = sessions.isEmpty() ? 0.0 : sessions.stream() + .mapToDouble(s -> s.getReservedSpots() * 100.0 / s.getCapacity()) + .average().orElse(0.0); + double avgDuration = sessions.isEmpty() ? 0.0 : sessions.stream() + .mapToLong(s -> s.getDuration().toMinutes()) + .average().orElse(0.0); + + return Map.of( + "startDate", startDate.toString(), + "endDate", endDate.toString(), + "totalSessions", totalSessions, + "totalCapacity", totalCapacity, + "totalReservedSpots", totalReserved, + "utilizationRate", String.format("%.2f", utilizationRate) + "%", + "averageUtilizationPerSession", String.format("%.2f", avgUtilization) + "%", + "averageSessionDurationMinutes", String.format("%.2f", avgDuration) + ); + } + + /* + * Maps a gym session to a row of data for the report. + * + * @param session The gym session to map. + * @return A list of strings representing the session data for the report. + */ + private List mapRow(GymSession session) { + return List.of( + session.getSessionDate().toString(), + String.valueOf(session.getCapacity()), + String.valueOf(session.getReservedSpots()), + String.format("%.2f", session.getReservedSpots() * 100.0 / session.getCapacity()) + "%", + String.format("%.2f", session.getReservedSpots() * 100.0 / session.getCapacity()), + String.format("%.2f", session.getDuration().toMinutes()) + ); + } + + /* + * Maps a gym session to a line of data for the report. + * + * @param session The gym session to map. + * @return A string representing the session data for the report. + */ +private String mapLine(GymSession session) { + return String.format( + "Fecha: %s | Capacidad Total: %d | Reservas Totales: %d | Tasa de Utilización: %.2f%% | Utilización Promedio: %.2f%% | Duración Promedio: %d minutos", + session.getSessionDate(), session.getCapacity(), session.getReservedSpots(), + session.getReservedSpots() * 100.0 / session.getCapacity(), + session.getReservedSpots() * 100.0 / session.getCapacity(), + session.getDuration().toMinutes() + ); +} + + + /** + * Generates attendance statistics for the gym sessions within a given date range. + * + * @param startDate The start date of the period for the attendance statistics. + * @param endDate The end date of the period for the attendance statistics. + * @param format The desired format for the statistics report (e.g., PDF, XLSX, CSV, JSON). + * @return A byte array containing the attendance statistics in the requested format. + */ + @Override + public byte[] getAttendanceStatistics(LocalDate startDate, LocalDate endDate, ReportFormat format) { + List sessions = gymSessionRepository.findBySessionDateBetween(startDate, endDate); + Map attendanceStats = new HashMap<>(); + for (GymSession session : sessions) { + attendanceStats.put(session.getSessionDate(), session.getReservedSpots()); + } + List headers = List.of("Fecha", "Asistencias"); + Function, List> rowMapper = entry -> List.of( + entry.getKey().toString(), + String.valueOf(entry.getValue()) + ); + + Function, String> lineMapper = entry -> + "Fecha: " + entry.getKey() + " | Asistencias: " + entry.getValue(); + + try { + return switch (format) { + case PDF -> + reportGenerator.generatePDF(attendanceStats.entrySet().stream().toList(), "Reporte de Asistencia al Gimnasio", lineMapper); + case XLSX -> + reportGenerator.generateXLSX(attendanceStats.entrySet().stream().toList(), headers, rowMapper); + case CSV -> + reportGenerator.generateCSV(attendanceStats.entrySet().stream().toList(), headers, rowMapper); + case JSON -> reportGenerator.generateJSON(Collections.singletonList(attendanceStats)); + }; + } catch (IOException e) { + throw new RuntimeException("Error generando reporte en formato: " + format, e); + } + } +} \ No newline at end of file diff --git a/src/main/java/edu/eci/cvds/prometeo/service/report/ReportGenerator.java b/src/main/java/edu/eci/cvds/prometeo/service/report/ReportGenerator.java new file mode 100644 index 0000000..5f3396f --- /dev/null +++ b/src/main/java/edu/eci/cvds/prometeo/service/report/ReportGenerator.java @@ -0,0 +1,133 @@ +package edu.eci.cvds.prometeo.service.report; + +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.eci.cvds.prometeo.model.PhysicalProgress; +import org.apache.pdfbox.pdmodel.*; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Component; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.function.Function; + +/** + * ReportGenerator is a utility component for generating reports in different formats such as + * JSON, CSV, XLSX (Excel), and PDF. It provides generic methods to serialize and format data, + * allowing reuse across various types of entities and data models. + */ +@Component +public class ReportGenerator { + private final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Generates a JSON report from a list of data objects. + * + * @param data The list of data to serialize. + * @param The type of the objects in the list. + * @return A byte array representing the JSON content. + * @throws IOException If serialization fails. + */ + public byte[] generateJSON(List data) throws IOException { + return objectMapper.writeValueAsBytes(data); + } + + /** + * Generates a CSV report from a list of data objects. + * + * @param data The list of data to serialize. + * @param headers The list of headers to include as the first row. + * @param rowMapper A function that maps each object to a list of string values. + * @param The type of the objects in the list. + * @return A byte array representing the CSV content. + */ + public byte[] generateCSV(List data, List headers, Function> rowMapper) { + StringBuilder builder = new StringBuilder(); + builder.append(String.join(",", headers)).append("\n"); + for (T item : data) { + builder.append(String.join(",", rowMapper.apply(item))).append("\n"); + } + return builder.toString().getBytes(); + } + + /** + * Generates an Excel (XLSX) report from a list of data objects. + * + * @param data The list of data to include. + * @param headers The column headers. + * @param rowMapper A function that maps each object to a list of string values for each column. + * @param The type of the objects in the list. + * @return A byte array representing the Excel file. + * @throws IOException If an error occurs during file writing. + */ + public byte[] generateXLSX(List data, List headers, Function> rowMapper) throws IOException { + try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { + Sheet sheet = workbook.createSheet("Reporte"); + Row headerRow = sheet.createRow(0); + for (int i = 0; i < headers.size(); i++) { + headerRow.createCell(i).setCellValue(headers.get(i)); + } + + int rowIdx = 1; + for (T item : data) { + Row row = sheet.createRow(rowIdx++); + List values = rowMapper.apply(item); + for (int i = 0; i < values.size(); i++) { + row.createCell(i).setCellValue(values.get(i)); + } + } + + workbook.write(out); + return out.toByteArray(); + } + } + + /** + * Generates a PDF report from a list of data objects. + * + * @param data The list of data to include in the report. + * @param title The title of the PDF document. + * @param lineMapper A function that maps each object to a string to be rendered as a line in the PDF. + * @param The type of the objects in the list. + * @return A byte array representing the PDF content. + * @throws IOException If an error occurs during PDF generation. + */ + public byte[] generatePDF(List data, String title, Function lineMapper) throws IOException { + try (PDDocument doc = new PDDocument(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { + PDPage page = new PDPage(PDRectangle.LETTER); + doc.addPage(page); + + PDPageContentStream content = new PDPageContentStream(doc, page); + content.beginText(); + content.setFont(PDType1Font.HELVETICA_BOLD, 14); + content.newLineAtOffset(50, 700); + content.showText(title); + content.endText(); + + int y = 680; + for (T item : data) { + content.beginText(); + content.setFont(PDType1Font.HELVETICA, 10); + content.newLineAtOffset(50, y); + content.showText(lineMapper.apply(item)); + content.endText(); + y -= 15; + if (y < 50) { + content.close(); + page = new PDPage(PDRectangle.LETTER); + doc.addPage(page); + content = new PDPageContentStream(doc, page); + y = 700; + } + } + + content.close(); + doc.save(out); + return out.toByteArray(); + } + } +}