diff --git a/src/main/java/com/classes/controllers/ClassController.java b/src/main/java/com/classes/controllers/ClassController.java index 7ff30fe..54aaac1 100644 --- a/src/main/java/com/classes/controllers/ClassController.java +++ b/src/main/java/com/classes/controllers/ClassController.java @@ -2,6 +2,7 @@ import com.classes.dtos.Class.ClassRequest; import com.classes.dtos.Class.ClassResponse; +import com.classes.dtos.Class.MonthlyCalendarDTO; import com.classes.services.ClassService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -65,4 +66,15 @@ public ResponseEntity deleteClass(@PathVariable UUID id) { classService.deleteClass(id); return ResponseEntity.ok("Clase eliminada correctamente"); } + + @Operation(summary = "Ver calendario mensual", description = "Vista de calendario por mes con clases agrupadas por fecha") + @GetMapping("/calendar/monthly") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity getMonthlyCalendar( + @RequestParam int year, + @RequestParam int month) { + log.info("📅 Admin consultando calendario mensual para {}/{}", month, year); + MonthlyCalendarDTO calendar = classService.getMonthlyCalendar(year, month); + return ResponseEntity.ok(calendar); + } } diff --git a/src/main/java/com/classes/controllers/ClassViewController.java b/src/main/java/com/classes/controllers/ClassViewController.java index 0e67036..61574f4 100644 --- a/src/main/java/com/classes/controllers/ClassViewController.java +++ b/src/main/java/com/classes/controllers/ClassViewController.java @@ -1,8 +1,10 @@ package com.classes.controllers; import com.classes.dtos.Class.*; +import com.classes.dtos.Class.MonthlyCalendarDTO; import com.classes.services.AuthorizationService; import com.classes.services.ClassService; +import com.classes.services.ClassStatsService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -26,6 +28,7 @@ public class ClassViewController { private final ClassService classService; + private final ClassStatsService classStatsService; private final AuthorizationService authorizationService; @Operation(summary = "Listar todas las clases", description = "Todos los roles pueden ver la lista") @@ -42,7 +45,7 @@ public ResponseEntity> findAllClasses() { public ResponseEntity> getMyClassesWithStats(Authentication authentication) { UUID trainerId = authorizationService.getUserId(authentication); log.info("Trainer {} consultando sus clases con estadísticas", trainerId); - List classes = classService.getClassesWithStatsByTrainer(trainerId); + List classes = classStatsService.getClassesWithStatsByTrainer(trainerId); return ResponseEntity.ok(classes); } @@ -51,7 +54,7 @@ public ResponseEntity> getMyClassesWithStats(Authen @PreAuthorize("hasRole('TRAINER') or hasRole('ADMIN')") public ResponseEntity getClassDetail(@PathVariable UUID id) { log.info("Consultando detalle de la clase {}", id); - ClassDetailResponse detail = classService.getClassDetail(id); + ClassDetailResponse detail = classStatsService.getClassDetail(id); return ResponseEntity.ok(detail); } @@ -65,10 +68,10 @@ public ResponseEntity> getClassesForCalendar( log.info("📅 Consultando clases para calendario"); if (startDate != null && endDate != null) { - List classes = classService.getClassesForCalendar(startDate, endDate); + List classes = classStatsService.getClassesForCalendar(startDate, endDate); return ResponseEntity.ok(classes); } else { - List classes = classService.getUpcomingClasses(); + List classes = classStatsService.getUpcomingClasses(); return ResponseEntity.ok(classes); } } @@ -78,7 +81,18 @@ public ResponseEntity> getClassesForCalendar( @PreAuthorize("hasRole('USER') or hasRole('TRAINER') or hasRole('ADMIN')") public ResponseEntity> getUpcomingClasses() { log.info("Consultando próximas clases"); - List classes = classService.getUpcomingClasses(); + List classes = classStatsService.getUpcomingClasses(); return ResponseEntity.ok(classes); } + + @Operation(summary = "Ver calendario mensual", description = "Vista de calendario por mes con clases agrupadas por fecha") + @GetMapping("/calendar/monthly") + @PreAuthorize("hasRole('USER') or hasRole('TRAINER') or hasRole('ADMIN')") + public ResponseEntity getMonthlyCalendar( + @RequestParam int year, + @RequestParam int month) { + log.info("📅 Consultando calendario mensual para {}/{}", month, year); + MonthlyCalendarDTO calendar = classService.getMonthlyCalendar(year, month); + return ResponseEntity.ok(calendar); + } } diff --git a/src/main/java/com/classes/entities/TrainerEntity.java b/src/main/java/com/classes/entities/TrainerEntity.java index 3e47d02..af4dcd9 100644 --- a/src/main/java/com/classes/entities/TrainerEntity.java +++ b/src/main/java/com/classes/entities/TrainerEntity.java @@ -24,7 +24,7 @@ @EntityListeners(AuditListener.class) public class TrainerEntity { @Id -// private UUID userid; + @GeneratedValue(strategy = GenerationType.UUID) private UUID id; private String firstName; private String lastName; diff --git a/src/main/java/com/classes/services/ClassService.java b/src/main/java/com/classes/services/ClassService.java index b927a91..64af71f 100644 --- a/src/main/java/com/classes/services/ClassService.java +++ b/src/main/java/com/classes/services/ClassService.java @@ -4,6 +4,7 @@ import org.springframework.data.domain.Page; import com.classes.dtos.Class.ClassRequest; import com.classes.dtos.Class.ClassResponse; +import com.classes.dtos.Class.MonthlyCalendarDTO; import java.time.LocalDate; import java.util.List; @@ -21,13 +22,6 @@ public interface ClassService { ClassResponse updateClass(UUID id, ClassRequest request); void deleteClass(UUID id); - - - List getClassesWithStatsByTrainer(UUID trainerId); - - ClassDetailResponse getClassDetail(UUID classId); - - List getClassesForCalendar(LocalDate startDate, LocalDate endDate); - - List getUpcomingClasses(); + + MonthlyCalendarDTO getMonthlyCalendar(int year, int month); } diff --git a/src/main/java/com/classes/services/Impl/ClassServiceImpl.java b/src/main/java/com/classes/services/Impl/ClassServiceImpl.java index b71ede3..6041ae1 100644 --- a/src/main/java/com/classes/services/Impl/ClassServiceImpl.java +++ b/src/main/java/com/classes/services/Impl/ClassServiceImpl.java @@ -1,5 +1,6 @@ package com.classes.services.Impl; +import com.classes.services.ClassStatsService; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -24,8 +25,9 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; -import java.util.List; -import java.util.UUID; +import java.time.YearMonth; +import java.time.format.TextStyle; +import java.util.*; import java.util.stream.Collectors; @@ -36,11 +38,9 @@ public class ClassServiceImpl implements ClassService { private final ClassRepository repository; private final ClassMapper classMapper; - private final ClassStatsMapper classStatsMapper; private final TrainerRepository trainerRepository; private final LocationRepository locationRepository; - private final ClassReservationRepository reservationRepository; - private final MemberClientService memberClientService; + private final ClassStatsService classStatsService; @Transactional @@ -121,152 +121,29 @@ private void validateTrainerAndLocation(ClassRequest request) { } } - //<---ESTADISTICAS----> - - @Override - @Transactional(readOnly = true) - public List getClassesWithStatsByTrainer(UUID trainerId) { - log.info("📊 Obteniendo clases con estadísticas para el trainer {}", trainerId); - List classes = repository.findByTrainerId(trainerId); - return classes.stream().map(classEntity -> { - ClassWithStatsResponse response = classStatsMapper.toClassWithStatsResponse(classEntity); - long currentStudents = reservationRepository.countByClassEntityIdAndStatus( - classEntity.getId(), ReservationStatus.RESERVADO); - response.setCurrentStudents((int) currentStudents); - Double avgAttendance = reservationRepository.calculateAverageAttendanceByClassId(classEntity.getId()); - response.setAverageAttendance(avgAttendance != null ? avgAttendance : 0.0); - response.setStatus(determineClassStatus(classEntity, (int) currentStudents)); - - return response; - }).collect(Collectors.toList()); - } - @Override @Transactional(readOnly = true) - public ClassDetailResponse getClassDetail(UUID classId) { - log.info("🔍 Obteniendo detalle de la clase {}", classId); - - ClassEntity classEntity = repository.findById(classId) - .orElseThrow(() -> new IllegalArgumentException("La clase con ID " + classId + " no existe")); - ClassDetailResponse response = classStatsMapper.toClassDetailResponse(classEntity); - List reservations = reservationRepository.findByClassEntityId(classId); - List memberIds = reservations.stream() - .map(ClassReservation::getMemberId) - .distinct() - .collect(Collectors.toList()); - List membersInfo = memberClientService.getMembersInfo(memberIds); - List students = reservations.stream() - .map(reservation -> buildStudentDTO(reservation, membersInfo)) - .collect(Collectors.toList()); - - response.setCurrentStudents(students.size()); - response.setStudents(students); - - return response; - } - - private StudentInClassDTO buildStudentDTO(ClassReservation reservation, List membersInfo) { - MemberInfoDTO memberInfo = membersInfo.stream() - .filter(m -> m.getId().equals(reservation.getMemberId())) - .findFirst() - .orElse(createDefaultMemberInfo(reservation.getMemberId())); - - List memberReservations = - reservationRepository.findByMemberId(reservation.getMemberId()); - - long totalClasses = memberReservations.size(); - - // ✅ Cambiado: uso de Boolean.TRUE.equals() para evitar errores por null - long attendedClasses = memberReservations.stream() - .filter(r -> Boolean.TRUE.equals(r.getAttended())) - .count(); - - double attendancePercentage = totalClasses > 0 - ? (attendedClasses * 100.0 / totalClasses) : 0.0; - - String initials = getInitials(memberInfo.getFirstName(), memberInfo.getLastName()); - - return StudentInClassDTO.builder() - .memberId(reservation.getMemberId()) - .name(memberInfo.getFirstName() + " " + memberInfo.getLastName()) - .email(memberInfo.getEmail()) - .avatarInitials(initials) - .status(memberInfo.getStatus()) - .membershipType(memberInfo.getMembershipType()) - .attendancePercentage(attendancePercentage) - .totalClasses((int) totalClasses) - .lastAccess(memberInfo.getLastAccess()) - .reservationId(reservation.getId()) + public MonthlyCalendarDTO getMonthlyCalendar(int year, int month) { + log.info("📅 Obteniendo calendario mensual para {}/{}", month, year); + YearMonth yearMonth = YearMonth.of(year, month); + LocalDate firstDay = yearMonth.atDay(1); + LocalDate lastDay = yearMonth.atEndOfMonth(); + List allClasses = classStatsService.getClassesForCalendar(firstDay, lastDay); + Map> classesByDate = allClasses.stream() + .collect(Collectors.groupingBy(CalendarClassDTO::getClassDate)); + List daysWithClasses = new ArrayList<>(classesByDate.keySet()); + Collections.sort(daysWithClasses); + String monthName = yearMonth.getMonth().getDisplayName(TextStyle.FULL, new java.util.Locale("es", "ES")); + monthName = monthName.substring(0, 1).toUpperCase() + monthName.substring(1); + return MonthlyCalendarDTO.builder() + .year(year) + .month(month) + .monthName(monthName) + .firstDayOfMonth(firstDay) + .lastDayOfMonth(lastDay) + .classesByDate(classesByDate) + .totalClasses(allClasses.size()) + .daysWithClasses(daysWithClasses) .build(); } - - @Override - @Transactional(readOnly = true) - public List getClassesForCalendar(LocalDate startDate, LocalDate endDate) { - log.info("Obteniendo clases para calendario entre {} y {}", startDate, endDate); - - List classes = repository.findByClassDateBetween(startDate, endDate); - return mapToCalendarDTOs(classes); - } - - @Override - @Transactional(readOnly = true) - public List getUpcomingClasses() { - log.info("Obteniendo próximas clases"); - - List classes = repository.findUpcomingClasses(LocalDate.now()); - return mapToCalendarDTOs(classes); - } - private List mapToCalendarDTOs(List classes) { - return classes.stream().map(classEntity -> { - CalendarClassDTO dto = classStatsMapper.toCalendarDTO(classEntity); - long currentStudents = reservationRepository.countByClassEntityIdAndStatus( - classEntity.getId(), ReservationStatus.RESERVADO); - dto.setCurrentStudents((int) currentStudents); - String action = determineAction(classEntity, (int) currentStudents); - dto.setAction(action); - return dto; - }).collect(Collectors.toList()); - } - - private String determineClassStatus(ClassEntity classEntity, int currentStudents) { - if (!classEntity.isActive()) { - return "Cancelada"; - } - if (currentStudents >= classEntity.getMaxCapacity()) { - return "Llena"; - } - return "Activa"; - } - - private String determineAction(ClassEntity classEntity, int currentStudents) { - if (!classEntity.isActive()) { - return "Cancelada"; - } - if (currentStudents >= classEntity.getMaxCapacity()) { - return "Llena"; - } - if (currentStudents >= classEntity.getMaxCapacity() * 0.9) { - return "Lista de espera"; - } - return "Reservar"; - } - - - private MemberInfoDTO createDefaultMemberInfo(UUID memberId) { - return MemberInfoDTO.builder() - .id(memberId) - .firstName("Usuario") - .lastName("Desconocido") - .email("no-disponible@email.com") - .status("DESCONOCIDO") - .membershipType("N/A") - .build(); - } - - private String getInitials(String firstName, String lastName) { - String first = firstName != null && !firstName.isEmpty() ? firstName.substring(0, 1) : ""; - String last = lastName != null && !lastName.isEmpty() ? lastName.substring(0, 1) : ""; - return (first + last).toUpperCase(); - } } diff --git a/src/main/java/com/classes/services/Impl/CloudinaryServiceImpl.java b/src/main/java/com/classes/services/Impl/CloudinaryServiceImpl.java index 9cf5a99..f3ec08f 100644 --- a/src/main/java/com/classes/services/Impl/CloudinaryServiceImpl.java +++ b/src/main/java/com/classes/services/Impl/CloudinaryServiceImpl.java @@ -26,7 +26,7 @@ public class CloudinaryServiceImpl implements CloudinaryService { private final Cloudinary cloudinary; - private static final String TRAINERS_FOLDER = "fitdesk/classes/profile"; + private static final String TRAINERS_FOLDER = "fitdesk/classes/trainerprofile"; private static final Set ALLOWED_EXTENSIONS = Set.of("jpg","jpeg","png","webp"); private static final long MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB