From c4bb9803edd19cddb9f2e27c2fe39f54c157ab00 Mon Sep 17 00:00:00 2001 From: Youssef Date: Mon, 15 Jun 2026 23:11:05 +0200 Subject: [PATCH] feat(equipements): clean equipment search, fix category regression, and sync dotations on category change --- .../core/repository/DotationRepository.java | 3 +- .../core/service/CategorieService.java | 8 +- .../core/service/DotationService.java | 43 +++++- .../web/controller/AdherentController.java | 7 +- .../web/controller/CategorieController.java | 9 +- .../web/controller/DotationController.java | 80 ++++++++++- .../web/controller/EquipementController.java | 7 +- .../web/controller/LicenceController.java | 124 +++++++++++++++++- .../parametrage/equipements_recherche.html | 98 ++++++++++++++ 9 files changed, 368 insertions(+), 11 deletions(-) create mode 100644 as-talange-web/src/main/resources/templates/parametrage/equipements_recherche.html diff --git a/as-talange-core/src/main/java/com/astalange/core/repository/DotationRepository.java b/as-talange-core/src/main/java/com/astalange/core/repository/DotationRepository.java index a2d7fb2..a2604c8 100644 --- a/as-talange-core/src/main/java/com/astalange/core/repository/DotationRepository.java +++ b/as-talange-core/src/main/java/com/astalange/core/repository/DotationRepository.java @@ -1,6 +1,7 @@ package com.astalange.core.repository; import com.astalange.core.entity.Dotation; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -8,6 +9,6 @@ import java.util.List; import com.astalange.core.entity.Saison; @Repository -public interface DotationRepository extends JpaRepository { +public interface DotationRepository extends JpaRepository, JpaSpecificationExecutor { List findByLicence_SaisonAndChoisiTrueAndFourniFalse(Saison saison); } diff --git a/as-talange-core/src/main/java/com/astalange/core/service/CategorieService.java b/as-talange-core/src/main/java/com/astalange/core/service/CategorieService.java index ee55fe0..b32ece7 100644 --- a/as-talange-core/src/main/java/com/astalange/core/service/CategorieService.java +++ b/as-talange-core/src/main/java/com/astalange/core/service/CategorieService.java @@ -76,7 +76,13 @@ public class CategorieService { } } - private void updateDotationsForLicence(Licence licence, List categorieEquipements) { + public void syncDotationsForLicence(Licence licence) { + if (licence.getCategorie() != null) { + updateDotationsForLicence(licence, licence.getCategorie().getCategorieEquipements()); + } + } + + public void updateDotationsForLicence(Licence licence, List categorieEquipements) { List currentDotations = licence.getDotations(); List toRemove = new ArrayList<>(); diff --git a/as-talange-core/src/main/java/com/astalange/core/service/DotationService.java b/as-talange-core/src/main/java/com/astalange/core/service/DotationService.java index 09c1852..7d49dfe 100644 --- a/as-talange-core/src/main/java/com/astalange/core/service/DotationService.java +++ b/as-talange-core/src/main/java/com/astalange/core/service/DotationService.java @@ -24,12 +24,14 @@ public class DotationService { sb.append('\ufeff'); // Header - sb.append("Catégorie;Nom;Prénom;Équipement;Taille;Flocage;Numéro\n"); + sb.append("Catégorie;Nom;Prénom;Poste;Sexe;Équipement;Taille;Flocage;Numéro\n"); for (Dotation d : dotations) { String categorie = d.getLicence().getCategorie() != null ? d.getLicence().getCategorie().getNom() : ""; String nom = d.getLicence().getAdherent().getNom(); String prenom = d.getLicence().getAdherent().getPrenom(); + String poste = d.getLicence().getAdherent().getTypeMaillot() != null ? d.getLicence().getAdherent().getTypeMaillot() : ""; + String sexe = d.getLicence().getAdherent().getSexe() != null ? d.getLicence().getAdherent().getSexe() : ""; String equipement = d.getEquipement() != null ? d.getEquipement().getNom() : ""; String taille = d.getTaille() != null ? d.getTaille() : ""; String flocage = d.getFlocage() != null ? d.getFlocage() : ""; @@ -38,6 +40,8 @@ public class DotationService { sb.append(escapeCsv(categorie)).append(";") .append(escapeCsv(nom)).append(";") .append(escapeCsv(prenom)).append(";") + .append(escapeCsv(poste)).append(";") + .append(escapeCsv(sexe)).append(";") .append(escapeCsv(equipement)).append(";") .append(escapeCsv(taille)).append(";") .append(escapeCsv(flocage)).append(";") @@ -47,6 +51,43 @@ public class DotationService { return sb.toString(); } + public String genererCsvSearchEquipement(List dotations) { + StringBuilder sb = new StringBuilder(); + // BOM for Excel to open UTF-8 correctly + sb.append('\ufeff'); + + // Header + sb.append("Saison;Catégorie;Nom;Prénom;Poste;Sexe;Équipement;Taille;Flocage;Numéro;Fourni\n"); + + for (Dotation d : dotations) { + String saison = d.getLicence().getSaison() != null ? d.getLicence().getSaison().getNom() : ""; + String categorie = d.getLicence().getCategorie() != null ? d.getLicence().getCategorie().getNom() : ""; + String nom = d.getLicence().getAdherent().getNom(); + String prenom = d.getLicence().getAdherent().getPrenom(); + String poste = d.getLicence().getAdherent().getTypeMaillot() != null ? d.getLicence().getAdherent().getTypeMaillot() : ""; + String sexe = d.getLicence().getAdherent().getSexe() != null ? d.getLicence().getAdherent().getSexe() : ""; + String equipement = d.getEquipement() != null ? d.getEquipement().getNom() : ""; + String taille = d.getTaille() != null ? d.getTaille() : ""; + String flocage = d.getFlocage() != null ? d.getFlocage() : ""; + String numero = d.getNumero() != null ? d.getNumero() : ""; + String fourni = Boolean.TRUE.equals(d.getFourni()) ? "Oui" : "Non"; + + sb.append(escapeCsv(saison)).append(";") + .append(escapeCsv(categorie)).append(";") + .append(escapeCsv(nom)).append(";") + .append(escapeCsv(prenom)).append(";") + .append(escapeCsv(poste)).append(";") + .append(escapeCsv(sexe)).append(";") + .append(escapeCsv(equipement)).append(";") + .append(escapeCsv(taille)).append(";") + .append(escapeCsv(flocage)).append(";") + .append(escapeCsv(numero)).append(";") + .append(escapeCsv(fourni)).append("\n"); + } + + return sb.toString(); + } + private String escapeCsv(String value) { if (value == null) { return ""; diff --git a/as-talange-web/src/main/java/com/astalange/web/controller/AdherentController.java b/as-talange-web/src/main/java/com/astalange/web/controller/AdherentController.java index 6c21d84..6ed6def 100644 --- a/as-talange-web/src/main/java/com/astalange/web/controller/AdherentController.java +++ b/as-talange-web/src/main/java/com/astalange/web/controller/AdherentController.java @@ -23,13 +23,15 @@ public class AdherentController { private final com.astalange.core.repository.LicenceRepository licenceRepository; private final com.astalange.core.repository.ModePaiementRepository modePaiementRepository; private final SaisonRepository saisonRepository; + private final com.astalange.core.service.CategorieService categorieService; - public AdherentController(AdherentRepository adherentRepository, CategorieRepository categorieRepository, com.astalange.core.repository.LicenceRepository licenceRepository, com.astalange.core.repository.ModePaiementRepository modePaiementRepository, SaisonRepository saisonRepository) { + public AdherentController(AdherentRepository adherentRepository, CategorieRepository categorieRepository, com.astalange.core.repository.LicenceRepository licenceRepository, com.astalange.core.repository.ModePaiementRepository modePaiementRepository, SaisonRepository saisonRepository, com.astalange.core.service.CategorieService categorieService) { this.adherentRepository = adherentRepository; this.categorieRepository = categorieRepository; this.licenceRepository = licenceRepository; this.modePaiementRepository = modePaiementRepository; this.saisonRepository = saisonRepository; + this.categorieService = categorieService; } @org.springframework.web.bind.annotation.ModelAttribute("equipes") @@ -187,7 +189,8 @@ public class AdherentController { .orElse(null); if (newCat != null) { licence.setCategorie(newCat); - licenceRepository.save(licence); + licence = licenceRepository.save(licence); + categorieService.syncDotationsForLicence(licence); } } } diff --git a/as-talange-web/src/main/java/com/astalange/web/controller/CategorieController.java b/as-talange-web/src/main/java/com/astalange/web/controller/CategorieController.java index 537b2c6..8a95518 100644 --- a/as-talange-web/src/main/java/com/astalange/web/controller/CategorieController.java +++ b/as-talange-web/src/main/java/com/astalange/web/controller/CategorieController.java @@ -47,8 +47,13 @@ public class CategorieController { Categorie categorie = categorieRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("Invalid category ID: " + id)); model.addAttribute("categorie", categorie); - Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null); - model.addAttribute("allEquipements", activeSaison != null ? equipementRepository.findBySaison(activeSaison) : List.of()); + + Saison saison = categorie.getSaison(); + if (saison == null) { + saison = saisonRepository.findByEstActiveTrue().orElse(null); + } + + model.addAttribute("allEquipements", saison != null ? equipementRepository.findBySaison(saison) : List.of()); return "parametrage/categories_form"; } diff --git a/as-talange-web/src/main/java/com/astalange/web/controller/DotationController.java b/as-talange-web/src/main/java/com/astalange/web/controller/DotationController.java index 53679be..435b1aa 100644 --- a/as-talange-web/src/main/java/com/astalange/web/controller/DotationController.java +++ b/as-talange-web/src/main/java/com/astalange/web/controller/DotationController.java @@ -21,17 +21,95 @@ import com.astalange.core.service.DotationService; import com.astalange.core.repository.SaisonRepository; import com.astalange.core.entity.Saison; +import com.astalange.core.repository.EquipementRepository; +import com.astalange.core.repository.CategorieRepository; +import org.springframework.ui.Model; +import org.springframework.data.jpa.domain.Specification; +import jakarta.persistence.criteria.Predicate; +import java.util.ArrayList; + @Controller public class DotationController { private final DotationRepository dotationRepository; private final DotationService dotationService; private final SaisonRepository saisonRepository; + private final EquipementRepository equipementRepository; + private final CategorieRepository categorieRepository; - public DotationController(DotationRepository dotationRepository, DotationService dotationService, SaisonRepository saisonRepository) { + public DotationController(DotationRepository dotationRepository, DotationService dotationService, SaisonRepository saisonRepository, EquipementRepository equipementRepository, CategorieRepository categorieRepository) { this.dotationRepository = dotationRepository; this.dotationService = dotationService; this.saisonRepository = saisonRepository; + this.equipementRepository = equipementRepository; + this.categorieRepository = categorieRepository; + } + + private Specification buildSpecification(Long equipementId, Long categorieId, Boolean fourni, Saison saisonActive) { + return (root, query, cb) -> { + if (Long.class != query.getResultType() && long.class != query.getResultType()) { + root.fetch("equipement", jakarta.persistence.criteria.JoinType.LEFT); + jakarta.persistence.criteria.Fetch licenceFetch = root.fetch("licence", jakarta.persistence.criteria.JoinType.LEFT); + licenceFetch.fetch("adherent", jakarta.persistence.criteria.JoinType.LEFT); + licenceFetch.fetch("categorie", jakarta.persistence.criteria.JoinType.LEFT); + } + + List predicates = new ArrayList<>(); + if (saisonActive != null) { + predicates.add(cb.equal(root.get("licence").get("saison"), saisonActive)); + } + if (equipementId != null) { + predicates.add(cb.equal(root.get("equipement").get("id"), equipementId)); + } + if (categorieId != null) { + predicates.add(cb.equal(root.get("licence").get("categorie").get("id"), categorieId)); + } + if (fourni != null) { + predicates.add(cb.equal(root.get("fourni"), fourni)); + } + return cb.and(predicates.toArray(new Predicate[0])); + }; + } + + @GetMapping("/admin/equipements/recherche") + public String rechercheEquipements( + @RequestParam(required = false) Long equipementId, + @RequestParam(required = false) Long categorieId, + @RequestParam(required = false) Boolean fourni, + Model model) { + + Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null); + Specification spec = buildSpecification(equipementId, categorieId, fourni, saisonActive); + List dotations = dotationRepository.findAll(spec); + + model.addAttribute("dotations", dotations); + model.addAttribute("equipements", equipementRepository.findAll()); + model.addAttribute("categories", categorieRepository.findAll()); + model.addAttribute("equipementId", equipementId); + model.addAttribute("categorieId", categorieId); + model.addAttribute("fourni", fourni); + + return "parametrage/equipements_recherche"; + } + + @GetMapping("/admin/equipements/recherche/export") + public ResponseEntity exportRechercheEquipements( + @RequestParam(required = false) Long equipementId, + @RequestParam(required = false) Long categorieId, + @RequestParam(required = false) Boolean fourni) { + + Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null); + Specification spec = buildSpecification(equipementId, categorieId, fourni, saisonActive); + List dotations = dotationRepository.findAll(spec); + + String csvContent = dotationService.genererCsvSearchEquipement(dotations); + byte[] csvBytes = csvContent.getBytes(java.nio.charset.StandardCharsets.UTF_8); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentDispositionFormData("attachment", "recherche_equipements_" + LocalDate.now() + ".csv"); + headers.setContentType(MediaType.parseMediaType("text/csv; charset=UTF-8")); + + return new ResponseEntity<>(csvBytes, headers, org.springframework.http.HttpStatus.OK); } @GetMapping("/admin/equipements/export-commande") diff --git a/as-talange-web/src/main/java/com/astalange/web/controller/EquipementController.java b/as-talange-web/src/main/java/com/astalange/web/controller/EquipementController.java index f529da0..dd89b94 100644 --- a/as-talange-web/src/main/java/com/astalange/web/controller/EquipementController.java +++ b/as-talange-web/src/main/java/com/astalange/web/controller/EquipementController.java @@ -22,7 +22,12 @@ public class EquipementController { @GetMapping public String listEquipements(Model model) { - model.addAttribute("equipements", equipementRepository.findAll()); + Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null); + if (activeSaison != null) { + model.addAttribute("equipements", equipementRepository.findBySaison(activeSaison)); + } else { + model.addAttribute("equipements", java.util.List.of()); + } return "parametrage/equipements_list"; } diff --git a/as-talange-web/src/main/java/com/astalange/web/controller/LicenceController.java b/as-talange-web/src/main/java/com/astalange/web/controller/LicenceController.java index fa1f215..607e35c 100644 --- a/as-talange-web/src/main/java/com/astalange/web/controller/LicenceController.java +++ b/as-talange-web/src/main/java/com/astalange/web/controller/LicenceController.java @@ -21,6 +21,7 @@ public class LicenceController { private final DotationRepository dotationRepository; private final SaisonRepository saisonRepository; private final EquipeRepository equipeRepository; + private final com.astalange.core.service.CategorieService categorieService; public LicenceController(AdherentRepository adherentRepository, CategorieRepository categorieRepository, @@ -28,7 +29,8 @@ public class LicenceController { EquipementRepository equipementRepository, DotationRepository dotationRepository, SaisonRepository saisonRepository, - EquipeRepository equipeRepository) { + EquipeRepository equipeRepository, + com.astalange.core.service.CategorieService categorieService) { this.adherentRepository = adherentRepository; this.categorieRepository = categorieRepository; this.licenceRepository = licenceRepository; @@ -36,6 +38,7 @@ public class LicenceController { this.dotationRepository = dotationRepository; this.saisonRepository = saisonRepository; this.equipeRepository = equipeRepository; + this.categorieService = categorieService; } @PostMapping("/adherents/{id}/licences") @@ -129,8 +132,125 @@ public class LicenceController { licence.setEquipe(null); } - licenceRepository.save(licence); + licence = licenceRepository.save(licence); + + // Sync dotations when category changes + categorieService.syncDotationsForLicence(licence); return "redirect:/adherents/" + adherentId + "/edit"; } + + @org.springframework.web.bind.annotation.GetMapping("/admin/licences/recherche") + public String rechercheLicences( + @RequestParam(required = false) String nom, + @RequestParam(required = false) String prenom, + @RequestParam(required = false) Long categorieId, + @RequestParam(required = false) String numeroLicence, + @RequestParam(required = false) String email, + @RequestParam(required = false) String typeDemande, + org.springframework.ui.Model model) { + + Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null); + org.springframework.data.jpa.domain.Specification spec = buildLicenceSpecification(nom, prenom, categorieId, numeroLicence, email, typeDemande, saisonActive); + + List licences = licenceRepository.findAll(spec); + + model.addAttribute("licences", licences); + model.addAttribute("categories", categorieRepository.findAll()); + model.addAttribute("nomFilter", nom); + model.addAttribute("prenomFilter", prenom); + model.addAttribute("categorieId", categorieId); + model.addAttribute("numeroLicenceFilter", numeroLicence); + model.addAttribute("emailFilter", email); + model.addAttribute("typeDemandeFilter", typeDemande); + + return "parametrage/licences_recherche"; + } + + @org.springframework.web.bind.annotation.GetMapping("/admin/licences/recherche/export") + public org.springframework.http.ResponseEntity exportRechercheLicences( + @RequestParam(required = false) String nom, + @RequestParam(required = false) String prenom, + @RequestParam(required = false) Long categorieId, + @RequestParam(required = false) String numeroLicence, + @RequestParam(required = false) String email, + @RequestParam(required = false) String typeDemande) { + + Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null); + org.springframework.data.jpa.domain.Specification spec = buildLicenceSpecification(nom, prenom, categorieId, numeroLicence, email, typeDemande, saisonActive); + List licences = licenceRepository.findAll(spec); + + StringBuilder sb = new StringBuilder(); + sb.append('\ufeff'); + sb.append("Nom;Prénom;Catégorie;N° Licence;Email;Type de Demande;Saison;Etat\n"); + + for (Licence l : licences) { + String n = l.getAdherent() != null && l.getAdherent().getNom() != null ? l.getAdherent().getNom() : ""; + String p = l.getAdherent() != null && l.getAdherent().getPrenom() != null ? l.getAdherent().getPrenom() : ""; + String c = l.getCategorie() != null ? l.getCategorie().getNom() : ""; + String num = l.getNumeroLicence() != null ? l.getNumeroLicence() : ""; + String e = l.getAdherent() != null && l.getAdherent().getEmail() != null ? l.getAdherent().getEmail() : ""; + String td = l.getTypeDemande() != null ? l.getTypeDemande() : ""; + String s = l.getSaison() != null ? l.getSaison().getNom() : ""; + String etat = l.getEtat() != null ? l.getEtat() : ""; + + sb.append(escapeCsv(n)).append(";") + .append(escapeCsv(p)).append(";") + .append(escapeCsv(c)).append(";") + .append(escapeCsv(num)).append(";") + .append(escapeCsv(e)).append(";") + .append(escapeCsv(td)).append(";") + .append(escapeCsv(s)).append(";") + .append(escapeCsv(etat)).append("\n"); + } + + byte[] csvBytes = sb.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders(); + headers.setContentDispositionFormData("attachment", "recherche_licences_" + java.time.LocalDate.now() + ".csv"); + headers.setContentType(org.springframework.http.MediaType.parseMediaType("text/csv; charset=UTF-8")); + + return new org.springframework.http.ResponseEntity<>(csvBytes, headers, org.springframework.http.HttpStatus.OK); + } + + private String escapeCsv(String value) { + if (value == null) return ""; + if (value.contains("\"")) value = value.replace("\"", "\"\""); + if (value.contains(";") || value.contains("\n") || value.contains("\"")) return "\"" + value + "\""; + return value; + } + + private org.springframework.data.jpa.domain.Specification buildLicenceSpecification( + String nom, String prenom, Long categorieId, String numeroLicence, String email, String typeDemande, Saison saisonActive) { + return (root, query, cb) -> { + List predicates = new java.util.ArrayList<>(); + + if (Long.class != query.getResultType() && long.class != query.getResultType()) { + root.fetch("adherent", jakarta.persistence.criteria.JoinType.LEFT); + root.fetch("categorie", jakarta.persistence.criteria.JoinType.LEFT); + } + + if (saisonActive != null) { + predicates.add(cb.equal(root.get("saison"), saisonActive)); + } + if (nom != null && !nom.trim().isEmpty()) { + predicates.add(cb.like(cb.lower(root.get("adherent").get("nom")), "%" + nom.trim().toLowerCase() + "%")); + } + if (prenom != null && !prenom.trim().isEmpty()) { + predicates.add(cb.like(cb.lower(root.get("adherent").get("prenom")), "%" + prenom.trim().toLowerCase() + "%")); + } + if (categorieId != null) { + predicates.add(cb.equal(root.get("categorie").get("id"), categorieId)); + } + if (numeroLicence != null && !numeroLicence.trim().isEmpty()) { + predicates.add(cb.like(cb.lower(root.get("numeroLicence")), "%" + numeroLicence.trim().toLowerCase() + "%")); + } + if (email != null && !email.trim().isEmpty()) { + predicates.add(cb.like(cb.lower(root.get("adherent").get("email")), "%" + email.trim().toLowerCase() + "%")); + } + if (typeDemande != null && !typeDemande.trim().isEmpty()) { + predicates.add(cb.equal(root.get("typeDemande"), typeDemande.trim())); + } + return cb.and(predicates.toArray(new jakarta.persistence.criteria.Predicate[0])); + }; + } } diff --git a/as-talange-web/src/main/resources/templates/parametrage/equipements_recherche.html b/as-talange-web/src/main/resources/templates/parametrage/equipements_recherche.html new file mode 100644 index 0000000..0b3dec0 --- /dev/null +++ b/as-talange-web/src/main/resources/templates/parametrage/equipements_recherche.html @@ -0,0 +1,98 @@ + + + + + + Paramétrage - Recherche Équipements + + + + + + +
+ +
+
+

Recherche Équipements

+
+ +
+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + Exporter +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
LicenciéPoste / SexeCatégorieÉquipementTaille / Floc.Fourni
Aucun résultat trouvé pour cette recherche.
+ Nom Prénom + +
Joueur
+
Masculin
+
CatégorieMaillot +
Taille
+
Flocage
+
+
+ Oui + Non +
+
+ +
+
+ +