diff --git a/backend/demo-group7/src/main/java/com/group7/demo/controllers/TrainingProgramController.java b/backend/demo-group7/src/main/java/com/group7/demo/controllers/TrainingProgramController.java index 9afa94a..94e5320 100644 --- a/backend/demo-group7/src/main/java/com/group7/demo/controllers/TrainingProgramController.java +++ b/backend/demo-group7/src/main/java/com/group7/demo/controllers/TrainingProgramController.java @@ -2,6 +2,7 @@ import com.group7.demo.dtos.TrainingProgramRequest; import com.group7.demo.dtos.TrainingProgramResponse; +import com.group7.demo.dtos.UserTrainingProgramResponse; import com.group7.demo.services.TrainingProgramService; import jakarta.servlet.http.HttpServletRequest; import lombok.AllArgsConstructor; @@ -9,7 +10,9 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.Collections; import java.util.List; +import java.util.Set; @RestController @RequestMapping("/api/training-programs") @@ -69,8 +72,8 @@ public ResponseEntity leaveProgram(@PathVariable Long programId, HttpSer } @GetMapping("/{programId}/participants") - public ResponseEntity> getRegisteredUsernames(@PathVariable Long programId) { - List usernames = trainingProgramService.getRegisteredUsernames(programId); + public ResponseEntity> getRegisteredUsernames(@PathVariable Long programId) { + Set usernames = trainingProgramService.getRegisteredUsernames(programId); return ResponseEntity.ok(usernames); } @@ -81,4 +84,42 @@ public ResponseEntity> getTrainingProgramsByTraine return ResponseEntity.ok(trainingPrograms); } + @PostMapping("/{trainingProgramId}/exercises/{exerciseId}/complete") + public ResponseEntity markExerciseAsCompleted( + @PathVariable Long trainingProgramId, + @PathVariable Long exerciseId, + HttpServletRequest request + ) { + trainingProgramService.markExerciseAsCompleted(trainingProgramId, exerciseId, request); + return ResponseEntity.ok().build(); + } + + @PostMapping("/{trainingProgramId}/exercises/{exerciseId}/uncomplete") + public ResponseEntity unmarkExerciseAsCompleted( + @PathVariable Long trainingProgramId, + @PathVariable Long exerciseId, + HttpServletRequest request) { + trainingProgramService.unmarkExerciseAsCompleted(trainingProgramId, exerciseId, request); + return ResponseEntity.ok().build(); + } + + @PostMapping("/{trainingProgramId}/complete") + public ResponseEntity markTrainingProgramAsCompleted( + @PathVariable Long trainingProgramId, + HttpServletRequest request + ) { + trainingProgramService.markTrainingProgramAsCompleted(trainingProgramId, request); + return ResponseEntity.ok().build(); + } + + @GetMapping("/joined/{username}") + public ResponseEntity> getJoinedTrainingPrograms(@PathVariable String username) { + try { + List responses = trainingProgramService.getJoinedTrainingPrograms(username); + return ResponseEntity.ok(responses); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Collections.emptyList()); + } + } } diff --git a/backend/demo-group7/src/main/java/com/group7/demo/dtos/ExerciseDetail.java b/backend/demo-group7/src/main/java/com/group7/demo/dtos/ExerciseDetail.java index 2aee738..5593e60 100644 --- a/backend/demo-group7/src/main/java/com/group7/demo/dtos/ExerciseDetail.java +++ b/backend/demo-group7/src/main/java/com/group7/demo/dtos/ExerciseDetail.java @@ -9,6 +9,7 @@ @Builder @AllArgsConstructor public class ExerciseDetail { + private Long id; private Exercise exercise; private int repetitions; private int sets; diff --git a/backend/demo-group7/src/main/java/com/group7/demo/dtos/TrainingProgramResponse.java b/backend/demo-group7/src/main/java/com/group7/demo/dtos/TrainingProgramResponse.java index 84faf29..efb4586 100644 --- a/backend/demo-group7/src/main/java/com/group7/demo/dtos/TrainingProgramResponse.java +++ b/backend/demo-group7/src/main/java/com/group7/demo/dtos/TrainingProgramResponse.java @@ -6,6 +6,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Set; @Data @Builder @@ -16,6 +17,6 @@ public class TrainingProgramResponse { private List exercises; private String description; private String trainerUsername; - private List participants; + private Set participants; private LocalDateTime createdAt; } \ No newline at end of file diff --git a/backend/demo-group7/src/main/java/com/group7/demo/dtos/UserExerciseDetail.java b/backend/demo-group7/src/main/java/com/group7/demo/dtos/UserExerciseDetail.java new file mode 100644 index 0000000..3110975 --- /dev/null +++ b/backend/demo-group7/src/main/java/com/group7/demo/dtos/UserExerciseDetail.java @@ -0,0 +1,17 @@ +package com.group7.demo.dtos; + +import com.group7.demo.models.Exercise; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +@AllArgsConstructor +public class UserExerciseDetail { + private Long id; + private Exercise exercise; + private int repetitions; + private int sets; + private boolean completed; +} diff --git a/backend/demo-group7/src/main/java/com/group7/demo/dtos/UserProfileResponse.java b/backend/demo-group7/src/main/java/com/group7/demo/dtos/UserProfileResponse.java index b28d8c1..9476ada 100644 --- a/backend/demo-group7/src/main/java/com/group7/demo/dtos/UserProfileResponse.java +++ b/backend/demo-group7/src/main/java/com/group7/demo/dtos/UserProfileResponse.java @@ -19,5 +19,5 @@ public class UserProfileResponse { private Set following; private List posts; private List trainingPrograms; - private List joinedPrograms; + private List joinedPrograms; } diff --git a/backend/demo-group7/src/main/java/com/group7/demo/dtos/UserTrainingProgramResponse.java b/backend/demo-group7/src/main/java/com/group7/demo/dtos/UserTrainingProgramResponse.java new file mode 100644 index 0000000..02bb190 --- /dev/null +++ b/backend/demo-group7/src/main/java/com/group7/demo/dtos/UserTrainingProgramResponse.java @@ -0,0 +1,24 @@ +package com.group7.demo.dtos; + +import com.group7.demo.models.enums.UserTrainingProgramStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; + +@Data +@Builder +@AllArgsConstructor +public class UserTrainingProgramResponse { + private Long id; + private String title; + private List exercises; + private String description; + private String trainerUsername; + private Set participants; + private UserTrainingProgramStatus status; + private LocalDateTime createdAt; +} diff --git a/backend/demo-group7/src/main/java/com/group7/demo/dtos/mapper/Mapper.java b/backend/demo-group7/src/main/java/com/group7/demo/dtos/mapper/Mapper.java index 3fc7d9f..e6ee821 100644 --- a/backend/demo-group7/src/main/java/com/group7/demo/dtos/mapper/Mapper.java +++ b/backend/demo-group7/src/main/java/com/group7/demo/dtos/mapper/Mapper.java @@ -4,7 +4,10 @@ import com.group7.demo.models.*; import org.springframework.stereotype.Component; +import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; @Component @@ -29,20 +32,56 @@ public TrainingProgramResponse mapToTrainingProgramResponse(TrainingProgram prog .createdAt(program.getCreatedAt()) .exercises(program.getExercises().stream() .map(this::mapToExerciseDetailResponse) + .sorted(Comparator.comparing(ExerciseDetail::getId)) .collect(Collectors.toList())) .participants(program.getParticipants() == null ? - List.of() : + Set.of() : program.getParticipants().stream() .map(userTrainingProgram -> userTrainingProgram.getUser().getUsername()) - .collect(Collectors.toList())) + .collect(Collectors.toSet())) .build(); } public ExerciseDetail mapToExerciseDetailResponse(TrainingProgramExercise trainingProgramExercise) { return ExerciseDetail.builder() + .id(trainingProgramExercise.getId()) .exercise(trainingProgramExercise.getExercise()) .repetitions(trainingProgramExercise.getRepetitions()) .sets(trainingProgramExercise.getSets()) .build(); } + + public UserTrainingProgramResponse mapToUserTrainingProgramResponse(UserTrainingProgram userTrainingProgram) { + TrainingProgram program = userTrainingProgram.getTrainingProgram(); + Map completedExercises = userTrainingProgram.getExerciseProgress(); // Now returns Map + + // Use the new mapper function for exercises + List exerciseDetails = program.getExercises().stream() + .map(exercise -> mapToUserExerciseDetailResponse(exercise, completedExercises)) + .sorted(Comparator.comparing(UserExerciseDetail::getId)) + .collect(Collectors.toList()); + + return UserTrainingProgramResponse.builder() + .id(program.getId()) + .title(program.getTitle()) + .description(program.getDescription()) + .trainerUsername(program.getTrainer().getUsername()) + .participants(program.getParticipants().stream() + .map(participant -> participant.getUser().getUsername()) + .collect(Collectors.toSet())) + .exercises(exerciseDetails) + .status(userTrainingProgram.getStatus()) + .createdAt(program.getCreatedAt()) + .build(); + } + + public UserExerciseDetail mapToUserExerciseDetailResponse(TrainingProgramExercise trainingProgramExercise, Map completedExercises) { + return UserExerciseDetail.builder() + .id(trainingProgramExercise.getId()) + .exercise(trainingProgramExercise.getExercise()) + .repetitions(trainingProgramExercise.getRepetitions()) + .sets(trainingProgramExercise.getSets()) + .completed(completedExercises.getOrDefault(trainingProgramExercise.getId(), false)) // Use `getOrDefault` to handle missing keys + .build(); + } } diff --git a/backend/demo-group7/src/main/java/com/group7/demo/models/UserTrainingProgram.java b/backend/demo-group7/src/main/java/com/group7/demo/models/UserTrainingProgram.java index 1d02559..db5e3c8 100644 --- a/backend/demo-group7/src/main/java/com/group7/demo/models/UserTrainingProgram.java +++ b/backend/demo-group7/src/main/java/com/group7/demo/models/UserTrainingProgram.java @@ -1,5 +1,8 @@ package com.group7.demo.models; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.group7.demo.models.enums.UserTrainingProgramStatus; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; @@ -7,6 +10,8 @@ import lombok.NoArgsConstructor; import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; @Entity @Builder @@ -27,5 +32,29 @@ public class UserTrainingProgram { @JoinColumn(name = "training_program_id", nullable = false) private TrainingProgram trainingProgram; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private UserTrainingProgramStatus status; + + @Lob + @Column(columnDefinition = "TEXT") + private String exerciseProgress; // JSON progress tracking + private LocalDateTime joinedAt; + + // Deserialize the JSON string into a Map + public Map getExerciseProgress() { + if (exerciseProgress == null) { + return new HashMap<>(); // return empty map if no progress is available + } + + try { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(exerciseProgress, new TypeReference>() {}); + } catch (Exception e) { + e.printStackTrace(); + return new HashMap<>(); // return empty map on error + } + } + } diff --git a/backend/demo-group7/src/main/java/com/group7/demo/models/enums/UserTrainingProgramStatus.java b/backend/demo-group7/src/main/java/com/group7/demo/models/enums/UserTrainingProgramStatus.java new file mode 100644 index 0000000..ae137c4 --- /dev/null +++ b/backend/demo-group7/src/main/java/com/group7/demo/models/enums/UserTrainingProgramStatus.java @@ -0,0 +1,7 @@ +package com.group7.demo.models.enums; + +public enum UserTrainingProgramStatus { + ONGOING, + LEFT, + COMPLETED +} diff --git a/backend/demo-group7/src/main/java/com/group7/demo/repository/UserTrainingProgramRepository.java b/backend/demo-group7/src/main/java/com/group7/demo/repository/UserTrainingProgramRepository.java index 2bb87f6..94e9538 100644 --- a/backend/demo-group7/src/main/java/com/group7/demo/repository/UserTrainingProgramRepository.java +++ b/backend/demo-group7/src/main/java/com/group7/demo/repository/UserTrainingProgramRepository.java @@ -1,6 +1,5 @@ package com.group7.demo.repository; -import com.group7.demo.models.TrainingProgram; import com.group7.demo.models.User; import com.group7.demo.models.UserTrainingProgram; import org.springframework.data.jpa.repository.JpaRepository; @@ -9,7 +8,7 @@ import java.util.Optional; public interface UserTrainingProgramRepository extends JpaRepository { - boolean existsByUserAndTrainingProgram(User user, TrainingProgram trainingProgram); + List findAllByUserAndTrainingProgramId(User user, Long trainingProgramId); Optional findByUserIdAndTrainingProgramId(Long userId, Long programId); List findByUser(User user); diff --git a/backend/demo-group7/src/main/java/com/group7/demo/services/TrainingProgramService.java b/backend/demo-group7/src/main/java/com/group7/demo/services/TrainingProgramService.java index 2ec93b4..8091645 100644 --- a/backend/demo-group7/src/main/java/com/group7/demo/services/TrainingProgramService.java +++ b/backend/demo-group7/src/main/java/com/group7/demo/services/TrainingProgramService.java @@ -1,10 +1,11 @@ package com.group7.demo.services; -import com.group7.demo.dtos.ExerciseDetail; -import com.group7.demo.dtos.TrainingProgramRequest; -import com.group7.demo.dtos.TrainingProgramResponse; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.group7.demo.dtos.*; import com.group7.demo.dtos.mapper.Mapper; import com.group7.demo.models.*; +import com.group7.demo.models.enums.UserTrainingProgramStatus; import com.group7.demo.repository.ExerciseRepository; import com.group7.demo.repository.TrainingProgramRepository; import com.group7.demo.repository.UserTrainingProgramRepository; @@ -17,6 +18,8 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; @Service @@ -76,6 +79,7 @@ public TrainingProgramResponse createTrainingProgram(TrainingProgramRequest trai return mapper.mapToTrainingProgramResponse(savedProgram); } + @Transactional public List getAllTrainingPrograms() { // Fetch all training programs from the repository List trainingPrograms = trainingProgramRepository.findAll(); @@ -86,6 +90,7 @@ public List getAllTrainingPrograms() { .collect(Collectors.toList()); } + @Transactional public TrainingProgramResponse getTrainingProgramById(Long id) { // Find the training program by ID, or throw an exception if not found TrainingProgram trainingProgram = trainingProgramRepository.findById(id) @@ -128,9 +133,29 @@ public void joinTrainingProgram(Long trainingProgramId , HttpServletRequest requ TrainingProgram trainingProgram = trainingProgramRepository.findById(trainingProgramId) .orElseThrow(() -> new EntityNotFoundException("Training Program not found with id: " + trainingProgramId)); - boolean alreadyJoined = userTrainingProgramRepository.existsByUserAndTrainingProgram(user, trainingProgram); - if (alreadyJoined) { - throw new IllegalStateException("User has already joined the training program."); + // Check for all past participations + List pastEntries = userTrainingProgramRepository.findAllByUserAndTrainingProgramId(user, trainingProgramId); + + boolean hasOngoingEntry = pastEntries.stream() + .anyMatch(entry -> entry.getStatus() == UserTrainingProgramStatus.ONGOING); + + if (hasOngoingEntry) { + throw new IllegalStateException("User is already actively participating in this training program."); + } + + // Initialize the progress JSON object + ObjectMapper mapper = new ObjectMapper(); + Map exerciseProgress = trainingProgram.getExercises().stream() + .collect(Collectors.toMap( + TrainingProgramExercise::getId, // Exercise ID + TrainingProgramExercise -> false // Not completed yet + )); + + String progressJson; + try { + progressJson = mapper.writeValueAsString(exerciseProgress); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Failed to initialize exercise progress JSON", e); } // Create a new UserTrainingProgram entity @@ -138,6 +163,8 @@ public void joinTrainingProgram(Long trainingProgramId , HttpServletRequest requ .user(user) .trainingProgram(trainingProgram) .joinedAt(LocalDateTime.now()) + .status(UserTrainingProgramStatus.ONGOING) + .exerciseProgress(progressJson) .build(); // Save the UserTrainingProgram entity @@ -145,23 +172,7 @@ public void joinTrainingProgram(Long trainingProgramId , HttpServletRequest requ } @Transactional - public void leaveTrainingProgram(Long trainingProgramId, HttpServletRequest request) { - // Fetch the authenticated user from the request - User user = authenticationService.getAuthenticatedUserInternal(request); - - // Fetch the training program by its ID - TrainingProgram trainingProgram = trainingProgramRepository.findById(trainingProgramId) - .orElseThrow(() -> new EntityNotFoundException("Training program not found with id: " + trainingProgramId)); - - // Check if the user is participating in the program - UserTrainingProgram userTrainingProgram = userTrainingProgramRepository.findByUserIdAndTrainingProgramId(user.getId(), trainingProgram.getId()) - .orElseThrow(() -> new IllegalStateException("User is not participating in this program.")); - - // Remove the user from the program - userTrainingProgramRepository.delete(userTrainingProgram); - } - - public List getRegisteredUsernames(Long trainingProgramId) { + public Set getRegisteredUsernames(Long trainingProgramId) { // Fetch the training program by its ID TrainingProgram trainingProgram = trainingProgramRepository.findById(trainingProgramId) .orElseThrow(() -> new EntityNotFoundException("Training program not found with id: " + trainingProgramId)); @@ -169,23 +180,107 @@ public List getRegisteredUsernames(Long trainingProgramId) { // Fetch the list of participants' usernames return trainingProgram.getParticipants().stream() .map(userTrainingProgram -> userTrainingProgram.getUser().getUsername()) - .collect(Collectors.toList()); + .collect(Collectors.toSet()); } // Return the list of joined training programs for the authenticated user - public List getJoinedTrainingPrograms(String username) throws Exception { + @Transactional + public List getJoinedTrainingPrograms(String username) throws Exception { // Fetch the authenticated user User user = userService.getUserByUsername(username); // Fetch the list of training programs the user has joined List userTrainingPrograms = userTrainingProgramRepository.findByUser(user); - // Map the list of UserTrainingProgram entities to TrainingProgramResponse DTOs + // Map the list of UserTrainingProgram entities to UserTrainingProgramResponse DTOs return userTrainingPrograms.stream() - .map(UserTrainingProgram::getTrainingProgram) - .map(mapper::mapToTrainingProgramResponse) + .map(mapper::mapToUserTrainingProgramResponse) // Map to UserTrainingProgramResponse instead of TrainingProgramResponse .collect(Collectors.toList()); } + private UserTrainingProgram getOngoingUserTrainingProgram(User user, Long trainingProgramId) { + // Fetch all entries and filter the ongoing one + return userTrainingProgramRepository.findAllByUserAndTrainingProgramId(user, trainingProgramId) + .stream() + .filter(entry -> entry.getStatus() == UserTrainingProgramStatus.ONGOING) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No ongoing training program found.")); + } + + @Transactional + public void markExerciseAsCompleted(Long trainingProgramId, Long exerciseId, HttpServletRequest request) { + User user = authenticationService.getAuthenticatedUserInternal(request); + + UserTrainingProgram userTrainingProgram = getOngoingUserTrainingProgram(user, trainingProgramId); + + // Get the current progress map + Map exerciseProgress = userTrainingProgram.getExerciseProgress(); + + // Mark the exercise as completed + exerciseProgress.put(exerciseId, true); + + // Serialize the updated exercise progress map back to JSON + ObjectMapper objectMapper = new ObjectMapper(); + try { + String updatedProgressJson = objectMapper.writeValueAsString(exerciseProgress); + userTrainingProgram.setExerciseProgress(updatedProgressJson); // Save the updated JSON string + } catch (Exception e) { + throw new IllegalStateException("Failed to update exercise progress JSON", e); + } + + userTrainingProgramRepository.save(userTrainingProgram); + } + + + + @Transactional + public void unmarkExerciseAsCompleted(Long trainingProgramId, Long exerciseId, HttpServletRequest request) { + User user = authenticationService.getAuthenticatedUserInternal(request); + + UserTrainingProgram userTrainingProgram = getOngoingUserTrainingProgram(user, trainingProgramId); + + // Get the current progress map from the serialized exerciseProgress + Map exerciseProgress = userTrainingProgram.getExerciseProgress(); + + // Mark the exercise as incomplete (unmark) + exerciseProgress.put(exerciseId, false); + + // Serialize the updated progress map back to JSON + ObjectMapper objectMapper = new ObjectMapper(); + try { + String updatedProgressJson = objectMapper.writeValueAsString(exerciseProgress); + userTrainingProgram.setExerciseProgress(updatedProgressJson); // Save the updated JSON string + } catch (Exception e) { + e.printStackTrace(); + // Handle exception, possibly throw a runtime exception or return an error response + } + + userTrainingProgramRepository.save(userTrainingProgram); + } + + @Transactional + public void markTrainingProgramAsCompleted(Long trainingProgramId, HttpServletRequest request) { + User user = authenticationService.getAuthenticatedUserInternal(request); + + UserTrainingProgram userTrainingProgram = getOngoingUserTrainingProgram(user, trainingProgramId); + + // Mark the entire training program as completed + userTrainingProgram.setStatus(UserTrainingProgramStatus.COMPLETED); + + userTrainingProgramRepository.save(userTrainingProgram); + } + + @Transactional + public void leaveTrainingProgram(Long trainingProgramId, HttpServletRequest request) { + User user = authenticationService.getAuthenticatedUserInternal(request); + + UserTrainingProgram userTrainingProgram = getOngoingUserTrainingProgram(user, trainingProgramId); + + + // Mark the training program as left + userTrainingProgram.setStatus(UserTrainingProgramStatus.LEFT); + + userTrainingProgramRepository.save(userTrainingProgram); + } } diff --git a/backend/demo-group7/src/main/java/com/group7/demo/services/UserService.java b/backend/demo-group7/src/main/java/com/group7/demo/services/UserService.java index db84e79..0e5e4c4 100644 --- a/backend/demo-group7/src/main/java/com/group7/demo/services/UserService.java +++ b/backend/demo-group7/src/main/java/com/group7/demo/services/UserService.java @@ -3,6 +3,7 @@ import com.group7.demo.dtos.PostResponse; import com.group7.demo.dtos.TrainingProgramResponse; import com.group7.demo.dtos.UserProfileResponse; +import com.group7.demo.dtos.UserTrainingProgramResponse; import com.group7.demo.models.User; import com.group7.demo.repository.UserRepository; import jakarta.servlet.http.HttpServletRequest; @@ -48,7 +49,7 @@ public UserProfileResponse getUserProfile(String username) throws Exception { List posts = postService.getPostsByUser(user.getUsername()); List trainingPrograms = trainingProgramService.getTrainingProgramByTrainer(user.getUsername()); - List joinedPrograms = trainingProgramService.getJoinedTrainingPrograms(user.getUsername()); + List joinedPrograms = trainingProgramService.getJoinedTrainingPrograms(user.getUsername()); return UserProfileResponse.builder() .username(user.getUsername())