Sync: code local unifié
Synchronisation du code source local (fait foi). Signed-off-by: lions dev Team
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user