fix(backend): Correction du système de réactions (favoris) pour les événements

This commit is contained in:
dahoud
2026-01-19 22:44:14 +00:00
parent a5fd9538fe
commit 7dd0969799
4 changed files with 240 additions and 46 deletions

View File

@@ -1,10 +1,16 @@
package com.lions.dev.dto.response.events; package com.lions.dev.dto.response.events;
import com.lions.dev.entity.events.Events; import com.lions.dev.entity.events.Events;
import com.lions.dev.repository.UsersRepository;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.UUID;
/** /**
* DTO pour renvoyer les informations d'un événement. * DTO pour renvoyer les informations d'un événement.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Ce DTO est utilisé pour structurer les données retournées dans les réponses * Ce DTO est utilisé pour structurer les données retournées dans les réponses
* après les opérations sur les événements (création, récupération). * après les opérations sur les événements (création, récupération).
*/ */
@@ -16,35 +22,89 @@ public class EventCreateResponseDTO {
private String description; // Description de l'événement private String description; // Description de l'événement
private LocalDateTime startDate; // Date de début de l'événement private LocalDateTime startDate; // Date de début de l'événement
private LocalDateTime endDate; // Date de fin de l'événement private LocalDateTime endDate; // Date de fin de l'événement
private String location; // Lieu de l'événement private String establishmentId; // v2.0 - ID de l'établissement où se déroule l'événement
private String establishmentName; // v2.0 - Nom de l'établissement
private String category; // Catégorie de l'événement private String category; // Catégorie de l'événement
private String link; // Lien vers plus d'informations private String link; // Lien vers plus d'informations
private String imageUrl; // URL d'une image pour l'événement private String imageUrl; // URL d'une image pour l'événement
private String creatorId; // ID du créateur de l'événement private String creatorId; // ID du créateur de l'événement
private String creatorEmail; // Email du créateur de l'événement private String creatorEmail; // Email du créateur de l'événement
private String creatorFirstName; // Prénom du créateur de l'événement private String creatorFirstName; // v2.0 - Prénom du créateur de l'événement
private String creatorLastName; // Nom de famille du création de l'événement private String creatorLastName; // v2.0 - Nom de famille du créateur de l'événement
private String status; // Statut de l'événement private String status; // Statut de l'événement (OPEN, CLOSED, CANCELLED, COMPLETED)
private Boolean isPrivate; // v2.0 - Indique si l'événement est privé
private Boolean waitlistEnabled; // v2.0 - Indique si la liste d'attente est activée
private Integer maxParticipants; // Nombre maximum de participants autorisés
private Integer participationFee; // Frais de participation en centimes
private Long reactionsCount; // ✅ Nombre de réactions (utilisateurs qui ont cet événement en favori)
private Boolean isFavorite; // ✅ Indique si l'utilisateur actuel a cet événement en favori (optionnel, dépend du contexte)
// Champ déprécié (v1.0) - conservé pour compatibilité
/**
* @deprecated Utiliser {@link #establishmentId} et {@link #establishmentName} à la place.
*/
@Deprecated
private String location;
/** /**
* Constructeur qui transforme une entité Events en DTO. * Constructeur qui transforme une entité Events en DTO (v2.0).
* Utilise UsersRepository pour calculer reactionsCount et isFavorite.
* *
* @param event L'événement à convertir en DTO. * @param event L'événement à convertir en DTO.
* @param usersRepository Le repository pour compter les réactions (peut être null).
* @param currentUserId L'ID de l'utilisateur actuel pour vérifier isFavorite (peut être null).
*/ */
public EventCreateResponseDTO(Events event) { public EventCreateResponseDTO(Events event, UsersRepository usersRepository, UUID currentUserId) {
this.id = event.getId().toString(); this.id = event.getId().toString();
this.title = event.getTitle(); this.title = event.getTitle();
this.description = event.getDescription(); this.description = event.getDescription();
this.startDate = event.getStartDate(); this.startDate = event.getStartDate();
this.endDate = event.getEndDate(); this.endDate = event.getEndDate();
this.location = event.getLocation();
this.category = event.getCategory(); this.category = event.getCategory();
this.link = event.getLink(); this.link = event.getLink();
this.imageUrl = event.getImageUrl(); this.imageUrl = event.getImageUrl();
this.creatorId = event.getCreator().getId().toString();
this.creatorEmail = event.getCreator().getEmail();
this.creatorFirstName = event.getCreator().getPrenoms();
this.creatorLastName = event.getCreator().getNom();
this.status = event.getStatus(); this.status = event.getStatus();
this.isPrivate = event.getIsPrivate(); // v2.0
this.waitlistEnabled = event.getWaitlistEnabled(); // v2.0
this.maxParticipants = event.getMaxParticipants();
this.participationFee = event.getParticipationFee();
// ✅ Calculer reactionsCount si usersRepository est fourni
if (usersRepository != null) {
this.reactionsCount = usersRepository.countUsersWithFavoriteEvent(event.getId());
} else {
this.reactionsCount = 0L;
}
// ✅ Vérifier isFavorite si currentUserId est fourni
if (currentUserId != null && usersRepository != null) {
this.isFavorite = usersRepository.hasUserFavoriteEvent(currentUserId, event.getId());
} else {
this.isFavorite = null;
}
// v2.0 - Informations sur l'établissement
if (event.getEstablishment() != null) {
this.establishmentId = event.getEstablishment().getId().toString();
this.establishmentName = event.getEstablishment().getName();
this.location = event.getLocation(); // Méthode qui retourne l'adresse de l'établissement
}
// v2.0 - Informations sur le créateur
if (event.getCreator() != null) {
this.creatorId = event.getCreator().getId().toString();
this.creatorEmail = event.getCreator().getEmail();
this.creatorFirstName = event.getCreator().getFirstName(); // v2.0
this.creatorLastName = event.getCreator().getLastName(); // v2.0
}
}
/**
* Constructeur simplifié sans calcul de réactions (pour compatibilité).
*
* @param event L'événement à convertir en DTO.
*/
public EventCreateResponseDTO(Events event) {
this(event, null, null);
} }
} }

View File

@@ -3,6 +3,8 @@ package com.lions.dev.entity.users;
import com.lions.dev.entity.BaseEntity; import com.lions.dev.entity.BaseEntity;
import com.lions.dev.entity.events.Events; import com.lions.dev.entity.events.Events;
import jakarta.persistence.*; import jakarta.persistence.*;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import lombok.Getter; import lombok.Getter;
@@ -13,6 +15,10 @@ import at.favre.lib.crypto.bcrypt.BCrypt;
/** /**
* Représentation de l'entité Utilisateur dans le système AfterWork. * Représentation de l'entité Utilisateur dans le système AfterWork.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Cette entité contient les informations de base sur un utilisateur, telles que le nom, * Cette entité contient les informations de base sur un utilisateur, telles que le nom,
* les prénoms, l'email, le mot de passe haché, et son rôle. * les prénoms, l'email, le mot de passe haché, et son rôle.
* *
@@ -26,17 +32,17 @@ import at.favre.lib.crypto.bcrypt.BCrypt;
@ToString @ToString
public class Users extends BaseEntity { public class Users extends BaseEntity {
@Column(name = "nom", nullable = false, length = 100) @Column(name = "first_name", nullable = false, length = 100)
private String nom; // Le nom de l'utilisateur private String firstName; // Le prénom de l'utilisateur (v2.0)
@Column(name = "prenoms", nullable = false, length = 100) @Column(name = "last_name", nullable = false, length = 100)
private String prenoms; // Les prénoms de l'utilisateur private String lastName; // Le nom de famille de l'utilisateur (v2.0)
@Column(name = "email", nullable = false, unique = true, length = 100) @Column(name = "email", nullable = false, unique = true, length = 100)
private String email; // L'adresse email unique de l'utilisateur private String email; // L'adresse email unique de l'utilisateur
@Column(name = "mot_de_passe", nullable = false) @Column(name = "password_hash", nullable = false)
private String motDePasse; // Mot de passe haché avec BCrypt private String passwordHash; // Mot de passe haché avec BCrypt (v2.0)
@Column(name = "role", nullable = false) @Column(name = "role", nullable = false)
private String role; // Le rôle de l'utilisateur (ADMIN, MODERATOR, USER, etc.) private String role; // Le rôle de l'utilisateur (ADMIN, MODERATOR, USER, etc.)
@@ -44,8 +50,15 @@ public class Users extends BaseEntity {
@Column(name = "profile_image_url") @Column(name = "profile_image_url")
private String profileImageUrl; // L'URL de l'image de profil de l'utilisateur private String profileImageUrl; // L'URL de l'image de profil de l'utilisateur
@Column(name = "preferred_category") @Column(name = "bio", length = 500)
private String preferredCategory; // La catégorie préférée de l'utilisateur private String bio; // Biographie courte de l'utilisateur (v2.0)
@Column(name = "loyalty_points", nullable = false)
private Integer loyaltyPoints = 0; // Points de fidélité accumulés (v2.0)
@Column(name = "preferences", nullable = false)
@org.hibernate.annotations.JdbcTypeCode(org.hibernate.type.SqlTypes.JSON)
private java.util.Map<String, Object> preferences = new java.util.HashMap<>(); // Préférences utilisateur en JSON (v2.0)
@Column(name = "is_verified", nullable = false) @Column(name = "is_verified", nullable = false)
private boolean isVerified = false; // Indique si l'utilisateur est vérifié (compte officiel) private boolean isVerified = false; // Indique si l'utilisateur est vérifié (compte officiel)
@@ -60,28 +73,57 @@ public class Users extends BaseEntity {
// private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); // private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
/** /**
* Hache le mot de passe avec BCrypt et le stocke dans l'attribut `motDePasse`. * Hache le mot de passe avec BCrypt et le stocke dans l'attribut `passwordHash`.
* Version 2.0 - Utilise passwordHash au lieu de motDePasse.
* *
* @param motDePasse Le mot de passe en texte clair à hacher. * @param password Le mot de passe en texte clair à hacher.
*/ */
public void setMotDePasse(String motDePasse) { public void setPassword(String password) {
this.motDePasse = BCrypt.withDefaults().hashToString(12, motDePasse.toCharArray()); this.passwordHash = BCrypt.withDefaults().hashToString(12, password.toCharArray());
System.out.println("[LOG] Mot de passe haché pour l'utilisateur : " + this.email); System.out.println("[LOG] Mot de passe haché pour l'utilisateur : " + this.email);
} }
/** /**
* Vérifie que le mot de passe fourni correspond au mot de passe haché de l'utilisateur. * Définit directement le hash du mot de passe (pour compatibilité).
* *
* @param motDePasse Le mot de passe en texte clair à vérifier. * @param passwordHash Le hash du mot de passe.
*/
public void setPasswordHash(String passwordHash) {
this.passwordHash = passwordHash;
}
/**
* Vérifie que le mot de passe fourni correspond au mot de passe haché de l'utilisateur.
* Version 2.0 - Utilise passwordHash au lieu de motDePasse.
*
* @param password Le mot de passe en texte clair à vérifier.
* @return true si le mot de passe est correct, false sinon. * @return true si le mot de passe est correct, false sinon.
*/ */
public boolean verifierMotDePasse(String motDePasse) { public boolean verifyPassword(String password) {
BCrypt.Result result = BCrypt.verifyer().verify(motDePasse.toCharArray(), this.motDePasse); BCrypt.Result result = BCrypt.verifyer().verify(password.toCharArray(), this.passwordHash);
boolean isValid = result.verified; boolean isValid = result.verified;
System.out.println("[LOG] Vérification du mot de passe pour l'utilisateur : " + this.email + " - Résultat : " + isValid); System.out.println("[LOG] Vérification du mot de passe pour l'utilisateur : " + this.email + " - Résultat : " + isValid);
return isValid; return isValid;
} }
/**
* Méthode de compatibilité avec l'ancienne API (dépréciée).
* @deprecated Utiliser {@link #setPassword(String)} à la place.
*/
@Deprecated
public void setMotDePasse(String motDePasse) {
setPassword(motDePasse);
}
/**
* Méthode de compatibilité avec l'ancienne API (dépréciée).
* @deprecated Utiliser {@link #verifyPassword(String)} à la place.
*/
@Deprecated
public boolean verifierMotDePasse(String motDePasse) {
return verifyPassword(motDePasse);
}
/** /**
* Vérifie si l'utilisateur a le rôle d'administrateur. * Vérifie si l'utilisateur a le rôle d'administrateur.
* *
@@ -93,8 +135,12 @@ public class Users extends BaseEntity {
return isAdmin; return isAdmin;
} }
@OneToMany(fetch = FetchType.LAZY) @ManyToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "favorite_events") @JoinTable(
name = "user_favorite_events",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "event_id")
)
private Set<Events> favoriteEvents = new HashSet<>(); // Liste des événements favoris private Set<Events> favoriteEvents = new HashSet<>(); // Liste des événements favoris
/** /**
@@ -107,6 +153,26 @@ public class Users extends BaseEntity {
System.out.println("[LOG] Événement ajouté aux favoris pour l'utilisateur : " + this.email); System.out.println("[LOG] Événement ajouté aux favoris pour l'utilisateur : " + this.email);
} }
/**
* Retire un événement des favoris de l'utilisateur.
*
* @param event L'événement à retirer.
*/
public void removeFavoriteEvent(Events event) {
favoriteEvents.remove(event);
System.out.println("[LOG] Événement retiré des favoris pour l'utilisateur : " + this.email);
}
/**
* Vérifie si un événement est dans les favoris de l'utilisateur.
*
* @param event L'événement à vérifier.
* @return true si l'événement est favori, false sinon.
*/
public boolean hasFavoriteEvent(Events event) {
return favoriteEvents.contains(event);
}
/** /**
* Retourne la liste des événements favoris de l'utilisateur. * Retourne la liste des événements favoris de l'utilisateur.
* *
@@ -118,25 +184,44 @@ public class Users extends BaseEntity {
} }
/** /**
* Retourne la catégorie préférée de l'utilisateur. * Retourne la catégorie préférée de l'utilisateur depuis preferences (v2.0).
* *
* @return La catégorie préférée de l'utilisateur. * @return La catégorie préférée de l'utilisateur, ou null si non définie.
*/ */
public String getPreferredCategory() { public String getPreferredCategory() {
System.out.println("[LOG] Récupération de la catégorie préférée pour l'utilisateur : " + this.email); if (preferences != null && preferences.containsKey("preferred_category")) {
return preferredCategory; Object category = preferences.get("preferred_category");
return category != null ? category.toString() : null;
}
return null;
} }
/** /**
* Définit la catégorie préférée de l'utilisateur. * Définit la catégorie préférée de l'utilisateur dans preferences (v2.0).
* *
* @param category La catégorie à définir. * @param category La catégorie à définir.
*/ */
public void setPreferredCategory(String category) { public void setPreferredCategory(String category) {
this.preferredCategory = category; if (preferences == null) {
preferences = new java.util.HashMap<>();
}
if (category != null) {
preferences.put("preferred_category", category);
} else {
preferences.remove("preferred_category");
}
System.out.println("[LOG] Catégorie préférée définie pour l'utilisateur : " + this.email + " - Catégorie : " + category); System.out.println("[LOG] Catégorie préférée définie pour l'utilisateur : " + this.email + " - Catégorie : " + category);
} }
/**
* Retourne le nom complet de l'utilisateur (v2.0).
*
* @return Le nom complet (firstName + lastName).
*/
public String getFullName() {
return (firstName != null ? firstName : "") + " " + (lastName != null ? lastName : "").trim();
}
/** /**
* Met à jour la présence de l'utilisateur (marque comme en ligne et met à jour lastSeen). * Met à jour la présence de l'utilisateur (marque comme en ligne et met à jour lastSeen).
*/ */

View File

@@ -1,8 +1,11 @@
package com.lions.dev.repository; package com.lions.dev.repository;
import com.lions.dev.entity.events.Events;
import com.lions.dev.entity.users.Users; import com.lions.dev.entity.users.Users;
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@@ -15,6 +18,43 @@ import java.util.UUID;
@ApplicationScoped @ApplicationScoped
public class UsersRepository implements PanacheRepositoryBase<Users, UUID> { public class UsersRepository implements PanacheRepositoryBase<Users, UUID> {
@PersistenceContext
EntityManager entityManager;
/**
* Compte le nombre d'utilisateurs qui ont un événement en favori.
*
* @param eventId L'ID de l'événement
* @return Le nombre d'utilisateurs qui ont cet événement en favori
*/
public long countUsersWithFavoriteEvent(UUID eventId) {
// ✅ Utiliser la table de jointure user_favorite_events
return entityManager.createQuery(
"SELECT COUNT(u) FROM Users u JOIN u.favoriteEvents e WHERE e.id = :eventId",
Long.class
)
.setParameter("eventId", eventId)
.getSingleResult();
}
/**
* Vérifie si un utilisateur a un événement en favori.
*
* @param userId L'ID de l'utilisateur
* @param eventId L'ID de l'événement
* @return true si l'utilisateur a cet événement en favori, false sinon
*/
public boolean hasUserFavoriteEvent(UUID userId, UUID eventId) {
Long count = entityManager.createQuery(
"SELECT COUNT(u) FROM Users u JOIN u.favoriteEvents e WHERE u.id = :userId AND e.id = :eventId",
Long.class
)
.setParameter("userId", userId)
.setParameter("eventId", eventId)
.getSingleResult();
return count > 0;
}
/** /**
* Recherche un utilisateur par son adresse email. * Recherche un utilisateur par son adresse email.
* *

View File

@@ -214,7 +214,6 @@ public class EventsResource {
event.setStartDate(eventUpdateRequestDTO.getStartDate()); event.setStartDate(eventUpdateRequestDTO.getStartDate());
event.setEndDate(eventUpdateRequestDTO.getEndDate()); event.setEndDate(eventUpdateRequestDTO.getEndDate());
event.setDescription(eventUpdateRequestDTO.getDescription()); event.setDescription(eventUpdateRequestDTO.getDescription());
event.setLocation(eventUpdateRequestDTO.getLocation());
event.setCategory(eventUpdateRequestDTO.getCategory()); event.setCategory(eventUpdateRequestDTO.getCategory());
event.setLink(eventUpdateRequestDTO.getLink()); event.setLink(eventUpdateRequestDTO.getLink());
event.setImageUrl(eventUpdateRequestDTO.getImageUrl()); event.setImageUrl(eventUpdateRequestDTO.getImageUrl());
@@ -252,9 +251,9 @@ public class EventsResource {
List<Events> events = eventsRepository.find("creator.id IN ?1", friendIds).list(); List<Events> events = eventsRepository.find("creator.id IN ?1", friendIds).list();
LOG.info("[LOG] Nombre d'événements récupérés dans la requête : " + events.size()); LOG.info("[LOG] Nombre d'événements récupérés dans la requête : " + events.size());
// Retourner une liste vide si aucun événement trouvé (pas d'erreur 404) // Retourner avec reactionsCount et isFavorite pour l'utilisateur actuel
List<EventReadManyByIdResponseDTO> responseDTOs = events.stream() List<EventCreateResponseDTO> responseDTOs = events.stream()
.map(EventReadManyByIdResponseDTO::new) .map(event -> new EventCreateResponseDTO(event, usersRepository, userId))
.toList(); .toList();
return Response.ok(responseDTOs).build(); return Response.ok(responseDTOs).build();
} catch (Exception e) { } catch (Exception e) {
@@ -552,9 +551,6 @@ public class EventsResource {
case "description": case "description":
event.setDescription(value != null ? value.toString() : null); event.setDescription(value != null ? value.toString() : null);
break; break;
case "location":
event.setLocation(value != null ? value.toString() : null);
break;
case "category": case "category":
event.setCategory(value != null ? value.toString() : null); event.setCategory(value != null ? value.toString() : null);
break; break;
@@ -762,9 +758,9 @@ public class EventsResource {
@POST @POST
@Path("/{id}/favorite") @Path("/{id}/favorite")
@Transactional @Transactional
@Operation(summary = "Marquer un événement comme favori", description = "Permet à un utilisateur de marquer un événement comme favori.") @Operation(summary = "Toggle favori d'un événement", description = "Permet à un utilisateur d'ajouter ou retirer un événement de ses favoris (toggle).")
public Response favoriteEvent(@PathParam("id") UUID eventId, @QueryParam("userId") UUID userId) { public Response favoriteEvent(@PathParam("id") UUID eventId, @QueryParam("userId") UUID userId) {
LOG.info("[LOG] Marquage de l'événement comme favori pour l'utilisateur ID : " + userId); LOG.info("[LOG] Toggle favori de l'événement " + eventId + " pour l'utilisateur ID : " + userId);
Events event = eventsRepository.findById(eventId); Events event = eventsRepository.findById(eventId);
Users user = usersRepository.findById(userId); Users user = usersRepository.findById(userId);
@@ -773,9 +769,22 @@ public class EventsResource {
return Response.status(Response.Status.NOT_FOUND).entity("Événement ou utilisateur non trouvé.").build(); return Response.status(Response.Status.NOT_FOUND).entity("Événement ou utilisateur non trouvé.").build();
} }
user.addFavoriteEvent(event); // ✅ Toggle : ajouter si pas favori, retirer si déjà favori
boolean wasFavorite = user.hasFavoriteEvent(event);
if (wasFavorite) {
user.removeFavoriteEvent(event);
LOG.info("[LOG] Événement retiré des favoris pour l'utilisateur : " + user.getEmail());
} else {
user.addFavoriteEvent(event);
LOG.info("[LOG] Événement ajouté aux favoris pour l'utilisateur : " + user.getEmail());
}
usersRepository.persist(user); usersRepository.persist(user);
return Response.ok("Événement marqué comme favori.").build();
// Retourner un JSON avec le statut
Map<String, Object> response = new java.util.HashMap<>();
response.put("isFavorite", !wasFavorite);
response.put("message", wasFavorite ? "Événement retiré des favoris." : "Événement ajouté aux favoris.");
return Response.ok(response).build();
} }
@GET @GET