From d2585147fb41f4bea1c42e899396e9d4400136e7 Mon Sep 17 00:00:00 2001 From: cris-eci Date: Sun, 11 May 2025 20:38:05 -0500 Subject: [PATCH 1/2] ci: add test dabate configuration --- .github/workflows/CI-CD-Test.yml | 9 +++++---- .gitignore | 1 + pom.xml | 2 ++ .../cvds/prometeo/PrometeoApplicationTests.java | 2 ++ src/test/resources/application-test.properties | 14 ++++++++++++++ 5 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 src/test/resources/application-test.properties diff --git a/.github/workflows/CI-CD-Test.yml b/.github/workflows/CI-CD-Test.yml index 8a66598..e51cb64 100644 --- a/.github/workflows/CI-CD-Test.yml +++ b/.github/workflows/CI-CD-Test.yml @@ -38,7 +38,7 @@ jobs: distribution: 'temurin' cache: maven - name: Maven Verify - run: mvn verify + run: mvn verify -Dspring.profiles.active=test - name: Ejecutar Tests de Reserva run: | echo "Ejecutando test: Dado que tengo 1 reserva registrada, Cuando lo consulto a nivel de servicio, Entonces la consulta será exitosa validando el campo id." @@ -46,6 +46,7 @@ jobs: echo "Ejecutando test: Dado que no hay ninguna reserva registrada, Cuándo lo creo a nivel de servicio, Entonces la creación será exitosa." echo "Ejecutando test: Dado que tengo 1 reserva registrada, Cuándo la elimino a nivel de servicio, Entonces la eliminación será exitosa." echo "Ejecutando test: Dado que tengo 1 reserva registrada, Cuándo la elimino y consulto a nivel de servicio, Entonces el resultado de la consulta no retornará ningún resultado." + deploy: name: Deploy needs: test @@ -58,6 +59,6 @@ jobs: - name: Deploy to Azure App Service uses: azure/webapps-deploy@v2 with: - app-name: crono # Reemplaza con el nombre de tu App Service para testing - publish-profile: ${{ secrets.AZURETESTENVIRONMENT }} - package: '*.jar' + app-name: crono + publish-profile: ${{ secrets.AZURETESTENVIRONMENT }} + package: '*.jar' \ No newline at end of file diff --git a/.gitignore b/.gitignore index 062bad1..2021304 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ build/ .env errorLog.txt requirements.pdf +4_Maven Verify.txt diff --git a/pom.xml b/pom.xml index ce05308..9a40eeb 100644 --- a/pom.xml +++ b/pom.xml @@ -170,6 +170,8 @@ dotenv-java 2.3.1 + + diff --git a/src/test/java/edu/eci/cvds/prometeo/PrometeoApplicationTests.java b/src/test/java/edu/eci/cvds/prometeo/PrometeoApplicationTests.java index dd32b4a..dbc4783 100644 --- a/src/test/java/edu/eci/cvds/prometeo/PrometeoApplicationTests.java +++ b/src/test/java/edu/eci/cvds/prometeo/PrometeoApplicationTests.java @@ -2,8 +2,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; @SpringBootTest +@ActiveProfiles("test") class PrometeoApplicationTests { @Test diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties new file mode 100644 index 0000000..dc53a99 --- /dev/null +++ b/src/test/resources/application-test.properties @@ -0,0 +1,14 @@ +# H2 in-memory database configuration for tests +spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 +spring.datasource.username=sa +spring.datasource.password= +spring.datasource.driver-class-name=org.h2.Driver + +# JPA configuration +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect +spring.jpa.show-sql=true + +# Disable OpenAI in tests +openai.api.key=test-key +openai.model=test-model \ No newline at end of file From c849e1eb8db7cb6584d7b276012a7f6a5dbc08e2 Mon Sep 17 00:00:00 2001 From: cris-eci Date: Mon, 12 May 2025 22:17:26 -0500 Subject: [PATCH 2/2] feat: add Slot and schedule management by trainers services and endpoints --- .../prometeo/controller/UserController.java | 1001 +++++++++-------- .../repository/GymSessionRepository.java | 5 + .../prometeo/service/GymSessionService.java | 97 +- .../service/impl/GymSessionServiceImpl.java | 135 ++- src/main/resources/application.properties | 4 +- .../prometeo/PrometeoApplicationTests.java | 1 + 6 files changed, 735 insertions(+), 508 deletions(-) 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 dbb9478..9d2b40e 100644 --- a/src/main/java/edu/eci/cvds/prometeo/controller/UserController.java +++ b/src/main/java/edu/eci/cvds/prometeo/controller/UserController.java @@ -62,7 +62,6 @@ public class UserController { @Autowired private UserService userService; - @Autowired private GymReservationService gymReservationService; @@ -267,14 +266,14 @@ public ResponseEntity updatePhysicalMeasurements( @Parameter(description = "Progress ID") @PathVariable UUID progressId, @RequestBody BodyMeasurementsDTO measurementsDTO) { - // Convertir DTO a entidad - BodyMeasurements measurements = new BodyMeasurements(); - measurements.setHeight(measurementsDTO.getHeight()); - measurements.setChestCircumference(measurementsDTO.getChestCircumference()); - measurements.setWaistCircumference(measurementsDTO.getWaistCircumference()); - measurements.setHipCircumference(measurementsDTO.getHipCircumference()); - measurements.setBicepsCircumference(measurementsDTO.getBicepsCircumference()); - measurements.setThighCircumference(measurementsDTO.getThighCircumference()); + // Convertir DTO a entidad + BodyMeasurements measurements = new BodyMeasurements(); + measurements.setHeight(measurementsDTO.getHeight()); + measurements.setChestCircumference(measurementsDTO.getChestCircumference()); + measurements.setWaistCircumference(measurementsDTO.getWaistCircumference()); + measurements.setHipCircumference(measurementsDTO.getHipCircumference()); + measurements.setBicepsCircumference(measurementsDTO.getBicepsCircumference()); + measurements.setThighCircumference(measurementsDTO.getThighCircumference()); PhysicalProgress updatedProgress = userService.updatePhysicalMeasurement(progressId, measurements); return ResponseEntity.ok(updatedProgress); } @@ -327,145 +326,145 @@ public ResponseEntity> getTraineePhysicalProgress( return ResponseEntity.ok(history); } // ----------------------------------------------------- -// Routine management endpoints -// ----------------------------------------------------- - -@GetMapping("/{userId}/routines") -@Operation(summary = "Get user routines", description = "Retrieves all routines assigned to a user") -@ApiResponse(responseCode = "200", description = "Routines retrieved successfully") -@ApiResponse(responseCode = "404", description = "User not found") -public ResponseEntity> getUserRoutines( - @Parameter(description = "User ID") @PathVariable UUID userId) { - - List routines = userService.getUserRoutines(userId); - return ResponseEntity.ok(routines); -} + // Routine management endpoints + // ----------------------------------------------------- -@GetMapping("/{userId}/routines/current") -@Operation(summary = "Get current routine", description = "Retrieves the user's current active routine") -@ApiResponse(responseCode = "200", description = "Routine retrieved successfully") -@ApiResponse(responseCode = "404", description = "No active routine found") -public ResponseEntity getCurrentRoutine( - @Parameter(description = "User ID") @PathVariable UUID userId) { - // TODO: Move this logic to userservice layer - return routineRepository.findCurrentRoutineByUserId(userId) - .map(routine -> ResponseEntity.ok(routine)) - .orElse(ResponseEntity.notFound().build()); -} + @GetMapping("/{userId}/routines") + @Operation(summary = "Get user routines", description = "Retrieves all routines assigned to a user") + @ApiResponse(responseCode = "200", description = "Routines retrieved successfully") + @ApiResponse(responseCode = "404", description = "User not found") + public ResponseEntity> getUserRoutines( + @Parameter(description = "User ID") @PathVariable UUID userId) { -@PostMapping("/{userId}/routines/assign/{routineId}") -@Operation(summary = "Assign routine to user", description = "Assigns an existing routine to a user") -@ApiResponse(responseCode = "204", description = "Routine assigned successfully") -@ApiResponse(responseCode = "404", description = "User or routine not found") -public ResponseEntity assignRoutineToUser( - @Parameter(description = "User ID") @PathVariable UUID userId, - @Parameter(description = "Routine ID") @PathVariable UUID routineId) { + List routines = userService.getUserRoutines(userId); + return ResponseEntity.ok(routines); + } - userService.assignRoutineToUser(userId, routineId); - return ResponseEntity.noContent().build(); -} + @GetMapping("/{userId}/routines/current") + @Operation(summary = "Get current routine", description = "Retrieves the user's current active routine") + @ApiResponse(responseCode = "200", description = "Routine retrieved successfully") + @ApiResponse(responseCode = "404", description = "No active routine found") + public ResponseEntity getCurrentRoutine( + @Parameter(description = "User ID") @PathVariable UUID userId) { + // TODO: Move this logic to userservice layer + return routineRepository.findCurrentRoutineByUserId(userId) + .map(routine -> ResponseEntity.ok(routine)) + .orElse(ResponseEntity.notFound().build()); + } -@PostMapping("/{userId}/routines/custom") -@Operation(summary = "Create custom routine", description = "Creates a custom routine for a user") -@ApiResponse(responseCode = "201", description = "Routine created successfully") -@ApiResponse(responseCode = "404", description = "User not found") -public ResponseEntity createCustomRoutine( - @Parameter(description = "User ID") @PathVariable UUID userId, - @Parameter(description = "Routine data") @RequestBody RoutineDTO routineDTO) { - - // Convertir DTO a entidad - Routine routine = new Routine(); - routine.setName(routineDTO.getName()); - routine.setDescription(routineDTO.getDescription()); - routine.setDifficulty(routineDTO.getDifficulty()); - routine.setGoal(routineDTO.getGoal()); - routine.setCreationDate(LocalDate.now()); - - // Crear una lista vacía de ejercicios desde el principio - routine.setExercises(new ArrayList<>()); - - // Crear primero la rutina con la lista vacía - Routine createdRoutine = userService.createCustomRoutine(userId, routine); - - // Ahora que la rutina tiene un ID, añadir los ejercicios uno por uno - if (routineDTO.getExercises() != null && !routineDTO.getExercises().isEmpty()) { - // Usar un enfoque de servicio para añadir cada ejercicio individualmente - for (RoutineExerciseDTO exerciseDTO : routineDTO.getExercises()) { - RoutineExercise exercise = new RoutineExercise(); - exercise.setBaseExerciseId(exerciseDTO.getBaseExerciseId()); - exercise.setRoutineId(createdRoutine.getId()); - exercise.setSets(exerciseDTO.getSets()); - exercise.setRepetitions(exerciseDTO.getRepetitions()); - exercise.setRestTime(exerciseDTO.getRestTime()); - exercise.setSequenceOrder(exerciseDTO.getSequenceOrder()); - - // Añadir a la base de datos directamente sin pasar por la colección de la rutina - routineExerciseRepository.save(exercise); - } + @PostMapping("/{userId}/routines/assign/{routineId}") + @Operation(summary = "Assign routine to user", description = "Assigns an existing routine to a user") + @ApiResponse(responseCode = "204", description = "Routine assigned successfully") + @ApiResponse(responseCode = "404", description = "User or routine not found") + public ResponseEntity assignRoutineToUser( + @Parameter(description = "User ID") @PathVariable UUID userId, + @Parameter(description = "Routine ID") @PathVariable UUID routineId) { + + userService.assignRoutineToUser(userId, routineId); + return ResponseEntity.noContent().build(); } - // Recargar la rutina para obtener todos los ejercicios asociados - return new ResponseEntity<>( - routineRepository.findById(createdRoutine.getId()) - .orElseThrow(() -> new RuntimeException("Failed to find newly created routine")), - HttpStatus.CREATED - ); -} + @PostMapping("/{userId}/routines/custom") + @Operation(summary = "Create custom routine", description = "Creates a custom routine for a user") + @ApiResponse(responseCode = "201", description = "Routine created successfully") + @ApiResponse(responseCode = "404", description = "User not found") + public ResponseEntity createCustomRoutine( + @Parameter(description = "User ID") @PathVariable UUID userId, + @Parameter(description = "Routine data") @RequestBody RoutineDTO routineDTO) { -@PutMapping("/routines/{routineId}") -@Operation(summary = "Update routine", description = "Updates an existing routine") -@ApiResponse(responseCode = "200", description = "Routine updated successfully") -@ApiResponse(responseCode = "404", description = "Routine not found") -public ResponseEntity updateRoutine( - @Parameter(description = "Routine ID") @PathVariable UUID routineId, - @Parameter(description = "Updated routine data") @RequestBody RoutineDTO routineDTO) { - // TODO: Move this logic to userservice layer - // Buscar la rutina existente - Routine existingRoutine = routineRepository.findById(routineId) - .orElseThrow(() -> new RuntimeException("Routine not found")); - - // Actualizar campos - existingRoutine.setName(routineDTO.getName()); - existingRoutine.setDescription(routineDTO.getDescription()); - existingRoutine.setDifficulty(routineDTO.getDifficulty()); - existingRoutine.setGoal(routineDTO.getGoal()); - - // Actualizar la rutina - Routine updatedRoutine = userService.updateRoutine(routineId, existingRoutine); - return ResponseEntity.ok(updatedRoutine); -} + // Convertir DTO a entidad + Routine routine = new Routine(); + routine.setName(routineDTO.getName()); + routine.setDescription(routineDTO.getDescription()); + routine.setDifficulty(routineDTO.getDifficulty()); + routine.setGoal(routineDTO.getGoal()); + routine.setCreationDate(LocalDate.now()); + + // Crear una lista vacía de ejercicios desde el principio + routine.setExercises(new ArrayList<>()); + + // Crear primero la rutina con la lista vacía + Routine createdRoutine = userService.createCustomRoutine(userId, routine); + + // Ahora que la rutina tiene un ID, añadir los ejercicios uno por uno + if (routineDTO.getExercises() != null && !routineDTO.getExercises().isEmpty()) { + // Usar un enfoque de servicio para añadir cada ejercicio individualmente + for (RoutineExerciseDTO exerciseDTO : routineDTO.getExercises()) { + RoutineExercise exercise = new RoutineExercise(); + exercise.setBaseExerciseId(exerciseDTO.getBaseExerciseId()); + exercise.setRoutineId(createdRoutine.getId()); + exercise.setSets(exerciseDTO.getSets()); + exercise.setRepetitions(exerciseDTO.getRepetitions()); + exercise.setRestTime(exerciseDTO.getRestTime()); + exercise.setSequenceOrder(exerciseDTO.getSequenceOrder()); + + // Añadir a la base de datos directamente sin pasar por la colección de la + // rutina + routineExerciseRepository.save(exercise); + } + } -@PostMapping("/{userId}/routines/{routineId}/progress") -@Operation(summary = "Log routine progress", description = "Records progress for a routine session") -@ApiResponse(responseCode = "204", description = "Progress logged successfully") -@ApiResponse(responseCode = "404", description = "User or routine not found") -public ResponseEntity logRoutineProgress( - @Parameter(description = "User ID") @PathVariable UUID userId, - @Parameter(description = "Routine ID") @PathVariable UUID routineId, - @Parameter(description = "Progress percentage") @RequestBody Map progressData) { + // Recargar la rutina para obtener todos los ejercicios asociados + return new ResponseEntity<>( + routineRepository.findById(createdRoutine.getId()) + .orElseThrow(() -> new RuntimeException("Failed to find newly created routine")), + HttpStatus.CREATED); + } - Integer completedPercentage = progressData.get("completed"); - if (completedPercentage == null) { - completedPercentage = 100; // Valor por defecto si no se proporciona + @PutMapping("/routines/{routineId}") + @Operation(summary = "Update routine", description = "Updates an existing routine") + @ApiResponse(responseCode = "200", description = "Routine updated successfully") + @ApiResponse(responseCode = "404", description = "Routine not found") + public ResponseEntity updateRoutine( + @Parameter(description = "Routine ID") @PathVariable UUID routineId, + @Parameter(description = "Updated routine data") @RequestBody RoutineDTO routineDTO) { + // TODO: Move this logic to userservice layer + // Buscar la rutina existente + Routine existingRoutine = routineRepository.findById(routineId) + .orElseThrow(() -> new RuntimeException("Routine not found")); + + // Actualizar campos + existingRoutine.setName(routineDTO.getName()); + existingRoutine.setDescription(routineDTO.getDescription()); + existingRoutine.setDifficulty(routineDTO.getDifficulty()); + existingRoutine.setGoal(routineDTO.getGoal()); + + // Actualizar la rutina + Routine updatedRoutine = userService.updateRoutine(routineId, existingRoutine); + return ResponseEntity.ok(updatedRoutine); } - userService.logRoutineProgress(userId, routineId, completedPercentage); - return ResponseEntity.noContent().build(); -} + @PostMapping("/{userId}/routines/{routineId}/progress") + @Operation(summary = "Log routine progress", description = "Records progress for a routine session") + @ApiResponse(responseCode = "204", description = "Progress logged successfully") + @ApiResponse(responseCode = "404", description = "User or routine not found") + public ResponseEntity logRoutineProgress( + @Parameter(description = "User ID") @PathVariable UUID userId, + @Parameter(description = "Routine ID") @PathVariable UUID routineId, + @Parameter(description = "Progress percentage") @RequestBody Map progressData) { -@GetMapping("/{userId}/recommended-routines") -@Operation(summary = "Get recommended routines", description = "Retrieves personalized routine recommendations for a user") -@ApiResponse(responseCode = "200", description = "Recommendations retrieved successfully") -@ApiResponse(responseCode = "404", description = "User not found") -public ResponseEntity> getRecommendedRoutines( - @Parameter(description = "User ID") @PathVariable UUID userId) { + Integer completedPercentage = progressData.get("completed"); + if (completedPercentage == null) { + completedPercentage = 100; // Valor por defecto si no se proporciona + } - List recommendations = userService.getRecommendedRoutines(userId); - return ResponseEntity.ok(recommendations); -} + userService.logRoutineProgress(userId, routineId, completedPercentage); + return ResponseEntity.noContent().build(); + } -// -------------------------- Exercise crud --------- -@GetMapping("/exercises") + @GetMapping("/{userId}/recommended-routines") + @Operation(summary = "Get recommended routines", description = "Retrieves personalized routine recommendations for a user") + @ApiResponse(responseCode = "200", description = "Recommendations retrieved successfully") + @ApiResponse(responseCode = "404", description = "User not found") + public ResponseEntity> getRecommendedRoutines( + @Parameter(description = "User ID") @PathVariable UUID userId) { + + List recommendations = userService.getRecommendedRoutines(userId); + return ResponseEntity.ok(recommendations); + } + + // -------------------------- Exercise crud --------- + @GetMapping("/exercises") @Operation(summary = "Get all exercises", description = "Retrieves all base exercises in the system") @ApiResponse(responseCode = "200", description = "Exercises retrieved successfully") public ResponseEntity> getAllExercises() { @@ -544,347 +543,409 @@ public ResponseEntity deleteExercise( } // ----------------------------------------------------- -// Gym reservations endpoints -// ----------------------------------------------------- -// TODO: implementar bien modulo, configurar endpoint para gestion de sesiones gym. - -@GetMapping("/gym/availability") -@Operation(summary = "Get gym availability", description = "Retrieves gym availability for a specific date") -@ApiResponse(responseCode = "200", description = "Availability information retrieved successfully") -public ResponseEntity> getGymAvailability( - @Parameter(description = "Date to check") - @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) { - List availableSlots = userService.getAvailableTimeSlots(date); - return ResponseEntity.ok(availableSlots); -} + // Gym reservations endpoints + // ----------------------------------------------------- + // TODO: implementar bien modulo, configurar endpoint para gestion de sesiones + // gym. + + @GetMapping("/gym/availability") + @Operation(summary = "Get gym availability", description = "Retrieves gym availability for a specific date") + @ApiResponse(responseCode = "200", description = "Availability information retrieved successfully") + public ResponseEntity> getGymAvailability( + @Parameter(description = "Date to check") @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) { + List availableSlots = userService.getAvailableTimeSlots(date); + return ResponseEntity.ok(availableSlots); + } -@GetMapping("/gym/availability/time") -@Operation(summary = "Check availability for specific time", description = "Checks gym availability for a specific date and time") -@ApiResponse(responseCode = "200", description = "Availability information retrieved successfully") -public ResponseEntity> checkAvailabilityForTime( - @Parameter(description = "Date to check") - @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, - @Parameter(description = "Time to check") - @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.TIME) LocalTime time) { - - Map availability = gymReservationService.getAvailability(date, time); - return ResponseEntity.ok(availability); -} + @GetMapping("/gym/availability/time") + @Operation(summary = "Check availability for specific time", description = "Checks gym availability for a specific date and time") + @ApiResponse(responseCode = "200", description = "Availability information retrieved successfully") + public ResponseEntity> checkAvailabilityForTime( + @Parameter(description = "Date to check") @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, + @Parameter(description = "Time to check") @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.TIME) LocalTime time) { -@PostMapping("/{userId}/reservations") -@Operation(summary = "Create reservation", description = "Creates a new gym reservation") -@ApiResponse(responseCode = "201", description = "Reservation created successfully") -@ApiResponse(responseCode = "404", description = "User not found") -@ApiResponse(responseCode = "400", description = "No available slots for the requested time") -public ResponseEntity createReservation( - @Parameter(description = "User ID") @PathVariable UUID userId, - @RequestBody ReservationDTO reservationDTO) { - try { - // Asegurarse de que el userId del path coincide con el del DTO - reservationDTO.setUserId(userId); - ReservationDTO created = gymReservationService.create(reservationDTO); + Map availability = gymReservationService.getAvailability(date, time); + return ResponseEntity.ok(availability); + } - Map response = new HashMap<>(); - response.put("reservationId", created.getId()); - response.put("message", "Reserva creada exitosamente"); + @PostMapping("/{userId}/reservations") + @Operation(summary = "Create reservation", description = "Creates a new gym reservation") + @ApiResponse(responseCode = "201", description = "Reservation created successfully") + @ApiResponse(responseCode = "404", description = "User not found") + @ApiResponse(responseCode = "400", description = "No available slots for the requested time") + public ResponseEntity createReservation( + @Parameter(description = "User ID") @PathVariable UUID userId, + @RequestBody ReservationDTO reservationDTO) { + try { + // Asegurarse de que el userId del path coincide con el del DTO + reservationDTO.setUserId(userId); + ReservationDTO created = gymReservationService.create(reservationDTO); - return new ResponseEntity<>(response, HttpStatus.CREATED); - } catch (IllegalArgumentException e) { - Map error = new HashMap<>(); - error.put("error", e.getMessage()); - return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + Map response = new HashMap<>(); + response.put("reservationId", created.getId()); + response.put("message", "Reserva creada exitosamente"); + + return new ResponseEntity<>(response, HttpStatus.CREATED); + } catch (IllegalArgumentException e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } } -} -@GetMapping("/{userId}/reservations") -@Operation(summary = "Get user reservations", description = "Retrieves all reservations for a user") -@ApiResponse(responseCode = "200", description = "Reservations retrieved successfully") -public ResponseEntity> getUserReservations( - @Parameter(description = "User ID") @PathVariable UUID userId) { - List reservations = gymReservationService.getByUserId(userId); - return ResponseEntity.ok(reservations); -} + @GetMapping("/{userId}/reservations") + @Operation(summary = "Get user reservations", description = "Retrieves all reservations for a user") + @ApiResponse(responseCode = "200", description = "Reservations retrieved successfully") + public ResponseEntity> getUserReservations( + @Parameter(description = "User ID") @PathVariable UUID userId) { + List reservations = gymReservationService.getByUserId(userId); + return ResponseEntity.ok(reservations); + } -@GetMapping("/{userId}/reservations/{reservationId}") -@Operation(summary = "Get reservation details", description = "Retrieves details of a specific reservation") -@ApiResponse(responseCode = "200", description = "Reservation details retrieved successfully") -@ApiResponse(responseCode = "404", description = "Reservation not found") -public ResponseEntity getReservationDetails( - @Parameter(description = "User ID") @PathVariable UUID userId, - @Parameter(description = "Reservation ID") @PathVariable UUID reservationId) { + @GetMapping("/{userId}/reservations/{reservationId}") + @Operation(summary = "Get reservation details", description = "Retrieves details of a specific reservation") + @ApiResponse(responseCode = "200", description = "Reservation details retrieved successfully") + @ApiResponse(responseCode = "404", description = "Reservation not found") + public ResponseEntity getReservationDetails( + @Parameter(description = "User ID") @PathVariable UUID userId, + @Parameter(description = "Reservation ID") @PathVariable UUID reservationId) { - Optional reservation = gymReservationService.getById(reservationId); + Optional reservation = gymReservationService.getById(reservationId); - return reservation - .filter(r -> r.getUserId().equals(userId)) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); -} + return reservation + .filter(r -> r.getUserId().equals(userId)) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } -@DeleteMapping("/{userId}/reservations/{reservationId}") -@Operation(summary = "Cancel reservation", description = "Cancels an existing reservation") -@ApiResponse(responseCode = "200", description = "Reservation cancelled successfully") -@ApiResponse(responseCode = "404", description = "Reservation not found") -@ApiResponse(responseCode = "403", description = "User not authorized to cancel this reservation") -public ResponseEntity cancelReservation( - @Parameter(description = "User ID") @PathVariable UUID userId, - @Parameter(description = "Reservation ID") @PathVariable UUID reservationId) { - try { - // Verifica primero si la reserva existe y pertenece al usuario - Optional reservation = gymReservationService.getById(reservationId); - if (reservation.isEmpty() || !reservation.get().getUserId().equals(userId)) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + @DeleteMapping("/{userId}/reservations/{reservationId}") + @Operation(summary = "Cancel reservation", description = "Cancels an existing reservation") + @ApiResponse(responseCode = "200", description = "Reservation cancelled successfully") + @ApiResponse(responseCode = "404", description = "Reservation not found") + @ApiResponse(responseCode = "403", description = "User not authorized to cancel this reservation") + public ResponseEntity cancelReservation( + @Parameter(description = "User ID") @PathVariable UUID userId, + @Parameter(description = "Reservation ID") @PathVariable UUID reservationId) { + try { + // Verifica primero si la reserva existe y pertenece al usuario + Optional reservation = gymReservationService.getById(reservationId); + if (reservation.isEmpty() || !reservation.get().getUserId().equals(userId)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + + gymReservationService.delete(reservationId); + + Map response = new HashMap<>(); + response.put("message", "Reserva cancelada exitosamente"); + + return ResponseEntity.ok(response); + } catch (IllegalArgumentException e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + } + + @PostMapping("/{userId}/sessions/{sessionId}/waitlist") + @Operation(summary = "Join waitlist", description = "Adds user to waitlist for a full session") + @ApiResponse(responseCode = "200", description = "Added to waitlist successfully") + @ApiResponse(responseCode = "404", description = "Session not found") + public ResponseEntity joinWaitlist( + @Parameter(description = "User ID") @PathVariable UUID userId, + @Parameter(description = "Session ID") @PathVariable UUID sessionId) { + try { + boolean added = gymReservationService.joinWaitlist(userId, sessionId); + + if (added) { + Map status = gymReservationService.getWaitlistStatus(userId, sessionId); + Map response = new HashMap<>(); + response.put("message", + "Has sido añadido a la lista de espera. Te notificaremos cuando haya cupo disponible."); + response.put("status", status); + + return ResponseEntity.ok(response); + } else { + return ResponseEntity.badRequest().build(); + } + } catch (IllegalArgumentException e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); } + } - gymReservationService.delete(reservationId); + @GetMapping("/{userId}/sessions/{sessionId}/waitlist") + @Operation(summary = "Get waitlist status", description = "Gets user's position in waitlist for a session") + @ApiResponse(responseCode = "200", description = "Waitlist status retrieved successfully") + public ResponseEntity> getWaitlistStatus( + @Parameter(description = "User ID") @PathVariable UUID userId, + @Parameter(description = "Session ID") @PathVariable UUID sessionId) { - Map response = new HashMap<>(); - response.put("message", "Reserva cancelada exitosamente"); + Map status = gymReservationService.getWaitlistStatus(userId, sessionId); + return ResponseEntity.ok(status); + } - return ResponseEntity.ok(response); - } catch (IllegalArgumentException e) { - Map error = new HashMap<>(); - error.put("error", e.getMessage()); - return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + @GetMapping("/{userId}/waitlists") + @Operation(summary = "Get all user waitlists", description = "Gets all sessions where user is in waitlist") + @ApiResponse(responseCode = "200", description = "Waitlists retrieved successfully") + public ResponseEntity>> getUserWaitlists( + @Parameter(description = "User ID") @PathVariable UUID userId) { + + List> waitlists = gymReservationService.getUserWaitlists(userId); + return ResponseEntity.ok(waitlists); } -} -@PostMapping("/{userId}/sessions/{sessionId}/waitlist") -@Operation(summary = "Join waitlist", description = "Adds user to waitlist for a full session") -@ApiResponse(responseCode = "200", description = "Added to waitlist successfully") -@ApiResponse(responseCode = "404", description = "Session not found") -public ResponseEntity joinWaitlist( - @Parameter(description = "User ID") @PathVariable UUID userId, - @Parameter(description = "Session ID") @PathVariable UUID sessionId) { - try { - boolean added = gymReservationService.joinWaitlist(userId, sessionId); + @DeleteMapping("/{userId}/sessions/{sessionId}/waitlist") + @Operation(summary = "Leave waitlist", description = "Removes user from waitlist for a session") + @ApiResponse(responseCode = "200", description = "Removed from waitlist successfully") + @ApiResponse(responseCode = "404", description = "User not in waitlist or session not found") + public ResponseEntity leaveWaitlist( + @Parameter(description = "User ID") @PathVariable UUID userId, + @Parameter(description = "Session ID") @PathVariable UUID sessionId) { - if (added) { - Map status = gymReservationService.getWaitlistStatus(userId, sessionId); - Map response = new HashMap<>(); - response.put("message", "Has sido añadido a la lista de espera. Te notificaremos cuando haya cupo disponible."); - response.put("status", status); + boolean removed = gymReservationService.leaveWaitlist(userId, sessionId); + if (removed) { + Map response = new HashMap<>(); + response.put("message", "Has sido removido de la lista de espera exitosamente"); return ResponseEntity.ok(response); } else { - return ResponseEntity.badRequest().build(); + return ResponseEntity.notFound().build(); } - } catch (IllegalArgumentException e) { - Map error = new HashMap<>(); - error.put("error", e.getMessage()); - return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); } -} -@GetMapping("/{userId}/sessions/{sessionId}/waitlist") -@Operation(summary = "Get waitlist status", description = "Gets user's position in waitlist for a session") -@ApiResponse(responseCode = "200", description = "Waitlist status retrieved successfully") -public ResponseEntity> getWaitlistStatus( - @Parameter(description = "User ID") @PathVariable UUID userId, - @Parameter(description = "Session ID") @PathVariable UUID sessionId) { + // ----------------------------------------------------- + // Gym session management endpoints (trainers) + // ----------------------------------------------------- - Map status = gymReservationService.getWaitlistStatus(userId, sessionId); - return ResponseEntity.ok(status); -} + @Autowired + private GymSessionService gymSessionService; -@GetMapping("/{userId}/waitlists") -@Operation(summary = "Get all user waitlists", description = "Gets all sessions where user is in waitlist") -@ApiResponse(responseCode = "200", description = "Waitlists retrieved successfully") -public ResponseEntity>> getUserWaitlists( - @Parameter(description = "User ID") @PathVariable UUID userId) { + @PostMapping("/trainer/sessions") + @Operation(summary = "Create gym session", description = "Creates a new gym session for users to book") + @ApiResponse(responseCode = "201", description = "Session created successfully") + @PreAuthorize("hasRole('TRAINER') or hasRole('ADMIN')") + public ResponseEntity> createSession( + @RequestBody Map sessionData) { - List> waitlists = gymReservationService.getUserWaitlists(userId); - return ResponseEntity.ok(waitlists); -} + try { + LocalDate date = LocalDate.parse((String) sessionData.get("date")); + LocalTime startTime = LocalTime.parse((String) sessionData.get("startTime")); + LocalTime endTime = LocalTime.parse((String) sessionData.get("endTime")); + int capacity = (Integer) sessionData.get("capacity"); + UUID trainerId = UUID.fromString((String) sessionData.get("trainerId")); + Optional description = Optional.ofNullable((String) sessionData.get("description")); -@DeleteMapping("/{userId}/sessions/{sessionId}/waitlist") -@Operation(summary = "Leave waitlist", description = "Removes user from waitlist for a session") -@ApiResponse(responseCode = "200", description = "Removed from waitlist successfully") -@ApiResponse(responseCode = "404", description = "User not in waitlist or session not found") -public ResponseEntity leaveWaitlist( - @Parameter(description = "User ID") @PathVariable UUID userId, - @Parameter(description = "Session ID") @PathVariable UUID sessionId) { + UUID sessionId = gymSessionService.createSession( + date, startTime, endTime, capacity, description, trainerId); - boolean removed = gymReservationService.leaveWaitlist(userId, sessionId); + Map response = new HashMap<>(); + response.put("sessionId", sessionId); + response.put("message", "Sesión creada exitosamente"); - if (removed) { - Map response = new HashMap<>(); - response.put("message", "Has sido removido de la lista de espera exitosamente"); - return ResponseEntity.ok(response); - } else { - return ResponseEntity.notFound().build(); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } } -} + @PutMapping("/trainer/sessions/{sessionId}") + @Operation(summary = "Update gym session", description = "Updates an existing gym session") + @ApiResponse(responseCode = "200", description = "Session updated successfully") + @ApiResponse(responseCode = "404", description = "Session not found") + @PreAuthorize("hasRole('TRAINER') or hasRole('ADMIN')") + public ResponseEntity updateSession( + @Parameter(description = "Session ID") @PathVariable UUID sessionId, + @RequestBody Map sessionData) { -// ----------------------------------------------------- -// Gym session management endpoints (trainers) -// ----------------------------------------------------- + try { + LocalDate date = LocalDate.parse((String) sessionData.get("date")); + LocalTime startTime = LocalTime.parse((String) sessionData.get("startTime")); + LocalTime endTime = LocalTime.parse((String) sessionData.get("endTime")); + int capacity = (Integer) sessionData.get("capacity"); + UUID trainerId = UUID.fromString((String) sessionData.get("trainerId")); + + boolean updated = gymSessionService.updateSession( + sessionId, date, startTime, endTime, capacity, trainerId); + + if (updated) { + Map response = new HashMap<>(); + response.put("message", "Sesión actualizada exitosamente"); + return ResponseEntity.ok(response); + } else { + return ResponseEntity.notFound().build(); + } + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + } -@Autowired -private GymSessionService gymSessionService; + @DeleteMapping("/trainer/sessions/{sessionId}") + @Operation(summary = "Cancel gym session", description = "Cancels an existing gym session") + @ApiResponse(responseCode = "200", description = "Session cancelled successfully") + @ApiResponse(responseCode = "404", description = "Session not found") + @PreAuthorize("hasRole('TRAINER') or hasRole('ADMIN')") + public ResponseEntity cancelSession( + @Parameter(description = "Session ID") @PathVariable UUID sessionId, + @RequestBody(required = false) Map requestBody) { -@PostMapping("/trainer/sessions") -@Operation(summary = "Create gym session", description = "Creates a new gym session for users to book") -@ApiResponse(responseCode = "201", description = "Session created successfully") -@PreAuthorize("hasRole('TRAINER') or hasRole('ADMIN')") -public ResponseEntity> createSession( - @RequestBody Map sessionData) { - - try { - LocalDate date = LocalDate.parse((String) sessionData.get("date")); - LocalTime startTime = LocalTime.parse((String) sessionData.get("startTime")); - LocalTime endTime = LocalTime.parse((String) sessionData.get("endTime")); - int capacity = (Integer) sessionData.get("capacity"); - UUID trainerId = UUID.fromString((String) sessionData.get("trainerId")); - Optional description = Optional.ofNullable((String) sessionData.get("description")); - - UUID sessionId = gymSessionService.createSession( - date, startTime, endTime, capacity, description, trainerId); - - Map response = new HashMap<>(); - response.put("sessionId", sessionId); - response.put("message", "Sesión creada exitosamente"); - - return new ResponseEntity<>(response, HttpStatus.CREATED); - } catch (Exception e) { - Map error = new HashMap<>(); - error.put("error", e.getMessage()); - return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + try { + String reason = (requestBody != null) ? requestBody.get("reason") : null; + UUID trainerId = UUID.fromString(requestBody != null ? requestBody.get("trainerId") : ""); + + boolean cancelled = gymSessionService.cancelSession(sessionId, reason, trainerId); + + if (cancelled) { + Map response = new HashMap<>(); + response.put("message", "Sesión cancelada exitosamente"); + return ResponseEntity.ok(response); + } else { + return ResponseEntity.notFound().build(); + } + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } } -} -@PutMapping("/trainer/sessions/{sessionId}") -@Operation(summary = "Update gym session", description = "Updates an existing gym session") -@ApiResponse(responseCode = "200", description = "Session updated successfully") -@ApiResponse(responseCode = "404", description = "Session not found") -@PreAuthorize("hasRole('TRAINER') or hasRole('ADMIN')") -public ResponseEntity updateSession( - @Parameter(description = "Session ID") @PathVariable UUID sessionId, - @RequestBody Map sessionData) { - - try { - LocalDate date = LocalDate.parse((String) sessionData.get("date")); - LocalTime startTime = LocalTime.parse((String) sessionData.get("startTime")); - LocalTime endTime = LocalTime.parse((String) sessionData.get("endTime")); - int capacity = (Integer) sessionData.get("capacity"); - UUID trainerId = UUID.fromString((String) sessionData.get("trainerId")); - - boolean updated = gymSessionService.updateSession( - sessionId, date, startTime, endTime, capacity, trainerId); - - if (updated) { - Map response = new HashMap<>(); - response.put("message", "Sesión actualizada exitosamente"); - return ResponseEntity.ok(response); - } else { - return ResponseEntity.notFound().build(); - } - } catch (Exception e) { - Map error = new HashMap<>(); - error.put("error", e.getMessage()); - return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + @GetMapping("/trainer/sessions") + @Operation(summary = "Get sessions by date", description = "Retrieves all gym sessions for a specific date") + @ApiResponse(responseCode = "200", description = "Sessions retrieved successfully") + @PreAuthorize("hasRole('TRAINER') or hasRole('ADMIN')") + public ResponseEntity> getSessionsByDate( + @Parameter(description = "Date to check") @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) { + + List sessions = gymSessionService.getSessionsByDate(date); + return ResponseEntity.ok(sessions); } -} -@DeleteMapping("/trainer/sessions/{sessionId}") -@Operation(summary = "Cancel gym session", description = "Cancels an existing gym session") -@ApiResponse(responseCode = "200", description = "Session cancelled successfully") -@ApiResponse(responseCode = "404", description = "Session not found") -@PreAuthorize("hasRole('TRAINER') or hasRole('ADMIN')") -public ResponseEntity cancelSession( - @Parameter(description = "Session ID") @PathVariable UUID sessionId, - @RequestBody(required = false) Map requestBody) { - - try { - String reason = (requestBody != null) ? requestBody.get("reason") : null; - UUID trainerId = UUID.fromString(requestBody != null ? requestBody.get("trainerId") : ""); - - boolean cancelled = gymSessionService.cancelSession(sessionId, reason, trainerId); - - if (cancelled) { - Map response = new HashMap<>(); - response.put("message", "Sesión cancelada exitosamente"); - return ResponseEntity.ok(response); - } else { - return ResponseEntity.notFound().build(); + @GetMapping("/trainer/{trainerId}/sessions") + @Operation(summary = "Get trainer's sessions", description = "Retrieves all sessions created by a specific trainer") + @ApiResponse(responseCode = "200", description = "Sessions retrieved successfully") + @PreAuthorize("hasRole('TRAINER') or hasRole('ADMIN')") + public ResponseEntity> getTrainerSessions( + @Parameter(description = "Trainer ID") @PathVariable UUID trainerId) { + + List sessions = gymSessionService.getSessionsByTrainer(trainerId); + return ResponseEntity.ok(sessions); + } + + @PostMapping("/trainer/sessions/recurring") + @Operation(summary = "Create recurring sessions", description = "Creates recurring gym sessions on specified days") + @ApiResponse(responseCode = "201", description = "Recurring sessions created successfully") + @PreAuthorize("hasRole('TRAINER') or hasRole('ADMIN')") + public ResponseEntity> createRecurringSessions( + @RequestBody Map recurringData) { + + try { + int dayOfWeek = (Integer) recurringData.get("dayOfWeek"); // 1=Monday, 7=Sunday + LocalTime startTime = LocalTime.parse((String) recurringData.get("startTime")); + LocalTime endTime = LocalTime.parse((String) recurringData.get("endTime")); + int capacity = (Integer) recurringData.get("capacity"); + LocalDate startDate = LocalDate.parse((String) recurringData.get("startDate")); + LocalDate endDate = LocalDate.parse((String) recurringData.get("endDate")); + UUID trainerId = UUID.fromString((String) recurringData.get("trainerId")); + Optional description = Optional.ofNullable((String) recurringData.get("description")); + + int sessionsCreated = gymSessionService.configureRecurringSessions( + dayOfWeek, startTime, endTime, capacity, description, trainerId, startDate, endDate); + + Map response = new HashMap<>(); + response.put("sessionsCreated", sessionsCreated); + response.put("message", "Sesiones recurrentes creadas exitosamente"); + + return new ResponseEntity<>(response, HttpStatus.CREATED); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); } - } catch (Exception e) { - Map error = new HashMap<>(); - error.put("error", e.getMessage()); - return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); } -} -@GetMapping("/trainer/sessions") -@Operation(summary = "Get sessions by date", description = "Retrieves all gym sessions for a specific date") -@ApiResponse(responseCode = "200", description = "Sessions retrieved successfully") -@PreAuthorize("hasRole('TRAINER') or hasRole('ADMIN')") -public ResponseEntity> getSessionsByDate( - @Parameter(description = "Date to check") - @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) { - - List sessions = gymSessionService.getSessionsByDate(date); - return ResponseEntity.ok(sessions); -} + @GetMapping("/trainer/sessions/stats") + @Operation(summary = "Get occupancy statistics", description = "Retrieves occupancy statistics for gym sessions") + @ApiResponse(responseCode = "200", description = "Statistics retrieved successfully") + @PreAuthorize("hasRole('TRAINER') or hasRole('ADMIN')") + public ResponseEntity> getOccupancyStatistics( + @Parameter(description = "Start date") @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @Parameter(description = "End date") @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { -@GetMapping("/trainer/{trainerId}/sessions") -@Operation(summary = "Get trainer's sessions", description = "Retrieves all sessions created by a specific trainer") -@ApiResponse(responseCode = "200", description = "Sessions retrieved successfully") -@PreAuthorize("hasRole('TRAINER') or hasRole('ADMIN')") -public ResponseEntity> getTrainerSessions( - @Parameter(description = "Trainer ID") @PathVariable UUID trainerId) { - - List sessions = gymSessionService.getSessionsByTrainer(trainerId); - return ResponseEntity.ok(sessions); -} + Map statistics = gymSessionService.getOccupancyStatistics(startDate, endDate); + return ResponseEntity.ok(statistics); + } + + // ----------------------------------------------------- + // Slot and schedule management by trainers + // ----------------------------------------------------- + @GetMapping("/trainer/sessions/{sessionId}/students") + @Operation(summary = "Get registered students", description = "Retrieves all students registered for a specific session") + @ApiResponse(responseCode = "200", description = "Students retrieved successfully") + @PreAuthorize("hasRole('TRAINER') or hasRole('ADMIN')") + public ResponseEntity>> getRegisteredStudents( + @Parameter(description = "Session ID") @PathVariable UUID sessionId) { + + List> students = gymSessionService.getRegisteredStudentsForSession(sessionId); + return ResponseEntity.ok(students); + } + + @PostMapping("/trainer/attendance") + @Operation(summary = "Record student attendance", description = "Records attendance for a student at a gym session") + @ApiResponse(responseCode = "200", description = "Attendance recorded successfully") + @PreAuthorize("hasRole('TRAINER') or hasRole('ADMIN')") + public ResponseEntity> recordStudentAttendance( + @RequestBody Map attendanceData) { -@PostMapping("/trainer/sessions/recurring") -@Operation(summary = "Create recurring sessions", description = "Creates recurring gym sessions on specified days") -@ApiResponse(responseCode = "201", description = "Recurring sessions created successfully") -@PreAuthorize("hasRole('TRAINER') or hasRole('ADMIN')") -public ResponseEntity> createRecurringSessions( - @RequestBody Map recurringData) { + UUID userId = UUID.fromString((String) attendanceData.get("userId")); + UUID reservationId = UUID.fromString((String) attendanceData.get("reservationId")); + LocalDateTime attendanceTime = attendanceData.containsKey("attendanceTime") + ? LocalDateTime.parse((String) attendanceData.get("attendanceTime")) + : LocalDateTime.now(); + + boolean success = userService.recordGymAttendance(userId, reservationId, attendanceTime); + + Map response = new HashMap<>(); + response.put("success", success); + response.put("message", success ? "Asistencia registrada correctamente" : "No se pudo registrar la asistencia"); + + return ResponseEntity.ok(response); + } + + @GetMapping("/trainer/{trainerId}/attendance/stats") + @Operation(summary = "Get attendance statistics", description = "Retrieves attendance statistics for a trainer's sessions") + @ApiResponse(responseCode = "200", description = "Statistics retrieved successfully") + @PreAuthorize("hasRole('TRAINER') or hasRole('ADMIN')") + public ResponseEntity> getAttendanceStatistics( + @Parameter(description = "Trainer ID") @PathVariable UUID trainerId, + @Parameter(description = "Start date") @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @Parameter(description = "End date") @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { + + Map statistics = gymSessionService.getTrainerAttendanceStatistics(trainerId, startDate, + endDate); + return ResponseEntity.ok(statistics); + } + + @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 { - int dayOfWeek = (Integer) recurringData.get("dayOfWeek"); // 1=Monday, 7=Sunday - LocalTime startTime = LocalTime.parse((String) recurringData.get("startTime")); - LocalTime endTime = LocalTime.parse((String) recurringData.get("endTime")); - int capacity = (Integer) recurringData.get("capacity"); - LocalDate startDate = LocalDate.parse((String) recurringData.get("startDate")); - LocalDate endDate = LocalDate.parse((String) recurringData.get("endDate")); - UUID trainerId = UUID.fromString((String) recurringData.get("trainerId")); - Optional description = Optional.ofNullable((String) recurringData.get("description")); - - int sessionsCreated = gymSessionService.configureRecurringSessions( - dayOfWeek, startTime, endTime, capacity, description, trainerId, startDate, endDate); - - Map response = new HashMap<>(); - response.put("sessionsCreated", sessionsCreated); - response.put("message", "Sesiones recurrentes creadas exitosamente"); - - return new ResponseEntity<>(response, HttpStatus.CREATED); + Object session = gymSessionService.getSessionById(sessionId); + return ResponseEntity.ok(session); } catch (Exception e) { - Map error = new HashMap<>(); + Map error = new HashMap<>(); error.put("error", e.getMessage()); - return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); } } - -@GetMapping("/trainer/sessions/stats") -@Operation(summary = "Get occupancy statistics", description = "Retrieves occupancy statistics for gym sessions") -@ApiResponse(responseCode = "200", description = "Statistics retrieved successfully") -@PreAuthorize("hasRole('TRAINER') or hasRole('ADMIN')") -public ResponseEntity> getOccupancyStatistics( - @Parameter(description = "Start date") - @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, - @Parameter(description = "End date") - @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { - - Map statistics = gymSessionService.getOccupancyStatistics(startDate, endDate); - return ResponseEntity.ok(statistics); -} - // // ----------------------------------------------------- + // // ------------------------------------------------------ // // Equipment reservations endpoints // // ----------------------------------------------------- @@ -935,46 +996,54 @@ public ResponseEntity> getOccupancyStatistics( // // 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); -// } + // @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/repository/GymSessionRepository.java b/src/main/java/edu/eci/cvds/prometeo/repository/GymSessionRepository.java index 5b78640..f7c61aa 100644 --- a/src/main/java/edu/eci/cvds/prometeo/repository/GymSessionRepository.java +++ b/src/main/java/edu/eci/cvds/prometeo/repository/GymSessionRepository.java @@ -43,4 +43,9 @@ Optional findBySessionDateAndStartTimeLessThanEqualAndEndTimeGreater * Find sessions by date */ List findBySessionDate(LocalDate date); + + /** + * Find sessions by trainer ID and date range + */ + List findByTrainerIdAndSessionDateBetween(UUID trainerId, LocalDate startDate, LocalDate endDate); } \ No newline at end of file diff --git a/src/main/java/edu/eci/cvds/prometeo/service/GymSessionService.java b/src/main/java/edu/eci/cvds/prometeo/service/GymSessionService.java index ea9a027..796a5bf 100644 --- a/src/main/java/edu/eci/cvds/prometeo/service/GymSessionService.java +++ b/src/main/java/edu/eci/cvds/prometeo/service/GymSessionService.java @@ -9,88 +9,121 @@ /** * Service for managing gym sessions and time slots - * Note: This service requires GymSession and TimeSlot entities that don't appear in the provided code. + * Note: This service requires GymSession and TimeSlot entities that don't + * appear in the provided code. * Implementation would need to create these entities. */ public interface GymSessionService { - + /** * Creates a new gym session - * @param date Date of the session - * @param startTime Start time - * @param endTime End time - * @param capacity Maximum capacity + * + * @param date Date of the session + * @param startTime Start time + * @param endTime End time + * @param capacity Maximum capacity * @param description Optional description - * @param trainerId ID of the trainer creating the session + * @param trainerId ID of the trainer creating the session * @return ID of the created session */ - UUID createSession(LocalDate date, LocalTime startTime, LocalTime endTime, - int capacity, Optional description, UUID trainerId); - + UUID createSession(LocalDate date, LocalTime startTime, LocalTime endTime, + int capacity, Optional description, UUID trainerId); + /** * Updates an existing gym session + * * @param sessionId ID of the session - * @param date New date + * @param date New date * @param startTime New start time - * @param endTime New end time - * @param capacity New capacity + * @param endTime New end time + * @param capacity New capacity * @param trainerId ID of the trainer making the update * @return true if successfully updated */ - boolean updateSession(UUID sessionId, LocalDate date, LocalTime startTime, - LocalTime endTime, int capacity, UUID trainerId); - + boolean updateSession(UUID sessionId, LocalDate date, LocalTime startTime, + LocalTime endTime, int capacity, UUID trainerId); + /** * Cancels a gym session + * * @param sessionId ID of the session - * @param reason Reason for cancellation + * @param reason Reason for cancellation * @param trainerId ID of the trainer canceling the session * @return true if successfully canceled */ boolean cancelSession(UUID sessionId, String reason, UUID trainerId); - + /** * Gets all sessions for a specific date + * * @param date Date to query * @return List of sessions on that date */ List getSessionsByDate(LocalDate date); - + /** * Gets sessions created by a specific trainer + * * @param trainerId ID of the trainer * @return List of sessions by the trainer */ List getSessionsByTrainer(UUID trainerId); - + /** * Gets available time slots for a date + * * @param date Date to check * @return List of available time slots */ List> getAvailableTimeSlots(LocalDate date); - + /** * Configures recurring sessions - * @param dayOfWeek Day of the week (1-7, where 1 is Monday) - * @param startTime Start time - * @param endTime End time - * @param capacity Maximum capacity + * + * @param dayOfWeek Day of the week (1-7, where 1 is Monday) + * @param startTime Start time + * @param endTime End time + * @param capacity Maximum capacity * @param description Optional description - * @param trainerId ID of the trainer - * @param startDate Start date for recurrence - * @param endDate End date for recurrence + * @param trainerId ID of the trainer + * @param startDate Start date for recurrence + * @param endDate End date for recurrence * @return Number of sessions created */ int configureRecurringSessions(int dayOfWeek, LocalTime startTime, LocalTime endTime, - int capacity, Optional description, UUID trainerId, - LocalDate startDate, LocalDate endDate); - + int capacity, Optional description, UUID trainerId, + LocalDate startDate, LocalDate endDate); + /** * Gets occupancy statistics for the gym + * * @param startDate Start date - * @param endDate End date + * @param endDate End date * @return Map of dates to occupancy percentages */ Map getOccupancyStatistics(LocalDate startDate, LocalDate endDate); + + /** + * Gets all students registered for a specific session + * + * @param sessionId ID of the session + * @return List of registered students with their details + */ + List> getRegisteredStudentsForSession(UUID sessionId); + + /** + * Gets attendance statistics for a trainer's sessions + * @param trainerId ID of the trainer + * @param startDate Start date for the period + * @param endDate End date for the period + * @return Map with attendance statistics + */ +Map getTrainerAttendanceStatistics(UUID trainerId, LocalDate startDate, LocalDate endDate); + +/** + * Gets a specific gym session by its ID + * @param sessionId ID of the session to retrieve + * @return Session details + */ +Object getSessionById(UUID sessionId); } \ No newline at end of file diff --git a/src/main/java/edu/eci/cvds/prometeo/service/impl/GymSessionServiceImpl.java b/src/main/java/edu/eci/cvds/prometeo/service/impl/GymSessionServiceImpl.java index 7aea307..0d0c6ee 100644 --- a/src/main/java/edu/eci/cvds/prometeo/service/impl/GymSessionServiceImpl.java +++ b/src/main/java/edu/eci/cvds/prometeo/service/impl/GymSessionServiceImpl.java @@ -1,7 +1,11 @@ package edu.eci.cvds.prometeo.service.impl; import edu.eci.cvds.prometeo.model.GymSession; +import edu.eci.cvds.prometeo.model.Reservation; +import edu.eci.cvds.prometeo.model.User; import edu.eci.cvds.prometeo.repository.GymSessionRepository; +import edu.eci.cvds.prometeo.repository.ReservationRepository; +import edu.eci.cvds.prometeo.repository.UserRepository; import edu.eci.cvds.prometeo.service.GymSessionService; import edu.eci.cvds.prometeo.PrometeoExceptions; import org.springframework.beans.factory.annotation.Autowired; @@ -15,16 +19,22 @@ @Service public class GymSessionServiceImpl implements GymSessionService { - private final GymSessionRepository gymSessionRepository; - @Autowired - public GymSessionServiceImpl(GymSessionRepository gymSessionRepository) { - this.gymSessionRepository = gymSessionRepository; - } + private GymSessionRepository gymSessionRepository; + @Autowired + private ReservationRepository reservationRepository; + @Autowired + private UserRepository userRepository; + + // @Autowired + // public GymSessionServiceImpl(GymSessionRepository gymSessionRepository) { + // this.gymSessionRepository = gymSessionRepository; + // } @Override @Transactional - public UUID createSession(LocalDate date, LocalTime startTime, LocalTime endTime, int capacity, Optional description, UUID trainerId) { + public UUID createSession(LocalDate date, LocalTime startTime, LocalTime endTime, int capacity, + Optional description, UUID trainerId) { // Prevent overlapping sessions Optional overlapping = gymSessionRepository .findBySessionDateAndStartTimeLessThanEqualAndEndTimeGreaterThanEqual(date, startTime, endTime); @@ -44,7 +54,8 @@ public UUID createSession(LocalDate date, LocalTime startTime, LocalTime endTime @Override @Transactional - public boolean updateSession(UUID sessionId, LocalDate date, LocalTime startTime, LocalTime endTime, int capacity, UUID trainerId) { + public boolean updateSession(UUID sessionId, LocalDate date, LocalTime startTime, LocalTime endTime, int capacity, + UUID trainerId) { GymSession session = gymSessionRepository.findById(sessionId) .orElseThrow(() -> new PrometeoExceptions(PrometeoExceptions.SESION_NO_ENCONTRADA)); // Prevent overlapping with other sessions @@ -130,7 +141,8 @@ public List> getAvailableTimeSlots(LocalDate date) { @Override @Transactional - public int configureRecurringSessions(int dayOfWeek, LocalTime startTime, LocalTime endTime, int capacity, Optional description, UUID trainerId, LocalDate startDate, LocalDate endDate) { + public int configureRecurringSessions(int dayOfWeek, LocalTime startTime, LocalTime endTime, int capacity, + Optional description, UUID trainerId, LocalDate startDate, LocalDate endDate) { int count = 0; LocalDate date = startDate; while (!date.isAfter(endDate)) { @@ -176,4 +188,111 @@ public Map getOccupancyStatistics(LocalDate startDate, Local } return stats; } + + @Override + public List> getRegisteredStudentsForSession(UUID sessionId) { + // Verificar que existe la sesión + GymSession session = gymSessionRepository.findById(sessionId) + .orElseThrow(() -> new PrometeoExceptions(PrometeoExceptions.SESION_NO_ENCONTRADA)); + + // Buscar todas las reservas para esa sesión + List reservations = reservationRepository.findBySessionId(sessionId); + + // Transformar los resultados + List> result = new ArrayList<>(); + for (Reservation reservation : reservations) { + // Obtener el usuario + User user = userRepository.findById(reservation.getUserId()) + .orElse(null); + + if (user != null) { + Map studentInfo = new HashMap<>(); + studentInfo.put("reservationId", reservation.getId()); + studentInfo.put("userId", user.getId()); + studentInfo.put("name", user.getName()); + studentInfo.put("institutionalId", user.getInstitutionalId()); + studentInfo.put("status", reservation.getStatus()); + studentInfo.put("attended", reservation.getAttended()); + studentInfo.put("attendanceTime", reservation.getAttendanceTime()); + + result.add(studentInfo); + } + } + + return result; + } + + @Override + public Map getTrainerAttendanceStatistics(UUID trainerId, LocalDate startDate, LocalDate endDate) { + // Buscar todas las sesiones del entrenador en el periodo + List sessions = gymSessionRepository.findByTrainerIdAndSessionDateBetween( + trainerId, startDate, endDate); + + int totalSessions = sessions.size(); + int totalCapacity = 0; + int totalReservations = 0; + int totalAttendance = 0; + + Map dailyAttendance = new HashMap<>(); + + for (GymSession session : sessions) { + totalCapacity += session.getCapacity(); + totalReservations += session.getReservedSpots(); + + // Buscar reservas para la sesión + List reservations = reservationRepository.findBySessionId(session.getId()); + int sessionAttendance = (int) reservations.stream() + .filter(r -> r.getAttended()) + .count(); + + totalAttendance += sessionAttendance; + + // Agregar a estadísticas diarias + dailyAttendance.compute(session.getSessionDate(), + (date, count) -> (count == null) ? sessionAttendance : count + sessionAttendance); + } + + // Calcular porcentajes + double occupancyRate = totalCapacity > 0 ? (double) totalReservations / totalCapacity * 100 : 0; + double attendanceRate = totalReservations > 0 ? (double) totalAttendance / totalReservations * 100 : 0; + + Map statistics = new HashMap<>(); + statistics.put("totalSessions", totalSessions); + statistics.put("totalCapacity", totalCapacity); + statistics.put("totalReservations", totalReservations); + statistics.put("totalAttendance", totalAttendance); + statistics.put("occupancyRate", Math.round(occupancyRate * 100) / 100.0); // Redondear a 2 decimales + statistics.put("attendanceRate", Math.round(attendanceRate * 100) / 100.0); // Redondear a 2 decimales + statistics.put("dailyAttendance", dailyAttendance); + + return statistics; + } + + @Override +public Object getSessionById(UUID sessionId) { + GymSession session = gymSessionRepository.findById(sessionId) + .orElseThrow(() -> new PrometeoExceptions(PrometeoExceptions.SESION_NO_ENCONTRADA)); + + // Obtener información adicional del entrenador + User trainer = userRepository.findById(session.getTrainerId()) + .orElse(null); + + Map sessionDetails = new HashMap<>(); + sessionDetails.put("id", session.getId()); + sessionDetails.put("date", session.getSessionDate()); + sessionDetails.put("startTime", session.getStartTime()); + sessionDetails.put("endTime", session.getEndTime()); + sessionDetails.put("capacity", session.getCapacity()); + sessionDetails.put("reservedSpots", session.getReservedSpots()); + + if (trainer != null) { + Map trainerInfo = new HashMap<>(); + trainerInfo.put("id", trainer.getId()); + trainerInfo.put("name", trainer.getName()); + sessionDetails.put("trainer", trainerInfo); + } + + + return sessionDetails; +} } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4da27c9..c1b47a0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -19,5 +19,5 @@ openai.api.url=${OPEN_AI_MODEL} spring.datasource.hikari.properties.ssl=true spring.datasource.hikari.properties.sslfactory=org.postgresql.ssl.NonValidatingFactory -# Server configuration -server.port=8081 \ No newline at end of file +# Server configuration, comentado porque no es necesario. +# server.port=8081 \ No newline at end of file diff --git a/src/test/java/edu/eci/cvds/prometeo/PrometeoApplicationTests.java b/src/test/java/edu/eci/cvds/prometeo/PrometeoApplicationTests.java index 4066eb9..157eb19 100644 --- a/src/test/java/edu/eci/cvds/prometeo/PrometeoApplicationTests.java +++ b/src/test/java/edu/eci/cvds/prometeo/PrometeoApplicationTests.java @@ -4,6 +4,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; +import edu.eci.cvds.prometeo.PrometeoApplication; // Añade este import @SpringBootTest(classes = PrometeoApplication.class) @ActiveProfiles("test")