package dev.lions.unionflow.server.service; import dev.lions.unionflow.server.entity.Evenement; import dev.lions.unionflow.server.entity.FeedbackEvenement; import dev.lions.unionflow.server.entity.InscriptionEvenement; import dev.lions.unionflow.server.entity.Membre; import dev.lions.unionflow.server.repository.EvenementRepository; import dev.lions.unionflow.server.repository.FeedbackEvenementRepository; import dev.lions.unionflow.server.repository.InscriptionEvenementRepository; import dev.lions.unionflow.server.repository.MembreRepository; import dev.lions.unionflow.server.repository.OrganisationRepository; import dev.lions.unionflow.server.service.KeycloakService; import io.quarkus.panache.common.Page; import io.quarkus.panache.common.Sort; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.transaction.Transactional; import java.time.LocalDateTime; import jakarta.ws.rs.NotFoundException; import org.hibernate.Hibernate; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import org.jboss.logging.Logger; /** * Service métier pour la gestion des événements Version simplifiée pour tester * les imports et * Lombok * * @author UnionFlow Team * @version 1.0 * @since 2025-01-15 */ @ApplicationScoped public class EvenementService { private static final Logger LOG = Logger.getLogger(EvenementService.class); @Inject EvenementRepository evenementRepository; @Inject MembreRepository membreRepository; @Inject OrganisationRepository organisationRepository; @Inject KeycloakService keycloakService; @Inject InscriptionEvenementRepository inscriptionRepository; @Inject FeedbackEvenementRepository feedbackRepository; /** * Crée un nouvel événement * * @param evenement l'événement à créer * @return l'événement créé * @throws IllegalArgumentException si les données sont invalides */ @Transactional public Evenement creerEvenement(Evenement evenement) { LOG.infof("Création événement: %s", evenement.getTitre()); // Validation des données validerEvenement(evenement); // Vérifier l'unicité du titre dans l'organisation if (evenement.getOrganisation() != null) { Optional existant = evenementRepository.findByTitre(evenement.getTitre()); if (existant.isPresent() && existant.get().getOrganisation().getId().equals(evenement.getOrganisation().getId())) { throw new IllegalArgumentException( "Un événement avec ce titre existe déjà dans cette organisation"); } } // Métadonnées de création evenement.setCreePar(keycloakService.getCurrentUserEmail()); // Valeurs par défaut if (evenement.getStatut() == null) { evenement.setStatut("PLANIFIE"); } if (evenement.getActif() == null) { evenement.setActif(true); } if (evenement.getVisiblePublic() == null) { evenement.setVisiblePublic(true); } if (evenement.getInscriptionRequise() == null) { evenement.setInscriptionRequise(true); } evenementRepository.persist(evenement); LOG.infof("Événement créé avec succès: ID=%s, Titre=%s", evenement.getId(), evenement.getTitre()); return evenement; } /** * Met à jour un événement existant * * @param id l'UUID de l'événement * @param evenementMisAJour les nouvelles données * @return l'événement mis à jour * @throws IllegalArgumentException si l'événement n'existe pas */ @Transactional public Evenement mettreAJourEvenement(UUID id, Evenement evenementMisAJour) { LOG.infof("Mise à jour événement ID: %s", id); Evenement evenementExistant = evenementRepository .findByIdOptional(id) .orElseThrow( () -> new NotFoundException("Événement non trouvé avec l'ID: " + id)); // Vérifier les permissions if (!peutModifierEvenement(evenementExistant)) { throw new SecurityException("Vous n'avez pas les permissions pour modifier cet événement"); } // Validation des nouvelles données validerEvenement(evenementMisAJour); // Mise à jour des champs evenementExistant.setTitre(evenementMisAJour.getTitre()); evenementExistant.setDescription(evenementMisAJour.getDescription()); evenementExistant.setDateDebut(evenementMisAJour.getDateDebut()); evenementExistant.setDateFin(evenementMisAJour.getDateFin()); evenementExistant.setLieu(evenementMisAJour.getLieu()); evenementExistant.setAdresse(evenementMisAJour.getAdresse()); evenementExistant.setTypeEvenement(evenementMisAJour.getTypeEvenement()); evenementExistant.setCapaciteMax(evenementMisAJour.getCapaciteMax()); evenementExistant.setPrix(evenementMisAJour.getPrix()); evenementExistant.setInscriptionRequise(evenementMisAJour.getInscriptionRequise()); evenementExistant.setDateLimiteInscription(evenementMisAJour.getDateLimiteInscription()); evenementExistant.setInstructionsParticulieres( evenementMisAJour.getInstructionsParticulieres()); evenementExistant.setContactOrganisateur(evenementMisAJour.getContactOrganisateur()); evenementExistant.setMaterielRequis(evenementMisAJour.getMaterielRequis()); evenementExistant.setVisiblePublic(evenementMisAJour.getVisiblePublic()); if (evenementMisAJour.getStatut() != null) { evenementExistant.setStatut(evenementMisAJour.getStatut()); } // Métadonnées de modification evenementExistant.setModifiePar(keycloakService.getCurrentUserEmail()); evenementRepository.update(evenementExistant); LOG.infof("Événement mis à jour avec succès: ID=%s", id); // Initialiser les relations lazy pour éviter LazyInitializationException lors // de la sérialisation JSON Hibernate.initialize(evenementExistant.getOrganisation()); Hibernate.initialize(evenementExistant.getOrganisateur()); return evenementExistant; } /** Trouve un événement par ID */ public Optional trouverParId(UUID id) { return evenementRepository.findByIdOptional(id); } /** Liste tous les événements actifs avec pagination */ public List listerEvenementsActifs(Page page, Sort sort) { return evenementRepository.findAllActifs(page, sort); } /** Liste les événements à venir */ public List listerEvenementsAVenir(Page page, Sort sort) { return evenementRepository.findEvenementsAVenir(page, sort); } /** Liste les événements publics */ public List listerEvenementsPublics(Page page, Sort sort) { return evenementRepository.findEvenementsPublics(page, sort); } /** Recherche d'événements par terme */ public List rechercherEvenements(String terme, Page page, Sort sort) { return evenementRepository.rechercheAvancee( terme, null, null, null, null, null, null, null, null, null, page, sort); } /** Liste les événements par type */ public List listerParType(String type, Page page, Sort sort) { return evenementRepository.findByType(type, page, sort); } /** * Supprime logiquement un événement * * @param id l'UUID de l'événement à supprimer * @throws IllegalArgumentException si l'événement n'existe pas */ @Transactional public void supprimerEvenement(UUID id) { LOG.infof("Suppression événement ID: %s", id); Evenement evenement = evenementRepository .findByIdOptional(id) .orElseThrow( () -> new NotFoundException("Événement non trouvé avec l'ID: " + id)); // Vérifier les permissions if (!peutModifierEvenement(evenement)) { throw new SecurityException("Vous n'avez pas les permissions pour supprimer cet événement"); } // Vérifier s'il y a des inscriptions if (evenement.getNombreInscrits() > 0) { throw new IllegalStateException("Impossible de supprimer un événement avec des inscriptions"); } // Suppression logique evenement.setActif(false); evenement.setModifiePar(keycloakService.getCurrentUserEmail()); evenementRepository.update(evenement); LOG.infof("Événement supprimé avec succès: ID=%s", id); } /** * Change le statut d'un événement * * @param id l'UUID de l'événement * @param nouveauStatut le nouveau statut * @return l'événement mis à jour */ @Transactional public Evenement changerStatut(UUID id, String nouveauStatut) { LOG.infof("Changement statut événement ID: %s vers %s", id, nouveauStatut); Evenement evenement = evenementRepository .findByIdOptional(id) .orElseThrow( () -> new NotFoundException("Événement non trouvé avec l'ID: " + id)); // Vérifier les permissions if (!peutModifierEvenement(evenement)) { throw new SecurityException("Vous n'avez pas les permissions pour modifier cet événement"); } // Valider le changement de statut validerChangementStatut(evenement.getStatut(), nouveauStatut); evenement.setStatut(nouveauStatut); evenement.setModifiePar(keycloakService.getCurrentUserEmail()); evenementRepository.update(evenement); LOG.infof("Statut événement changé avec succès: ID=%s, Nouveau statut=%s", id, nouveauStatut); return evenement; } /** * Compte le nombre total d'événements * * @return le nombre total d'événements */ public long countEvenements() { return evenementRepository.count(); } /** * Compte le nombre d'événements actifs * * @return le nombre d'événements actifs */ public long countEvenementsActifs() { return evenementRepository.countActifs(); } /** * Obtient les statistiques des événements * * @return les statistiques sous forme de Map */ public Map obtenirStatistiques() { Map statsBase = evenementRepository.getStatistiques(); long total = statsBase.getOrDefault("total", 0L); long actifs = statsBase.getOrDefault("actifs", 0L); long aVenir = statsBase.getOrDefault("aVenir", 0L); long enCours = statsBase.getOrDefault("enCours", 0L); Map result = new java.util.HashMap<>(); result.put("total", total); result.put("actifs", actifs); result.put("aVenir", aVenir); result.put("enCours", enCours); result.put("passes", statsBase.getOrDefault("passes", 0L)); result.put("publics", statsBase.getOrDefault("publics", 0L)); result.put("avecInscription", statsBase.getOrDefault("avecInscription", 0L)); result.put("tauxActivite", total > 0 ? (actifs * 100.0 / total) : 0.0); result.put("tauxEvenementsAVenir", total > 0 ? (aVenir * 100.0 / total) : 0.0); result.put("tauxEvenementsEnCours", total > 0 ? (enCours * 100.0 / total) : 0.0); result.put("timestamp", LocalDateTime.now()); return result; } // Méthodes privées de validation et permissions /** Valide les données d'un événement */ private void validerEvenement(Evenement evenement) { if (evenement.getTitre() == null || evenement.getTitre().trim().isEmpty()) { throw new IllegalArgumentException("Le titre de l'événement est obligatoire"); } if (evenement.getDateDebut() == null) { throw new IllegalArgumentException("La date de début est obligatoire"); } if (evenement.getDateDebut().isBefore(LocalDateTime.now().minusHours(1))) { throw new IllegalArgumentException("La date de début ne peut pas être dans le passé"); } if (evenement.getDateFin() != null && evenement.getDateFin().isBefore(evenement.getDateDebut())) { throw new IllegalArgumentException( "La date de fin ne peut pas être antérieure à la date de début"); } if (evenement.getCapaciteMax() != null && evenement.getCapaciteMax() <= 0) { throw new IllegalArgumentException("La capacité maximale doit être positive"); } if (evenement.getPrix() != null && evenement.getPrix().compareTo(java.math.BigDecimal.ZERO) < 0) { throw new IllegalArgumentException("Le prix ne peut pas être négatif"); } } /** Valide un changement de statut */ private void validerChangementStatut( String statutActuel, String nouveauStatut) { // Règles de transition simplifiées pour la version mobile if ("TERMINE".equals(statutActuel) || "ANNULE".equals(statutActuel)) { throw new IllegalArgumentException( "Impossible de changer le statut d'un événement terminé ou annulé"); } } /** Vérifie les permissions de modification pour l'application mobile */ private boolean peutModifierEvenement(Evenement evenement) { if (keycloakService.hasRole("ADMIN") || keycloakService.hasRole("ORGANISATEUR_EVENEMENT")) { return true; } String utilisateurActuel = keycloakService.getCurrentUserEmail(); return utilisateurActuel != null && utilisateurActuel.equals(evenement.getCreePar()); } /** * Indique si l'utilisateur connecté est inscrit à l'événement. * Utilisé par l'app mobile pour afficher le statut d'inscription sur la page détail. */ public boolean isUserInscrit(UUID evenementId) { Evenement evenement = evenementRepository.findByIdOptional(evenementId).orElse(null); if (evenement == null) { return false; } String email = keycloakService.getCurrentUserEmail(); if (email == null || email.isBlank()) { return false; } return membreRepository.findByEmail(email) .map(m -> evenement.isMemberInscrit(m.getId())) .orElse(false); } // === GESTION DES INSCRIPTIONS === /** * Inscrit l'utilisateur connecté à un événement * * @param evenementId UUID de l'événement * @return L'inscription créée */ @Transactional public InscriptionEvenement inscrireEvenement(UUID evenementId) { String email = keycloakService.getCurrentUserEmail(); if (email == null || email.isBlank()) { throw new IllegalStateException("Utilisateur non authentifié"); } Membre membre = membreRepository .findByEmail(email) .orElseThrow(() -> new NotFoundException("Membre non trouvé")); Evenement evenement = evenementRepository .findByIdOptional(evenementId) .orElseThrow(() -> new NotFoundException("Événement non trouvé")); // Vérifier si déjà inscrit Optional existante = inscriptionRepository.findByMembreAndEvenement(membre.getId(), evenementId); if (existante.isPresent()) { throw new IllegalStateException("Vous êtes déjà inscrit à cet événement"); } // Vérifier capacité if (evenement.getCapaciteMax() != null) { long nbInscrits = inscriptionRepository.countConfirmeesByEvenement(evenementId); if (nbInscrits >= evenement.getCapaciteMax()) { throw new IllegalStateException("L'événement est complet"); } } InscriptionEvenement inscription = InscriptionEvenement.builder() .membre(membre) .evenement(evenement) .statut(InscriptionEvenement.StatutInscription.CONFIRMEE.name()) .dateInscription(LocalDateTime.now()) .build(); inscriptionRepository.persist(inscription); LOG.infof( "Inscription créée: membre=%s, événement=%s", membre.getEmail(), evenement.getTitre()); return inscription; } /** * Désinscrit l'utilisateur connecté d'un événement * * @param evenementId UUID de l'événement */ @Transactional public void desinscrireEvenement(UUID evenementId) { String email = keycloakService.getCurrentUserEmail(); if (email == null || email.isBlank()) { throw new IllegalStateException("Utilisateur non authentifié"); } Membre membre = membreRepository .findByEmail(email) .orElseThrow(() -> new NotFoundException("Membre non trouvé")); InscriptionEvenement inscription = inscriptionRepository .findByMembreAndEvenement(membre.getId(), evenementId) .orElseThrow(() -> new NotFoundException("Inscription non trouvée")); inscriptionRepository.softDelete(inscription); LOG.infof("Désinscription: membre=%s, événement=%s", membre.getEmail(), evenementId); } /** * Liste les participants d'un événement * * @param evenementId UUID de l'événement * @return Liste des inscriptions confirmées */ public List getParticipants(UUID evenementId) { return inscriptionRepository.findConfirmeesByEvenement(evenementId); } /** * Liste les inscriptions de l'utilisateur connecté * * @return Liste des inscriptions du membre */ public List getMesInscriptions() { String email = keycloakService.getCurrentUserEmail(); if (email == null || email.isBlank()) { throw new IllegalStateException("Utilisateur non authentifié"); } Membre membre = membreRepository .findByEmail(email) .orElseThrow(() -> new NotFoundException("Membre non trouvé")); return inscriptionRepository.findByMembre(membre.getId()); } // === GESTION DES FEEDBACKS === /** * Soumet un feedback pour un événement * * @param evenementId UUID de l'événement * @param note Note de 1 à 5 * @param commentaire Commentaire optionnel * @return Le feedback créé */ @Transactional public FeedbackEvenement soumetteFeedback( UUID evenementId, Integer note, String commentaire) { String email = keycloakService.getCurrentUserEmail(); if (email == null || email.isBlank()) { throw new IllegalStateException("Utilisateur non authentifié"); } Membre membre = membreRepository .findByEmail(email) .orElseThrow(() -> new NotFoundException("Membre non trouvé")); Evenement evenement = evenementRepository .findByIdOptional(evenementId) .orElseThrow(() -> new NotFoundException("Événement non trouvé")); // Vérifier si déjà soumis Optional existant = feedbackRepository.findByMembreAndEvenement(membre.getId(), evenementId); if (existant.isPresent()) { throw new IllegalStateException("Vous avez déjà soumis un feedback pour cet événement"); } // Vérifier que le membre était inscrit boolean etaitInscrit = inscriptionRepository.isMembreInscrit(membre.getId(), evenementId); if (!etaitInscrit) { throw new IllegalStateException( "Seuls les participants peuvent donner un feedback"); } // Vérifier que l'événement est terminé if (evenement.getDateFin() == null || evenement.getDateFin().isAfter(LocalDateTime.now())) { throw new IllegalStateException( "Vous ne pouvez donner un feedback qu'après la fin de l'événement"); } FeedbackEvenement feedback = FeedbackEvenement.builder() .membre(membre) .evenement(evenement) .note(note) .commentaire(commentaire) .dateFeedback(LocalDateTime.now()) .moderationStatut(FeedbackEvenement.ModerationStatut.PUBLIE.name()) .build(); feedbackRepository.persist(feedback); LOG.infof( "Feedback créé: membre=%s, événement=%s, note=%d", membre.getEmail(), evenement.getTitre(), note); return feedback; } /** * Liste les feedbacks d'un événement * * @param evenementId UUID de l'événement * @return Liste des feedbacks publiés */ public List getFeedbacks(UUID evenementId) { return feedbackRepository.findPubliesByEvenement(evenementId); } /** * Calcule les statistiques de feedback pour un événement * * @param evenementId UUID de l'événement * @return Map contenant noteMovenne et nombreFeedbacks */ public Map getStatistiquesFeedback(UUID evenementId) { Double noteMoyenne = feedbackRepository.calculateAverageNote(evenementId); long nombreFeedbacks = feedbackRepository.countPubliesByEvenement(evenementId); return Map.of( "noteMoyenne", noteMoyenne, "nombreFeedbacks", nombreFeedbacks); } }