Alignement design systeme OK

This commit is contained in:
DahoudG
2025-09-20 03:56:11 +00:00
parent a1214bc116
commit 96a17eadbd
34 changed files with 11720 additions and 766 deletions

View File

@@ -1,10 +1,13 @@
package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.api.dto.membre.MembreDTO;
import dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria;
import dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.service.MembreService;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
@@ -12,8 +15,15 @@ import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.ExampleObject;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.logging.Logger;
@@ -186,8 +196,9 @@ public class MembreResource {
@GET
@Path("/recherche-avancee")
@Operation(summary = "Recherche avancée de membres avec filtres multiples")
@Operation(summary = "Recherche avancée de membres avec filtres multiples (DEPRECATED)")
@APIResponse(responseCode = "200", description = "Résultats de la recherche avancée")
@Deprecated
public Response rechercheAvancee(
@Parameter(description = "Terme de recherche") @QueryParam("q") String recherche,
@Parameter(description = "Statut actif (true/false)") @QueryParam("actif") Boolean actif,
@@ -198,7 +209,7 @@ public class MembreResource {
@Parameter(description = "Champ de tri") @QueryParam("sort") @DefaultValue("nom") String sortField,
@Parameter(description = "Direction du tri (asc/desc)") @QueryParam("direction") @DefaultValue("asc") String sortDirection) {
LOG.infof("Recherche avancée de membres - recherche: %s, actif: %s", recherche, actif);
LOG.infof("Recherche avancée de membres (DEPRECATED) - recherche: %s, actif: %s", recherche, actif);
try {
Sort sort = "desc".equalsIgnoreCase(sortDirection) ?
@@ -222,4 +233,178 @@ public class MembreResource {
.build();
}
}
/**
* Nouvelle recherche avancée avec critères complets et résultats enrichis
* Réservée aux super administrateurs pour des recherches sophistiquées
*/
@POST
@Path("/search/advanced")
@RolesAllowed({"SUPER_ADMIN", "ADMIN"})
@Operation(
summary = "Recherche avancée de membres avec critères multiples",
description = """
Recherche sophistiquée de membres avec de nombreux critères de filtrage :
- Recherche textuelle dans nom, prénom, email
- Filtres par organisation, rôles, statut
- Filtres par âge, région, profession
- Filtres par dates d'adhésion
- Résultats paginés avec statistiques
Réservée aux super administrateurs et administrateurs.
"""
)
@APIResponses({
@APIResponse(
responseCode = "200",
description = "Recherche effectuée avec succès",
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = MembreSearchResultDTO.class),
examples = @ExampleObject(
name = "Exemple de résultats",
value = """
{
"membres": [...],
"totalElements": 247,
"totalPages": 13,
"currentPage": 0,
"pageSize": 20,
"hasNext": true,
"hasPrevious": false,
"executionTimeMs": 45,
"statistics": {
"membresActifs": 230,
"membresInactifs": 17,
"ageMoyen": 34.5,
"nombreOrganisations": 12
}
}
"""
)
)
),
@APIResponse(
responseCode = "400",
description = "Critères de recherche invalides",
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
examples = @ExampleObject(
value = """
{
"message": "Critères de recherche invalides",
"details": "La date minimum ne peut pas être postérieure à la date maximum"
}
"""
)
)
),
@APIResponse(
responseCode = "403",
description = "Accès non autorisé - Rôle SUPER_ADMIN ou ADMIN requis"
),
@APIResponse(
responseCode = "500",
description = "Erreur interne du serveur"
)
})
@SecurityRequirement(name = "keycloak")
public Response searchMembresAdvanced(
@RequestBody(
description = "Critères de recherche avancée",
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = MembreSearchCriteria.class),
examples = @ExampleObject(
name = "Exemple de critères",
value = """
{
"query": "marie",
"statut": "ACTIF",
"ageMin": 25,
"ageMax": 45,
"region": "Dakar",
"roles": ["PRESIDENT", "SECRETAIRE"],
"dateAdhesionMin": "2020-01-01",
"includeInactifs": false
}
"""
)
)
)
@Valid MembreSearchCriteria criteria,
@Parameter(description = "Numéro de page (0-based)", example = "0")
@QueryParam("page") @DefaultValue("0") int page,
@Parameter(description = "Taille de la page", example = "20")
@QueryParam("size") @DefaultValue("20") int size,
@Parameter(description = "Champ de tri", example = "nom")
@QueryParam("sort") @DefaultValue("nom") String sortField,
@Parameter(description = "Direction du tri (asc/desc)", example = "asc")
@QueryParam("direction") @DefaultValue("asc") String sortDirection) {
long startTime = System.currentTimeMillis();
LOG.infof("Recherche avancée de membres - critères: %s, page: %d, size: %d",
criteria.getDescription(), page, size);
try {
// Validation des critères
if (criteria == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("message", "Les critères de recherche sont requis"))
.build();
}
// Nettoyage et validation des critères
criteria.sanitize();
if (!criteria.hasAnyCriteria()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("message", "Au moins un critère de recherche doit être spécifié"))
.build();
}
if (!criteria.isValid()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of(
"message", "Critères de recherche invalides",
"details", "Vérifiez la cohérence des dates et des âges"
))
.build();
}
// Construction du tri
Sort sort = "desc".equalsIgnoreCase(sortDirection) ?
Sort.by(sortField).descending() : Sort.by(sortField).ascending();
// Exécution de la recherche
MembreSearchResultDTO result = membreService.searchMembresAdvanced(
criteria, Page.of(page, size), sort);
// Calcul du temps d'exécution
long executionTime = System.currentTimeMillis() - startTime;
result.setExecutionTimeMs(executionTime);
LOG.infof("Recherche avancée terminée - %d résultats trouvés en %d ms",
result.getTotalElements(), executionTime);
return Response.ok(result).build();
} catch (IllegalArgumentException e) {
LOG.warnf("Erreur de validation dans la recherche avancée: %s", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("message", "Paramètres de recherche invalides", "details", e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche avancée de membres");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("message", "Erreur interne lors de la recherche", "error", e.getMessage()))
.build();
}
}
}

View File

@@ -1,6 +1,8 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.membre.MembreDTO;
import dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria;
import dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.repository.MembreRepository;
import io.quarkus.panache.common.Page;
@@ -12,6 +14,9 @@ import org.jboss.logging.Logger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -285,14 +290,240 @@ public class MembreService {
}
/**
* Recherche avancée de membres avec filtres multiples
* Recherche avancée de membres avec filtres multiples (DEPRECATED)
*/
public List<Membre> rechercheAvancee(String recherche, Boolean actif,
LocalDate dateAdhesionMin, LocalDate dateAdhesionMax,
Page page, Sort sort) {
LOG.infof("Recherche avancée - recherche: %s, actif: %s, dateMin: %s, dateMax: %s",
LOG.infof("Recherche avancée (DEPRECATED) - recherche: %s, actif: %s, dateMin: %s, dateMax: %s",
recherche, actif, dateAdhesionMin, dateAdhesionMax);
return membreRepository.rechercheAvancee(recherche, actif, dateAdhesionMin, dateAdhesionMax, page, sort);
}
/**
* 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
* @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());
try {
// Construction de la requête dynamique
StringBuilder queryBuilder = new StringBuilder("SELECT m FROM Membre m WHERE 1=1");
Map<String, Object> parameters = new HashMap<>();
// Ajout des critères de recherche
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");
// Exécution de la requête de comptage
long totalElements = Membre.find(countQuery, parameters).count();
if (totalElements == 0) {
return MembreSearchResultDTO.empty(criteria);
}
// Ajout du tri et pagination
String finalQuery = queryBuilder.toString();
if (sort != null) {
finalQuery += " ORDER BY " + buildOrderByClause(sort);
}
// Exécution de la requête principale
List<Membre> membres = Membre.find(finalQuery, parameters)
.page(page)
.list();
// Conversion en DTOs
List<MembreDTO> membresDTO = convertToDTOList(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();
// Calcul des indicateurs de pagination
result.calculatePaginationFlags();
return result;
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche avancée de membres");
throw new RuntimeException("Erreur lors de la recherche avancée", e);
}
}
/**
* Ajoute les critères de recherche à la requête
*/
private void addSearchCriteria(StringBuilder queryBuilder, Map<String, Object> parameters, MembreSearchCriteria criteria) {
// Recherche générale dans nom, prénom, email
if (criteria.getQuery() != null) {
queryBuilder.append(" AND (LOWER(m.nom) LIKE LOWER(:query) OR LOWER(m.prenom) LIKE LOWER(:query) OR LOWER(m.email) LIKE LOWER(:query))");
parameters.put("query", "%" + criteria.getQuery() + "%");
}
// Recherche par nom
if (criteria.getNom() != null) {
queryBuilder.append(" AND LOWER(m.nom) LIKE LOWER(:nom)");
parameters.put("nom", "%" + criteria.getNom() + "%");
}
// Recherche par prénom
if (criteria.getPrenom() != null) {
queryBuilder.append(" AND LOWER(m.prenom) LIKE LOWER(:prenom)");
parameters.put("prenom", "%" + criteria.getPrenom() + "%");
}
// Recherche par email
if (criteria.getEmail() != null) {
queryBuilder.append(" AND LOWER(m.email) LIKE LOWER(:email)");
parameters.put("email", "%" + criteria.getEmail() + "%");
}
// Recherche par téléphone
if (criteria.getTelephone() != null) {
queryBuilder.append(" AND m.telephone LIKE :telephone");
parameters.put("telephone", "%" + criteria.getTelephone() + "%");
}
// Filtre par statut
if (criteria.getStatut() != null) {
boolean isActif = "ACTIF".equals(criteria.getStatut());
queryBuilder.append(" AND m.actif = :actif");
parameters.put("actif", isActif);
} else if (!Boolean.TRUE.equals(criteria.getIncludeInactifs())) {
// Par défaut, exclure les inactifs
queryBuilder.append(" AND m.actif = true");
}
// Filtre par dates d'adhésion
if (criteria.getDateAdhesionMin() != null) {
queryBuilder.append(" AND m.dateAdhesion >= :dateAdhesionMin");
parameters.put("dateAdhesionMin", criteria.getDateAdhesionMin());
}
if (criteria.getDateAdhesionMax() != null) {
queryBuilder.append(" AND m.dateAdhesion <= :dateAdhesionMax");
parameters.put("dateAdhesionMax", criteria.getDateAdhesionMax());
}
// Filtre par âge (calculé à partir de la date de naissance)
if (criteria.getAgeMin() != null) {
LocalDate maxBirthDate = LocalDate.now().minusYears(criteria.getAgeMin());
queryBuilder.append(" AND m.dateNaissance <= :maxBirthDateForMinAge");
parameters.put("maxBirthDateForMinAge", maxBirthDate);
}
if (criteria.getAgeMax() != null) {
LocalDate minBirthDate = LocalDate.now().minusYears(criteria.getAgeMax() + 1).plusDays(1);
queryBuilder.append(" AND m.dateNaissance >= :minBirthDateForMaxAge");
parameters.put("minBirthDateForMaxAge", minBirthDate);
}
// Filtre par organisations (si implémenté dans l'entité)
if (criteria.getOrganisationIds() != null && !criteria.getOrganisationIds().isEmpty()) {
queryBuilder.append(" AND m.organisation.id IN :organisationIds");
parameters.put("organisationIds", criteria.getOrganisationIds());
}
// Filtre par rôles (recherche dans le champ roles)
if (criteria.getRoles() != null && !criteria.getRoles().isEmpty()) {
StringBuilder roleCondition = new StringBuilder(" AND (");
for (int i = 0; i < criteria.getRoles().size(); i++) {
if (i > 0) roleCondition.append(" OR ");
roleCondition.append("m.roles LIKE :role").append(i);
parameters.put("role" + i, "%" + criteria.getRoles().get(i) + "%");
}
roleCondition.append(")");
queryBuilder.append(roleCondition);
}
}
/**
* Construit la clause ORDER BY à partir du Sort
*/
private String buildOrderByClause(Sort sort) {
if (sort == null || sort.getColumns().isEmpty()) {
return "m.nom ASC";
}
return sort.getColumns().stream()
.map(column -> "m." + column.getName() + " " + column.getDirection().name())
.collect(Collectors.joining(", "));
}
/**
* Calcule les statistiques sur les résultats de recherche
*/
private MembreSearchResultDTO.SearchStatistics calculateSearchStatistics(List<Membre> membres) {
if (membres.isEmpty()) {
return MembreSearchResultDTO.SearchStatistics.builder()
.membresActifs(0)
.membresInactifs(0)
.ageMoyen(0.0)
.ageMin(0)
.ageMax(0)
.nombreOrganisations(0)
.nombreRegions(0)
.ancienneteMoyenne(0.0)
.build();
}
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());
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);
// Nombre d'organisations (si relation disponible)
long nombreOrganisations = membres.stream()
.filter(m -> m.getOrganisation() != null)
.map(m -> m.getOrganisation().id)
.distinct()
.count();
return MembreSearchResultDTO.SearchStatistics.builder()
.membresActifs(membresActifs)
.membresInactifs(membresInactifs)
.ageMoyen(ageMoyen)
.ageMin(ageMin)
.ageMax(ageMax)
.nombreOrganisations(nombreOrganisations)
.nombreRegions(0) // À implémenter si champ région disponible
.ancienneteMoyenne(ancienneteMoyenne)
.build();
}
}

View File

@@ -32,7 +32,7 @@ quarkus.flyway.baseline-on-migrate=true
quarkus.flyway.baseline-version=1.0.0
# Configuration Keycloak OIDC
quarkus.oidc.auth-server-url=http://192.168.1.145:8180/realms/unionflow
quarkus.oidc.auth-server-url=http://192.168.1.11:8180/realms/unionflow
quarkus.oidc.client-id=unionflow-server
quarkus.oidc.credentials.secret=unionflow-secret-2025
quarkus.oidc.tls.verification=none
@@ -85,7 +85,7 @@ quarkus.log.category."io.quarkus".level=INFO
# Configuration Keycloak pour développement (temporairement désactivé)
%dev.quarkus.oidc.tenant-enabled=false
%dev.quarkus.oidc.auth-server-url=http://192.168.1.145:8180/realms/unionflow
%dev.quarkus.oidc.auth-server-url=http://192.168.1.11:8180/realms/unionflow
%dev.quarkus.oidc.client-id=unionflow-server
%dev.quarkus.oidc.credentials.secret=unionflow-secret-2025
%dev.quarkus.oidc.tls.verification=none
@@ -114,7 +114,7 @@ quarkus.log.category."io.quarkus".level=INFO
%prod.quarkus.log.category.root.level=WARN
# Configuration Keycloak pour production
%prod.quarkus.oidc.auth-server-url=${KEYCLOAK_SERVER_URL:http://192.168.1.145:8180/realms/unionflow}
%prod.quarkus.oidc.auth-server-url=${KEYCLOAK_SERVER_URL:http://192.168.1.11:8180/realms/unionflow}
%prod.quarkus.oidc.client-id=${KEYCLOAK_CLIENT_ID:unionflow-server}
%prod.quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET}
%prod.quarkus.oidc.tls.verification=required