Files
as-talange/as-talange-web/src/main/java/com/astalange/web/controller/AdherentController.java
T

438 lines
24 KiB
Java

package com.astalange.web.controller;
import com.astalange.core.entity.Adherent;
import com.astalange.core.entity.Categorie;
import com.astalange.core.entity.Licence;
import com.astalange.core.entity.Saison;
import com.astalange.core.repository.AdherentRepository;
import com.astalange.core.repository.CategorieRepository;
import com.astalange.core.repository.SaisonRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@Controller
@RequestMapping("/adherents")
public class AdherentController {
private final AdherentRepository adherentRepository;
private final CategorieRepository categorieRepository;
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;
private final com.astalange.core.service.SportEasyEmailService sportEasyEmailService;
private final com.astalange.core.service.RelanceEmailService relanceEmailService;
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, com.astalange.core.service.SportEasyEmailService sportEasyEmailService, com.astalange.core.service.RelanceEmailService relanceEmailService) {
this.adherentRepository = adherentRepository;
this.categorieRepository = categorieRepository;
this.licenceRepository = licenceRepository;
this.modePaiementRepository = modePaiementRepository;
this.saisonRepository = saisonRepository;
this.categorieService = categorieService;
this.sportEasyEmailService = sportEasyEmailService;
this.relanceEmailService = relanceEmailService;
}
@org.springframework.web.bind.annotation.ModelAttribute("equipes")
public List<com.astalange.core.entity.Equipe> populateEquipes() {
return List.of();
}
@GetMapping
public String listAdherents(
@org.springframework.web.bind.annotation.RequestParam(required = false) String query,
@org.springframework.web.bind.annotation.RequestParam(required = false) String nom,
@org.springframework.web.bind.annotation.RequestParam(required = false) String prenom,
@org.springframework.web.bind.annotation.RequestParam(required = false) String categorie,
@org.springframework.web.bind.annotation.RequestParam(required = false) String equipements,
@org.springframework.web.bind.annotation.RequestParam(required = false) String licence,
@org.springframework.web.bind.annotation.RequestParam(required = false) String email,
@org.springframework.web.bind.annotation.RequestParam(required = false) String paiement,
@org.springframework.web.bind.annotation.RequestParam(required = false) String sporteasy,
@org.springframework.web.bind.annotation.RequestParam(required = false, defaultValue = "nom") String sortField,
@org.springframework.web.bind.annotation.RequestParam(required = false, defaultValue = "asc") String sortDirection,
@org.springframework.web.bind.annotation.RequestParam(defaultValue = "0") int page,
@org.springframework.web.bind.annotation.RequestParam(defaultValue = "20") int size,
Model model) {
List<Adherent> adherents = adherentRepository.findAllWithLicencesAndCategories();
// 1. Filter by global query
if (query != null && !query.trim().isEmpty()) {
String q = query.trim().toLowerCase();
adherents = adherents.stream().filter(a ->
(a.getNom() != null && a.getNom().toLowerCase().contains(q)) ||
(a.getPrenom() != null && a.getPrenom().toLowerCase().contains(q)) ||
(a.getLicenceActuelle() != null && a.getLicenceActuelle().getNumeroLicence() != null && a.getLicenceActuelle().getNumeroLicence().toLowerCase().contains(q))
).collect(java.util.stream.Collectors.toList());
model.addAttribute("searchQuery", query.trim());
}
// 2. Filter by specific column filters
if (nom != null && !nom.trim().isEmpty()) {
String n = nom.trim().toLowerCase();
adherents = adherents.stream().filter(a -> a.getNom() != null && a.getNom().toLowerCase().contains(n)).collect(java.util.stream.Collectors.toList());
model.addAttribute("nomFilter", nom.trim());
}
if (prenom != null && !prenom.trim().isEmpty()) {
String p = prenom.trim().toLowerCase();
adherents = adherents.stream().filter(a -> a.getPrenom() != null && a.getPrenom().toLowerCase().contains(p)).collect(java.util.stream.Collectors.toList());
model.addAttribute("prenomFilter", prenom.trim());
}
if (categorie != null && !categorie.trim().isEmpty()) {
adherents = adherents.stream().filter(a ->
a.getLicenceActuelle() != null &&
a.getLicenceActuelle().getCategorie() != null &&
a.getLicenceActuelle().getCategorie().getNom().equalsIgnoreCase(categorie.trim())
).collect(java.util.stream.Collectors.toList());
model.addAttribute("categorieFilter", categorie.trim());
}
if (equipements != null && !equipements.trim().isEmpty()) {
String eqFilter = equipements.trim();
adherents = adherents.stream().filter(a -> {
Licence lic = a.getLicenceActuelle();
if (lic == null || lic.getDotations() == null || lic.getDotations().isEmpty()) {
return false;
}
List<com.astalange.core.entity.Dotation> choisis = lic.getDotations().stream()
.filter(d -> Boolean.TRUE.equals(d.getChoisi()))
.collect(java.util.stream.Collectors.toList());
if (choisis.isEmpty()) {
return false;
}
long total = choisis.size();
long fournis = choisis.stream().filter(d -> Boolean.TRUE.equals(d.getFourni())).count();
if ("TOUT_FOURNI".equalsIgnoreCase(eqFilter)) {
return fournis == total;
} else if ("PARTIEL".equalsIgnoreCase(eqFilter)) {
return fournis > 0 && fournis < total;
} else if ("NON_FOURNI".equalsIgnoreCase(eqFilter)) {
return fournis == 0;
}
return true;
}).collect(java.util.stream.Collectors.toList());
model.addAttribute("equipementsFilter", eqFilter);
}
if (licence != null && !licence.trim().isEmpty()) {
String l = licence.trim().toLowerCase();
adherents = adherents.stream().filter(a ->
a.getLicenceActuelle() != null &&
a.getLicenceActuelle().getNumeroLicence() != null &&
a.getLicenceActuelle().getNumeroLicence().toLowerCase().contains(l)
).collect(java.util.stream.Collectors.toList());
model.addAttribute("licenceFilter", licence.trim());
}
if (email != null && !email.trim().isEmpty()) {
String e = email.trim().toLowerCase();
adherents = adherents.stream().filter(a -> a.getEmail() != null && a.getEmail().toLowerCase().contains(e)).collect(java.util.stream.Collectors.toList());
model.addAttribute("emailFilter", email.trim());
}
if (paiement != null && !paiement.trim().isEmpty()) {
adherents = adherents.stream().filter(a -> {
String statut = a.getStatutPaiement();
if ("A_JOUR".equalsIgnoreCase(paiement)) {
return "À jour".equalsIgnoreCase(statut);
} else if ("RESTE".equalsIgnoreCase(paiement)) {
return statut != null && statut.startsWith("Reste");
} else if ("AUCUNE".equalsIgnoreCase(paiement)) {
return "Aucune licence".equalsIgnoreCase(statut);
}
return true;
}).collect(java.util.stream.Collectors.toList());
model.addAttribute("paiementFilter", paiement.trim());
}
if (sporteasy != null && !sporteasy.trim().isEmpty()) {
adherents = adherents.stream().filter(a -> {
Licence lic = a.getLicenceActuelle();
if (lic == null) return false;
if ("NON_INVITE".equalsIgnoreCase(sporteasy)) {
return !lic.isRenouvellement() && !Boolean.TRUE.equals(lic.getSportEasyEmailSent());
} else if ("INVITE".equalsIgnoreCase(sporteasy)) {
return !lic.isRenouvellement() && Boolean.TRUE.equals(lic.getSportEasyEmailSent());
} else if ("RENOUVELLEMENT".equalsIgnoreCase(sporteasy)) {
return lic.isRenouvellement();
}
return true;
}).collect(java.util.stream.Collectors.toList());
model.addAttribute("sporteasyFilter", sporteasy.trim());
}
// 3. Sort
java.util.Comparator<Adherent> comparator = (a1, a2) -> 0;
if ("nom".equalsIgnoreCase(sortField)) {
comparator = java.util.Comparator.comparing(a -> a.getNom() != null ? a.getNom() : "");
} else if ("prenom".equalsIgnoreCase(sortField)) {
comparator = java.util.Comparator.comparing(a -> a.getPrenom() != null ? a.getPrenom() : "");
} else if ("categorie".equalsIgnoreCase(sortField)) {
comparator = java.util.Comparator.comparing(a -> (a.getLicenceActuelle() != null && a.getLicenceActuelle().getCategorie() != null && a.getLicenceActuelle().getCategorie().getNom() != null) ? a.getLicenceActuelle().getCategorie().getNom() : "");
} else if ("licence".equalsIgnoreCase(sortField)) {
comparator = java.util.Comparator.comparing(a -> (a.getLicenceActuelle() != null && a.getLicenceActuelle().getNumeroLicence() != null) ? a.getLicenceActuelle().getNumeroLicence() : "");
} else if ("email".equalsIgnoreCase(sortField)) {
comparator = java.util.Comparator.comparing(a -> a.getEmail() != null ? a.getEmail() : "");
}
if ("desc".equalsIgnoreCase(sortDirection)) {
comparator = comparator.reversed();
}
adherents.sort(comparator);
model.addAttribute("sortField", sortField);
model.addAttribute("sortDirection", sortDirection);
// 4. Paginate
int totalElements = adherents.size();
int totalPages = (int) Math.ceil((double) totalElements / size);
if (page < 0) page = 0;
if (page >= totalPages && totalPages > 0) page = totalPages - 1;
int start = page * size;
int end = Math.min(start + size, totalElements);
List<Adherent> pageContent = (start < totalElements) ? adherents.subList(start, end) : List.of();
model.addAttribute("adherents", pageContent);
model.addAttribute("currentPage", page);
model.addAttribute("totalPages", totalPages);
model.addAttribute("totalElements", totalElements);
model.addAttribute("pageSize", size);
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
if (saisonActive != null) {
model.addAttribute("categories", categorieRepository.findBySaison(saisonActive));
} else {
model.addAttribute("categories", categorieRepository.findAll());
}
return "adherents/list";
}
@GetMapping("/new")
public String showCreateForm(Model model) {
model.addAttribute("adherent", new Adherent());
return "adherents/form";
}
@org.springframework.web.bind.annotation.PostMapping
public String saveAdherent(
@jakarta.validation.Valid @org.springframework.web.bind.annotation.ModelAttribute("adherent") Adherent adherent,
org.springframework.validation.BindingResult result,
Model model) {
if (result.hasErrors()) {
if (adherent.getId() != null) {
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
if (saisonActive != null) {
model.addAttribute("categories", categorieRepository.findBySaison(saisonActive));
model.addAttribute("saisonActive", saisonActive);
} else {
model.addAttribute("categories", categorieRepository.findAll());
}
model.addAttribute("licences", licenceRepository.findByAdherentId(adherent.getId()));
model.addAttribute("modesPaiement", modePaiementRepository.findAll());
} else {
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
if (saisonActive != null) {
model.addAttribute("categories", categorieRepository.findBySaison(saisonActive));
} else {
model.addAttribute("categories", categorieRepository.findAll());
}
}
return "adherents/form";
}
if (adherent.getId() != null) {
Adherent existing = adherentRepository.findById(adherent.getId()).orElse(null);
if (existing != null) {
boolean dateChanged = !existing.getDateNaissance().equals(adherent.getDateNaissance());
boolean typeMaillotChanged = !existing.getTypeMaillot().equals(adherent.getTypeMaillot());
if (dateChanged || typeMaillotChanged) {
int newBirthYear = adherent.getDateNaissance().getYear();
List<Licence> licences = licenceRepository.findByAdherentId(adherent.getId());
for (Licence licence : licences) {
if (dateChanged) {
Saison saison = licence.getSaison();
Categorie newCat = categorieRepository.findBySaison(saison).stream()
.filter(c -> newBirthYear >= c.getAnneeMin() && newBirthYear <= c.getAnneeMax())
.findFirst()
.orElse(null);
if (newCat != null) {
licence.setCategorie(newCat);
licence = licenceRepository.save(licence);
}
}
// We need to update the entity's state before syncing dotations so it reads the new typeMaillot.
// We haven't updated 'existing' yet, so let's temporarily set it on the licence's adherent or wait.
// Actually, 'adherent' has the new typeMaillot.
licence.getAdherent().setTypeMaillot(adherent.getTypeMaillot());
categorieService.syncDotationsForLicence(licence);
}
}
existing.setNom(adherent.getNom());
existing.setPrenom(adherent.getPrenom());
existing.setDateNaissance(adherent.getDateNaissance());
existing.setLieuNaissanceVille(adherent.getLieuNaissanceVille());
existing.setLieuNaissancePays(adherent.getLieuNaissancePays());
existing.setNationalite(adherent.getNationalite());
existing.setEmail(adherent.getEmail());
existing.setTelephone(adherent.getTelephone());
existing.setRepresentantLegal(adherent.getRepresentantLegal());
existing.setResidentTalange(adherent.isResidentTalange());
existing.setSexe(adherent.getSexe());
existing.setTypeMaillot(adherent.getTypeMaillot());
existing.setTailleVetement(adherent.getTailleVetement());
existing.setPointure(adherent.getPointure());
adherentRepository.save(existing);
List<Licence> licences = licenceRepository.findByAdherentId(existing.getId());
for (Licence licence : licences) {
categorieService.syncDotationsForLicence(licence);
}
} else {
adherentRepository.save(adherent);
}
return "redirect:/adherents";
} else {
Adherent saved = adherentRepository.save(adherent);
return "redirect:/adherents/" + saved.getId() + "/edit";
}
}
@GetMapping("/{id}/edit")
public String editAdherent(@org.springframework.web.bind.annotation.PathVariable Long id, Model model) {
Adherent adherent = adherentRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Adhérent invalide : " + id));
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
if (saisonActive != null) {
model.addAttribute("categories", categorieRepository.findBySaison(saisonActive));
model.addAttribute("saisonActive", saisonActive);
} else {
model.addAttribute("categories", categorieRepository.findAll());
}
model.addAttribute("adherent", adherent);
model.addAttribute("licences", licenceRepository.findByAdherentId(id));
model.addAttribute("modesPaiement", modePaiementRepository.findAll());
return "adherents/form";
}
@org.springframework.web.bind.annotation.PostMapping("/{id}/delete")
public String deleteAdherent(@org.springframework.web.bind.annotation.PathVariable Long id) {
Adherent adherent = adherentRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Adhérent invalide : " + id));
adherentRepository.delete(adherent);
return "redirect:/adherents";
}
@org.springframework.web.bind.annotation.PostMapping("/{id}/sporteasy-invite")
public String sendSportEasyInvite(
@org.springframework.web.bind.annotation.PathVariable Long id,
@org.springframework.web.bind.annotation.RequestParam(required = false, defaultValue = "false") boolean force,
org.springframework.web.servlet.mvc.support.RedirectAttributes redirectAttributes) {
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
if (activeSaison == null) {
redirectAttributes.addFlashAttribute("errorMessage", "Erreur : Aucune saison active trouvée.");
return "redirect:/adherents";
}
Licence licence = licenceRepository.findByAdherentIdAndSaison(id, activeSaison).orElse(null);
if (licence == null) {
redirectAttributes.addFlashAttribute("errorMessage", "Cet adhérent n'a pas de licence pour la saison active.");
return "redirect:/adherents";
}
if (licence.isRenouvellement()) {
redirectAttributes.addFlashAttribute("infoMessage", "Les invitations SportEasy sont réservées aux nouveaux adhérents (nouvelle licence).");
return "redirect:/adherents";
}
try {
boolean success = sportEasyEmailService.sendInvitationForLicence(licence.getId(), force);
if (success) {
redirectAttributes.addFlashAttribute("successMessage", "L'invitation SportEasy a été envoyée avec succès à l'adhérent.");
} else {
redirectAttributes.addFlashAttribute("infoMessage", "L'invitation SportEasy avait déjà été envoyée à cet adhérent.");
}
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Erreur lors de l'envoi de l'invitation : " + e.getMessage());
}
return "redirect:/adherents";
}
@org.springframework.web.bind.annotation.PostMapping("/sporteasy-invite-batch")
public String sendSportEasyInviteBatch(
@org.springframework.web.bind.annotation.RequestParam(required = false) Long categoryId,
org.springframework.web.servlet.mvc.support.RedirectAttributes redirectAttributes) {
try {
com.astalange.core.service.SportEasyEmailService.BatchSendResult result =
sportEasyEmailService.sendBatchInvitationsForActiveSaison(categoryId);
redirectAttributes.addFlashAttribute("successMessage", result.message());
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Erreur lors de l'envoi des invitations SportEasy : " + e.getMessage());
}
return "redirect:/adherents";
}
@org.springframework.web.bind.annotation.PostMapping("/{id}/relancer-paiement")
public String relancerPaiement(
@org.springframework.web.bind.annotation.PathVariable Long id,
org.springframework.web.servlet.mvc.support.RedirectAttributes redirectAttributes) {
Adherent adherent = adherentRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Adhérent invalide : " + id));
Licence licence = adherent.getLicenceActuelle();
if (licence == null) {
redirectAttributes.addFlashAttribute("errorMessage", "Cet adhérent n'a pas de licence enregistrée.");
return "redirect:/adherents";
}
if (licence.getResteAPayer().compareTo(java.math.BigDecimal.ZERO) <= 0) {
redirectAttributes.addFlashAttribute("infoMessage", "Cet adhérent est déjà à jour de sa cotisation.");
return "redirect:/adherents";
}
boolean sent = relanceEmailService.sendRelancePaiementCotisation(licence);
if (sent) {
redirectAttributes.addFlashAttribute("successMessage", "L'e-mail de relance de paiement pour " + adherent.getPrenom() + " " + adherent.getNom() + " a été envoyé avec succès.");
} else {
redirectAttributes.addFlashAttribute("errorMessage", "Impossible d'envoyer l'e-mail de relance (adresse e-mail manquante).");
}
return "redirect:/adherents";
}
@org.springframework.web.bind.annotation.PostMapping("/{id}/relancer-paiement-htmx")
@org.springframework.web.bind.annotation.ResponseBody
public String relancerPaiementHtmx(@org.springframework.web.bind.annotation.PathVariable Long id) {
Adherent adherent = adherentRepository.findById(id).orElse(null);
if (adherent == null || adherent.getLicenceActuelle() == null) {
return "<span class=\"text-xs text-red-600 font-medium bg-red-50 px-2.5 py-1 rounded-full border border-red-200\">Introuvable</span>";
}
Licence licence = adherent.getLicenceActuelle();
if (licence.getResteAPayer().compareTo(java.math.BigDecimal.ZERO) <= 0) {
return "<span class=\"text-xs text-green-700 font-medium bg-green-50 px-2.5 py-1 rounded-full border border-green-200\">Déjà réglé</span>";
}
boolean sent = relanceEmailService.sendRelancePaiementCotisation(licence);
if (sent) {
return "<span class=\"text-xs text-amber-700 font-medium bg-amber-50 px-2 py-0.5 rounded-full border border-amber-200 inline-flex items-center gap-1\"><svg class=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 002-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"/></svg> Relance envoyée</span>";
} else {
return "<span class=\"text-xs text-red-600 font-medium bg-red-50 px-2 py-0.5 rounded-full border border-red-200\">Email manquant</span>";
}
}
}