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);