Aller au contenu

TP – EcoTrack : Suivi des Actions Écologiques

Niveau : Intermédiaire – Avancé
Technologies : Spring Boot 3, Spring Data JPA, Hibernate, PostgreSQL, Maven


Objectifs

La classe DataInitializer est fournie en fin de fichier.


Contexte du projet

Vous travaillez pour une association environnementale qui souhaite motiver ses membres à adopter des comportements écologiques. Elle vous demande de développer EcoTrack, une application de suivi des actions écologiques.

Chaque membre peut enregistrer ses actions du quotidien (planter un arbre, utiliser les transports en commun, composter ses déchets…). Chaque action rapporte des points d’impact selon sa catégorie. L’association veut pouvoir suivre les performances de ses membres, établir des classements et générer des statistiques.


Modèle de données

Entité Utilisateur

Champ Type Contraintes
id Long PK, auto-généré
pseudo String non null, unique
email String non null, unique
ville String nullable
dateInscription LocalDate non null
actionsRealisees List ActionUtilisateur, OneToMany
niveauEco NiveauEco enum, non null

Enum NiveauEco : DEBUTANT, INTERMEDIAIRE, EXPERT, CHAMPION

Entité CategorieAction

Champ Type Contraintes
id Long PK, auto-généré
nom String non null, unique
description String nullable
pointsBase Integer non null (points par défaut pour cette catégorie)
icone String nullable (ex: “🌳”, “🚲”, “♻️”)

Entité Action (Table de référence des actions possibles)

Champ Type Contraintes
id Long PK, auto-généré
nom String non null
description String nullable
pointsImpact Integer non null
frequenceMax Integer nullable (nombre max de fois par jour)
categorie CategorieAction ManyToOne, non null

Entité ActionUtilisateur (Action réalisée par un utilisateur)

Champ Type Contraintes
id Long PK, auto-généré
utilisateur Utilisateur ManyToOne, non null
action Action ManyToOne, non null
dateRealisation LocalDate non null
quantite Integer non null (ex: nb d’arbres plantés, km en vélo…)
commentaire String nullable
valide Boolean non null, défaut: true

Entité Badge

Champ Type Contraintes
id Long PK, auto-généré
nom String non null, unique
description String nullable
pointsRequis Integer non null
icone String nullable

Relation : Un Utilisateur peut avoir plusieurs Badge (ManyToMany, table de jointure à préciser : utilisateur_badge)


Partie 1 – Mise en place du projet

1.1 Création du projet

Créez un projet Spring Boot avec Spring Initializr (start.spring.io) avec les dépendances suivantes :

1.2 Configuration

Configurez application.yml pour vous connecter à votre base PostgreSQL locale. Créez une base de données nommée ecotrack. Pensez à modifier le username et le mot de passe, ainsi que le port du server.

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/ecotrack
    username: postgres
    password: test
    driver-class-name: org.postgresql.Driver

  jpa:
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    hibernate:
      ddl-auto: create-drop
    show-sql: true
    properties:
      hibernate:
        format_sql: true

logging:
  level:
    org.hibernate.SQL: DEBUG
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE

server:
  port: 8887

1.3 Création des entités

Créez toutes les entités Java décrites dans le modèle de données ci-dessus. N’oubliez pas :

1.4 Données de test

Créez un composant DataInitializer annoté avec @Component qui implémente CommandLineRunner et insère des données de test au démarrage :

Pas besoin d’utiliser new pour initialiser le DataInitializer, Spring Boot s’en charge pour vous. Une fois les entités créées, la méthode run() de CommandLineRunner se charge de faire les insertions en base de données.


Partie 2 – Repositories et méthodes dérivées

Créez les interfaces Repository pour chaque entité et implémentez les méthodes ci-dessous.

2.1 UtilisateurRepository

Implémentez les méthodes dérivées suivantes (sans @Query) :

// 1. Trouver un utilisateur par son pseudo (exact)
Optional<Utilisateur> findByPseudo(String pseudo);

// 2. Trouver les utilisateurs d'une ville donnée
List<Utilisateur> findByVille(String ville);

// 3. Trouver les utilisateurs par niveau
List<Utilisateur> findByNiveauEco(NiveauEco niveau);

// 4. Vérifier si un email existe déjà
boolean existsByEmail(String email);

// 5. Trouver les utilisateurs inscrits après une date
List<Utilisateur> findByDateInscriptionAfter(LocalDate date);

// 6. Trouver les utilisateurs dont le pseudo contient un mot-clé
List<Utilisateur> findByPseudoContainingIgnoreCase(String motCle);

// 7. Compter les utilisateurs par niveau
Long countByNiveauEco(NiveauEco niveau);

// 8. Trouver les utilisateurs d'une ville et d'un niveau donnés, triés par pseudo
List<Utilisateur> findByVilleAndNiveauEcoOrderByPseudoAsc(String ville, NiveauEco niveau);

2.2 ActionRepository

// 1. Trouver les actions par catégorie
List<Action> findByCategorieId(Long categorieId);

// 2. Trouver les actions dont les points sont supérieurs à X
List<Action> findByPointsImpactGreaterThan(Integer points);

// 3. Trouver les actions dont les points sont entre X et Y, triées par points décroissants
List<Action> findByPointsImpactBetweenOrderByPointsImpactDesc(Integer min, Integer max);

// 4. Trouver les actions par nom de catégorie (navigation)
List<Action> findByCategorieNom(String nomCategorie);

// 5. Top 5 des actions les plus rémunératrices
List<Action> findTop5ByOrderByPointsImpactDesc();

2.3 ActionUtilisateurRepository

// 1. Trouver toutes les actions d'un utilisateur, triées par date décroissante
List<ActionUtilisateur> findByUtilisateurIdOrderByDateRealisationDesc(Long utilisateurId);

// 2. Trouver les actions d'un utilisateur sur une période donnée
List<ActionUtilisateur> findByUtilisateurIdAndDateRealisationBetween(
    Long utilisateurId, LocalDate debut, LocalDate fin
);

// 3. Compter le nombre d'actions réalisées par un utilisateur
Long countByUtilisateurId(Long utilisateurId);

// 4. Trouver les actions valides d'une catégorie pour un utilisateur
List<ActionUtilisateur> findByUtilisateurIdAndActionCategorieIdAndValideTrue(
    Long utilisateurId, Long categorieId
);

Partie 3 – Requêtes JPQL avancées avec @Query

3.1 Statistiques des utilisateurs

Dans UtilisateurRepository, ajoutez avec @Query :

// 1. Calculer le score total d'un utilisateur
// Score = SUM(action.pointsImpact * actionUtilisateur.quantite)
// Ne compter que les actions valides
@Query("SELECT SUM(a.pointsImpact * au.quantite) " +
       "FROM ActionUtilisateur au JOIN au.action a " +
       "WHERE au.utilisateur.id = :utilisateurId AND au.valide = true")
Long calculerScoreUtilisateur(@Param("utilisateurId") Long utilisateurId);

// 2. Classement général des utilisateurs par score décroissant
// Retourner : pseudo, ville, score total
// Indice : GROUP BY avec SUM
@Query("SELECT u.pseudo, u.ville, SUM(a.pointsImpact * au.quantite) as score " +
       "FROM Utilisateur u " +
       "LEFT JOIN u.actionsRealisees au " +
       "LEFT JOIN au.action a " +
       "WHERE au.valide = true OR au IS NULL " +
       "GROUP BY u.id, u.pseudo, u.ville " +
       "ORDER BY score DESC NULLS LAST")
List<Object[]> classementGeneral();

// 3. Top 10 des utilisateurs du mois en cours
@Query("SELECT u.pseudo, SUM(a.pointsImpact * au.quantite) as score " +
       "FROM ActionUtilisateur au " +
       "JOIN au.utilisateur u " +
       "JOIN au.action a " +
       "WHERE MONTH(au.dateRealisation) = MONTH(CURRENT_DATE) " +
       "AND YEAR(au.dateRealisation) = YEAR(CURRENT_DATE) " +
       "AND au.valide = true " +
       "GROUP BY u.id, u.pseudo " +
       "ORDER BY score DESC")
List<Object[]> top10DuMois(Pageable pageable);  // Passez PageRequest.of(0, 10)

// 4. Utilisateurs n'ayant réalisé aucune action depuis X jours
@Query("SELECT u FROM Utilisateur u WHERE NOT EXISTS (" +
       "    SELECT au FROM ActionUtilisateur au " +
       "    WHERE au.utilisateur = u " +
       "    AND au.dateRealisation >= :dateLimit" +
       ")")
List<Utilisateur> findInactifDepuis(@Param("dateLimit") LocalDate dateLimit);

3.2 Statistiques des actions

Dans ActionUtilisateurRepository, ajoutez :

// 1. Nombre d'actions réalisées par catégorie (pour un utilisateur)
@Query("SELECT c.nom, c.icone, COUNT(au), SUM(a.pointsImpact * au.quantite) " +
       "FROM ActionUtilisateur au " +
       "JOIN au.action a " +
       "JOIN a.categorie c " +
       "WHERE au.utilisateur.id = :utilisateurId AND au.valide = true " +
       "GROUP BY c.id, c.nom, c.icone " +
       "ORDER BY SUM(a.pointsImpact * au.quantite) DESC")
List<Object[]> statsParCategoriePourUtilisateur(@Param("utilisateurId") Long id);

// 2. Actions les plus populaires (les plus souvent réalisées)
@Query("SELECT a.nom, COUNT(au) as nbRealisation, SUM(au.quantite) as totalQuantite " +
       "FROM ActionUtilisateur au JOIN au.action a " +
       "WHERE au.valide = true " +
       "GROUP BY a.id, a.nom " +
       "ORDER BY nbRealisation DESC")
List<Object[]> actionsLesPlusPopulaires(Pageable pageable);

// 3. Évolution du score d'un utilisateur semaine par semaine
// Retourner : semaine (numéro), année, points gagnés cette semaine
@Query(value = "SELECT EXTRACT(WEEK FROM date_realisation) as semaine, " +
               "EXTRACT(YEAR FROM date_realisation) as annee, " +
               "SUM(a.points_impact * au.quantite) as points " +
               "FROM action_utilisateur au " +
               "JOIN action a ON au.action_id = a.id " +
               "WHERE au.utilisateur_id = :utilisateurId " +
               "AND au.valide = true " +
               "GROUP BY semaine, annee " +
               "ORDER BY annee, semaine",
       nativeQuery = true)
List<Object[]> evolutionHebdomadaire(@Param("utilisateurId") Long utilisateurId);

// 4. Score total de l'application (pour afficher sur le dashboard)
@Query("SELECT SUM(a.pointsImpact * au.quantite) FROM ActionUtilisateur au " +
       "JOIN au.action a WHERE au.valide = true")
Long scoreTotalCommunaute();

3.3 Requêtes avec mises à jour

// Dans ActionUtilisateurRepository :

// 1. Invalider toutes les actions d'un utilisateur pour une date donnée
@Modifying
@Transactional
@Query("UPDATE ActionUtilisateur au SET au.valide = false " +
       "WHERE au.utilisateur.id = :utilisateurId " +
       "AND au.dateRealisation = :date")
int invaliderActionsJour(
    @Param("utilisateurId") Long utilisateurId,
    @Param("date") LocalDate date
);

// 2. Supprimer les actions invalides de plus de 1 an
@Modifying
@Transactional
@Query("DELETE FROM ActionUtilisateur au " +
       "WHERE au.valide = false " +
       "AND au.dateRealisation < :dateLimit")
int supprimerAnciennesActionsInvalides(@Param("dateLimit") LocalDate dateLimit);

Partie 4 – Projections et DTOs

4.1 Créer les interfaces de projection

On parle bien d’interfaces, pas de classe ni de record.

// projection pour l'affichage d'une carte utilisateur
public interface UtilisateurCard {
    String getPseudo();
    String getVille();
    NiveauEco getNiveauEco();
    LocalDate getDateInscription();
}

// projection pour un résumé d'action
public interface ActionResume {
    String getNom();
    Integer getPointsImpact();
    
    CategorieResume getCategorie();
    
    interface CategorieResume {
        String getNom();
        String getIcone();
    }
}

Ajoutez les méthodes correspondantes dans les repositories :

// Dans UtilisateurRepository
List<UtilisateurCard> findByVille(String ville);
// (avec projection)
<T> List<T> findByNiveauEco(NiveauEco niveau, Class<T> type);

// Dans ActionRepository
List<ActionResume> findByCategorieNom(String nomCategorie);

4.2 Créer des DTOs avec constructeur JPQL

// DTO pour le tableau de bord d'un utilisateur
public record UtilisateurDashboardDTO(
    Long id,
    String pseudo,
    String ville,
    NiveauEco niveau,
    Long nombreActionsTotal,
    Long scoreTotal
) {}

// DTO pour les statistiques par catégorie
public record StatsCategorieDTO(
    String nomCategorie,
    String icone,
    Long nombreActions,
    Long pointsTotal
) {}

Dans les repositories, ajoutez des méthodes @Query utilisant NEW :

// Dans UtilisateurRepository
@Query("SELECT NEW com.ecotrack.dto.UtilisateurDashboardDTO(" +
       "    u.id, u.pseudo, u.ville, u.niveauEco, " +
       "    COUNT(au), COALESCE(SUM(a.pointsImpact * au.quantite), 0)" +
       ") " +
       "FROM Utilisateur u " +
       "LEFT JOIN u.actionsRealisees au " +
       "LEFT JOIN au.action a " +
       "WHERE (au.valide = true OR au IS NULL) " +
       "GROUP BY u.id, u.pseudo, u.ville, u.niveauEco")
List<UtilisateurDashboardDTO> findAllDashboard();

Partie 5 – Pagination et recherche avancée

5.1 Pagination du classement

// Dans UtilisateurRepository
@Query(
    value = "SELECT u FROM Utilisateur u LEFT JOIN FETCH u.actionsRealisees",
    countQuery = "SELECT COUNT(u) FROM Utilisateur u"
)
Page<Utilisateur> findAllAvecActions(Pageable pageable);

Créez un service ClassementService avec une méthode :

public Page<UtilisateurDashboardDTO> getClassementPagine(int page, int taille) {
    Pageable pageable = PageRequest.of(page, taille);
    // récupérer et retourner le classement paginé
}

5.2 Recherche dynamique avec Specifications

Créez une classe UtilisateurSpecifications avec des méthodes statiques ci-dessous.

Reprenez l’exemple dans la page de cours sur les Specifications pour Spring Data JPA (Lien vers le cours complet sur JPQL & Spring Data), voir le chapitre 13.4 Spring DataJPA Specifications

public class UtilisateurSpecifications {

    // Filtre par ville (optionnel)
    public static Specification<Utilisateur> villeEgale(String ville) { ... }

    // Filtre par niveau (optionnel)
    public static Specification<Utilisateur> niveauEco(NiveauEco niveau) { ... }

    // Filtre : inscrit après une date (optionnel)
    public static Specification<Utilisateur> inscritApres(LocalDate date) { ... }

    // Filtre : pseudo contient (optionnel)
    public static Specification<Utilisateur> pseudoContient(String motCle) { ... }
}

Puis créez une classe de critères et un service de recherche :

public record RechercheUtilisateurCriteres(
    String pseudo,
    String ville,
    NiveauEco niveau,
    LocalDate inscritDepuis
) {}

@Service
public class RechercheUtilisateurService {
    
    public Page<Utilisateur> rechercher(RechercheUtilisateurCriteres criteres, Pageable pageable) {
        Specification<Utilisateur> spec = Specification
            .where(UtilisateurSpecifications.pseudoContient(criteres.pseudo()))
            .and(UtilisateurSpecifications.villeEgale(criteres.ville()))
            .and(UtilisateurSpecifications.niveauEco(criteres.niveau()))
            .and(UtilisateurSpecifications.inscritApres(criteres.inscritDepuis()));
        
        return utilisateurRepository.findAll(spec, pageable);
    }
}

Partie 6 – Fonctionnalités bonus (pour aller plus loin)

Si vous avez terminé les parties précédentes, voici des défis supplémentaires… mais je n’ai moi-même pas encore codé cette partie ;)

Bonus 1 – Système de badges automatique

Créez un service BadgeService qui attribue automatiquement les badges en fonction du score d’un utilisateur :

@Service
public class BadgeService {

    // Vérifier et attribuer les badges mérités après chaque action
    @Transactional
    public List<Badge> attribuerBadgesManquants(Long utilisateurId) {
        // 1. Calculer le score de l'utilisateur
        // 2. Trouver les badges dont les pointsRequis <= score
        // 3. Exclure les badges que l'utilisateur a déjà
        // 4. Attribuer les nouveaux badges
        // 5. Retourner la liste des nouveaux badges obtenus
    }
}

Requête à implémenter dans BadgeRepository :

// Trouver les badges que l'utilisateur n'a pas encore mais qu'il mérite
@Query("SELECT b FROM Badge b " +
       "WHERE b.pointsRequis <= :score " +
       "AND b NOT IN (" +
       "    SELECT b2 FROM Utilisateur u JOIN u.badges b2 WHERE u.id = :utilisateurId" +
       ")")
List<Badge> findBadgesAAttribuer(
    @Param("utilisateurId") Long utilisateurId, 
    @Param("score") Long score
);

Bonus 2 – Rapport hebdomadaire

Créez une méthode qui génère un rapport textuel de la semaine pour un utilisateur :

Bonus 3 – Mise à jour automatique du niveau

Basé sur le score, mettez à jour automatiquement le niveauEco des utilisateurs :

// Dans UtilisateurRepository
@Modifying
@Transactional
@Query("UPDATE Utilisateur u SET u.niveauEco = " +
       "CASE " +
       "    WHEN :score >= 2000 THEN com.ecotrack.model.NiveauEco.CHAMPION " +
       "    WHEN :score >= 500  THEN com.ecotrack.model.NiveauEco.EXPERT " +
       "    WHEN :score >= 100  THEN com.ecotrack.model.NiveauEco.INTERMEDIAIRE " +
       "    ELSE com.ecotrack.model.NiveauEco.DEBUTANT " +
       "END " +
       "WHERE u.id = :utilisateurId")
int mettreAJourNiveau(@Param("utilisateurId") Long utilisateurId, @Param("score") Long score);

Conseils et pièges à éviter

  1. Activez les logs SQL dès le début pour voir ce que Hibernate génère réellement.
  2. Évitez le problème N+1 : utilisez JOIN FETCH ou @EntityGraph dès que vous accédez à des collections dans une boucle.
  3. Nommez bien vos méthodes dérivées : un mauvais nom générera une mauvaise requête ou une erreur au démarrage.
  4. La countQuery dans @Query est nécessaire quand la requête principale contient un JOIN FETCH.
  5. @Transactional est indispensable sur les méthodes @Modifying.
  6. PostgreSQL avec JPQL : certaines fonctions SQL (comme EXTRACT, ILIKE) nécessitent du SQL natif.

DataInitializer

Voici le code d’initilisation des données pour tester :

package com.ecotrack.config;

import java.time.LocalDate;
import java.util.List;
import java.util.Random;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.ecotrack.model.Action;
import com.ecotrack.model.ActionUtilisateur;
import com.ecotrack.model.Badge;
import com.ecotrack.model.CategorieAction;
import com.ecotrack.model.NiveauEco;
import com.ecotrack.model.Utilisateur;
import com.ecotrack.repository.ActionRepository;
import com.ecotrack.repository.ActionUtilisateurRepository;
import com.ecotrack.repository.BadgeRepository;
import com.ecotrack.repository.CategorieActionRepository;
import com.ecotrack.repository.UtilisateurRepository;

@Component
public class DataInitializer implements CommandLineRunner {

    private final CategorieActionRepository categorieRepo;
    private final ActionRepository actionRepo;
    private final UtilisateurRepository utilisateurRepo;
    private final ActionUtilisateurRepository auRepo;
    private final BadgeRepository badgeRepo;

    public DataInitializer(CategorieActionRepository categorieRepo,
                           ActionRepository actionRepo,
                           UtilisateurRepository utilisateurRepo,
                           ActionUtilisateurRepository auRepo,
                           BadgeRepository badgeRepo) {
        this.categorieRepo = categorieRepo;
        this.actionRepo = actionRepo;
        this.utilisateurRepo = utilisateurRepo;
        this.auRepo = auRepo;
        this.badgeRepo = badgeRepo;
    }

    @Override
    @Transactional
    public void run(String... args) {
        // Catégories avec icones récupérées sur le web
        CategorieAction mobilite = categorieRepo.save(new CategorieAction("Mobilité", "Transport écologique", 10, "🚲"));
        CategorieAction alimentation = categorieRepo.save(new CategorieAction("Alimentation", "Manger responsable", 15, "🥗"));
        CategorieAction energie = categorieRepo.save(new CategorieAction("Énergie", "Réduire sa consommation", 12, "💡"));
        CategorieAction nature = categorieRepo.save(new CategorieAction("Nature", "Préserver la biodiversité", 20, "🌳"));
        CategorieAction dechets = categorieRepo.save(new CategorieAction("Déchets", "Réduire et recycler", 8, "♻️"));

        // Actions (vous pouvez créer les vôtres si besoin)
        Action velo = actionRepo.save(new Action("Trajet à vélo", "Kms parcourus à vélo", 10, null, mobilite));
        Action covoiturage = actionRepo.save(new Action("Covoiturage", "Trajets en covoiturage", 15, 3, mobilite));
        Action transportsEnCommun = actionRepo.save(new Action("Transport en commun", "Trajets TC", 8, null, mobilite));
        Action repasVege = actionRepo.save(new Action("Repas végétarien", "Repas sans viande", 20, 3, alimentation));
        Action marcheLocal = actionRepo.save(new Action("Marché local", "Achats producteurs locaux", 15, 1, alimentation));
        Action compost = actionRepo.save(new Action("Compostage", "Kg compostés", 12, null, alimentation));
        Action veille = actionRepo.save(new Action("Veille électronique", "Appareils mis en veille", 5, 1, energie));
        Action led = actionRepo.save(new Action("Ampoule LED", "Ampoules remplacées", 30, null, energie));
        Action doucheCourte = actionRepo.save(new Action("Douche courte", "Douches inférieures à 5 min", 8, 2, energie));
        Action planterArbre = actionRepo.save(new Action("Planter un arbre", "Arbres plantés", 100, null, nature));
        Action ramasserDechets = actionRepo.save(new Action("Ramasser des déchets", "Kg ramassés", 30, null, nature));
        Action jardinBio = actionRepo.save(new Action("Jardinage bio", "Heures de jardinage bio", 20, null, nature));
        Action trier = actionRepo.save(new Action("Tri sélectif", "Poubelles triées", 10, 1, dechets));
        Action reparation = actionRepo.save(new Action("Réparation objet", "Objets réparés", 25, null, dechets));
        Action zerodechet = actionRepo.save(new Action("Achat zéro déchet", "Achats sans emballage", 15, null, dechets));

        // badges avec icones récupérées sur le web
        badgeRepo.save(new Badge("Première pousse", "Première action enregistrée", 1, "🌱"));
        badgeRepo.save(new Badge("Éco-débutant", "100 points atteints", 100, "🥉"));
        badgeRepo.save(new Badge("Éco-acteur", "500 points atteints", 500, "🥈"));
        badgeRepo.save(new Badge("Éco-champion", "2000 points atteints", 2000, "🥇"));
        badgeRepo.save(new Badge("Forêt vivante", "10 arbres plantés", 1000, "🌲"));
        badgeRepo.save(new Badge("Cycliste engagé", "500 km à vélo", 500, "🚴"));

        // Utilisateurs
        List<Utilisateur> utilisateurs = utilisateurRepo.saveAll(List.of(
            new Utilisateur("Feuillage42 ", "feuillage42@ca.com", "Paris", LocalDate.now().minusDays(120), NiveauEco.EXPERT),
            new Utilisateur("ÉcoGuerrier ", "ecoguerrier@ca.com", "Lyon", LocalDate.now().minusDays(200), NiveauEco.CHAMPION),
            new Utilisateur("NatureAmour", "natureamour@ca.com", "Bordeaux", LocalDate.now().minusDays(60), NiveauEco.INTERMEDIAIRE),
            new Utilisateur("VéloPassion", "velopassion@ca.com", "Paris", LocalDate.now().minusDays(90), NiveauEco.EXPERT),
            new Utilisateur("ZéroDéchet", "zerodechet@ca.com", "Nantes", LocalDate.now().minusDays(300), NiveauEco.CHAMPION),
            new Utilisateur("SuperVégé", "supervege@ca.com", "Toulouse", LocalDate.now().minusDays(45), NiveauEco.INTERMEDIAIRE),
            new Utilisateur("SolarPunk", "solarpunk@ca.com", "Rennes", LocalDate.now().minusDays(10), NiveauEco.DEBUTANT),
            new Utilisateur("ÉcoloCâlin", "ecolocalin@ca.com", "Strasbourg", LocalDate.now().minusDays(150), NiveauEco.EXPERT),
            new Utilisateur("OcéanPropre ", "oceanpropre@ca.com", "Marseille", LocalDate.now().minusDays(80), NiveauEco.INTERMEDIAIRE),
            new Utilisateur("VilleVerte ", "villeverte@ca.com", "Mer", LocalDate.now().minusDays(5), NiveauEco.DEBUTANT)
        ));

        // liste des actions réalisées (données de test variées)
        List<Action> allActions = List.of(velo, covoiturage, repasVege, planterArbre, trier, reparation, jardinBio, ramasserDechets, zerodechet, veille);
        Random random = new Random(42);

        for (Utilisateur utilisateur : utilisateurs) {
            int nbActions = 5 + random.nextInt(15);
            for (int i = 0; i < nbActions; i++) {
            	
                Action action = allActions.get(random.nextInt(allActions.size()));
                LocalDate date = LocalDate.now().minusDays(random.nextInt(90));
                int quantite = 1 + random.nextInt(5);
                auRepo.save(new ActionUtilisateur(utilisateur, action, date, quantite, null));
            }
        }

        System.out.println("Données EcoTrack initialisées avec succès... enfin j'espère !");
    }
}

Bon courage et pensez à la planète !