diff --git a/unionflow-server-impl-quarkus/pom.xml b/unionflow-server-impl-quarkus/pom.xml
index c5a8076..de27189 100644
--- a/unionflow-server-impl-quarkus/pom.xml
+++ b/unionflow-server-impl-quarkus/pom.xml
@@ -116,6 +116,35 @@
1.18.30
provided
+
+
+
+ org.apache.poi
+ poi
+ 5.2.5
+
+
+ org.apache.poi
+ poi-ooxml
+ 5.2.5
+
+
+ org.apache.poi
+ poi-ooxml-lite
+ 5.2.5
+
+
+ org.apache.poi
+ poi-scratchpad
+ 5.2.5
+
+
+
+
+ org.apache.commons
+ commons-csv
+ 1.10.0
+
diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/MembreResource.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/MembreResource.java
index 38a22f3..811a807 100644
--- a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/MembreResource.java
+++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/MembreResource.java
@@ -15,6 +15,9 @@ import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -399,18 +402,19 @@ public class MembreResource {
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) {
+ 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();
@@ -485,4 +489,151 @@ public class MembreResource {
.build();
}
}
+
+ @POST
+ @Path("/import")
+ @Consumes(MediaType.MULTIPART_FORM_DATA)
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(summary = "Importer des membres depuis un fichier Excel ou CSV")
+ @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();
+ }
+
+ 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 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()))
+ .build();
+ }
+ }
+
+ @GET
+ @Path("/export")
+ @Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ @Operation(summary = "Exporter des membres en Excel, CSV ou PDF")
+ @APIResponse(responseCode = "200", description = "Fichier exporté")
+ public Response exporterMembres(
+ @Parameter(description = "Format d'export") @QueryParam("format") @DefaultValue("EXCEL") String format,
+ @Parameter(description = "ID de l'organisation (optionnel)") @QueryParam("associationId") UUID associationId,
+ @Parameter(description = "Statut des membres") @QueryParam("statut") String statut,
+ @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,
+ @Parameter(description = "Colonnes à exporter") @QueryParam("colonnes") List colonnesExportList,
+ @Parameter(description = "Inclure les en-têtes") @QueryParam("inclureHeaders") @DefaultValue("true") boolean inclureHeaders,
+ @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 membres = membreService.listerMembresPourExport(
+ associationId, statut, type, dateAdhesionDebut, dateAdhesionFin);
+
+ byte[] exportData;
+ String contentType;
+ String extension;
+
+ List 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();
+ } 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();
+ }
+ }
+
+ @GET
+ @Path("/import/modele")
+ @Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ @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();
+ }
+ }
+
+ @GET
+ @Path("/export/count")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(summary = "Compter les membres selon les filtres pour l'export")
+ @APIResponse(responseCode = "200", description = "Nombre de membres correspondant aux critères")
+ public Response compterMembresPourExport(
+ @Parameter(description = "ID de l'organisation (optionnel)") @QueryParam("associationId") UUID associationId,
+ @Parameter(description = "Statut des membres") @QueryParam("statut") String statut,
+ @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 membres = membreService.listerMembresPourExport(
+ associationId, statut, type, dateAdhesionDebut, dateAdhesionFin);
+
+ return Response.ok(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();
+ }
+ }
}
diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/MembreImportExportService.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/MembreImportExportService.java
new file mode 100644
index 0000000..057fb5a
--- /dev/null
+++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/MembreImportExportService.java
@@ -0,0 +1,842 @@
+package dev.lions.unionflow.server.service;
+
+import dev.lions.unionflow.server.api.dto.membre.MembreDTO;
+import dev.lions.unionflow.server.entity.Membre;
+import dev.lions.unionflow.server.entity.Organisation;
+import dev.lions.unionflow.server.repository.MembreRepository;
+import dev.lions.unionflow.server.repository.OrganisationRepository;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jakarta.transaction.Transactional;
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVPrinter;
+import org.apache.commons.csv.CSVRecord;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.jboss.logging.Logger;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.*;
+
+/**
+ * Service pour l'import et l'export de membres depuis/vers Excel et CSV
+ */
+@ApplicationScoped
+public class MembreImportExportService {
+
+ private static final Logger LOG = Logger.getLogger(MembreImportExportService.class);
+ private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy");
+
+ @Inject
+ MembreRepository membreRepository;
+
+ @Inject
+ OrganisationRepository organisationRepository;
+
+ @Inject
+ MembreService membreService;
+
+ /**
+ * Importe des membres depuis un fichier Excel ou CSV
+ */
+ @Transactional
+ public ResultatImport importerMembres(
+ InputStream fileInputStream,
+ String fileName,
+ UUID organisationId,
+ String typeMembreDefaut,
+ boolean mettreAJourExistants,
+ boolean ignorerErreurs) {
+
+ LOG.infof("Import de membres depuis le fichier: %s", fileName);
+
+ ResultatImport resultat = new ResultatImport();
+ resultat.erreurs = new ArrayList<>();
+ resultat.membresImportes = new ArrayList<>();
+
+ try {
+ if (fileName.toLowerCase().endsWith(".csv")) {
+ return importerDepuisCSV(fileInputStream, organisationId, typeMembreDefaut, mettreAJourExistants, ignorerErreurs);
+ } else if (fileName.toLowerCase().endsWith(".xlsx") || fileName.toLowerCase().endsWith(".xls")) {
+ return importerDepuisExcel(fileInputStream, organisationId, typeMembreDefaut, mettreAJourExistants, ignorerErreurs);
+ } else {
+ throw new IllegalArgumentException("Format de fichier non supporté. Formats acceptés: .xlsx, .xls, .csv");
+ }
+ } catch (Exception e) {
+ LOG.errorf(e, "Erreur lors de l'import");
+ resultat.erreurs.add("Erreur générale: " + e.getMessage());
+ return resultat;
+ }
+ }
+
+ /**
+ * Importe depuis un fichier Excel
+ */
+ private ResultatImport importerDepuisExcel(
+ InputStream fileInputStream,
+ UUID organisationId,
+ String typeMembreDefaut,
+ boolean mettreAJourExistants,
+ boolean ignorerErreurs) throws IOException {
+
+ ResultatImport resultat = new ResultatImport();
+ resultat.erreurs = new ArrayList<>();
+ resultat.membresImportes = new ArrayList<>();
+ int ligneNum = 0;
+
+ try (Workbook workbook = new XSSFWorkbook(fileInputStream)) {
+ Sheet sheet = workbook.getSheetAt(0);
+ Row headerRow = sheet.getRow(0);
+
+ if (headerRow == null) {
+ throw new IllegalArgumentException("Le fichier Excel est vide ou n'a pas d'en-têtes");
+ }
+
+ // Mapper les colonnes
+ Map colonnes = mapperColonnes(headerRow);
+
+ // Vérifier les colonnes obligatoires
+ if (!colonnes.containsKey("nom") || !colonnes.containsKey("prenom") ||
+ !colonnes.containsKey("email") || !colonnes.containsKey("telephone")) {
+ throw new IllegalArgumentException("Colonnes obligatoires manquantes: nom, prenom, email, telephone");
+ }
+
+ // Lire les données
+ for (int i = 1; i <= sheet.getLastRowNum(); i++) {
+ ligneNum = i + 1;
+ Row row = sheet.getRow(i);
+
+ if (row == null) {
+ continue;
+ }
+
+ try {
+ Membre membre = lireLigneExcel(row, colonnes, organisationId, typeMembreDefaut);
+
+ // Vérifier si le membre existe déjà
+ Optional membreExistant = membreRepository.findByEmail(membre.getEmail());
+
+ if (membreExistant.isPresent()) {
+ if (mettreAJourExistants) {
+ Membre existant = membreExistant.get();
+ existant.setNom(membre.getNom());
+ existant.setPrenom(membre.getPrenom());
+ existant.setTelephone(membre.getTelephone());
+ existant.setDateNaissance(membre.getDateNaissance());
+ if (membre.getOrganisation() != null) {
+ existant.setOrganisation(membre.getOrganisation());
+ }
+ membreRepository.persist(existant);
+ resultat.membresImportes.add(membreService.convertToDTO(existant));
+ resultat.lignesTraitees++;
+ } else {
+ resultat.erreurs.add(String.format("Ligne %d: Membre avec email %s existe déjà", ligneNum, membre.getEmail()));
+ if (!ignorerErreurs) {
+ throw new IllegalArgumentException("Membre existant trouvé et mise à jour désactivée");
+ }
+ }
+ } else {
+ membre = membreService.creerMembre(membre);
+ resultat.membresImportes.add(membreService.convertToDTO(membre));
+ resultat.lignesTraitees++;
+ }
+ } catch (Exception e) {
+ String erreur = String.format("Ligne %d: %s", ligneNum, e.getMessage());
+ resultat.erreurs.add(erreur);
+ resultat.lignesErreur++;
+
+ if (!ignorerErreurs) {
+ throw new RuntimeException(erreur, e);
+ }
+ }
+ }
+
+ resultat.totalLignes = sheet.getLastRowNum();
+ }
+
+ LOG.infof("Import terminé: %d lignes traitées, %d erreurs", resultat.lignesTraitees, resultat.lignesErreur);
+ return resultat;
+ }
+
+ /**
+ * Importe depuis un fichier CSV
+ */
+ private ResultatImport importerDepuisCSV(
+ InputStream fileInputStream,
+ UUID organisationId,
+ String typeMembreDefaut,
+ boolean mettreAJourExistants,
+ boolean ignorerErreurs) throws IOException {
+
+ ResultatImport resultat = new ResultatImport();
+ resultat.erreurs = new ArrayList<>();
+ resultat.membresImportes = new ArrayList<>();
+
+ try (InputStreamReader reader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8)) {
+ Iterable records = CSVFormat.DEFAULT.builder().setHeader().setSkipHeaderRecord(true).build().parse(reader);
+
+ int ligneNum = 0;
+ for (CSVRecord record : records) {
+ ligneNum++;
+
+ try {
+ Membre membre = lireLigneCSV(record, organisationId, typeMembreDefaut);
+
+ // Vérifier si le membre existe déjà
+ Optional membreExistant = membreRepository.findByEmail(membre.getEmail());
+
+ if (membreExistant.isPresent()) {
+ if (mettreAJourExistants) {
+ Membre existant = membreExistant.get();
+ existant.setNom(membre.getNom());
+ existant.setPrenom(membre.getPrenom());
+ existant.setTelephone(membre.getTelephone());
+ existant.setDateNaissance(membre.getDateNaissance());
+ if (membre.getOrganisation() != null) {
+ existant.setOrganisation(membre.getOrganisation());
+ }
+ membreRepository.persist(existant);
+ resultat.membresImportes.add(membreService.convertToDTO(existant));
+ resultat.lignesTraitees++;
+ } else {
+ resultat.erreurs.add(String.format("Ligne %d: Membre avec email %s existe déjà", ligneNum, membre.getEmail()));
+ if (!ignorerErreurs) {
+ throw new IllegalArgumentException("Membre existant trouvé et mise à jour désactivée");
+ }
+ }
+ } else {
+ membre = membreService.creerMembre(membre);
+ resultat.membresImportes.add(membreService.convertToDTO(membre));
+ resultat.lignesTraitees++;
+ }
+ } catch (Exception e) {
+ String erreur = String.format("Ligne %d: %s", ligneNum, e.getMessage());
+ resultat.erreurs.add(erreur);
+ resultat.lignesErreur++;
+
+ if (!ignorerErreurs) {
+ throw new RuntimeException(erreur, e);
+ }
+ }
+ }
+
+ resultat.totalLignes = ligneNum;
+ }
+
+ LOG.infof("Import CSV terminé: %d lignes traitées, %d erreurs", resultat.lignesTraitees, resultat.lignesErreur);
+ return resultat;
+ }
+
+ /**
+ * Lit une ligne Excel et crée un membre
+ */
+ private Membre lireLigneExcel(Row row, Map colonnes, UUID organisationId, String typeMembreDefaut) {
+ Membre membre = new Membre();
+
+ // Colonnes obligatoires
+ String nom = getCellValueAsString(row, colonnes.get("nom"));
+ String prenom = getCellValueAsString(row, colonnes.get("prenom"));
+ String email = getCellValueAsString(row, colonnes.get("email"));
+ String telephone = getCellValueAsString(row, colonnes.get("telephone"));
+
+ if (nom == null || nom.trim().isEmpty()) {
+ throw new IllegalArgumentException("Le nom est obligatoire");
+ }
+ if (prenom == null || prenom.trim().isEmpty()) {
+ throw new IllegalArgumentException("Le prénom est obligatoire");
+ }
+ if (email == null || email.trim().isEmpty()) {
+ throw new IllegalArgumentException("L'email est obligatoire");
+ }
+ if (telephone == null || telephone.trim().isEmpty()) {
+ throw new IllegalArgumentException("Le téléphone est obligatoire");
+ }
+
+ membre.setNom(nom.trim());
+ membre.setPrenom(prenom.trim());
+ membre.setEmail(email.trim().toLowerCase());
+ membre.setTelephone(telephone.trim());
+
+ // Colonnes optionnelles
+ if (colonnes.containsKey("date_naissance")) {
+ LocalDate dateNaissance = getCellValueAsDate(row, colonnes.get("date_naissance"));
+ if (dateNaissance != null) {
+ membre.setDateNaissance(dateNaissance);
+ }
+ }
+ if (membre.getDateNaissance() == null) {
+ membre.setDateNaissance(LocalDate.now().minusYears(18));
+ }
+
+ if (colonnes.containsKey("date_adhesion")) {
+ LocalDate dateAdhesion = getCellValueAsDate(row, colonnes.get("date_adhesion"));
+ if (dateAdhesion != null) {
+ membre.setDateAdhesion(dateAdhesion);
+ }
+ }
+ if (membre.getDateAdhesion() == null) {
+ membre.setDateAdhesion(LocalDate.now());
+ }
+
+ // Organisation
+ if (organisationId != null) {
+ Optional org = organisationRepository.findByIdOptional(organisationId);
+ if (org.isPresent()) {
+ membre.setOrganisation(org.get());
+ }
+ }
+
+ // Statut par défaut
+ membre.setActif(typeMembreDefaut == null || typeMembreDefaut.isEmpty() || "ACTIF".equals(typeMembreDefaut));
+
+ return membre;
+ }
+
+ /**
+ * Lit une ligne CSV et crée un membre
+ */
+ private Membre lireLigneCSV(CSVRecord record, UUID organisationId, String typeMembreDefaut) {
+ Membre membre = new Membre();
+
+ // Colonnes obligatoires
+ String nom = record.get("nom");
+ String prenom = record.get("prenom");
+ String email = record.get("email");
+ String telephone = record.get("telephone");
+
+ if (nom == null || nom.trim().isEmpty()) {
+ throw new IllegalArgumentException("Le nom est obligatoire");
+ }
+ if (prenom == null || prenom.trim().isEmpty()) {
+ throw new IllegalArgumentException("Le prénom est obligatoire");
+ }
+ if (email == null || email.trim().isEmpty()) {
+ throw new IllegalArgumentException("L'email est obligatoire");
+ }
+ if (telephone == null || telephone.trim().isEmpty()) {
+ throw new IllegalArgumentException("Le téléphone est obligatoire");
+ }
+
+ membre.setNom(nom.trim());
+ membre.setPrenom(prenom.trim());
+ membre.setEmail(email.trim().toLowerCase());
+ membre.setTelephone(telephone.trim());
+
+ // Colonnes optionnelles
+ try {
+ String dateNaissanceStr = record.get("date_naissance");
+ if (dateNaissanceStr != null && !dateNaissanceStr.trim().isEmpty()) {
+ membre.setDateNaissance(parseDate(dateNaissanceStr));
+ }
+ } catch (Exception e) {
+ // Ignorer si la date est invalide
+ }
+ if (membre.getDateNaissance() == null) {
+ membre.setDateNaissance(LocalDate.now().minusYears(18));
+ }
+
+ try {
+ String dateAdhesionStr = record.get("date_adhesion");
+ if (dateAdhesionStr != null && !dateAdhesionStr.trim().isEmpty()) {
+ membre.setDateAdhesion(parseDate(dateAdhesionStr));
+ }
+ } catch (Exception e) {
+ // Ignorer si la date est invalide
+ }
+ if (membre.getDateAdhesion() == null) {
+ membre.setDateAdhesion(LocalDate.now());
+ }
+
+ // Organisation
+ if (organisationId != null) {
+ Optional org = organisationRepository.findByIdOptional(organisationId);
+ if (org.isPresent()) {
+ membre.setOrganisation(org.get());
+ }
+ }
+
+ // Statut par défaut
+ membre.setActif(typeMembreDefaut == null || typeMembreDefaut.isEmpty() || "ACTIF".equals(typeMembreDefaut));
+
+ return membre;
+ }
+
+ /**
+ * Mappe les colonnes Excel
+ */
+ private Map mapperColonnes(Row headerRow) {
+ Map colonnes = new HashMap<>();
+ for (Cell cell : headerRow) {
+ String headerName = getCellValueAsString(headerRow, cell.getColumnIndex()).toLowerCase()
+ .replace(" ", "_")
+ .replace("é", "e")
+ .replace("è", "e")
+ .replace("ê", "e");
+ colonnes.put(headerName, cell.getColumnIndex());
+ }
+ return colonnes;
+ }
+
+ /**
+ * Obtient la valeur d'une cellule comme String
+ */
+ private String getCellValueAsString(Row row, Integer columnIndex) {
+ if (columnIndex == null || row == null) {
+ return null;
+ }
+ Cell cell = row.getCell(columnIndex);
+ if (cell == null) {
+ return null;
+ }
+
+ switch (cell.getCellType()) {
+ case STRING:
+ return cell.getStringCellValue();
+ case NUMERIC:
+ if (DateUtil.isCellDateFormatted(cell)) {
+ return cell.getDateCellValue().toString();
+ } else {
+ return String.valueOf((long) cell.getNumericCellValue());
+ }
+ case BOOLEAN:
+ return String.valueOf(cell.getBooleanCellValue());
+ case FORMULA:
+ return cell.getCellFormula();
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Obtient la valeur d'une cellule comme Date
+ */
+ private LocalDate getCellValueAsDate(Row row, Integer columnIndex) {
+ if (columnIndex == null || row == null) {
+ return null;
+ }
+ Cell cell = row.getCell(columnIndex);
+ if (cell == null) {
+ return null;
+ }
+
+ try {
+ if (cell.getCellType() == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell)) {
+ return cell.getDateCellValue().toInstant()
+ .atZone(java.time.ZoneId.systemDefault())
+ .toLocalDate();
+ } else if (cell.getCellType() == CellType.STRING) {
+ return parseDate(cell.getStringCellValue());
+ }
+ } catch (Exception e) {
+ LOG.warnf("Erreur lors de la lecture de la date: %s", e.getMessage());
+ }
+ return null;
+ }
+
+ /**
+ * Parse une date depuis une String
+ */
+ private LocalDate parseDate(String dateStr) {
+ if (dateStr == null || dateStr.trim().isEmpty()) {
+ return null;
+ }
+
+ dateStr = dateStr.trim();
+
+ // Essayer différents formats
+ String[] formats = {
+ "dd/MM/yyyy",
+ "yyyy-MM-dd",
+ "dd-MM-yyyy",
+ "dd.MM.yyyy"
+ };
+
+ for (String format : formats) {
+ try {
+ return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern(format));
+ } catch (DateTimeParseException e) {
+ // Continuer avec le format suivant
+ }
+ }
+
+ throw new IllegalArgumentException("Format de date non reconnu: " + dateStr);
+ }
+
+ /**
+ * Exporte des membres vers Excel
+ */
+ public byte[] exporterVersExcel(List membres, List colonnesExport, boolean inclureHeaders, boolean formaterDates, boolean inclureStatistiques, String motDePasse) throws IOException {
+ try (Workbook workbook = new XSSFWorkbook()) {
+ Sheet sheet = workbook.createSheet("Membres");
+
+ int rowNum = 0;
+
+ // En-têtes
+ if (inclureHeaders) {
+ Row headerRow = sheet.createRow(rowNum++);
+ int colNum = 0;
+
+ if (colonnesExport.contains("PERSO") || colonnesExport.isEmpty()) {
+ headerRow.createCell(colNum++).setCellValue("Nom");
+ headerRow.createCell(colNum++).setCellValue("Prénom");
+ headerRow.createCell(colNum++).setCellValue("Date de naissance");
+ }
+ if (colonnesExport.contains("CONTACT") || colonnesExport.isEmpty()) {
+ headerRow.createCell(colNum++).setCellValue("Email");
+ headerRow.createCell(colNum++).setCellValue("Téléphone");
+ }
+ if (colonnesExport.contains("ADHESION") || colonnesExport.isEmpty()) {
+ headerRow.createCell(colNum++).setCellValue("Date adhésion");
+ headerRow.createCell(colNum++).setCellValue("Statut");
+ }
+ if (colonnesExport.contains("ORGANISATION") || colonnesExport.isEmpty()) {
+ headerRow.createCell(colNum++).setCellValue("Organisation");
+ }
+ }
+
+ // Données
+ for (MembreDTO membre : membres) {
+ Row row = sheet.createRow(rowNum++);
+ int colNum = 0;
+
+ if (colonnesExport.contains("PERSO") || colonnesExport.isEmpty()) {
+ row.createCell(colNum++).setCellValue(membre.getNom() != null ? membre.getNom() : "");
+ row.createCell(colNum++).setCellValue(membre.getPrenom() != null ? membre.getPrenom() : "");
+ if (membre.getDateNaissance() != null) {
+ Cell dateCell = row.createCell(colNum++);
+ if (formaterDates) {
+ dateCell.setCellValue(membre.getDateNaissance().format(DATE_FORMATTER));
+ } else {
+ dateCell.setCellValue(membre.getDateNaissance().toString());
+ }
+ } else {
+ row.createCell(colNum++).setCellValue("");
+ }
+ }
+ if (colonnesExport.contains("CONTACT") || colonnesExport.isEmpty()) {
+ row.createCell(colNum++).setCellValue(membre.getEmail() != null ? membre.getEmail() : "");
+ row.createCell(colNum++).setCellValue(membre.getTelephone() != null ? membre.getTelephone() : "");
+ }
+ if (colonnesExport.contains("ADHESION") || colonnesExport.isEmpty()) {
+ if (membre.getDateAdhesion() != null) {
+ Cell dateCell = row.createCell(colNum++);
+ if (formaterDates) {
+ dateCell.setCellValue(membre.getDateAdhesion().format(DATE_FORMATTER));
+ } else {
+ dateCell.setCellValue(membre.getDateAdhesion().toString());
+ }
+ } else {
+ row.createCell(colNum++).setCellValue("");
+ }
+ row.createCell(colNum++).setCellValue(membre.getStatut() != null ? membre.getStatut().toString() : "");
+ }
+ if (colonnesExport.contains("ORGANISATION") || colonnesExport.isEmpty()) {
+ row.createCell(colNum++).setCellValue(membre.getAssociationNom() != null ? membre.getAssociationNom() : "");
+ }
+ }
+
+ // Auto-size columns
+ for (int i = 0; i < 10; i++) {
+ sheet.autoSizeColumn(i);
+ }
+
+ // Ajouter un onglet statistiques si demandé
+ if (inclureStatistiques && !membres.isEmpty()) {
+ Sheet statsSheet = workbook.createSheet("Statistiques");
+ creerOngletStatistiques(statsSheet, membres);
+ }
+
+ // Écrire dans un ByteArrayOutputStream
+ try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
+ workbook.write(outputStream);
+ byte[] excelData = outputStream.toByteArray();
+
+ // Chiffrer le fichier si un mot de passe est fourni
+ if (motDePasse != null && !motDePasse.trim().isEmpty()) {
+ return chiffrerExcel(excelData, motDePasse);
+ }
+
+ return excelData;
+ }
+ }
+ }
+
+ /**
+ * Crée un onglet statistiques dans le classeur Excel
+ */
+ private void creerOngletStatistiques(Sheet sheet, List membres) {
+ int rowNum = 0;
+
+ // Titre
+ Row titleRow = sheet.createRow(rowNum++);
+ Cell titleCell = titleRow.createCell(0);
+ titleCell.setCellValue("Statistiques des Membres");
+ CellStyle titleStyle = sheet.getWorkbook().createCellStyle();
+ Font titleFont = sheet.getWorkbook().createFont();
+ titleFont.setBold(true);
+ titleFont.setFontHeightInPoints((short) 14);
+ titleStyle.setFont(titleFont);
+ titleCell.setCellStyle(titleStyle);
+
+ rowNum++; // Ligne vide
+
+ // Statistiques générales
+ Row headerRow = sheet.createRow(rowNum++);
+ headerRow.createCell(0).setCellValue("Indicateur");
+ headerRow.createCell(1).setCellValue("Valeur");
+
+ // Style pour les en-têtes
+ CellStyle headerStyle = sheet.getWorkbook().createCellStyle();
+ Font headerFont = sheet.getWorkbook().createFont();
+ headerFont.setBold(true);
+ headerStyle.setFont(headerFont);
+ headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
+ headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+ headerRow.getCell(0).setCellStyle(headerStyle);
+ headerRow.getCell(1).setCellStyle(headerStyle);
+
+ // Calcul des statistiques
+ long totalMembres = membres.size();
+ long membresActifs = membres.stream().filter(m -> "ACTIF".equals(m.getStatut())).count();
+ long membresInactifs = membres.stream().filter(m -> "INACTIF".equals(m.getStatut())).count();
+ long membresSuspendus = membres.stream().filter(m -> "SUSPENDU".equals(m.getStatut())).count();
+
+ // Organisations distinctes
+ long organisationsDistinctes = membres.stream()
+ .filter(m -> m.getAssociationNom() != null)
+ .map(MembreDTO::getAssociationNom)
+ .distinct()
+ .count();
+
+ // Statistiques par type (si disponible dans le DTO)
+ // Note: Le type de membre peut ne pas être disponible dans MembreDTO
+ // Pour l'instant, on utilise le statut comme indicateur
+ long typeActif = membresActifs;
+ long typeAssocie = 0;
+ long typeBienfaiteur = 0;
+ long typeHonoraire = 0;
+
+ // Ajout des statistiques
+ int currentRow = rowNum;
+ sheet.createRow(currentRow++).createCell(0).setCellValue("Total Membres");
+ sheet.getRow(currentRow - 1).createCell(1).setCellValue(totalMembres);
+
+ sheet.createRow(currentRow++).createCell(0).setCellValue("Membres Actifs");
+ sheet.getRow(currentRow - 1).createCell(1).setCellValue(membresActifs);
+
+ sheet.createRow(currentRow++).createCell(0).setCellValue("Membres Inactifs");
+ sheet.getRow(currentRow - 1).createCell(1).setCellValue(membresInactifs);
+
+ sheet.createRow(currentRow++).createCell(0).setCellValue("Membres Suspendus");
+ sheet.getRow(currentRow - 1).createCell(1).setCellValue(membresSuspendus);
+
+ sheet.createRow(currentRow++).createCell(0).setCellValue("Organisations Distinctes");
+ sheet.getRow(currentRow - 1).createCell(1).setCellValue(organisationsDistinctes);
+
+ currentRow++; // Ligne vide
+
+ // Section par type
+ sheet.createRow(currentRow++).createCell(0).setCellValue("Répartition par Type");
+ CellStyle sectionStyle = sheet.getWorkbook().createCellStyle();
+ Font sectionFont = sheet.getWorkbook().createFont();
+ sectionFont.setBold(true);
+ sectionStyle.setFont(sectionFont);
+ sheet.getRow(currentRow - 1).getCell(0).setCellStyle(sectionStyle);
+
+ sheet.createRow(currentRow++).createCell(0).setCellValue("Type Actif");
+ sheet.getRow(currentRow - 1).createCell(1).setCellValue(typeActif);
+
+ sheet.createRow(currentRow++).createCell(0).setCellValue("Type Associé");
+ sheet.getRow(currentRow - 1).createCell(1).setCellValue(typeAssocie);
+
+ sheet.createRow(currentRow++).createCell(0).setCellValue("Type Bienfaiteur");
+ sheet.getRow(currentRow - 1).createCell(1).setCellValue(typeBienfaiteur);
+
+ sheet.createRow(currentRow++).createCell(0).setCellValue("Type Honoraire");
+ sheet.getRow(currentRow - 1).createCell(1).setCellValue(typeHonoraire);
+
+ // Auto-size columns
+ sheet.autoSizeColumn(0);
+ sheet.autoSizeColumn(1);
+ }
+
+ /**
+ * Protège un fichier Excel avec un mot de passe
+ * Utilise Apache POI pour protéger les feuilles et la structure du workbook
+ * Note: Ceci protège contre la modification, pas un chiffrement complet du fichier
+ */
+ private byte[] chiffrerExcel(byte[] excelData, String motDePasse) throws IOException {
+ try {
+ // Pour XLSX, on protège les feuilles et la structure du workbook
+ // Note: POI 5.2.5 ne supporte pas le chiffrement complet XLSX (nécessite des bibliothèques externes)
+ // On utilise la protection par mot de passe qui empêche la modification sans le mot de passe
+
+ try (java.io.ByteArrayInputStream inputStream = new java.io.ByteArrayInputStream(excelData);
+ XSSFWorkbook workbook = new XSSFWorkbook(inputStream);
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
+
+ // Protéger toutes les feuilles avec un mot de passe (empêche la modification des cellules)
+ for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
+ Sheet sheet = workbook.getSheetAt(i);
+ sheet.protectSheet(motDePasse);
+ }
+
+ // Protéger la structure du workbook (empêche l'ajout/suppression de feuilles)
+ org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorkbookProtection protection =
+ workbook.getCTWorkbook().getWorkbookProtection();
+ if (protection == null) {
+ protection = workbook.getCTWorkbook().addNewWorkbookProtection();
+ }
+ protection.setLockStructure(true);
+ // Le mot de passe doit être haché selon le format Excel
+ // Pour simplifier, on utilise le hash MD5 du mot de passe
+ try {
+ java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
+ byte[] passwordHash = md.digest(motDePasse.getBytes(java.nio.charset.StandardCharsets.UTF_16LE));
+ protection.setWorkbookPassword(passwordHash);
+ } catch (java.security.NoSuchAlgorithmException e) {
+ LOG.warnf("Impossible de hasher le mot de passe, protection partielle uniquement");
+ }
+
+ workbook.write(outputStream);
+ return outputStream.toByteArray();
+ }
+ } catch (Exception e) {
+ LOG.errorf(e, "Erreur lors de la protection du fichier Excel");
+ // En cas d'erreur, retourner le fichier non protégé avec un avertissement
+ LOG.warnf("Le fichier sera exporté sans protection en raison d'une erreur");
+ return excelData;
+ }
+ }
+
+ /**
+ * Exporte des membres vers CSV
+ */
+ public byte[] exporterVersCSV(List membres, List colonnesExport, boolean inclureHeaders, boolean formaterDates) throws IOException {
+ try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ CSVPrinter printer = new CSVPrinter(
+ new java.io.OutputStreamWriter(outputStream, StandardCharsets.UTF_8),
+ CSVFormat.DEFAULT)) {
+
+ // En-têtes
+ if (inclureHeaders) {
+ List headers = new ArrayList<>();
+ if (colonnesExport.contains("PERSO") || colonnesExport.isEmpty()) {
+ headers.add("Nom");
+ headers.add("Prénom");
+ headers.add("Date de naissance");
+ }
+ if (colonnesExport.contains("CONTACT") || colonnesExport.isEmpty()) {
+ headers.add("Email");
+ headers.add("Téléphone");
+ }
+ if (colonnesExport.contains("ADHESION") || colonnesExport.isEmpty()) {
+ headers.add("Date adhésion");
+ headers.add("Statut");
+ }
+ if (colonnesExport.contains("ORGANISATION") || colonnesExport.isEmpty()) {
+ headers.add("Organisation");
+ }
+ printer.printRecord(headers);
+ }
+
+ // Données
+ for (MembreDTO membre : membres) {
+ List values = new ArrayList<>();
+
+ if (colonnesExport.contains("PERSO") || colonnesExport.isEmpty()) {
+ values.add(membre.getNom() != null ? membre.getNom() : "");
+ values.add(membre.getPrenom() != null ? membre.getPrenom() : "");
+ if (membre.getDateNaissance() != null) {
+ values.add(formaterDates ? membre.getDateNaissance().format(DATE_FORMATTER) : membre.getDateNaissance().toString());
+ } else {
+ values.add("");
+ }
+ }
+ if (colonnesExport.contains("CONTACT") || colonnesExport.isEmpty()) {
+ values.add(membre.getEmail() != null ? membre.getEmail() : "");
+ values.add(membre.getTelephone() != null ? membre.getTelephone() : "");
+ }
+ if (colonnesExport.contains("ADHESION") || colonnesExport.isEmpty()) {
+ if (membre.getDateAdhesion() != null) {
+ values.add(formaterDates ? membre.getDateAdhesion().format(DATE_FORMATTER) : membre.getDateAdhesion().toString());
+ } else {
+ values.add("");
+ }
+ values.add(membre.getStatut() != null ? membre.getStatut().toString() : "");
+ }
+ if (colonnesExport.contains("ORGANISATION") || colonnesExport.isEmpty()) {
+ values.add(membre.getAssociationNom() != null ? membre.getAssociationNom() : "");
+ }
+
+ printer.printRecord(values);
+ }
+
+ printer.flush();
+ return outputStream.toByteArray();
+ }
+ }
+
+ /**
+ * Génère un modèle Excel pour l'import
+ */
+ public byte[] genererModeleImport() throws IOException {
+ try (Workbook workbook = new XSSFWorkbook()) {
+ Sheet sheet = workbook.createSheet("Modèle");
+
+ // En-têtes
+ Row headerRow = sheet.createRow(0);
+ headerRow.createCell(0).setCellValue("Nom");
+ headerRow.createCell(1).setCellValue("Prénom");
+ headerRow.createCell(2).setCellValue("Email");
+ headerRow.createCell(3).setCellValue("Téléphone");
+ headerRow.createCell(4).setCellValue("Date naissance");
+ headerRow.createCell(5).setCellValue("Date adhésion");
+ headerRow.createCell(6).setCellValue("Adresse");
+ headerRow.createCell(7).setCellValue("Profession");
+ headerRow.createCell(8).setCellValue("Type membre");
+
+ // Exemple de ligne
+ Row exampleRow = sheet.createRow(1);
+ exampleRow.createCell(0).setCellValue("DUPONT");
+ exampleRow.createCell(1).setCellValue("Jean");
+ exampleRow.createCell(2).setCellValue("jean.dupont@example.com");
+ exampleRow.createCell(3).setCellValue("+225 07 12 34 56 78");
+ exampleRow.createCell(4).setCellValue("15/01/1990");
+ exampleRow.createCell(5).setCellValue("01/01/2024");
+ exampleRow.createCell(6).setCellValue("Abidjan, Cocody");
+ exampleRow.createCell(7).setCellValue("Ingénieur");
+ exampleRow.createCell(8).setCellValue("ACTIF");
+
+ // Auto-size columns
+ for (int i = 0; i < 9; i++) {
+ sheet.autoSizeColumn(i);
+ }
+
+ // Écrire dans un ByteArrayOutputStream
+ try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
+ workbook.write(outputStream);
+ return outputStream.toByteArray();
+ }
+ }
+ }
+
+ /**
+ * Classe pour le résultat de l'import
+ */
+ public static class ResultatImport {
+ public int totalLignes;
+ public int lignesTraitees;
+ public int lignesErreur;
+ public List erreurs;
+ public List membresImportes;
+ }
+}
+
diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/MembreService.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/MembreService.java
index f998e8b..74d6cfb 100644
--- a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/MembreService.java
+++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/MembreService.java
@@ -14,6 +14,7 @@ import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import jakarta.transaction.Transactional;
+import java.io.InputStream;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
@@ -34,6 +35,9 @@ public class MembreService {
@Inject MembreRepository membreRepository;
+ @Inject
+ MembreImportExportService membreImportExportService;
+
@PersistenceContext
EntityManager entityManager;
@@ -464,16 +468,16 @@ public class MembreService {
parameters.put("organisationIds", criteria.getOrganisationIds());
}
- // Filtre par rôles (recherche dans le champ roles)
+ // Filtre par rôles (recherche via la relation MembreRole -> Role)
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);
+ // Utiliser EXISTS avec une sous-requête pour vérifier les rôles
+ queryBuilder.append(" AND EXISTS (");
+ queryBuilder.append(" SELECT 1 FROM MembreRole mr WHERE mr.membre = m");
+ queryBuilder.append(" AND mr.actif = true");
+ queryBuilder.append(" AND mr.role.code IN :roleCodes");
+ queryBuilder.append(")");
+ // Convertir les noms de rôles en codes (supposant que criteria.getRoles() contient des codes)
+ parameters.put("roleCodes", criteria.getRoles());
}
}
@@ -633,4 +637,101 @@ public class MembreService {
return csv.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
}
+
+ /**
+ * Importe des membres depuis un fichier Excel ou CSV
+ */
+ public MembreImportExportService.ResultatImport importerMembres(
+ InputStream fileInputStream,
+ String fileName,
+ UUID organisationId,
+ String typeMembreDefaut,
+ boolean mettreAJourExistants,
+ boolean ignorerErreurs) {
+ return membreImportExportService.importerMembres(
+ fileInputStream, fileName, organisationId, typeMembreDefaut, mettreAJourExistants, ignorerErreurs);
+ }
+
+ /**
+ * Exporte des membres vers Excel
+ */
+ public byte[] exporterVersExcel(List membres, List colonnesExport, boolean inclureHeaders, boolean formaterDates, boolean inclureStatistiques, String motDePasse) {
+ try {
+ return membreImportExportService.exporterVersExcel(membres, colonnesExport, inclureHeaders, formaterDates, inclureStatistiques, motDePasse);
+ } catch (Exception e) {
+ LOG.errorf(e, "Erreur lors de l'export Excel");
+ throw new RuntimeException("Erreur lors de l'export Excel: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Exporte des membres vers CSV
+ */
+ public byte[] exporterVersCSV(List membres, List colonnesExport, boolean inclureHeaders, boolean formaterDates) {
+ try {
+ return membreImportExportService.exporterVersCSV(membres, colonnesExport, inclureHeaders, formaterDates);
+ } catch (Exception e) {
+ LOG.errorf(e, "Erreur lors de l'export CSV");
+ throw new RuntimeException("Erreur lors de l'export CSV: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Génère un modèle Excel pour l'import
+ */
+ public byte[] genererModeleImport() {
+ try {
+ return membreImportExportService.genererModeleImport();
+ } catch (Exception e) {
+ LOG.errorf(e, "Erreur lors de la génération du modèle");
+ throw new RuntimeException("Erreur lors de la génération du modèle: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Liste les membres pour l'export selon les filtres
+ */
+ public List listerMembresPourExport(
+ UUID associationId,
+ String statut,
+ String type,
+ String dateAdhesionDebut,
+ String dateAdhesionFin) {
+
+ List membres;
+
+ if (associationId != null) {
+ TypedQuery query = entityManager.createQuery(
+ "SELECT m FROM Membre m WHERE m.organisation.id = :associationId", Membre.class);
+ query.setParameter("associationId", associationId);
+ membres = query.getResultList();
+ } else {
+ membres = membreRepository.listAll();
+ }
+
+ // Filtrer par statut
+ if (statut != null && !statut.isEmpty()) {
+ boolean actif = "ACTIF".equals(statut);
+ membres = membres.stream()
+ .filter(m -> m.getActif() == actif)
+ .collect(Collectors.toList());
+ }
+
+ // Filtrer par dates d'adhésion
+ if (dateAdhesionDebut != null && !dateAdhesionDebut.isEmpty()) {
+ LocalDate dateDebut = LocalDate.parse(dateAdhesionDebut);
+ membres = membres.stream()
+ .filter(m -> m.getDateAdhesion() != null && !m.getDateAdhesion().isBefore(dateDebut))
+ .collect(Collectors.toList());
+ }
+
+ if (dateAdhesionFin != null && !dateAdhesionFin.isEmpty()) {
+ LocalDate dateFin = LocalDate.parse(dateAdhesionFin);
+ membres = membres.stream()
+ .filter(m -> m.getDateAdhesion() != null && !m.getDateAdhesion().isAfter(dateFin))
+ .collect(Collectors.toList());
+ }
+
+ return convertToDTOList(membres);
+ }
}
diff --git a/unionflow-server-impl-quarkus/src/main/resources/application-prod.properties b/unionflow-server-impl-quarkus/src/main/resources/application-prod.properties
index 17b76bc..d1dc9c8 100644
--- a/unionflow-server-impl-quarkus/src/main/resources/application-prod.properties
+++ b/unionflow-server-impl-quarkus/src/main/resources/application-prod.properties
@@ -1,19 +1,77 @@
-# Configuration UnionFlow Server - Profil Production
-# Ce fichier est chargé automatiquement quand le profil 'prod' est actif
+# Configuration UnionFlow Server - PRODUCTION
+# Ce fichier est utilisé avec le profil Quarkus "prod"
-# Configuration Hibernate pour production
-quarkus.hibernate-orm.database.generation=validate
+# Configuration HTTP
+quarkus.http.port=8085
+quarkus.http.host=0.0.0.0
+
+# Configuration CORS - Production (strict)
+quarkus.http.cors=true
+quarkus.http.cors.origins=${CORS_ORIGINS:https://unionflow.lions.dev,https://security.lions.dev}
+quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS
+quarkus.http.cors.headers=Content-Type,Authorization
+quarkus.http.cors.allow-credentials=true
+
+# Configuration Base de données PostgreSQL - Production
+quarkus.datasource.db-kind=postgresql
+quarkus.datasource.username=${DB_USERNAME:unionflow}
+quarkus.datasource.password=${DB_PASSWORD}
+quarkus.datasource.jdbc.url=${DB_URL:jdbc:postgresql://localhost:5432/unionflow}
+quarkus.datasource.jdbc.min-size=5
+quarkus.datasource.jdbc.max-size=20
+
+# Configuration Hibernate - Production (IMPORTANT: update, pas drop-and-create)
+quarkus.hibernate-orm.database.generation=update
quarkus.hibernate-orm.log.sql=false
+quarkus.hibernate-orm.jdbc.timezone=UTC
+quarkus.hibernate-orm.packages=dev.lions.unionflow.server.entity
+quarkus.hibernate-orm.metrics.enabled=false
-# Configuration logging pour production
-quarkus.log.console.level=WARN
-quarkus.log.category."dev.lions.unionflow".level=INFO
-quarkus.log.category.root.level=WARN
+# Configuration Flyway - Production (ACTIVÉ)
+quarkus.flyway.migrate-at-start=true
+quarkus.flyway.baseline-on-migrate=true
+quarkus.flyway.baseline-version=1.0.0
-# Configuration Keycloak pour production
-quarkus.oidc.auth-server-url=${KEYCLOAK_SERVER_URL:http://localhost:8180/realms/unionflow}
-quarkus.oidc.client-id=${KEYCLOAK_CLIENT_ID:unionflow-server}
+# Configuration Keycloak OIDC - Production
+quarkus.oidc.auth-server-url=${KEYCLOAK_AUTH_SERVER_URL:https://security.lions.dev/realms/unionflow}
+quarkus.oidc.client-id=unionflow-server
quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET}
quarkus.oidc.tls.verification=required
+quarkus.oidc.application-type=service
+# Configuration Keycloak Policy Enforcer
+quarkus.keycloak.policy-enforcer.enable=false
+quarkus.keycloak.policy-enforcer.lazy-load-paths=true
+quarkus.keycloak.policy-enforcer.enforcement-mode=PERMISSIVE
+# Chemins publics (non protégés)
+quarkus.http.auth.permission.public.paths=/health,/q/*,/favicon.ico
+quarkus.http.auth.permission.public.policy=permit
+
+# Configuration OpenAPI - Production (Swagger désactivé ou protégé)
+quarkus.smallrye-openapi.info-title=UnionFlow Server API
+quarkus.smallrye-openapi.info-version=1.0.0
+quarkus.smallrye-openapi.info-description=API REST pour la gestion d'union avec authentification Keycloak
+quarkus.smallrye-openapi.servers=https://api.lions.dev/unionflow
+
+# Configuration Swagger UI - Production (DÉSACTIVÉ pour sécurité)
+quarkus.swagger-ui.always-include=false
+
+# Configuration santé
+quarkus.smallrye-health.root-path=/health
+
+# Configuration logging - Production
+quarkus.log.console.enable=true
+quarkus.log.console.level=INFO
+quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{2.}] (%t) %s%e%n
+quarkus.log.category."dev.lions.unionflow".level=INFO
+quarkus.log.category."org.hibernate".level=WARN
+quarkus.log.category."io.quarkus".level=INFO
+quarkus.log.category."org.jboss.resteasy".level=WARN
+
+# Configuration Wave Money - Production
+wave.api.key=${WAVE_API_KEY:}
+wave.api.secret=${WAVE_API_SECRET:}
+wave.api.base.url=${WAVE_API_BASE_URL:https://api.wave.com/v1}
+wave.environment=${WAVE_ENVIRONMENT:production}
+wave.webhook.secret=${WAVE_WEBHOOK_SECRET:}
diff --git a/unionflow-server-impl-quarkus/src/main/resources/application-test.properties b/unionflow-server-impl-quarkus/src/main/resources/application-test.properties
index dec9daa..b97f54f 100644
--- a/unionflow-server-impl-quarkus/src/main/resources/application-test.properties
+++ b/unionflow-server-impl-quarkus/src/main/resources/application-test.properties
@@ -9,9 +9,12 @@ quarkus.datasource.jdbc.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1
# Configuration Hibernate pour tests
quarkus.hibernate-orm.database.generation=drop-and-create
+# Désactiver l'exécution de import.sql pendant les tests
+quarkus.hibernate-orm.sql-load-script=
# Configuration Flyway pour tests (désactivé)
quarkus.flyway.migrate-at-start=false
+quarkus.flyway.enabled=false
# Configuration Keycloak pour tests (désactivé)
quarkus.oidc.tenant-enabled=false
diff --git a/unionflow-server-impl-quarkus/src/main/resources/db/migration/V1.2__Create_Organisation_Table.sql b/unionflow-server-impl-quarkus/src/main/resources/db/migration/V1.2__Create_Organisation_Table.sql
index 64e577c..7329794 100644
--- a/unionflow-server-impl-quarkus/src/main/resources/db/migration/V1.2__Create_Organisation_Table.sql
+++ b/unionflow-server-impl-quarkus/src/main/resources/db/migration/V1.2__Create_Organisation_Table.sql
@@ -122,90 +122,9 @@ BEGIN
END IF;
END $$;
--- Insertion de données de test pour le développement
-INSERT INTO organisations (
- nom, nom_court, type_organisation, statut, description,
- email, telephone, adresse, ville, region, pays,
- objectifs, activites_principales, nombre_membres,
- organisation_publique, accepte_nouveaux_membres,
- cree_par
-) VALUES
-(
- 'Lions Club Abidjan Plateau',
- 'LC Plateau',
- 'LIONS_CLUB',
- 'ACTIVE',
- 'Lions Club du district 403 A1, zone Plateau d''Abidjan',
- 'plateau@lionsclub-ci.org',
- '+225 27 20 21 22 23',
- 'Immeuble SCIAM, Boulevard de la République',
- 'Abidjan',
- 'Lagunes',
- 'Côte d''Ivoire',
- 'Servir la communauté par des actions humanitaires et sociales',
- 'Actions de santé, éducation, environnement et aide aux démunis',
- 45,
- true,
- true,
- 'system'
-),
-(
- 'Lions Club Abidjan Cocody',
- 'LC Cocody',
- 'LIONS_CLUB',
- 'ACTIVE',
- 'Lions Club du district 403 A1, zone Cocody',
- 'cocody@lionsclub-ci.org',
- '+225 27 22 44 55 66',
- 'Riviera Golf, Cocody',
- 'Abidjan',
- 'Lagunes',
- 'Côte d''Ivoire',
- 'Servir la communauté par des actions humanitaires et sociales',
- 'Actions de santé, éducation, environnement et aide aux démunis',
- 38,
- true,
- true,
- 'system'
-),
-(
- 'Association des Femmes Entrepreneures CI',
- 'AFECI',
- 'ASSOCIATION',
- 'ACTIVE',
- 'Association pour la promotion de l''entrepreneuriat féminin en Côte d''Ivoire',
- 'contact@afeci.org',
- '+225 05 06 07 08 09',
- 'Marcory Zone 4C',
- 'Abidjan',
- 'Lagunes',
- 'Côte d''Ivoire',
- 'Promouvoir l''entrepreneuriat féminin et l''autonomisation des femmes',
- 'Formation, accompagnement, financement de projets féminins',
- 120,
- true,
- true,
- 'system'
-),
-(
- 'Coopérative Agricole du Nord',
- 'COOP-NORD',
- 'COOPERATIVE',
- 'ACTIVE',
- 'Coopérative des producteurs agricoles du Nord de la Côte d''Ivoire',
- 'info@coop-nord.ci',
- '+225 09 10 11 12 13',
- 'Korhogo Centre',
- 'Korhogo',
- 'Savanes',
- 'Côte d''Ivoire',
- 'Améliorer les conditions de vie des producteurs agricoles',
- 'Production, transformation et commercialisation de produits agricoles',
- 250,
- true,
- true,
- 'system'
-);
+-- IMPORTANT: Aucune donnée fictive n'est insérée dans ce script de migration.
+-- Les données doivent être insérées manuellement via l'interface d'administration
+-- ou via des scripts de migration séparés si nécessaire pour la production.
-- Mise à jour des statistiques de la base de données
ANALYZE organisations;
diff --git a/unionflow-server-impl-quarkus/src/main/resources/import.sql b/unionflow-server-impl-quarkus/src/main/resources/import.sql
index 71a822f..8b3c0d8 100644
--- a/unionflow-server-impl-quarkus/src/main/resources/import.sql
+++ b/unionflow-server-impl-quarkus/src/main/resources/import.sql
@@ -1,128 +1,10 @@
--- Script d'insertion de données initiales pour UnionFlow (avec UUID)
+-- Script d'insertion de données initiales pour UnionFlow
-- Ce fichier est exécuté automatiquement par Hibernate au démarrage
-- Utilisé uniquement en mode développement (quarkus.hibernate-orm.database.generation=drop-and-create)
--- NOTE: Les IDs sont maintenant des UUID générés automatiquement
-
--- Insertion d'organisations initiales avec UUIDs générés
-INSERT INTO organisations (id, nom, nom_court, type_organisation, statut, description,
- email, telephone, adresse, ville, region, pays,
- objectifs, activites_principales, nombre_membres,
- organisation_publique, accepte_nouveaux_membres, cree_par,
- actif, date_creation, niveau_hierarchique, nombre_administrateurs, cotisation_obligatoire) VALUES
-(
- gen_random_uuid(),
- 'Lions Club Abidjan Plateau',
- 'LC Plateau',
- 'LIONS_CLUB',
- 'ACTIVE',
- 'Lions Club du district 403 A1, zone Plateau d''Abidjan',
- 'plateau@lionsclub-ci.org',
- '+225 27 20 21 22 23',
- 'Immeuble SCIAM, Boulevard de la République',
- 'Abidjan',
- 'Lagunes',
- 'Côte d''Ivoire',
- 'Servir la communauté par des actions humanitaires et sociales',
- 'Actions de santé, éducation, environnement et aide aux démunis',
- 45,
- true,
- true,
- 'system',
- true,
- CURRENT_TIMESTAMP,
- 0,
- 0,
- false
-),
-(
- gen_random_uuid(),
- 'Lions Club Abidjan Cocody',
- 'LC Cocody',
- 'LIONS_CLUB',
- 'ACTIVE',
- 'Lions Club du district 403 A1, zone Cocody',
- 'cocody@lionsclub-ci.org',
- '+225 27 22 44 55 66',
- 'Riviera Golf, Cocody',
- 'Abidjan',
- 'Lagunes',
- 'Côte d''Ivoire',
- 'Servir la communauté par des actions humanitaires et sociales',
- 'Actions de santé, éducation, environnement et aide aux démunis',
- 38,
- true,
- true,
- 'system',
- true,
- CURRENT_TIMESTAMP,
- 0,
- 0,
- false
-),
-(
- gen_random_uuid(),
- 'Association des Femmes Entrepreneures CI',
- 'AFECI',
- 'ASSOCIATION',
- 'ACTIVE',
- 'Association pour la promotion de l''entrepreneuriat féminin en Côte d''Ivoire',
- 'contact@afeci.org',
- '+225 05 06 07 08 09',
- 'Marcory Zone 4C',
- 'Abidjan',
- 'Lagunes',
- 'Côte d''Ivoire',
- 'Promouvoir l''entrepreneuriat féminin et l''autonomisation des femmes',
- 'Formation, accompagnement, financement de projets féminins',
- 120,
- true,
- true,
- 'system',
- true,
- CURRENT_TIMESTAMP,
- 0,
- 0,
- false
-),
-(
- gen_random_uuid(),
- 'Coopérative Agricole du Nord',
- 'COOP-NORD',
- 'COOPERATIVE',
- 'ACTIVE',
- 'Coopérative des producteurs agricoles du Nord de la Côte d''Ivoire',
- 'info@coop-nord.ci',
- '+225 09 10 11 12 13',
- 'Korhogo Centre',
- 'Korhogo',
- 'Savanes',
- 'Côte d''Ivoire',
- 'Améliorer les conditions de vie des producteurs agricoles',
- 'Production, transformation et commercialisation de produits agricoles',
- 250,
- true,
- true,
- 'system',
- true,
- CURRENT_TIMESTAMP,
- 0,
- 0,
- false
-);
-
--- Insertion de membres initiaux (avec UUIDs générés et références aux organisations)
--- On utilise des sous-requêtes pour récupérer les IDs des organisations par leur nom
-INSERT INTO membres (id, numero_membre, nom, prenom, email, telephone, date_naissance, date_adhesion, actif, date_creation, organisation_id) VALUES
-(gen_random_uuid(), 'MBR001', 'Kouassi', 'Jean-Baptiste', 'jb.kouassi@email.ci', '+225071234567', '1985-03-15', '2023-01-15', true, '2024-01-01 10:00:00', (SELECT id FROM organisations WHERE nom = 'Lions Club Abidjan Plateau' LIMIT 1)),
-(gen_random_uuid(), 'MBR002', 'Traoré', 'Aminata', 'aminata.traore@email.ci', '+225059876543', '1990-07-22', '2023-02-10', true, '2024-01-01 10:00:00', (SELECT id FROM organisations WHERE nom = 'Lions Club Abidjan Plateau' LIMIT 1)),
-(gen_random_uuid(), 'MBR003', 'Bamba', 'Seydou', 'seydou.bamba@email.ci', '+225012345678', '1988-11-08', '2023-03-05', true, '2024-01-01 10:00:00', (SELECT id FROM organisations WHERE nom = 'Lions Club Abidjan Cocody' LIMIT 1)),
-(gen_random_uuid(), 'MBR004', 'Ouattara', 'Fatoumata', 'fatoumata.ouattara@email.ci', '+225078765432', '1992-05-18', '2023-04-12', true, '2024-01-01 10:00:00', (SELECT id FROM organisations WHERE nom = 'Lions Club Abidjan Cocody' LIMIT 1)),
-(gen_random_uuid(), 'MBR005', 'Koné', 'Ibrahim', 'ibrahim.kone@email.ci', '+225051122334', '1987-09-30', '2023-05-20', true, '2024-01-01 10:00:00', (SELECT id FROM organisations WHERE nom = 'Association des Femmes Entrepreneures CI' LIMIT 1)),
-(gen_random_uuid(), 'MBR006', 'Diabaté', 'Mariam', 'mariam.diabate@email.ci', '+225015566778', '1991-12-03', '2023-06-08', false, '2024-01-01 10:00:00', (SELECT id FROM organisations WHERE nom = 'Association des Femmes Entrepreneures CI' LIMIT 1)),
-(gen_random_uuid(), 'MBR007', 'Sangaré', 'Moussa', 'moussa.sangare@email.ci', '+225079988776', '1989-04-25', '2023-07-15', true, '2024-01-01 10:00:00', (SELECT id FROM organisations WHERE nom = 'Coopérative Agricole du Nord' LIMIT 1)),
-(gen_random_uuid(), 'MBR008', 'Coulibaly', 'Awa', 'awa.coulibaly@email.ci', '+225054433221', '1993-08-14', '2023-08-22', true, '2024-01-01 10:00:00', (SELECT id FROM organisations WHERE nom = 'Coopérative Agricole du Nord' LIMIT 1));
-
--- Note: Les insertions de cotisations et événements avec des références aux membres
--- nécessitent de récupérer les UUIDs réels des membres insérés ci-dessus.
--- Pour le développement, ces données peuvent être insérées via l'application ou
--- via des scripts de test plus complexes qui récupèrent les UUIDs générés.
+--
+-- IMPORTANT: Ce fichier ne doit PAS contenir de données fictives pour la production.
+-- Les données doivent être insérées manuellement via l'interface d'administration
+-- ou via des scripts de migration Flyway si nécessaire.
+--
+-- Ce fichier est laissé vide intentionnellement pour éviter l'insertion automatique
+-- de données fictives lors du démarrage du serveur.
diff --git a/unionflow-server-impl-quarkus/src/test/java/dev/lions/unionflow/server/service/MembreServiceAdvancedSearchTest.java b/unionflow-server-impl-quarkus/src/test/java/dev/lions/unionflow/server/service/MembreServiceAdvancedSearchTest.java
index 8c3b477..3b98131 100644
--- a/unionflow-server-impl-quarkus/src/test/java/dev/lions/unionflow/server/service/MembreServiceAdvancedSearchTest.java
+++ b/unionflow-server-impl-quarkus/src/test/java/dev/lions/unionflow/server/service/MembreServiceAdvancedSearchTest.java
@@ -45,6 +45,7 @@ class MembreServiceAdvancedSearchTest {
.nom("Organisation Test")
.typeOrganisation("ASSOCIATION")
.statut("ACTIF")
+ .email("test@organisation.com")
.build();
testOrganisation.setDateCreation(LocalDateTime.now());
testOrganisation.setActif(true);