feat(backend): endpoints inscriptions + feedback événements

Ajoute infrastructure complète pour gérer inscriptions et feedbacks événements.

## Entités
- FeedbackEvenement : note 1-5, commentaire, modération (PUBLIE/EN_ATTENTE/REJETE)
- InscriptionEvenement : déjà existait, utilisation ajoutée

## Repositories
- InscriptionEvenementRepository : findByMembreAndEvenement, findByEvenement, countConfirmees, isMembreInscrit
- FeedbackEvenementRepository : findByMembreAndEvenement, findPubliesByEvenement, calculateAverageNote

## Service (EvenementService)
Inscriptions :
- inscrireEvenement() : vérifie capacité, crée inscription CONFIRMEE
- desinscrireEvenement() : soft delete inscription
- getParticipants() : liste inscriptions confirmées
- getMesInscriptions() : inscriptions du membre connecté

Feedbacks :
- soumetteFeedback() : note 1-5 + commentaire, vérifie participation, événement terminé
- getFeedbacks() : liste feedbacks publiés
- getStatistiquesFeedback() : note moyenne + nombre feedbacks

## REST Endpoints (6 total)
Inscriptions :
- POST /api/evenements/{id}/inscriptions - S'inscrire
- DELETE /api/evenements/{id}/inscriptions - Se désinscrire
- GET /api/evenements/{id}/participants - Liste participants
- GET /api/evenements/mes-inscriptions - Mes inscriptions

Feedbacks :
- POST /api/evenements/{id}/feedback - Soumettre feedback (note+commentaire)
- GET /api/evenements/{id}/feedbacks - Liste feedbacks + stats

## Database
- Migration V7 : table feedbacks_evenement
- Contrainte unique: un feedback par membre/événement
- Index: membre_id, evenement_id, date_feedback, moderation_statut

Débloquer fonctionnalités événements mobile.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
dahoud
2026-03-16 20:11:03 +00:00
parent 3be01e28a7
commit a7bcaf9277
6 changed files with 690 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
package dev.lions.unionflow.server.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import lombok.*;
/**
* Entité FeedbackEvenement représentant l'évaluation d'un membre sur un événement
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-16
*/
@Entity
@Table(
name = "feedbacks_evenement",
indexes = {
@Index(name = "idx_feedback_membre", columnList = "membre_id"),
@Index(name = "idx_feedback_evenement", columnList = "evenement_id"),
@Index(name = "idx_feedback_date", columnList = "date_feedback")
},
uniqueConstraints = {
@UniqueConstraint(
name = "uk_feedback_membre_evenement",
columnNames = {"membre_id", "evenement_id"})
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = true)
public class FeedbackEvenement extends BaseEntity {
@NotNull
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "membre_id", nullable = false)
private Membre membre;
@NotNull
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "evenement_id", nullable = false)
private Evenement evenement;
@NotNull
@Min(1)
@Max(5)
@Column(name = "note", nullable = false)
private Integer note;
@Column(name = "commentaire", length = 1000)
private String commentaire;
@Builder.Default
@Column(name = "date_feedback", nullable = false)
private LocalDateTime dateFeedback = LocalDateTime.now();
@Column(name = "moderation_statut", length = 20)
@Builder.Default
private String moderationStatut = ModerationStatut.PUBLIE.name();
@Column(name = "raison_moderation", length = 500)
private String raisonModeration;
/** Énumération des statuts de modération */
public enum ModerationStatut {
PUBLIE, // Visible publiquement
EN_ATTENTE, // En attente de modération
REJETE // Rejeté par modération
}
// Méthodes utilitaires
/** Vérifie si le feedback est publié */
public boolean isPublie() {
return ModerationStatut.PUBLIE.name().equals(this.moderationStatut);
}
/** Marque le feedback comme en attente de modération */
public void mettreEnAttente(String raison) {
this.moderationStatut = ModerationStatut.EN_ATTENTE.name();
this.raisonModeration = raison;
setDateModification(LocalDateTime.now());
}
/** Publie le feedback */
public void publier() {
this.moderationStatut = ModerationStatut.PUBLIE.name();
this.raisonModeration = null;
setDateModification(LocalDateTime.now());
}
/** Rejette le feedback */
public void rejeter(String raison) {
this.moderationStatut = ModerationStatut.REJETE.name();
this.raisonModeration = raison;
setDateModification(LocalDateTime.now());
}
@PreUpdate
public void preUpdate() {
super.onUpdate();
}
@Override
public String toString() {
return String.format(
"FeedbackEvenement{id=%s, membre=%s, evenement=%s, note=%d, dateFeedback=%s}",
getId(),
membre != null ? membre.getEmail() : "null",
evenement != null ? evenement.getTitre() : "null",
note,
dateFeedback);
}
}

View File

@@ -0,0 +1,95 @@
package dev.lions.unionflow.server.repository;
import dev.lions.unionflow.server.entity.FeedbackEvenement;
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* Repository pour les feedbacks d'événements
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-16
*/
@ApplicationScoped
public class FeedbackEvenementRepository
implements PanacheRepositoryBase<FeedbackEvenement, UUID> {
/**
* Trouve un feedback par membre et événement
*
* @param membreId UUID du membre
* @param evenementId UUID de l'événement
* @return Optional de FeedbackEvenement
*/
public Optional<FeedbackEvenement> findByMembreAndEvenement(
UUID membreId, UUID evenementId) {
return find("membre.id = ?1 and evenement.id = ?2 and actif = true", membreId, evenementId)
.firstResultOptional();
}
/**
* Liste tous les feedbacks d'un événement (publiés uniquement)
*
* @param evenementId UUID de l'événement
* @return Liste des feedbacks publiés
*/
public List<FeedbackEvenement> findPubliesByEvenement(UUID evenementId) {
return find(
"evenement.id = ?1 and moderationStatut = 'PUBLIE' and actif = true order by dateFeedback desc",
evenementId)
.list();
}
/**
* Liste tous les feedbacks d'un événement (tous statuts)
*
* @param evenementId UUID de l'événement
* @return Liste de tous les feedbacks
*/
public List<FeedbackEvenement> findAllByEvenement(UUID evenementId) {
return find("evenement.id = ?1 and actif = true order by dateFeedback desc", evenementId)
.list();
}
/**
* Calcule la note moyenne d'un événement
*
* @param evenementId UUID de l'événement
* @return Note moyenne (ou 0.0 si aucun feedback)
*/
public Double calculateAverageNote(UUID evenementId) {
Double avg =
find(
"select avg(f.note) from FeedbackEvenement f where f.evenement.id = ?1 and f.moderationStatut = 'PUBLIE' and f.actif = true",
evenementId)
.project(Double.class)
.firstResult();
return avg != null ? avg : 0.0;
}
/**
* Compte le nombre de feedbacks publiés pour un événement
*
* @param evenementId UUID de l'événement
* @return Nombre de feedbacks publiés
*/
public long countPubliesByEvenement(UUID evenementId) {
return count(
"evenement.id = ?1 and moderationStatut = 'PUBLIE' and actif = true", evenementId);
}
/**
* Liste les feedbacks en attente de modération
*
* @return Liste des feedbacks en attente
*/
public List<FeedbackEvenement> findEnAttente() {
return find(
"moderationStatut = 'EN_ATTENTE' and actif = true order by dateFeedback desc")
.list();
}
}

View File

@@ -0,0 +1,110 @@
package dev.lions.unionflow.server.repository;
import dev.lions.unionflow.server.entity.Evenement;
import dev.lions.unionflow.server.entity.InscriptionEvenement;
import dev.lions.unionflow.server.entity.Membre;
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* Repository pour les inscriptions aux événements
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-16
*/
@ApplicationScoped
public class InscriptionEvenementRepository
implements PanacheRepositoryBase<InscriptionEvenement, UUID> {
/**
* Trouve une inscription par membre et événement
*
* @param membreId UUID du membre
* @param evenementId UUID de l'événement
* @return Optional d'InscriptionEvenement
*/
public Optional<InscriptionEvenement> findByMembreAndEvenement(
UUID membreId, UUID evenementId) {
return find("membre.id = ?1 and evenement.id = ?2 and actif = true", membreId, evenementId)
.firstResultOptional();
}
/**
* Liste toutes les inscriptions d'un membre
*
* @param membreId UUID du membre
* @return Liste des inscriptions
*/
public List<InscriptionEvenement> findByMembre(UUID membreId) {
return find(
"membre.id = ?1 and actif = true order by dateInscription desc",
membreId)
.list();
}
/**
* Liste toutes les inscriptions à un événement
*
* @param evenementId UUID de l'événement
* @return Liste des inscriptions
*/
public List<InscriptionEvenement> findByEvenement(UUID evenementId) {
return find(
"evenement.id = ?1 and actif = true order by dateInscription asc",
evenementId)
.list();
}
/**
* Liste les inscriptions confirmées pour un événement
*
* @param evenementId UUID de l'événement
* @return Liste des inscriptions confirmées
*/
public List<InscriptionEvenement> findConfirmeesByEvenement(UUID evenementId) {
return find(
"evenement.id = ?1 and statut = 'CONFIRMEE' and actif = true order by dateInscription asc",
evenementId)
.list();
}
/**
* Compte le nombre d'inscriptions confirmées pour un événement
*
* @param evenementId UUID de l'événement
* @return Nombre d'inscriptions confirmées
*/
public long countConfirmeesByEvenement(UUID evenementId) {
return count(
"evenement.id = ?1 and statut = 'CONFIRMEE' and actif = true", evenementId);
}
/**
* Vérifie si un membre est inscrit à un événement
*
* @param membreId UUID du membre
* @param evenementId UUID de l'événement
* @return true si le membre est inscrit et l'inscription est confirmée
*/
public boolean isMembreInscrit(UUID membreId, UUID evenementId) {
return count(
"membre.id = ?1 and evenement.id = ?2 and statut = 'CONFIRMEE' and actif = true",
membreId,
evenementId)
> 0;
}
/**
* Supprime logiquement une inscription
*
* @param inscription L'inscription à supprimer
*/
public void softDelete(InscriptionEvenement inscription) {
inscription.setActif(false);
persist(inscription);
}
}

View File

@@ -3,11 +3,14 @@ package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.dto.EvenementMobileDTO;
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.service.EvenementService;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.ws.rs.*;
@@ -286,4 +289,112 @@ public class EvenementResource {
boolean inscrit = evenementService.isUserInscrit(id);
return Response.ok(Map.of("inscrit", inscrit)).build();
}
// === GESTION DES INSCRIPTIONS ===
/** S'inscrire à un événement */
@POST
@Path("/{id}/inscriptions")
@Operation(summary = "S'inscrire à un événement")
@APIResponse(responseCode = "201", description = "Inscription créée")
@APIResponse(responseCode = "400", description = "Déjà inscrit ou événement complet")
@APIResponse(responseCode = "404", description = "Événement non trouvé")
@RolesAllowed({ "ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT", "MEMBRE", "USER" })
@Transactional
public Response inscrireEvenement(@PathParam("id") UUID evenementId) {
try {
InscriptionEvenement inscription = evenementService.inscrireEvenement(evenementId);
return Response.status(Response.Status.CREATED).entity(inscription).build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", e.getMessage()))
.build();
}
}
/** Se désinscrire d'un événement */
@DELETE
@Path("/{id}/inscriptions")
@Operation(summary = "Se désinscrire d'un événement")
@APIResponse(responseCode = "204", description = "Désinscription effectuée")
@APIResponse(responseCode = "404", description = "Inscription non trouvée")
@RolesAllowed({ "ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT", "MEMBRE", "USER" })
@Transactional
public Response desinscrireEvenement(@PathParam("id") UUID evenementId) {
evenementService.desinscrireEvenement(evenementId);
return Response.noContent().build();
}
/** Liste des participants d'un événement */
@GET
@Path("/{id}/participants")
@Operation(summary = "Liste des participants")
@APIResponse(responseCode = "200", description = "Liste des participants")
@RolesAllowed({ "ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT", "MEMBRE" })
public Response getParticipants(@PathParam("id") UUID evenementId) {
List<InscriptionEvenement> participants = evenementService.getParticipants(evenementId);
return Response.ok(participants).build();
}
/** Mes inscriptions */
@GET
@Path("/mes-inscriptions")
@Operation(summary = "Mes inscriptions aux événements")
@APIResponse(responseCode = "200", description = "Liste de mes inscriptions")
@RolesAllowed({ "ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT", "MEMBRE", "USER" })
public Response getMesInscriptions() {
List<InscriptionEvenement> inscriptions = evenementService.getMesInscriptions();
return Response.ok(inscriptions).build();
}
// === GESTION DES FEEDBACKS ===
/** Soumettre un feedback */
@POST
@Path("/{id}/feedback")
@Operation(summary = "Soumettre un feedback sur l'événement")
@APIResponse(responseCode = "201", description = "Feedback créé")
@APIResponse(responseCode = "400", description = "Données invalides ou feedback déjà soumis")
@APIResponse(responseCode = "404", description = "Événement non trouvé")
@RolesAllowed({ "ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT", "MEMBRE", "USER" })
@Transactional
public Response soumetteFeedback(
@PathParam("id") UUID evenementId, Map<String, Object> requestBody) {
Integer note = (Integer) requestBody.get("note");
String commentaire = (String) requestBody.get("commentaire");
if (note == null || note < 1 || note > 5) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "La note doit être entre 1 et 5"))
.build();
}
try {
FeedbackEvenement feedback =
evenementService.soumetteFeedback(evenementId, note, commentaire);
return Response.status(Response.Status.CREATED).entity(feedback).build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", e.getMessage()))
.build();
}
}
/** Liste des feedbacks d'un événement */
@GET
@Path("/{id}/feedbacks")
@Operation(summary = "Liste des feedbacks de l'événement")
@APIResponse(responseCode = "200", description = "Liste des feedbacks")
public Response getFeedbacks(@PathParam("id") UUID evenementId) {
List<FeedbackEvenement> feedbacks = evenementService.getFeedbacks(evenementId);
Map<String, Object> stats = evenementService.getStatistiquesFeedback(evenementId);
return Response.ok(
Map.of(
"feedbacks", feedbacks,
"noteMoyenne", stats.get("noteMoyenne"),
"nombreFeedbacks", stats.get("nombreFeedbacks")))
.build();
}
}

View File

@@ -1,7 +1,12 @@
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;
@@ -45,6 +50,12 @@ public class EvenementService {
@Inject
KeycloakService keycloakService;
@Inject
InscriptionEvenementRepository inscriptionRepository;
@Inject
FeedbackEvenementRepository feedbackRepository;
/**
* Crée un nouvel événement
*
@@ -364,4 +375,204 @@ public class EvenementService {
.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<InscriptionEvenement> 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<InscriptionEvenement> getParticipants(UUID evenementId) {
return inscriptionRepository.findConfirmeesByEvenement(evenementId);
}
/**
* Liste les inscriptions de l'utilisateur connecté
*
* @return Liste des inscriptions du membre
*/
public List<InscriptionEvenement> 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<FeedbackEvenement> 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<FeedbackEvenement> 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<String, Object> getStatistiquesFeedback(UUID evenementId) {
Double noteMoyenne = feedbackRepository.calculateAverageNote(evenementId);
long nombreFeedbacks = feedbackRepository.countPubliesByEvenement(evenementId);
return Map.of(
"noteMoyenne", noteMoyenne,
"nombreFeedbacks", nombreFeedbacks);
}
}

View File

@@ -0,0 +1,46 @@
-- Migration V7: Création table feedbacks_evenement
-- Auteur: UnionFlow Team
-- Date: 2026-03-16
-- Description: Ajoute la table pour les feedbacks/évaluations des événements
-- Table feedbacks_evenement (évaluations post-événement)
CREATE TABLE IF NOT EXISTS feedbacks_evenement (
-- Colonnes BaseEntity
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
date_modification TIMESTAMP,
cree_par VARCHAR(255),
modifie_par VARCHAR(255),
version INTEGER NOT NULL DEFAULT 0,
actif BOOLEAN NOT NULL DEFAULT TRUE,
-- Relations
membre_id UUID NOT NULL,
evenement_id UUID NOT NULL,
-- Données de feedback
note INTEGER NOT NULL CHECK (note >= 1 AND note <= 5),
commentaire VARCHAR(1000),
date_feedback TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Modération
moderation_statut VARCHAR(20) NOT NULL DEFAULT 'PUBLIE' CHECK (moderation_statut IN ('PUBLIE', 'EN_ATTENTE', 'REJETE')),
raison_moderation VARCHAR(500),
-- Contraintes
CONSTRAINT fk_feedback_membre FOREIGN KEY (membre_id) REFERENCES utilisateurs(id) ON DELETE CASCADE,
CONSTRAINT fk_feedback_evenement FOREIGN KEY (evenement_id) REFERENCES evenements(id) ON DELETE CASCADE,
CONSTRAINT uk_feedback_membre_evenement UNIQUE (membre_id, evenement_id)
);
-- Index pour performance
CREATE INDEX IF NOT EXISTS idx_feedback_membre ON feedbacks_evenement(membre_id);
CREATE INDEX IF NOT EXISTS idx_feedback_evenement ON feedbacks_evenement(evenement_id);
CREATE INDEX IF NOT EXISTS idx_feedback_date ON feedbacks_evenement(date_feedback);
CREATE INDEX IF NOT EXISTS idx_feedback_moderation ON feedbacks_evenement(moderation_statut);
-- Commentaires
COMMENT ON TABLE feedbacks_evenement IS 'Feedbacks et évaluations des participants après un événement';
COMMENT ON COLUMN feedbacks_evenement.note IS 'Note de 1 à 5 étoiles';
COMMENT ON COLUMN feedbacks_evenement.moderation_statut IS 'Statut de modération: PUBLIE, EN_ATTENTE, REJETE';
COMMENT ON COLUMN feedbacks_evenement.date_feedback IS 'Date de soumission du feedback';