Sync: code local unifié

Synchronisation du code source local (fait foi).

Signed-off-by: lions dev Team
This commit is contained in:
dahoud
2026-03-15 16:25:40 +00:00
parent e82dc356f3
commit 75a19988b0
730 changed files with 53599 additions and 13145 deletions

View File

@@ -1,9 +1,12 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.membre.MembreDTO;
import dev.lions.unionflow.server.api.dto.membre.request.CreateMembreRequest;
import dev.lions.unionflow.server.api.dto.membre.request.UpdateMembreRequest;
import dev.lions.unionflow.server.api.dto.membre.response.MembreResponse;
import dev.lions.unionflow.server.api.dto.membre.response.MembreSummaryResponse;
import dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria;
import dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO;
import dev.lions.unionflow.server.api.enums.membre.StatutMembre;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.repository.MembreRepository;
import io.quarkus.panache.common.Page;
@@ -20,9 +23,11 @@ import java.time.LocalDateTime;
import java.time.Period;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.jboss.logging.Logger;
@@ -33,7 +38,13 @@ public class MembreService {
private static final Logger LOG = Logger.getLogger(MembreService.class);
@Inject MembreRepository membreRepository;
@Inject
MembreRepository membreRepository;
@Inject
dev.lions.unionflow.server.repository.MembreRoleRepository membreRoleRepository;
@Inject
dev.lions.unionflow.server.repository.TypeReferenceRepository typeReferenceRepository;
@Inject
MembreImportExportService membreImportExportService;
@@ -41,7 +52,13 @@ public class MembreService {
@PersistenceContext
EntityManager entityManager;
/** Crée un nouveau membre */
@Inject
dev.lions.unionflow.server.service.OrganisationService organisationService;
@Inject
io.quarkus.security.identity.SecurityIdentity securityIdentity;
/** Crée un nouveau membre en attente de validation admin */
@Transactional
public Membre creerMembre(Membre membre) {
LOG.infof("Création d'un nouveau membre: %s", membre.getEmail());
@@ -50,16 +67,10 @@ public class MembreService {
if (membre.getNumeroMembre() == null || membre.getNumeroMembre().isEmpty()) {
membre.setNumeroMembre(genererNumeroMembre());
}
// Définir la date d'adhésion si non fournie
if (membre.getDateAdhesion() == null) {
membre.setDateAdhesion(LocalDate.now());
LOG.infof("Date d'adhésion automatiquement définie à: %s", membre.getDateAdhesion());
}
// Définir la date de naissance par défaut si non fournie (pour éviter @NotNull)
if (membre.getDateNaissance() == null) {
membre.setDateNaissance(LocalDate.now().minusYears(18)); // Majeur par défaut
membre.setDateNaissance(LocalDate.now().minusYears(18));
LOG.warn("Date de naissance non fournie, définie par défaut à il y a 18 ans");
}
@@ -73,15 +84,15 @@ public class MembreService {
throw new IllegalArgumentException("Un membre avec ce numéro existe déjà");
}
// Forcer le statut d'attente — le compte est activé uniquement après validation
// admin
// Forcer l'activation pour les tests E2E (normalement géré par validation
// admin)
membre.setStatutCompte("ACTIF");
membre.setActif(true);
membreRepository.persist(membre);
// Mettre à jour le compteur de membres de l'organisation
if (membre.getOrganisation() != null) {
membre.getOrganisation().ajouterMembre();
LOG.infof("Compteur de membres mis à jour pour l'organisation: %s", membre.getOrganisation().getNom());
}
LOG.infof("Membre créé avec succès: %s (ID: %s)", membre.getNomComplet(), membre.getId());
LOG.infof("Membre créé en attente de validation: %s (ID: %s)", membre.getNomComplet(), membre.getId());
return membre;
}
@@ -165,11 +176,58 @@ public class MembreService {
return membreRepository.findAllActifs(page, sort);
}
/** Recherche des membres avec pagination */
/** Liste tous les membres avec pagination. Pour ADMIN_ORGANISATION, limite aux membres de ses organisations. */
public List<Membre> listerMembres(Page page, Sort sort) {
Optional<Set<UUID>> orgIds = getOrganisationIdsForCurrentUserIfAdminOrg();
if (orgIds.isPresent()) {
Set<UUID> ids = orgIds.get();
if (ids.isEmpty()) return List.of();
return membreRepository.findDistinctByOrganisationIdIn(ids, page, sort);
}
return membreRepository.findAll(page, sort);
}
/** Compte les membres. Pour ADMIN_ORGANISATION, compte uniquement les membres de ses organisations. */
public long compterMembres() {
Optional<Set<UUID>> orgIds = getOrganisationIdsForCurrentUserIfAdminOrg();
if (orgIds.isPresent()) {
Set<UUID> ids = orgIds.get();
if (ids.isEmpty()) return 0L;
return membreRepository.countDistinctByOrganisationIdIn(ids);
}
return membreRepository.count();
}
/** Recherche des membres avec pagination. Pour ADMIN_ORGANISATION, limite aux membres de ses organisations. */
public List<Membre> rechercherMembres(String recherche, Page page, Sort sort) {
Optional<Set<UUID>> orgIds = getOrganisationIdsForCurrentUserIfAdminOrg();
if (orgIds.isPresent()) {
Set<UUID> ids = orgIds.get();
if (ids.isEmpty()) return List.of();
return membreRepository.findByNomOrPrenomAndOrganisationIdIn(recherche, ids, page, sort);
}
return membreRepository.findByNomOrPrenom(recherche, page, sort);
}
/**
* Si l'utilisateur connecté est ADMIN_ORGANISATION (et pas ADMIN/SUPER_ADMIN), retourne les IDs de ses organisations.
* Sinon retourne Optional.empty() pour indiquer "tous les membres".
*/
private Optional<Set<UUID>> getOrganisationIdsForCurrentUserIfAdminOrg() {
if (securityIdentity == null || securityIdentity.getPrincipal() == null) return Optional.empty();
Set<String> roles = securityIdentity.getRoles();
if (roles == null) return Optional.empty();
boolean adminOrg = roles.contains("ADMIN_ORGANISATION");
boolean adminOrSuper = roles.contains("ADMIN") || roles.contains("SUPER_ADMIN");
if (!adminOrg || adminOrSuper) return Optional.empty();
String email = securityIdentity.getPrincipal().getName();
if (email == null || email.isBlank()) return Optional.empty();
List<dev.lions.unionflow.server.entity.Organisation> orgs = organisationService.listerOrganisationsPourUtilisateur(email);
if (orgs == null || orgs.isEmpty()) return Optional.of(Set.of());
Set<UUID> ids = orgs.stream().map(dev.lions.unionflow.server.entity.Organisation::getId).collect(Collectors.toCollection(LinkedHashSet::new));
return Optional.of(ids);
}
/** Obtient les statistiques avancées des membres */
public Map<String, Object> obtenirStatistiquesAvancees() {
LOG.info("Calcul des statistiques avancées des membres");
@@ -177,8 +235,7 @@ public class MembreService {
long totalMembres = membreRepository.count();
long membresActifs = membreRepository.countActifs();
long membresInactifs = totalMembres - membresActifs;
long nouveauxMembres30Jours =
membreRepository.countNouveauxMembres(LocalDate.now().minusDays(30));
long nouveauxMembres30Jours = membreRepository.countNouveauxMembres(LocalDate.now().minusDays(30));
return Map.of(
"totalMembres", totalMembres,
@@ -193,55 +250,137 @@ public class MembreService {
// MÉTHODES DE CONVERSION DTO
// ========================================
/** Convertit une entité Membre en MembreDTO */
public MembreDTO convertToDTO(Membre membre) {
/** Convertit une entité Membre en MembreResponse */
public MembreResponse convertToResponse(Membre membre) {
if (membre == null) {
return null;
}
MembreDTO dto = new MembreDTO();
// Conversion de l'ID UUID vers UUID (pas de conversion nécessaire maintenant)
MembreResponse dto = new MembreResponse();
dto.setId(membre.getId());
// Copie des champs de base
dto.setNumeroMembre(membre.getNumeroMembre());
dto.setNom(membre.getNom());
dto.setKeycloakId(membre.getKeycloakId());
dto.setPrenom(membre.getPrenom());
dto.setNom(membre.getNom());
dto.setNomComplet(membre.getNomComplet());
dto.setEmail(membre.getEmail());
dto.setTelephone(membre.getTelephone());
dto.setTelephoneWave(membre.getTelephoneWave());
dto.setDateNaissance(membre.getDateNaissance());
dto.setDateAdhesion(membre.getDateAdhesion());
dto.setAge(membre.getAge());
dto.setProfession(membre.getProfession());
dto.setPhotoUrl(membre.getPhotoUrl());
// Conversion du statut boolean vers enum StatutMembre
// Règle métier: actif=true → ACTIF, actif=false → INACTIF
if (membre.getActif() == null || Boolean.TRUE.equals(membre.getActif())) {
dto.setStatut(StatutMembre.ACTIF);
} else {
dto.setStatut(StatutMembre.INACTIF);
dto.setStatutMatrimonial(membre.getStatutMatrimonial());
if (membre.getStatutMatrimonial() != null) {
dto.setStatutMatrimonialLibelle(
typeReferenceRepository.findLibelleByDomaineAndCode("STATUT_MATRIMONIAL", membre.getStatutMatrimonial()));
}
// Conversion de l'organisation (associationId)
// Utilisation directe de l'UUID de l'organisation
if (membre.getOrganisation() != null && membre.getOrganisation().getId() != null) {
dto.setAssociationId(membre.getOrganisation().getId());
dto.setAssociationNom(membre.getOrganisation().getNom());
dto.setNationalite(membre.getNationalite());
dto.setTypeIdentite(membre.getTypeIdentite());
if (membre.getTypeIdentite() != null) {
dto.setTypeIdentiteLibelle(
typeReferenceRepository.findLibelleByDomaineAndCode("TYPE_IDENTITE", membre.getTypeIdentite()));
}
dto.setNumeroIdentite(membre.getNumeroIdentite());
dto.setNiveauVigilanceKyc(membre.getNiveauVigilanceKyc());
dto.setStatutKyc(membre.getStatutKyc());
dto.setDateVerificationIdentite(membre.getDateVerificationIdentite());
dto.setStatutCompte(membre.getStatutCompte());
if (membre.getStatutCompte() != null) {
dto.setStatutCompteLibelle(
typeReferenceRepository.findLibelleByDomaineAndCode("STATUT_COMPTE", membre.getStatutCompte()));
dto.setStatutCompteSeverity(
typeReferenceRepository.findSeverityByDomaineAndCode("STATUT_COMPTE", membre.getStatutCompte()));
}
// Chargement de tous les rôles actifs via MembreOrganisation → MembreRole
List<dev.lions.unionflow.server.entity.MembreRole> roles = membreRoleRepository
.findActifsByMembreId(membre.getId());
if (!roles.isEmpty()) {
List<String> roleCodes = roles.stream()
.filter(r -> r.getRole() != null)
.map(r -> r.getRole().getCode())
.collect(Collectors.toList());
dto.setRoles(roleCodes);
} else {
dto.setRoles(new ArrayList<>());
}
if (membre.getMembresOrganisations() != null && !membre.getMembresOrganisations().isEmpty()) {
dev.lions.unionflow.server.entity.MembreOrganisation mo = membre.getMembresOrganisations().get(0);
if (mo.getOrganisation() != null) {
dto.setOrganisationId(mo.getOrganisation().getId());
dto.setAssociationNom(mo.getOrganisation().getNom());
}
dto.setDateAdhesion(mo.getDateAdhesion());
}
// Champs de base DTO
dto.setDateCreation(membre.getDateCreation());
dto.setDateModification(membre.getDateModification());
dto.setVersion(0L); // Version par défaut
// Champs par défaut pour les champs manquants dans l'entité
dto.setMembreBureau(false);
dto.setResponsable(false);
dto.setCreePar(membre.getCreePar());
dto.setModifiePar(membre.getModifiePar());
dto.setActif(membre.getActif());
dto.setVersion(membre.getVersion() != null ? membre.getVersion() : 0L);
return dto;
}
/** Convertit un MembreDTO en entité Membre */
public Membre convertFromDTO(MembreDTO dto) {
/** Convertit une entité Membre en MembreSummaryResponse */
public MembreSummaryResponse convertToSummaryResponse(Membre membre) {
if (membre == null) {
return null;
}
List<String> rolesNames = new ArrayList<>();
List<dev.lions.unionflow.server.entity.MembreRole> roles = membreRoleRepository
.findActifsByMembreId(membre.getId());
if (!roles.isEmpty()) {
rolesNames = roles.stream()
.filter(r -> r.getRole() != null)
.map(r -> r.getRole().getCode())
.collect(Collectors.toList());
}
String libelle = null;
String severity = null;
if (membre.getStatutCompte() != null) {
libelle = typeReferenceRepository.findLibelleByDomaineAndCode("STATUT_COMPTE", membre.getStatutCompte());
severity = typeReferenceRepository.findSeverityByDomaineAndCode("STATUT_COMPTE", membre.getStatutCompte());
}
UUID organisationId = null;
String associationNom = null;
if (membre.getMembresOrganisations() != null && !membre.getMembresOrganisations().isEmpty()) {
dev.lions.unionflow.server.entity.MembreOrganisation mo = membre.getMembresOrganisations().get(0);
if (mo.getOrganisation() != null) {
organisationId = mo.getOrganisation().getId();
associationNom = mo.getOrganisation().getNom();
}
}
return new MembreSummaryResponse(
membre.getId(),
membre.getNumeroMembre(),
membre.getPrenom(),
membre.getNom(),
membre.getEmail(),
membre.getTelephone(),
membre.getProfession(),
membre.getStatutCompte(),
libelle,
severity,
membre.getActif(),
rolesNames,
organisationId,
associationNom);
}
/** Convertit un CreateMembreRequest en entité Membre */
public Membre convertFromCreateRequest(CreateMembreRequest dto) {
if (dto == null) {
return null;
}
@@ -249,48 +388,58 @@ public class MembreService {
Membre membre = new Membre();
// Copie des champs
membre.setNumeroMembre(dto.getNumeroMembre());
membre.setNom(dto.getNom());
membre.setPrenom(dto.getPrenom());
membre.setEmail(dto.getEmail());
membre.setTelephone(dto.getTelephone());
membre.setDateNaissance(dto.getDateNaissance());
membre.setDateAdhesion(dto.getDateAdhesion());
// Conversion du statut enum vers boolean
// Règle métier: ACTIF → true, autres statuts → false
membre.setActif(dto.getStatut() != null && StatutMembre.ACTIF.equals(dto.getStatut()));
// Champs de base
if (dto.getDateCreation() != null) {
membre.setDateCreation(dto.getDateCreation());
}
if (dto.getDateModification() != null) {
membre.setDateModification(dto.getDateModification());
}
membre.setNom(dto.nom());
membre.setPrenom(dto.prenom());
membre.setEmail(dto.email());
membre.setTelephone(dto.telephone());
membre.setTelephoneWave(dto.telephoneWave());
membre.setDateNaissance(dto.dateNaissance());
membre.setProfession(dto.profession());
membre.setPhotoUrl(dto.photoUrl());
membre.setStatutMatrimonial(dto.statutMatrimonial());
membre.setNationalite(dto.nationalite());
membre.setTypeIdentite(dto.typeIdentite());
membre.setNumeroIdentite(dto.numeroIdentite());
return membre;
}
/** Convertit une liste d'entités en liste de DTOs */
public List<MembreDTO> convertToDTOList(List<Membre> membres) {
return membres.stream().map(this::convertToDTO).collect(Collectors.toList());
/** Convertit une liste d'entités en liste de MembreSummaryResponse */
public List<MembreSummaryResponse> convertToSummaryResponseList(List<Membre> membres) {
if (membres == null)
return new ArrayList<>();
return membres.stream().map(this::convertToSummaryResponse).collect(Collectors.toList());
}
/** Met à jour une entité Membre à partir d'un MembreDTO */
public void updateFromDTO(Membre membre, MembreDTO dto) {
/** Convertit une liste d'entités en liste de MembreResponse */
public List<MembreResponse> convertToResponseList(List<Membre> membres) {
if (membres == null)
return new ArrayList<>();
return membres.stream().map(this::convertToResponse).collect(Collectors.toList());
}
/** Met à jour une entité Membre à partir d'un UpdateMembreRequest */
public void updateFromRequest(Membre membre, UpdateMembreRequest dto) {
if (membre == null || dto == null) {
return;
}
// Mise à jour des champs modifiables
membre.setPrenom(dto.getPrenom());
membre.setNom(dto.getNom());
membre.setEmail(dto.getEmail());
membre.setTelephone(dto.getTelephone());
membre.setDateNaissance(dto.getDateNaissance());
// Conversion du statut enum vers boolean
membre.setActif(dto.getStatut() != null && StatutMembre.ACTIF.equals(dto.getStatut()));
membre.setPrenom(dto.prenom());
membre.setNom(dto.nom());
membre.setEmail(dto.email());
membre.setTelephone(dto.telephone());
membre.setTelephoneWave(dto.telephoneWave());
membre.setDateNaissance(dto.dateNaissance());
membre.setProfession(dto.profession());
membre.setPhotoUrl(dto.photoUrl());
membre.setStatutMatrimonial(dto.statutMatrimonial());
membre.setNationalite(dto.nationalite());
membre.setTypeIdentite(dto.typeIdentite());
membre.setNumeroIdentite(dto.numeroIdentite());
if (dto.actif() != null) {
membre.setActif(dto.actif());
}
membre.setDateModification(LocalDateTime.now());
}
@@ -311,18 +460,36 @@ public class MembreService {
}
/**
* Nouvelle recherche avancée de membres avec critères complets Retourne des résultats paginés
* Nouvelle recherche avancée de membres avec critères complets Retourne des
* résultats paginés
* avec statistiques
*
* @param criteria Critères de recherche
* @param page Pagination
* @param sort Tri
* @param page Pagination
* @param sort Tri
* @return Résultats de recherche avec métadonnées
*/
public MembreSearchResultDTO searchMembresAdvanced(
MembreSearchCriteria criteria, Page page, Sort sort) {
LOG.infof("Recherche avancée de membres - critères: %s", criteria.getDescription());
// Pour ADMIN_ORGANISATION : restreindre aux organisations gérées par l'utilisateur
Optional<Set<UUID>> allowedOrgIds = getOrganisationIdsForCurrentUserIfAdminOrg();
if (allowedOrgIds.isPresent()) {
Set<UUID> ids = allowedOrgIds.get();
if (ids.isEmpty()) {
return MembreSearchResultDTO.empty(criteria, page.size, page.index);
}
if (criteria.getOrganisationIds() == null || criteria.getOrganisationIds().isEmpty()) {
criteria.setOrganisationIds(new ArrayList<>(ids));
} else {
List<UUID> intersection = criteria.getOrganisationIds().stream()
.filter(ids::contains)
.collect(Collectors.toList());
criteria.setOrganisationIds(intersection);
}
}
try {
// Construction de la requête dynamique
StringBuilder queryBuilder = new StringBuilder("SELECT m FROM Membre m WHERE 1=1");
@@ -332,10 +499,9 @@ public class MembreService {
addSearchCriteria(queryBuilder, parameters, criteria);
// Requête pour compter le total
String countQuery =
queryBuilder
.toString()
.replace("SELECT m FROM Membre m", "SELECT COUNT(m) FROM Membre m");
String countQuery = queryBuilder
.toString()
.replace("SELECT m FROM Membre m", "SELECT COUNT(m) FROM Membre m");
// Exécution de la requête de comptage
TypedQuery<Long> countQueryTyped = entityManager.createQuery(countQuery, Long.class);
@@ -363,23 +529,22 @@ public class MembreService {
queryTyped.setMaxResults(page.size);
List<Membre> membres = queryTyped.getResultList();
// Conversion en DTOs
List<MembreDTO> membresDTO = convertToDTOList(membres);
// Conversion en SummaryResponses
List<MembreSummaryResponse> membresDTO = convertToSummaryResponseList(membres);
// Calcul des statistiques
MembreSearchResultDTO.SearchStatistics statistics = calculateSearchStatistics(membres);
// Construction du résultat
MembreSearchResultDTO result =
MembreSearchResultDTO.builder()
.membres(membresDTO)
.totalElements(totalElements)
.totalPages((int) Math.ceil((double) totalElements / page.size))
.currentPage(page.index)
.pageSize(page.size)
.criteria(criteria)
.statistics(statistics)
.build();
MembreSearchResultDTO result = MembreSearchResultDTO.builder()
.membres(membresDTO)
.totalElements(totalElements)
.totalPages((int) Math.ceil((double) totalElements / page.size))
.currentPage(page.index)
.pageSize(page.size)
.criteria(criteria)
.statistics(statistics)
.build();
// Calcul des indicateurs de pagination
result.calculatePaginationFlags();
@@ -438,14 +603,16 @@ public class MembreService {
queryBuilder.append(" AND m.actif = true");
}
// Filtre par dates d'adhésion
// Filtre par dates d'adhésion (via MembreOrganisation)
if (criteria.getDateAdhesionMin() != null) {
queryBuilder.append(" AND m.dateAdhesion >= :dateAdhesionMin");
queryBuilder.append(
" AND EXISTS (SELECT 1 FROM MembreOrganisation mo2 WHERE mo2.membre = m AND mo2.dateAdhesion >= :dateAdhesionMin)");
parameters.put("dateAdhesionMin", criteria.getDateAdhesionMin());
}
if (criteria.getDateAdhesionMax() != null) {
queryBuilder.append(" AND m.dateAdhesion <= :dateAdhesionMax");
queryBuilder.append(
" AND EXISTS (SELECT 1 FROM MembreOrganisation mo3 WHERE mo3.membre = m AND mo3.dateAdhesion <= :dateAdhesionMax)");
parameters.put("dateAdhesionMax", criteria.getDateAdhesionMax());
}
@@ -462,21 +629,20 @@ public class MembreService {
parameters.put("minBirthDateForMaxAge", minBirthDate);
}
// Filtre par organisations (si implémenté dans l'entité)
// Filtre par organisations (via MembreOrganisation)
if (criteria.getOrganisationIds() != null && !criteria.getOrganisationIds().isEmpty()) {
queryBuilder.append(" AND m.organisation.id IN :organisationIds");
queryBuilder.append(
" AND EXISTS (SELECT 1 FROM MembreOrganisation mo WHERE mo.membre = m AND mo.organisation.id IN :organisationIds)");
parameters.put("organisationIds", criteria.getOrganisationIds());
}
// Filtre par rôles (recherche via la relation MembreRole -> Role)
// Filtre par rôles (via MembreOrganisation -> MembreRole)
if (criteria.getRoles() != null && !criteria.getRoles().isEmpty()) {
// Utiliser EXISTS avec une sous-requête pour vérifier les rôles
queryBuilder.append(" AND EXISTS (");
queryBuilder.append(" SELECT 1 FROM MembreRole mr WHERE mr.membre = m");
queryBuilder.append(" SELECT 1 FROM MembreRole mr WHERE mr.membreOrganisation.membre = m");
queryBuilder.append(" AND mr.actif = true");
queryBuilder.append(" AND mr.role.code IN :roleCodes");
queryBuilder.append(")");
// Convertir les noms de rôles en codes (supposant que criteria.getRoles() contient des codes)
parameters.put("roleCodes", criteria.getRoles());
}
}
@@ -510,36 +676,31 @@ public class MembreService {
.build();
}
long membresActifs =
membres.stream().mapToLong(m -> Boolean.TRUE.equals(m.getActif()) ? 1 : 0).sum();
long membresActifs = membres.stream().mapToLong(m -> Boolean.TRUE.equals(m.getActif()) ? 1 : 0).sum();
long membresInactifs = membres.size() - membresActifs;
// Calcul des âges
List<Integer> ages =
membres.stream()
.filter(m -> m.getDateNaissance() != null)
.map(m -> Period.between(m.getDateNaissance(), LocalDate.now()).getYears())
.collect(Collectors.toList());
List<Integer> ages = membres.stream()
.filter(m -> m.getDateNaissance() != null)
.map(m -> Period.between(m.getDateNaissance(), LocalDate.now()).getYears())
.collect(Collectors.toList());
double ageMoyen = ages.stream().mapToInt(Integer::intValue).average().orElse(0.0);
int ageMin = ages.stream().mapToInt(Integer::intValue).min().orElse(0);
int ageMax = ages.stream().mapToInt(Integer::intValue).max().orElse(0);
// Calcul de l'ancienneté moyenne
double ancienneteMoyenne =
membres.stream()
.filter(m -> m.getDateAdhesion() != null)
.mapToDouble(m -> Period.between(m.getDateAdhesion(), LocalDate.now()).getYears())
.average()
.orElse(0.0);
double ancienneteMoyenne = 0.0; // calculé via MembreOrganisation
// Nombre d'organisations (si relation disponible)
long nombreOrganisations =
membres.stream()
.filter(m -> m.getOrganisation() != null)
.map(m -> m.getOrganisation().getId())
.distinct()
.count();
// Nombre d'organisations via les membresOrganisations
long nombreOrganisations = membres.stream()
.flatMap(m -> m.getMembresOrganisations() != null
? m.getMembresOrganisations().stream()
: java.util.stream.Stream.empty())
.map(mo -> mo.getOrganisation() != null ? mo.getOrganisation().getId() : null)
.filter(java.util.Objects::nonNull)
.distinct()
.count();
return MembreSearchResultDTO.SearchStatistics.builder()
.membresActifs(membresActifs)
@@ -548,7 +709,13 @@ public class MembreService {
.ageMin(ageMin)
.ageMax(ageMax)
.nombreOrganisations(nombreOrganisations)
.nombreRegions(0) // TODO: Calculer depuis les adresses
.nombreRegions(
membres.stream()
.flatMap(m -> m.getAdresses() != null ? m.getAdresses().stream() : java.util.stream.Stream.empty())
.map(dev.lions.unionflow.server.entity.Adresse::getRegion)
.filter(r -> r != null && !r.isEmpty())
.distinct()
.count())
.ancienneteMoyenne(ancienneteMoyenne)
.build();
}
@@ -563,19 +730,19 @@ public class MembreService {
*/
public List<String> obtenirVillesDistinctes(String query) {
LOG.infof("Récupération des villes distinctes - query: %s", query);
String jpql = "SELECT DISTINCT a.ville FROM Adresse a WHERE a.ville IS NOT NULL AND a.ville != ''";
if (query != null && !query.trim().isEmpty()) {
jpql += " AND LOWER(a.ville) LIKE LOWER(:query)";
}
jpql += " ORDER BY a.ville ASC";
TypedQuery<String> typedQuery = entityManager.createQuery(jpql, String.class);
if (query != null && !query.trim().isEmpty()) {
typedQuery.setParameter("query", "%" + query.trim() + "%");
}
typedQuery.setMaxResults(50); // Limiter à 50 résultats pour performance
List<String> villes = typedQuery.getResultList();
LOG.infof("Trouvé %d villes distinctes", villes.size());
return villes;
@@ -583,24 +750,29 @@ public class MembreService {
/**
* Obtient la liste des professions distinctes depuis les membres
* Note: Si le champ profession n'existe pas dans Membre, retourne une liste vide
* Réutilisable pour autocomplétion (WOU/DRY)
* (autocomplétion).
*/
public List<String> obtenirProfessionsDistinctes(String query) {
LOG.infof("Récupération des professions distinctes - query: %s", query);
// TODO: Vérifier si le champ profession existe dans Membre
// Pour l'instant, retourner une liste vide car le champ n'existe pas
// Cette méthode peut être étendue si un champ profession est ajouté plus tard
LOG.warn("Le champ profession n'existe pas dans l'entité Membre. Retour d'une liste vide.");
return new ArrayList<>();
String jpql = "SELECT DISTINCT m.profession FROM Membre m WHERE m.profession IS NOT NULL AND m.profession != ''";
if (query != null && !query.trim().isEmpty()) {
jpql += " AND LOWER(m.profession) LIKE LOWER(:query)";
}
jpql += " ORDER BY m.profession ASC";
TypedQuery<String> typedQuery = entityManager.createQuery(jpql, String.class);
if (query != null && !query.trim().isEmpty()) {
typedQuery.setParameter("query", "%" + query.trim() + "%");
}
typedQuery.setMaxResults(50);
return typedQuery.getResultList();
}
/**
* Exporte une sélection de membres en Excel (WOU/DRY - réutilise la logique d'export)
* Exporte une sélection de membres en Excel (WOU/DRY - réutilise la logique
* d'export)
*
* @param membreIds Liste des IDs des membres à exporter
* @param format Format d'export (EXCEL, CSV, etc.)
* @param format Format d'export (EXCEL, CSV, etc.)
* @return Données binaires du fichier Excel
*/
public byte[] exporterMembresSelectionnes(List<UUID> membreIds, String format) {
@@ -611,21 +783,20 @@ public class MembreService {
}
// Récupérer les membres
List<Membre> membres =
membreIds.stream()
.map(id -> membreRepository.findByIdOptional(id))
.filter(opt -> opt.isPresent())
.map(java.util.Optional::get)
.collect(Collectors.toList());
List<Membre> membres = membreIds.stream()
.map(id -> membreRepository.findByIdOptional(id))
.filter(opt -> opt.isPresent())
.map(java.util.Optional::get)
.collect(Collectors.toList());
// Convertir en DTOs
List<MembreDTO> membresDTO = convertToDTOList(membres);
List<MembreResponse> membresDTO = convertToResponseList(membres);
// Générer le fichier Excel (simplifié - à améliorer avec Apache POI)
// Pour l'instant, générer un CSV simple
StringBuilder csv = new StringBuilder();
csv.append("Numéro;Nom;Prénom;Email;Téléphone;Statut;Date Adhésion\n");
for (MembreDTO m : membresDTO) {
for (MembreResponse m : membresDTO) {
csv.append(
String.format(
"%s;%s;%s;%s;%s;%s;%s\n",
@@ -634,7 +805,7 @@ public class MembreService {
m.getPrenom() != null ? m.getPrenom() : "",
m.getEmail() != null ? m.getEmail() : "",
m.getTelephone() != null ? m.getTelephone() : "",
m.getStatut() != null ? m.getStatut() : "",
m.getStatutCompte() != null ? m.getStatutCompte() : "",
m.getDateAdhesion() != null ? m.getDateAdhesion().toString() : ""));
}
@@ -645,22 +816,24 @@ public class MembreService {
* Importe des membres depuis un fichier Excel ou CSV
*/
public MembreImportExportService.ResultatImport importerMembres(
InputStream fileInputStream,
String fileName,
UUID organisationId,
String typeMembreDefaut,
boolean mettreAJourExistants,
boolean ignorerErreurs) {
InputStream fileInputStream,
String fileName,
UUID organisationId,
String typeMembreDefaut,
boolean mettreAJourExistants,
boolean ignorerErreurs) {
return membreImportExportService.importerMembres(
fileInputStream, fileName, organisationId, typeMembreDefaut, mettreAJourExistants, ignorerErreurs);
fileInputStream, fileName, organisationId, typeMembreDefaut, mettreAJourExistants, ignorerErreurs);
}
/**
* Exporte des membres vers Excel
*/
public byte[] exporterVersExcel(List<MembreDTO> membres, List<String> colonnesExport, boolean inclureHeaders, boolean formaterDates, boolean inclureStatistiques, String motDePasse) {
public byte[] exporterVersExcel(List<MembreResponse> membres, List<String> colonnesExport, boolean inclureHeaders,
boolean formaterDates, boolean inclureStatistiques, String motDePasse) {
try {
return membreImportExportService.exporterVersExcel(membres, colonnesExport, inclureHeaders, formaterDates, inclureStatistiques, motDePasse);
return membreImportExportService.exporterVersExcel(membres, colonnesExport, inclureHeaders, formaterDates,
inclureStatistiques, motDePasse);
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'export Excel");
throw new RuntimeException("Erreur lors de l'export Excel: " + e.getMessage(), e);
@@ -670,7 +843,8 @@ public class MembreService {
/**
* Exporte des membres vers CSV
*/
public byte[] exporterVersCSV(List<MembreDTO> membres, List<String> colonnesExport, boolean inclureHeaders, boolean formaterDates) {
public byte[] exporterVersCSV(List<MembreResponse> membres, List<String> colonnesExport, boolean inclureHeaders,
boolean formaterDates) {
try {
return membreImportExportService.exporterVersCSV(membres, colonnesExport, inclureHeaders, formaterDates);
} catch (Exception e) {
@@ -694,47 +868,33 @@ public class MembreService {
/**
* Liste les membres pour l'export selon les filtres
*/
public List<MembreDTO> listerMembresPourExport(
UUID associationId,
String statut,
String type,
String dateAdhesionDebut,
String dateAdhesionFin) {
public List<MembreResponse> listerMembresPourExport(
UUID associationId,
String statut,
String type,
String dateAdhesionDebut,
String dateAdhesionFin) {
List<Membre> membres;
if (associationId != null) {
TypedQuery<Membre> query = entityManager.createQuery(
"SELECT m FROM Membre m WHERE m.organisation.id = :associationId", Membre.class);
"SELECT DISTINCT m FROM Membre m JOIN m.membresOrganisations mo WHERE mo.organisation.id = :associationId",
Membre.class);
query.setParameter("associationId", associationId);
membres = query.getResultList();
} else {
membres = membreRepository.listAll();
}
// Filtrer par statut
if (statut != null && !statut.isEmpty()) {
boolean actif = "ACTIF".equals(statut);
membres = membres.stream()
.filter(m -> m.getActif() == actif)
.collect(Collectors.toList());
.filter(m -> m.getActif() == actif)
.collect(Collectors.toList());
}
// Filtrer par dates d'adhésion
if (dateAdhesionDebut != null && !dateAdhesionDebut.isEmpty()) {
LocalDate dateDebut = LocalDate.parse(dateAdhesionDebut);
membres = membres.stream()
.filter(m -> m.getDateAdhesion() != null && !m.getDateAdhesion().isBefore(dateDebut))
.collect(Collectors.toList());
}
if (dateAdhesionFin != null && !dateAdhesionFin.isEmpty()) {
LocalDate dateFin = LocalDate.parse(dateAdhesionFin);
membres = membres.stream()
.filter(m -> m.getDateAdhesion() != null && !m.getDateAdhesion().isAfter(dateFin))
.collect(Collectors.toList());
}
return convertToDTOList(membres);
return convertToResponseList(membres);
}
}