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,16 +1,23 @@
package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.api.dto.membre.MembreDTO;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
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.entity.Membre;
import dev.lions.unionflow.server.service.MembreKeycloakSyncService;
import dev.lions.unionflow.server.service.MembreService;
import dev.lions.unionflow.server.service.MembreSuiviService;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
@@ -32,6 +39,7 @@ 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;
import org.jboss.resteasy.reactive.RestForm;
/** Resource REST pour la gestion des membres */
@Path("/api/membres")
@@ -39,38 +47,43 @@ import org.jboss.logging.Logger;
@Consumes(MediaType.APPLICATION_JSON)
@ApplicationScoped
@Tag(name = "Membres", description = "API de gestion des membres")
@Transactional
public class MembreResource {
private static final Logger LOG = Logger.getLogger(MembreResource.class);
@Inject MembreService membreService;
@Inject
MembreService membreService;
@Inject
MembreKeycloakSyncService keycloakSyncService;
@Inject
MembreSuiviService membreSuiviService;
@Inject
io.quarkus.security.identity.SecurityIdentity securityIdentity;
@GET
@Operation(summary = "Lister tous les membres actifs")
@APIResponse(responseCode = "200", description = "Liste des membres actifs")
public Response listerMembres(
@Parameter(description = "Numéro de page (0-based)") @QueryParam("page") @DefaultValue("0")
int page,
@Parameter(description = "Taille de la page") @QueryParam("size") @DefaultValue("20")
int size,
@Parameter(description = "Champ de tri") @QueryParam("sort") @DefaultValue("nom")
String sortField,
@Parameter(description = "Direction du tri (asc/desc)")
@QueryParam("direction")
@DefaultValue("asc")
String sortDirection) {
@Operation(summary = "Lister les membres")
@APIResponse(responseCode = "200", description = "Liste des membres avec pagination")
public PagedResponse<MembreSummaryResponse> listerMembres(
@Parameter(description = "Numéro de page (0-based)") @QueryParam("page") @DefaultValue("0") int page,
@Parameter(description = "Taille de la page") @QueryParam("size") @DefaultValue("20") int size,
@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("Récupération de la liste des membres actifs - page: %d, size: %d", page, size);
LOG.infof("Récupération de la liste des membres - page: %d, size: %d", page, size);
Sort sort =
"desc".equalsIgnoreCase(sortDirection)
? Sort.by(sortField).descending()
: Sort.by(sortField).ascending();
Sort sort = "desc".equalsIgnoreCase(sortDirection)
? Sort.by(sortField).descending()
: Sort.by(sortField).ascending();
List<Membre> membres = membreService.listerMembresActifs(Page.of(page, size), sort);
List<MembreDTO> membresDTO = membreService.convertToDTOList(membres);
List<Membre> membres = membreService.listerMembres(Page.of(page, size), sort);
List<MembreSummaryResponse> membresDTO = membreService.convertToSummaryResponseList(membres);
long totalElements = membreService.compterMembres();
return Response.ok(membresDTO).build();
return new PagedResponse<>(membresDTO, totalElements, page, size);
}
@GET
@@ -80,17 +93,39 @@ public class MembreResource {
@APIResponse(responseCode = "404", description = "Membre non trouvé")
public Response obtenirMembre(@Parameter(description = "UUID du membre") @PathParam("id") UUID id) {
LOG.infof("Récupération du membre ID: %s", id);
return membreService
.trouverParId(id)
.map(
membre -> {
MembreDTO membreDTO = membreService.convertToDTO(membre);
return Response.ok(membreDTO).build();
})
.orElse(
Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("message", "Membre non trouvé"))
.build());
Membre membre = membreService.trouverParId(id)
.filter(m -> m.getActif() == null || m.getActif())
.orElseThrow(() -> new NotFoundException("Membre non trouvé avec l'ID: " + id));
return Response.ok(membreService.convertToResponse(membre)).build();
}
@GET
@Path("/me/suivis")
@RolesAllowed({ "USER", "MEMBRE", "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "MODERATEUR", "CONSULTANT", "SECRETAIRE", "GESTIONNAIRE_RH" })
@Operation(summary = "Liste des ids des membres suivis (réseau)")
@APIResponse(responseCode = "200", description = "Liste des UUID suivis")
public Response obtenirMesSuivis() {
String email = securityIdentity.getPrincipal().getName();
List<UUID> ids = membreSuiviService.getFollowedIds(email);
return Response.ok(ids).build();
}
@GET
@Path("/me")
@RolesAllowed({ "USER", "MEMBRE", "ADMIN", "SUPER_ADMIN" })
@Operation(summary = "Récupérer le membre connecté")
@APIResponse(responseCode = "200", description = "Membre connecté trouvé")
@APIResponse(responseCode = "404", description = "Membre non trouvé")
public Response obtenirMembreConnecte() {
String email = securityIdentity.getPrincipal().getName();
LOG.infof("Récupération du membre connecté: %s", email);
Membre membre = membreService.trouverParEmail(email)
.filter(m -> m.getActif() == null || m.getActif())
.orElseThrow(() -> new NotFoundException("Membre non trouvé pour l'email: " + email));
return Response.ok(membreService.convertToResponse(membre)).build();
}
@POST
@@ -98,24 +133,19 @@ public class MembreResource {
@Operation(summary = "Créer un nouveau membre")
@APIResponse(responseCode = "201", description = "Membre créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response creerMembre(@Valid MembreDTO membreDTO) {
LOG.infof("Création d'un nouveau membre: %s", membreDTO.getEmail());
try {
// Conversion DTO vers entité
Membre membre = membreService.convertFromDTO(membreDTO);
public Response creerMembre(@Valid CreateMembreRequest membreDTO) {
LOG.infof("Création d'un nouveau membre: %s", membreDTO.email());
// Conversion DTO vers entité
Membre membre = membreService.convertFromCreateRequest(membreDTO);
// Création du membre
Membre nouveauMembre = membreService.creerMembre(membre);
// Création du membre — statut EN_ATTENTE_VALIDATION, Keycloak provisionné à
// l'approbation
Membre nouveauMembre = membreService.creerMembre(membre);
// Conversion de retour vers DTO
MembreDTO nouveauMembreDTO = membreService.convertToDTO(nouveauMembre);
// Conversion de retour vers DTO
MembreResponse nouveauMembreDTO = membreService.convertToResponse(nouveauMembre);
return Response.status(Response.Status.CREATED).entity(nouveauMembreDTO).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("message", e.getMessage()))
.build();
}
return Response.status(Response.Status.CREATED).entity(nouveauMembreDTO).build();
}
@PUT
@@ -126,24 +156,23 @@ public class MembreResource {
@APIResponse(responseCode = "400", description = "Données invalides")
public Response mettreAJourMembre(
@Parameter(description = "UUID du membre") @PathParam("id") UUID id,
@Valid MembreDTO membreDTO) {
@Valid UpdateMembreRequest membreDTO) {
LOG.infof("Mise à jour du membre ID: %s", id);
try {
// Conversion DTO vers entité
Membre membre = membreService.convertFromDTO(membreDTO);
// Mise à jour du membre
Membre membreMisAJour = membreService.mettreAJourMembre(id, membre);
// Recupérer le membre
Membre membreAModifier = membreService.trouverParId(id)
.orElseThrow(() -> new NotFoundException("Membre non trouvé avec l'ID: " + id));
// Conversion de retour vers DTO
MembreDTO membreMisAJourDTO = membreService.convertToDTO(membreMisAJour);
// Mettre à jour depuis la requête
membreService.updateFromRequest(membreAModifier, membreDTO);
return Response.ok(membreMisAJourDTO).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("message", e.getMessage()))
.build();
}
// Mise à jour en base
Membre membreMisAJour = membreService.mettreAJourMembre(id, membreAModifier);
// Conversion de retour
MembreResponse membreMisAJourDTO = membreService.convertToResponse(membreMisAJour);
return Response.ok(membreMisAJourDTO).build();
}
@DELETE
@@ -154,13 +183,43 @@ public class MembreResource {
public Response desactiverMembre(
@Parameter(description = "UUID du membre") @PathParam("id") UUID id) {
LOG.infof("Désactivation du membre ID: %s", id);
membreService.desactiverMembre(id);
return Response.noContent().build();
}
@POST
@Path("/{id}/suivre")
@RolesAllowed({ "USER", "MEMBRE", "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "MODERATEUR", "CONSULTANT", "SECRETAIRE", "GESTIONNAIRE_RH" })
@Operation(summary = "Suivre un membre (réseau)")
@APIResponse(responseCode = "200", description = "Suivi activé")
@APIResponse(responseCode = "400", description = "Requête invalide")
@APIResponse(responseCode = "404", description = "Membre cible non trouvé")
public Response suivreMembre(@Parameter(description = "UUID du membre à suivre") @PathParam("id") UUID id) {
String email = securityIdentity.getPrincipal().getName();
try {
membreService.desactiverMembre(id);
return Response.noContent().build();
boolean following = membreSuiviService.follow(email, id);
return Response.ok(Map.of("following", following)).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("message", e.getMessage()))
.build();
if (e.getMessage().contains("introuvable")) {
return Response.status(Response.Status.NOT_FOUND).entity(Map.of("message", e.getMessage())).build();
}
return Response.status(Response.Status.BAD_REQUEST).entity(Map.of("message", e.getMessage())).build();
}
}
@DELETE
@Path("/{id}/suivre")
@RolesAllowed({ "USER", "MEMBRE", "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "MODERATEUR", "CONSULTANT", "SECRETAIRE", "GESTIONNAIRE_RH" })
@Operation(summary = "Ne plus suivre un membre (réseau)")
@APIResponse(responseCode = "200", description = "Suivi désactivé")
@APIResponse(responseCode = "400", description = "Requête invalide")
public Response nePlusSuivreMembre(@Parameter(description = "UUID du membre à ne plus suivre") @PathParam("id") UUID id) {
String email = securityIdentity.getPrincipal().getName();
try {
boolean following = membreSuiviService.unfollow(email, id);
return Response.ok(Map.of("following", following)).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(Map.of("message", e.getMessage())).build();
}
}
@@ -170,16 +229,10 @@ public class MembreResource {
@APIResponse(responseCode = "200", description = "Résultats de la recherche")
public Response rechercherMembres(
@Parameter(description = "Terme de recherche") @QueryParam("q") String recherche,
@Parameter(description = "Numéro de page (0-based)") @QueryParam("page") @DefaultValue("0")
int page,
@Parameter(description = "Taille de la page") @QueryParam("size") @DefaultValue("20")
int size,
@Parameter(description = "Champ de tri") @QueryParam("sort") @DefaultValue("nom")
String sortField,
@Parameter(description = "Direction du tri (asc/desc)")
@QueryParam("direction")
@DefaultValue("asc")
String sortDirection) {
@Parameter(description = "Numéro de page (0-based)") @QueryParam("page") @DefaultValue("0") int page,
@Parameter(description = "Taille de la page") @QueryParam("size") @DefaultValue("20") int size,
@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 de membres avec le terme: %s", recherche);
if (recherche == null || recherche.trim().isEmpty()) {
@@ -188,14 +241,12 @@ public class MembreResource {
.build();
}
Sort sort =
"desc".equalsIgnoreCase(sortDirection)
? Sort.by(sortField).descending()
: Sort.by(sortField).ascending();
Sort sort = "desc".equalsIgnoreCase(sortDirection)
? Sort.by(sortField).descending()
: Sort.by(sortField).ascending();
List<Membre> membres =
membreService.rechercherMembres(recherche.trim(), Page.of(page, size), sort);
List<MembreDTO> membresDTO = membreService.convertToDTOList(membres);
List<Membre> membres = membreService.rechercherMembres(recherche.trim(), Page.of(page, size), sort);
List<MembreSummaryResponse> membresDTO = membreService.convertToSummaryResponseList(membres);
return Response.ok(membresDTO).build();
}
@@ -240,234 +291,137 @@ public class MembreResource {
public Response rechercheAvancee(
@Parameter(description = "Terme de recherche") @QueryParam("q") String recherche,
@Parameter(description = "Statut actif (true/false)") @QueryParam("actif") Boolean actif,
@Parameter(description = "Date d'adhésion minimum (YYYY-MM-DD)")
@QueryParam("dateAdhesionMin")
String dateAdhesionMin,
@Parameter(description = "Date d'adhésion maximum (YYYY-MM-DD)")
@QueryParam("dateAdhesionMax")
String dateAdhesionMax,
@Parameter(description = "Numéro de page (0-based)") @QueryParam("page") @DefaultValue("0")
int page,
@Parameter(description = "Taille de la page") @QueryParam("size") @DefaultValue("20")
int size,
@Parameter(description = "Champ de tri") @QueryParam("sort") @DefaultValue("nom")
String sortField,
@Parameter(description = "Direction du tri (asc/desc)")
@QueryParam("direction")
@DefaultValue("asc")
String sortDirection) {
@Parameter(description = "Date d'adhésion minimum (YYYY-MM-DD)") @QueryParam("dateAdhesionMin") String dateAdhesionMin,
@Parameter(description = "Date d'adhésion maximum (YYYY-MM-DD)") @QueryParam("dateAdhesionMax") String dateAdhesionMax,
@Parameter(description = "Numéro de page (0-based)") @QueryParam("page") @DefaultValue("0") int page,
@Parameter(description = "Taille de la page") @QueryParam("size") @DefaultValue("20") int size,
@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 (DEPRECATED) - recherche: %s, actif: %s", recherche, actif);
try {
Sort sort =
"desc".equalsIgnoreCase(sortDirection)
? Sort.by(sortField).descending()
: Sort.by(sortField).ascending();
Sort sort = "desc".equalsIgnoreCase(sortDirection)
? Sort.by(sortField).descending()
: Sort.by(sortField).ascending();
// Conversion des dates si fournies
java.time.LocalDate dateMin =
dateAdhesionMin != null ? java.time.LocalDate.parse(dateAdhesionMin) : null;
java.time.LocalDate dateMax =
dateAdhesionMax != null ? java.time.LocalDate.parse(dateAdhesionMax) : null;
// Conversion des dates si fournies
java.time.LocalDate dateMin = dateAdhesionMin != null ? java.time.LocalDate.parse(dateAdhesionMin) : null;
java.time.LocalDate dateMax = dateAdhesionMax != null ? java.time.LocalDate.parse(dateAdhesionMax) : null;
List<Membre> membres =
membreService.rechercheAvancee(
recherche, actif, dateMin, dateMax, Page.of(page, size), sort);
List<MembreDTO> membresDTO = membreService.convertToDTOList(membres);
List<Membre> membres = membreService.rechercheAvancee(
recherche, actif, dateMin, dateMax, Page.of(page, size), sort);
List<MembreSummaryResponse> membresDTO = membreService.convertToSummaryResponseList(membres);
return Response.ok(membresDTO).build();
} catch (Exception e) {
LOG.errorf("Erreur lors de la recherche avancée: %s", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("message", "Erreur dans les paramètres de recherche: " + e.getMessage()))
.build();
}
return Response.ok(membresDTO).build();
}
/**
* Nouvelle recherche avancée avec critères complets et résultats enrichis Réservée aux super
* 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
@RolesAllowed({ "SUPER_ADMIN", "ADMIN", "ADMIN_ORGANISATION" })
@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.
""")
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")
@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, ADMIN ou ADMIN_ORGANISATION requis"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
@SecurityRequirement(name = "keycloak")
public Response searchMembresAdvanced(
@RequestBody(
description = "Critères de recherche avancée",
required = false,
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
}
""")))
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) {
@RequestBody(description = "Critères de recherche avancée", required = false, 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
}
"""))) 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();
try {
// Validation des critères
if (criteria == null) {
LOG.warn("Recherche avancée de membres - critères null rejetés");
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("message", "Les critères de recherche sont requis"))
.build();
}
LOG.infof(
"Recherche avancée de membres - critères: %s, page: %d, size: %d",
criteria.getDescription(), page, size);
// 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 (jakarta.validation.ConstraintViolationException e) {
LOG.warnf("Erreur de validation Jakarta dans la recherche avancée: %s", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("message", "Critères de recherche invalides", "details", e.getMessage()))
.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();
// Validation des critères
if (criteria == null) {
LOG.warn("Recherche avancée de membres - critères null rejetés");
throw new IllegalArgumentException("Les critères de recherche sont requis");
}
LOG.infof(
"Recherche avancée de membres - critères: %s, page: %d, size: %d",
criteria.getDescription(), page, size);
// Nettoyage et validation des critères
criteria.sanitize();
if (!criteria.hasAnyCriteria()) {
throw new IllegalArgumentException("Au moins un critère de recherche doit être spécifié");
}
if (!criteria.isValid()) {
throw new IllegalArgumentException(
"Critères de recherche invalides: Vérifiez la cohérence des dates et des âges");
}
// 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();
}
@POST
@@ -480,67 +434,67 @@ public class MembreResource {
@Parameter(description = "Liste des IDs des membres à exporter") List<UUID> membreIds,
@Parameter(description = "Format d'export") @QueryParam("format") @DefaultValue("EXCEL") String format) {
LOG.infof("Export de %d membres sélectionnés", membreIds.size());
try {
byte[] excelData = membreService.exporterMembresSelectionnes(membreIds, format);
return Response.ok(excelData)
.header("Content-Disposition", "attachment; filename=\"membres_selection_" +
java.time.LocalDate.now() + "." + (format != null ? format.toLowerCase() : "xlsx") + "\"")
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'export de la sélection");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de l'export: " + e.getMessage()))
.build();
}
LOG.infof("Export de %d membres sélectionnés", membreIds.size());
byte[] excelData = membreService.exporterMembresSelectionnes(membreIds, format);
return Response.ok(excelData)
.header("Content-Disposition", "attachment; filename=\"membres_selection_" +
java.time.LocalDate.now() + "." + (format != null ? format.toLowerCase() : "xlsx") + "\"")
.build();
}
@POST
@Path("/import")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Importer des membres depuis un fichier Excel ou CSV")
@RolesAllowed({ "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION" })
@Operation(summary = "Importer des membres depuis un fichier Excel ou CSV",
description = "Format strict (colonnes obligatoires: nom, prenom, email, telephone). " +
"Si organisationId est fourni, les membres sont rattachés à l'organisation et le quota souscription (tranche) est respecté.")
@APIResponse(responseCode = "200", description = "Import terminé")
public Response importerMembres(
@Parameter(description = "Contenu du fichier à importer") @FormParam("file") byte[] fileContent,
@Parameter(description = "Nom du fichier") @FormParam("fileName") String fileName,
@Parameter(description = "ID de l'organisation (optionnel)") @FormParam("organisationId") UUID organisationId,
@Parameter(description = "Type de membre par défaut") @FormParam("typeMembreDefaut") String typeMembreDefaut,
@Parameter(description = "Mettre à jour les membres existants") @FormParam("mettreAJourExistants") boolean mettreAJourExistants,
@Parameter(description = "Ignorer les erreurs") @FormParam("ignorerErreurs") boolean ignorerErreurs) {
try {
if (fileContent == null || fileContent.length == 0) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Aucun fichier fourni"))
.build();
}
@RestForm("file") org.jboss.resteasy.reactive.multipart.FileUpload file,
@RestForm("fileName") String fileName,
@RestForm("organisationId") String organisationIdStr,
@RestForm("typeMembreDefaut") String typeMembreDefaut,
@RestForm("mettreAJourExistants") boolean mettreAJourExistants,
@RestForm("ignorerErreurs") boolean ignorerErreurs) {
if (fileName == null || fileName.isEmpty()) {
fileName = "import.xlsx";
}
if (typeMembreDefaut == null || typeMembreDefaut.isEmpty()) {
typeMembreDefaut = "ACTIF";
}
InputStream fileInputStream = new java.io.ByteArrayInputStream(fileContent);
dev.lions.unionflow.server.service.MembreImportExportService.ResultatImport resultat = membreService.importerMembres(
fileInputStream, fileName, organisationId, typeMembreDefaut, mettreAJourExistants, ignorerErreurs);
Map<String, Object> response = new HashMap<>();
response.put("totalLignes", resultat.totalLignes);
response.put("lignesTraitees", resultat.lignesTraitees);
response.put("lignesErreur", resultat.lignesErreur);
response.put("erreurs", resultat.erreurs);
response.put("membresImportes", resultat.membresImportes);
return Response.ok(response).build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'import");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de l'import: " + e.getMessage()))
if (file == null || file.size() == 0) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Aucun fichier fourni"))
.build();
}
if (fileName == null || fileName.isEmpty()) {
fileName = file.fileName() != null ? file.fileName() : "import.xlsx";
}
if (typeMembreDefaut == null || typeMembreDefaut.isEmpty()) {
typeMembreDefaut = "ACTIF";
}
UUID organisationId = (organisationIdStr != null && !organisationIdStr.isEmpty())
? UUID.fromString(organisationIdStr)
: null;
InputStream fileInputStream;
try {
fileInputStream = java.nio.file.Files.newInputStream(file.uploadedFile());
} catch (java.io.IOException e) {
throw new RuntimeException("Erreur lors de la lecture du fichier: " + e.getMessage(), e);
}
dev.lions.unionflow.server.service.MembreImportExportService.ResultatImport resultat = membreService
.importerMembres(
fileInputStream, fileName, organisationId, typeMembreDefaut, mettreAJourExistants, ignorerErreurs);
Map<String, Object> response = new HashMap<>();
response.put("totalLignes", resultat.totalLignes);
response.put("lignesTraitees", resultat.lignesTraitees);
response.put("lignesErreur", resultat.lignesErreur);
response.put("erreurs", resultat.erreurs);
response.put("membresImportes", resultat.membresImportes);
return Response.ok(response).build();
}
@GET
@@ -560,41 +514,34 @@ public class MembreResource {
@Parameter(description = "Formater les dates") @QueryParam("formaterDates") @DefaultValue("true") boolean formaterDates,
@Parameter(description = "Inclure un onglet statistiques (Excel uniquement)") @QueryParam("inclureStatistiques") @DefaultValue("false") boolean inclureStatistiques,
@Parameter(description = "Mot de passe pour chiffrer le fichier (optionnel)") @QueryParam("motDePasse") String motDePasse) {
try {
// Récupérer les membres selon les filtres
List<MembreDTO> membres = membreService.listerMembresPourExport(
associationId, statut, type, dateAdhesionDebut, dateAdhesionFin);
byte[] exportData;
String contentType;
String extension;
List<MembreResponse> membres = membreService.listerMembresPourExport(
associationId, statut, type, dateAdhesionDebut, dateAdhesionFin);
List<String> colonnesExport = colonnesExportList != null ? colonnesExportList : new ArrayList<>();
if ("CSV".equalsIgnoreCase(format)) {
exportData = membreService.exporterVersCSV(membres, colonnesExport, inclureHeaders, formaterDates);
contentType = "text/csv";
extension = "csv";
} else {
// Pour Excel, inclure les statistiques uniquement si demandé et si format Excel
boolean stats = inclureStatistiques && "EXCEL".equalsIgnoreCase(format);
exportData = membreService.exporterVersExcel(membres, colonnesExport, inclureHeaders, formaterDates, stats, motDePasse);
contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
extension = "xlsx";
}
byte[] exportData;
String contentType;
String extension;
return Response.ok(exportData)
.type(contentType)
.header("Content-Disposition", "attachment; filename=\"membres_export_" +
java.time.LocalDate.now() + "." + extension + "\"")
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'export");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de l'export: " + e.getMessage()))
.build();
List<String> colonnesExport = colonnesExportList != null ? colonnesExportList : new ArrayList<>();
if ("CSV".equalsIgnoreCase(format)) {
exportData = membreService.exporterVersCSV(membres, colonnesExport, inclureHeaders, formaterDates);
contentType = "text/csv";
extension = "csv";
} else {
// Pour Excel, inclure les statistiques uniquement si demandé et si format Excel
boolean stats = inclureStatistiques && "EXCEL".equalsIgnoreCase(format);
exportData = membreService.exporterVersExcel(membres, colonnesExport, inclureHeaders, formaterDates, stats,
motDePasse);
contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
extension = "xlsx";
}
return Response.ok(exportData)
.type(contentType)
.header("Content-Disposition", "attachment; filename=\"membres_export_" +
java.time.LocalDate.now() + "." + extension + "\"")
.build();
}
@GET
@@ -603,17 +550,10 @@ public class MembreResource {
@Operation(summary = "Télécharger le modèle Excel pour l'import")
@APIResponse(responseCode = "200", description = "Modèle Excel généré")
public Response telechargerModeleImport() {
try {
byte[] modele = membreService.genererModeleImport();
return Response.ok(modele)
.header("Content-Disposition", "attachment; filename=\"modele_import_membres.xlsx\"")
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la génération du modèle");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la génération du modèle: " + e.getMessage()))
.build();
}
byte[] modele = membreService.genererModeleImport();
return Response.ok(modele)
.header("Content-Disposition", "attachment; filename=\"modele_import_membres.xlsx\"")
.build();
}
@GET
@@ -627,17 +567,10 @@ public class MembreResource {
@Parameter(description = "Type de membre") @QueryParam("type") String type,
@Parameter(description = "Date adhésion début") @QueryParam("dateAdhesionDebut") String dateAdhesionDebut,
@Parameter(description = "Date adhésion fin") @QueryParam("dateAdhesionFin") String dateAdhesionFin) {
try {
List<MembreDTO> membres = membreService.listerMembresPourExport(
associationId, statut, type, dateAdhesionDebut, dateAdhesionFin);
return Response.ok(Map.of("count", membres.size())).build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors du comptage des membres");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors du comptage: " + e.getMessage()))
.build();
}
List<MembreResponse> membres = membreService.listerMembresPourExport(
associationId, statut, type, dateAdhesionDebut, dateAdhesionFin);
return Response.ok(Map.of("count", membres.size())).build();
}
}