Compare commits

...

3 Commits

Author SHA1 Message Date
dahoud
bba28595f6 Ajout des Dockerfiles et configuration de production pour déploiement unionflow 2025-12-09 19:37:23 +00:00
dahoud
1e835ba2c3 refactoring 2025-12-07 17:29:09 +00:00
dahoud
af18b42767 refactoring 2025-12-07 17:15:48 +00:00
44 changed files with 2984 additions and 1884 deletions

View File

@@ -122,6 +122,13 @@
<artifactId>unionflow-server-api</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Lions User Manager Client - Module réutilisable de gestion d'utilisateurs Keycloak -->
<dependency>
<groupId>dev.lions.user.manager</groupId>
<artifactId>lions-user-manager-client-quarkus-primefaces-freya</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Scheduler pour le rafraîchissement des tokens -->
<dependency>

View File

@@ -82,20 +82,42 @@ public interface MembreService {
@GET
@Path("/export")
@Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
@Produces({"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "text/csv", "application/pdf", "application/json"})
byte[] exporterExcel(
@QueryParam("format") @DefaultValue("EXCEL") String format,
@QueryParam("associationId") UUID associationId,
@QueryParam("statut") String statut
@QueryParam("statut") String statut,
@QueryParam("type") String type,
@QueryParam("dateAdhesionDebut") String dateAdhesionDebut,
@QueryParam("dateAdhesionFin") String dateAdhesionFin,
@QueryParam("colonnes") List<String> colonnesExport,
@QueryParam("inclureHeaders") @DefaultValue("true") boolean inclureHeaders,
@QueryParam("formaterDates") @DefaultValue("true") boolean formaterDates,
@QueryParam("inclureStatistiques") @DefaultValue("false") boolean inclureStatistiques,
@QueryParam("motDePasse") String motDePasse
);
@GET
@Path("/export/count")
@Produces(MediaType.APPLICATION_JSON)
Long compterMembresPourExport(
@QueryParam("associationId") UUID associationId,
@QueryParam("statut") String statut,
@QueryParam("type") String type,
@QueryParam("dateAdhesionDebut") String dateAdhesionDebut,
@QueryParam("dateAdhesionFin") String dateAdhesionFin
);
@POST
@Path("/import")
@Consumes(MediaType.MULTIPART_FORM_DATA)
ResultatImportDTO importerDonnees(
@FormParam("file") java.io.InputStream fileInputStream,
@FormParam("associationId") UUID associationId
);
@Produces(MediaType.APPLICATION_JSON)
ResultatImportDTO importerDonnees(MembreImportMultipartForm form);
@GET
@Path("/import/modele")
@Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
byte[] telechargerModeleImport();
@GET
@Path("/autocomplete/villes")

View File

@@ -114,7 +114,7 @@ public class AuditBean implements Serializable {
} catch (Exception e) {
LOGGER.severe("Erreur lors du chargement des logs: " + e.getMessage());
e.printStackTrace();
LOGGER.log(java.util.logging.Level.SEVERE, "Détails de l'erreur de chargement des logs d'audit", e);
tousLesLogs = new ArrayList<>();
ajouterMessage(FacesMessage.SEVERITY_ERROR, "Erreur",
"Erreur lors du chargement des logs: " + e.getMessage());

View File

@@ -5,8 +5,6 @@ import jakarta.inject.Named;
import jakarta.annotation.PostConstruct;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
@@ -42,51 +40,58 @@ public class DemandesBean implements Serializable {
private String commentaireAssignation;
// Statistiques
private int enAttente = 12;
private int urgentes = 5;
private int traitees = 143;
private int delaiMoyenTraitement = 3;
private int enAttente = 0;
private int urgentes = 0;
private int traitees = 0;
private int delaiMoyenTraitement = 0;
@PostConstruct
public void init() {
initializeDemandes();
initializeGestionnaires();
initializeNouvelleDemande();
calculerStatistiques();
selectedDemandes = new ArrayList<>();
}
private void initializeDemandes() {
demandes = new ArrayList<>();
String[] objets = {
"Demande d'aide financière urgente", "Certificat de membership",
"Mutation vers club Dakar", "Réclamation cotisation",
"Aide médicale famille", "Certificat de bonne conduite"
};
String[] types = {"AIDE_FINANCIERE", "CERTIFICAT", "MUTATION", "RECLAMATION", "AIDE_FINANCIERE", "CERTIFICAT"};
String[] statuts = {"EN_ATTENTE", "EN_COURS", "APPROUVEE", "EN_ATTENTE", "URGENTE", "EN_COURS"};
String[] priorites = {"URGENTE", "NORMALE", "NORMALE", "BASSE", "URGENTE", "NORMALE"};
String[] demandeurs = {"Marie Kouassi", "Paul Traoré", "Fatou Sanogo", "Jean Ouattara", "Aissata Koné", "Ibrahim Touré"};
for (int i = 0; i < objets.length; i++) {
Demande demande = new Demande();
demande.setId(UUID.fromString(String.format("00000000-0000-0000-0000-%012d", i + 1)));
demande.setReference("DEM-2024-" + String.format("%04d", i + 1));
demande.setObjet(objets[i]);
demande.setType(types[i]);
demande.setStatut(statuts[i]);
demande.setPriorite(priorites[i]);
demande.setNomDemandeur(demandeurs[i]);
demande.setNumeroMembre("M" + String.format("%06d", 1000 + i));
demande.setTelephoneDemandeur("+225 07 " + String.format("%02d", 10 + i) + " " + String.format("%02d", 20 + i) + " " + String.format("%02d", 30 + i));
demande.setDateDepot(LocalDate.now().minusDays(i * 2));
demande.setDateEcheance(LocalDate.now().plusDays(7 + i));
demande.setHeureDepot(LocalDateTime.now().minusHours(i * 3).format(DateTimeFormatter.ofPattern("HH:mm")));
if (i % 3 != 0) demande.setAssigneA("Gestionnaire " + (i % 3 + 1));
demandes.add(demande);
// TODO: Charger depuis le backend via DemandeAideService
// Pour l'instant, liste vide - les données viendront du backend
}
private void calculerStatistiques() {
if (demandes == null || demandes.isEmpty()) {
enAttente = 0;
urgentes = 0;
traitees = 0;
delaiMoyenTraitement = 0;
return;
}
// Calculer depuis les données réelles
enAttente = (int) demandes.stream()
.filter(d -> "EN_ATTENTE".equals(d.getStatut()))
.count();
urgentes = (int) demandes.stream()
.filter(d -> "URGENTE".equals(d.getPriorite()))
.count();
traitees = (int) demandes.stream()
.filter(d -> "APPROUVEE".equals(d.getStatut()) || "REJETEE".equals(d.getStatut()))
.count();
// Calculer le délai moyen de traitement
long totalJours = demandes.stream()
.filter(d -> d.getDateDepot() != null && "APPROUVEE".equals(d.getStatut()))
.mapToLong(d -> ChronoUnit.DAYS.between(d.getDateDepot(), LocalDate.now()))
.sum();
long countTraitees = demandes.stream()
.filter(d -> d.getDateDepot() != null && "APPROUVEE".equals(d.getStatut()))
.count();
delaiMoyenTraitement = countTraitees > 0 ? (int) (totalJours / countTraitees) : 0;
// Initialiser les sous-listes
demandesUrgentes = demandes.stream()
.filter(d -> "URGENTE".equals(d.getPriorite()) || "EN_ATTENTE".equals(d.getStatut()))
@@ -94,6 +99,11 @@ public class DemandesBean implements Serializable {
.collect(Collectors.toList());
dernieresDemandes = demandes.stream()
.sorted((d1, d2) -> {
if (d1.getDateDepot() == null) return 1;
if (d2.getDateDepot() == null) return -1;
return d2.getDateDepot().compareTo(d1.getDateDepot());
})
.limit(4)
.collect(Collectors.toList());
}
@@ -181,6 +191,8 @@ public class DemandesBean implements Serializable {
public void actualiser() {
LOGGER.info("Actualisation des données");
initializeDemandes();
calculerStatistiques();
}
public void filtrerUrgentes() {

View File

@@ -147,7 +147,7 @@ public class EvenementsBean implements Serializable {
} catch (Exception e) {
LOGGER.severe("Erreur lors du chargement des événements: " + e.getMessage());
e.printStackTrace();
LOGGER.log(java.util.logging.Level.SEVERE, "Détails de l'erreur de chargement des événements", e);
tousLesEvenements = new ArrayList<>();
ajouterMessage(FacesMessage.SEVERITY_ERROR, "Erreur",
"Erreur lors du chargement des événements: " + e.getMessage());
@@ -522,7 +522,7 @@ public class EvenementsBean implements Serializable {
} catch (Exception e) {
LOGGER.severe("Erreur lors de la création de l'événement: " + e.getMessage());
e.printStackTrace();
LOGGER.log(java.util.logging.Level.SEVERE, "Détails de l'erreur de création d'événement", e);
ajouterMessage(FacesMessage.SEVERITY_ERROR, "Erreur",
"Erreur lors de la création de l'événement: " + e.getMessage());
}

View File

@@ -385,6 +385,51 @@ public class SuperAdminBean implements Serializable {
public int getUtilisateursActifs() { return utilisateursActifs; }
public void setUtilisateursActifs(int utilisateursActifs) { this.utilisateursActifs = utilisateursActifs; }
/**
* Calcule les pourcentages pour les progress bars (jauges) basés sur des objectifs réalistes
*/
private void calculerPourcentagesJauges() {
// Objectif : 1000 membres (100%)
int objectifMembres = 1000;
pourcentageMembres = totalMembres > 0 ? Math.min(100, (totalMembres * 100) / objectifMembres) : 0;
// Objectif : 50 organisations (100%)
int objectifOrganisations = 50;
pourcentageOrganisations = totalEntites > 0 ? Math.min(100, (totalEntites * 100) / objectifOrganisations) : 0;
// Objectif : 10 000 000 FCFA de revenus (100%)
// Pour l'instant, si revenus = 0, on met 0%
try {
String revenusStr = revenusGlobaux.replaceAll("[^0-9]", "");
if (!revenusStr.isEmpty()) {
long revenusLong = Long.parseLong(revenusStr);
long objectifRevenus = 10_000_000L; // 10 millions FCFA
pourcentageRevenus = revenusLong > 0 ? Math.min(100, (int) ((revenusLong * 100) / objectifRevenus)) : 0;
} else {
pourcentageRevenus = 0;
}
} catch (Exception e) {
pourcentageRevenus = 0;
}
// Objectif : 100 activités journalières (100%)
int objectifActivite = 100;
pourcentageActivite = activiteJournaliere > 0 ? Math.min(100, (activiteJournaliere * 100) / objectifActivite) : 0;
}
// Getters pour les pourcentages des jauges
public int getPourcentageMembres() { return pourcentageMembres; }
public void setPourcentageMembres(int pourcentageMembres) { this.pourcentageMembres = pourcentageMembres; }
public int getPourcentageOrganisations() { return pourcentageOrganisations; }
public void setPourcentageOrganisations(int pourcentageOrganisations) { this.pourcentageOrganisations = pourcentageOrganisations; }
public int getPourcentageRevenus() { return pourcentageRevenus; }
public void setPourcentageRevenus(int pourcentageRevenus) { this.pourcentageRevenus = pourcentageRevenus; }
public int getPourcentageActivite() { return pourcentageActivite; }
public void setPourcentageActivite(int pourcentageActivite) { this.pourcentageActivite = pourcentageActivite; }
// Classes internes
public static class Alerte {
private UUID id;

View File

@@ -23,14 +23,14 @@
<ui:param name="onclick" value="PF('exportDialog').show(); return false;" />
<ui:param name="outlined" value="true" />
</ui:include>
<ui:include src="/templates/components/buttons/button-icon.xhtml">
<ui:include src="/templates/components/buttons/button-secondary.xhtml">
<ui:param name="value" value="" />
<ui:param name="icon" value="pi pi-refresh" />
<ui:param name="action" value="#{auditBean.actualiser}" />
<ui:param name="update" value="@form" />
<ui:param name="update" value=":formFiltres :formTableau:tableauAudit" />
<ui:param name="title" value="Actualiser" />
<ui:param name="rounded" value="true" />
<ui:param name="text" value="false" />
<ui:param name="styleClass" value="ui-button-outlined ui-button-secondary" />
<ui:param name="outlined" value="true" />
<ui:param name="styleClass" value="ui-button-sm" />
</ui:include>
</div>
</h:form>
@@ -39,32 +39,44 @@
<!-- Statistiques -->
<div class="grid">
<ui:include src="/templates/components/cards/stat-card.xhtml">
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Événements Totaux" />
<ui:param name="value" value="#{auditBean.totalEvenements}" />
<ui:param name="label" value="Événements Totaux" />
<ui:param name="icon" value="pi pi-history" />
<ui:param name="bgColor" value="blue" />
<ui:param name="icon" value="pi-history" />
<ui:param name="iconColor" value="blue-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
<ui:param name="colSize" value="col-12 md:col-6 lg:col-3" />
</ui:include>
<ui:include src="/templates/components/cards/stat-card.xhtml">
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Connexions Réussies" />
<ui:param name="value" value="#{auditBean.connexionsReussies}" />
<ui:param name="label" value="Connexions Réussies" />
<ui:param name="icon" value="pi pi-check-circle" />
<ui:param name="bgColor" value="green" />
<ui:param name="icon" value="pi-check-circle" />
<ui:param name="iconColor" value="green-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
<ui:param name="colSize" value="col-12 md:col-6 lg:col-3" />
</ui:include>
<ui:include src="/templates/components/cards/stat-card.xhtml">
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Tentatives Échouées" />
<ui:param name="value" value="#{auditBean.tentativesEchouees}" />
<ui:param name="label" value="Tentatives Échouées" />
<ui:param name="icon" value="pi pi-times-circle" />
<ui:param name="bgColor" value="orange" />
<ui:param name="icon" value="pi-times-circle" />
<ui:param name="iconColor" value="orange-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
<ui:param name="colSize" value="col-12 md:col-6 lg:col-3" />
</ui:include>
<ui:include src="/templates/components/cards/stat-card.xhtml">
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Alertes Sécurité" />
<ui:param name="value" value="#{auditBean.alertesSecurite}" />
<ui:param name="label" value="Alertes Sécurité" />
<ui:param name="icon" value="pi pi-exclamation-triangle" />
<ui:param name="bgColor" value="red" />
<ui:param name="icon" value="pi-exclamation-triangle" />
<ui:param name="iconColor" value="red-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
<ui:param name="colSize" value="col-12 md:col-6 lg:col-3" />
</ui:include>
</div>
@@ -181,7 +193,7 @@
<ui:param name="value" value="Réinitialiser" />
<ui:param name="icon" value="pi pi-refresh" />
<ui:param name="action" value="#{auditBean.reinitialiserFiltres}" />
<ui:param name="update" value="@form :formTableau:tableauAudit" />
<ui:param name="update" value=":formFiltres :formTableau:tableauAudit" />
<ui:param name="outlined" value="true" />
</ui:include>
</div>
@@ -258,12 +270,15 @@
<p:column headerText="Actions" style="width: 6%" styleClass="text-center">
<div class="flex justify-content-center gap-1">
<p:commandButton icon="pi pi-eye"
title="Voir les détails"
styleClass="p-button-sm p-button-rounded p-button-info"
action="#{auditBean.selectionnerLog(log)}"
update=":formDetails:dlgDetails"
oncomplete="PF('dlgDetails').show();" />
<ui:include src="/templates/components/buttons/button-info.xhtml">
<ui:param name="value" value="" />
<ui:param name="icon" value="pi pi-eye" />
<ui:param name="action" value="#{auditBean.selectionnerLog(log)}" />
<ui:param name="update" value=":formDetails:dlgDetails" />
<ui:param name="oncomplete" value="PF('dlgDetails').show();" />
<ui:param name="title" value="Voir les détails" />
<ui:param name="styleClass" value="p-button-sm p-button-rounded" />
</ui:include>
</div>
</p:column>
</p:dataTable>
@@ -380,10 +395,12 @@
</div>
<f:facet name="footer">
<p:commandButton value="Fermer"
icon="pi pi-times"
onclick="PF('dlgDetails').hide();"
styleClass="p-button-outlined" />
<ui:include src="/templates/components/buttons/button-secondary.xhtml">
<ui:param name="value" value="Fermer" />
<ui:param name="icon" value="pi pi-times" />
<ui:param name="onclick" value="PF('dlgDetails').hide();" />
<ui:param name="outlined" value="true" />
</ui:include>
</f:facet>
</p:dialog>
</h:form>
@@ -423,14 +440,17 @@
<f:facet name="footer">
<div class="flex justify-content-end gap-2">
<p:commandButton value="Annuler"
styleClass="p-button-outlined"
onclick="PF('exportDialog').hide();"
type="button" />
<p:commandButton value="Exporter"
icon="pi pi-download"
styleClass="p-button-success"
action="#{auditBean.exporter}" />
<ui:include src="/templates/components/buttons/button-secondary.xhtml">
<ui:param name="value" value="Annuler" />
<ui:param name="onclick" value="PF('exportDialog').hide();" />
<ui:param name="outlined" value="true" />
</ui:include>
<ui:include src="/templates/components/buttons/button-success.xhtml">
<ui:param name="value" value="Exporter" />
<ui:param name="icon" value="pi pi-download" />
<ui:param name="action" value="#{auditBean.exporter}" />
<ui:param name="update" value="none" />
</ui:include>
</div>
</f:facet>
</p:dialog>

View File

@@ -47,144 +47,83 @@
<!-- KPIs Financiers avec grille Freya stricte -->
<div class="formgrid grid">
<!-- KPI 1: Montant Collecté -->
<div class="field col-12 md:col-6 lg:col-3 xl:col-2">
<div class="card surface-50 border-round-lg">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Collecté ce mois</span>
<div class="flex align-items-center justify-content-center bg-green-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-check text-green-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-xl mb-2">#{cotisationsGestionBean.montantCollecte}</div>
<p:progressBar value="#{cotisationsGestionBean.progressionMensuelle}"
showValue="false"
styleClass="surface-200"
style="height: 0.5rem;" />
<div class="text-500 text-xs mt-2">
<i class="pi pi-arrow-up text-green-500 text-xs"></i>
#{cotisationsGestionBean.progressionMensuelle}% de l'objectif
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Collecté ce mois" />
<ui:param name="value" value="#{cotisationsGestionBean.montantCollecte}" />
<ui:param name="icon" value="pi-check" />
<ui:param name="iconColor" value="green-600" />
<ui:param name="growthValue" value="#{cotisationsGestionBean.progressionMensuelle}" />
<ui:param name="growthLabel" value="de l'objectif" />
<ui:param name="progressValue" value="#{cotisationsGestionBean.progressionMensuelle}" />
<ui:param name="colSize" value="col-12 md:col-6 lg:col-3 xl:col-2" />
</ui:include>
<!-- KPI 2: Membres à jour -->
<div class="field col-12 md:col-6 lg:col-3 xl:col-2">
<div class="card surface-50 border-round-lg">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Membres à jour</span>
<div class="flex align-items-center justify-content-center bg-blue-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-users text-blue-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-xl mb-2">#{cotisationsGestionBean.membresAJour}</div>
<p:progressBar value="#{cotisationsGestionBean.pourcentageMembresAJour}"
showValue="false"
styleClass="surface-200"
style="height: 0.5rem;" />
<div class="text-500 text-xs mt-2">
<i class="pi pi-circle-fill text-blue-500 text-xs"></i>
#{cotisationsGestionBean.pourcentageMembresAJour}% conformes
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Membres à jour" />
<ui:param name="value" value="#{cotisationsGestionBean.membresAJour}" />
<ui:param name="icon" value="pi-users" />
<ui:param name="iconColor" value="blue-600" />
<ui:param name="statusIcon" value="pi-circle-fill" />
<ui:param name="statusLabel" value="Conformes" />
<ui:param name="statusValue" value="#{cotisationsGestionBean.pourcentageMembresAJour}%" />
<ui:param name="progressValue" value="#{cotisationsGestionBean.pourcentageMembresAJour}" />
<ui:param name="colSize" value="col-12 md:col-6 lg:col-3 xl:col-2" />
</ui:include>
<!-- KPI 3: En Attente -->
<div class="field col-12 md:col-6 lg:col-3 xl:col-2">
<div class="card surface-50 border-round-lg">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">En attente</span>
<div class="flex align-items-center justify-content-center bg-orange-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-clock text-orange-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-xl mb-2">#{cotisationsGestionBean.montantEnAttente}</div>
<div class="text-orange-600 font-semibold text-sm">
#{cotisationsGestionBean.nombreCotisationsEnAttente} cotisations
</div>
<div class="text-500 text-xs mt-2">
<i class="pi pi-exclamation-triangle text-orange-500 text-xs"></i>
À traiter rapidement
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="En attente" />
<ui:param name="value" value="#{cotisationsGestionBean.montantEnAttente}" />
<ui:param name="icon" value="pi-clock" />
<ui:param name="iconColor" value="orange-600" />
<ui:param name="statusIcon" value="pi-exclamation-triangle" />
<ui:param name="statusLabel" value="Cotisations" />
<ui:param name="statusValue" value="#{cotisationsGestionBean.nombreCotisationsEnAttente}" />
<ui:param name="noDataLabel" value="À traiter rapidement" />
<ui:param name="colSize" value="col-12 md:col-6 lg:col-3 xl:col-2" />
<ui:param name="showProgress" value="false" />
</ui:include>
<!-- KPI 4: Impayés -->
<div class="field col-12 md:col-6 lg:col-3 xl:col-2">
<div class="card surface-50 border-round-lg">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Impayés</span>
<div class="flex align-items-center justify-content-center bg-red-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-exclamation-circle text-red-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-xl mb-2">#{cotisationsGestionBean.montantImpayes}</div>
<div class="text-red-600 font-semibold text-sm">
#{cotisationsGestionBean.joursRetardMoyen}j de retard moy.
</div>
<div class="text-500 text-xs mt-2">
<i class="pi pi-arrow-down text-red-500 text-xs"></i>
Action requise
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Impayés" />
<ui:param name="value" value="#{cotisationsGestionBean.montantImpayes}" />
<ui:param name="icon" value="pi-exclamation-circle" />
<ui:param name="iconColor" value="red-600" />
<ui:param name="statusIcon" value="pi-arrow-down" />
<ui:param name="statusLabel" value="Retard moyen" />
<ui:param name="statusValue" value="#{cotisationsGestionBean.joursRetardMoyen}j" />
<ui:param name="noDataLabel" value="Action requise" />
<ui:param name="colSize" value="col-12 md:col-6 lg:col-3 xl:col-2" />
<ui:param name="showProgress" value="false" />
</ui:include>
<!-- KPI 5: Revenus 2024 -->
<div class="field col-12 md:col-6 lg:col-3 xl:col-2">
<div class="card surface-50 border-round-lg">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Revenus 2024</span>
<div class="flex align-items-center justify-content-center bg-purple-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-chart-line text-purple-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-xl mb-2">#{cotisationsGestionBean.revenus2024}</div>
<div class="text-green-600 font-semibold text-sm">
<i class="pi pi-arrow-up text-green-500"></i>
#{cotisationsGestionBean.croissanceAnnuelle}
</div>
<div class="text-500 text-xs mt-2">
Croissance annuelle
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Revenus 2024" />
<ui:param name="value" value="#{cotisationsGestionBean.revenus2024}" />
<ui:param name="icon" value="pi-chart-line" />
<ui:param name="iconColor" value="purple-600" />
<ui:param name="growthValue" value="#{cotisationsGestionBean.croissanceAnnuelle}" />
<ui:param name="growthLabel" value="Croissance annuelle" />
<ui:param name="colSize" value="col-12 md:col-6 lg:col-3 xl:col-2" />
<ui:param name="showProgress" value="false" />
</ui:include>
<!-- KPI 6: Wave Money -->
<div class="field col-12 md:col-6 lg:col-3 xl:col-2">
<div class="card surface-50 border-round-lg">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Prélèvements Auto</span>
<div class="flex align-items-center justify-content-center bg-teal-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-mobile text-teal-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-xl mb-2">#{cotisationsGestionBean.prelevementsActifs}</div>
<div class="text-teal-600 font-semibold text-sm">
#{cotisationsGestionBean.montantPrelevementsPrevu} FCFA/mois
</div>
<div class="text-500 text-xs mt-2">
<i class="pi pi-sync text-teal-500 text-xs"></i>
Automatique
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Prélèvements Auto" />
<ui:param name="value" value="#{cotisationsGestionBean.prelevementsActifs}" />
<ui:param name="icon" value="pi-mobile" />
<ui:param name="iconColor" value="teal-600" />
<ui:param name="statusIcon" value="pi-sync" />
<ui:param name="statusLabel" value="Montant/mois" />
<ui:param name="statusValue" value="#{cotisationsGestionBean.montantPrelevementsPrevu} FCFA" />
<ui:param name="noDataLabel" value="Automatique" />
<ui:param name="colSize" value="col-12 md:col-6 lg:col-3 xl:col-2" />
<ui:param name="showProgress" value="false" />
</ui:include>
</div>
<!-- Section Analytics avec disposition Freya -->

View File

@@ -47,85 +47,51 @@
<!-- KPIs avec grille Freya stricte -->
<div class="formgrid grid">
<!-- KPI 1: Total Événements -->
<div class="field col-12 md:col-6 lg:col-3">
<div class="card surface-50 border-round-lg">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Total Événements</span>
<div class="flex align-items-center justify-content-center bg-blue-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-calendar text-blue-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-2xl mb-2">#{evenementsBean.statistiques.totalEvenements}</div>
<div class="flex align-items-center">
<i class="pi pi-arrow-up text-green-500 text-sm mr-1"></i>
<span class="text-green-600 font-semibold text-sm mr-2">+#{evenementsBean.statistiques.evenementsCeMois}</span>
<span class="text-500 text-xs">ce mois</span>
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Total Événements" />
<ui:param name="value" value="#{evenementsBean.statistiques.totalEvenements}" />
<ui:param name="icon" value="pi-calendar" />
<ui:param name="iconColor" value="blue-600" />
<ui:param name="growthValue" value="#{evenementsBean.statistiques.evenementsCeMois}" />
<ui:param name="growthLabel" value="ce mois" />
<ui:param name="growthType" value="number" />
<ui:param name="showProgress" value="false" />
</ui:include>
<!-- KPI 2: Événements Actifs -->
<div class="field col-12 md:col-6 lg:col-3">
<div class="card surface-50 border-round-lg">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Événements Actifs</span>
<div class="flex align-items-center justify-content-center bg-green-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-check text-green-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-2xl mb-2">#{evenementsBean.statistiques.evenementsActifs}</div>
<p:progressBar value="#{evenementsBean.statistiques.tauxParticipationMoyen}"
showValue="false"
styleClass="surface-200 mt-2"
style="height: 0.5rem;" />
<div class="text-500 text-xs mt-2">#{evenementsBean.statistiques.tauxParticipationMoyen}% de participation</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Événements Actifs" />
<ui:param name="value" value="#{evenementsBean.statistiques.evenementsActifs}" />
<ui:param name="icon" value="pi-check" />
<ui:param name="iconColor" value="green-600" />
<ui:param name="progressValue" value="#{evenementsBean.statistiques.tauxParticipationMoyen}" />
<ui:param name="noDataLabel" value="#{evenementsBean.statistiques.tauxParticipationMoyen}% de participation" />
<ui:param name="showGrowth" value="false" />
</ui:include>
<!-- KPI 3: Participants Inscrits -->
<div class="field col-12 md:col-6 lg:col-3">
<div class="card surface-50 border-round-lg">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Participants</span>
<div class="flex align-items-center justify-content-center bg-orange-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-users text-orange-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-2xl mb-2">#{evenementsBean.statistiques.participantsTotal}</div>
<div class="text-orange-600 font-semibold text-sm">
Moyenne: #{evenementsBean.statistiques.moyenneParticipants}/événement
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Participants" />
<ui:param name="value" value="#{evenementsBean.statistiques.participantsTotal}" />
<ui:param name="icon" value="pi-users" />
<ui:param name="iconColor" value="orange-600" />
<ui:param name="statusIcon" value="pi-info-circle" />
<ui:param name="statusLabel" value="Moyenne" />
<ui:param name="statusValue" value="#{evenementsBean.statistiques.moyenneParticipants}/événement" />
<ui:param name="showProgress" value="false" />
</ui:include>
<!-- KPI 4: Budget Total -->
<div class="field col-12 md:col-6 lg:col-3">
<div class="card surface-50 border-round-lg">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Budget Total</span>
<div class="flex align-items-center justify-content-center bg-purple-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-dollar text-purple-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-xl mb-2">#{evenementsBean.statistiques.budgetTotal}</div>
<div class="flex align-items-center">
<i class="pi pi-trending-up text-purple-500 text-sm mr-1"></i>
<span class="text-purple-600 text-sm">Suivi budgétaire optimal</span>
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Budget Total" />
<ui:param name="value" value="#{evenementsBean.statistiques.budgetTotal}" />
<ui:param name="icon" value="pi-dollar" />
<ui:param name="iconColor" value="purple-600" />
<ui:param name="statusIcon" value="pi-trending-up" />
<ui:param name="statusLabel" value="Suivi budgétaire" />
<ui:param name="statusValue" value="optimal" />
<ui:param name="showProgress" value="false" />
</ui:include>
</div>

View File

@@ -41,66 +41,42 @@
</div>
<!-- Soldes et Statistiques -->
<div class="grid">
<div class="col-12 md:col-3">
<div class="card bg-purple-100 border-left-3 border-purple-500">
<div class="flex justify-content-between">
<div>
<div class="text-purple-900 font-bold text-2xl">#{caisseBean.soldePrincipal}</div>
<div class="text-purple-700">Solde Principal</div>
</div>
<div class="bg-purple-500 text-white border-round text-center"
style="width: 3rem; height: 3rem; line-height: 3rem;">
<i class="pi pi-wallet text-xl"></i>
</div>
</div>
</div>
</div>
<div class="formgrid grid">
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Solde Principal" />
<ui:param name="value" value="#{caisseBean.soldePrincipal}" />
<ui:param name="icon" value="pi-wallet" />
<ui:param name="iconColor" value="purple-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
</ui:include>
<div class="col-12 md:col-3">
<div class="card bg-green-100 border-left-3 border-green-500">
<div class="flex justify-content-between">
<div>
<div class="text-green-900 font-bold text-2xl">#{caisseBean.totalEntrees}</div>
<div class="text-green-700">Entrées (30j)</div>
</div>
<div class="bg-green-500 text-white border-round text-center"
style="width: 3rem; height: 3rem; line-height: 3rem;">
<i class="pi pi-arrow-down text-xl"></i>
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Entrées (30j)" />
<ui:param name="value" value="#{caisseBean.totalEntrees}" />
<ui:param name="icon" value="pi-arrow-down" />
<ui:param name="iconColor" value="green-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
</ui:include>
<div class="col-12 md:col-3">
<div class="card bg-red-100 border-left-3 border-red-500">
<div class="flex justify-content-between">
<div>
<div class="text-red-900 font-bold text-2xl">#{caisseBean.totalSorties}</div>
<div class="text-red-700">Sorties (30j)</div>
</div>
<div class="bg-red-500 text-white border-round text-center"
style="width: 3rem; height: 3rem; line-height: 3rem;">
<i class="pi pi-arrow-up text-xl"></i>
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Sorties (30j)" />
<ui:param name="value" value="#{caisseBean.totalSorties}" />
<ui:param name="icon" value="pi-arrow-up" />
<ui:param name="iconColor" value="red-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
</ui:include>
<div class="col-12 md:col-3">
<div class="card bg-blue-100 border-left-3 border-blue-500">
<div class="flex justify-content-between">
<div>
<div class="text-blue-900 font-bold text-2xl">#{caisseBean.soldeWaveMoney}</div>
<div class="text-blue-700">Wave Money</div>
</div>
<div class="bg-blue-500 text-white border-round text-center"
style="width: 3rem; height: 3rem; line-height: 3rem;">
<i class="pi pi-mobile text-xl"></i>
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Wave Money" />
<ui:param name="value" value="#{caisseBean.soldeWaveMoney}" />
<ui:param name="icon" value="pi-mobile" />
<ui:param name="iconColor" value="blue-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
</ui:include>
</div>
<!-- Graphiques -->

View File

@@ -77,53 +77,46 @@
<!-- KPIs Financiers -->
<h:panelGroup id="kpiPanel">
<div class="grid">
<div class="col-12 md:col-3">
<div class="card text-center">
<div class="text-900 text-xl mb-3 font-medium">Revenus Totaux</div>
<div class="text-primary text-5xl font-bold mb-3">#{rapportBean.revenusTotaux}</div>
<div class="flex align-items-center justify-content-center">
<i class="pi pi-arrow-up text-green-500 mr-2"></i>
<span class="text-green-500 font-medium">+#{rapportBean.croissanceRevenus}%</span>
<span class="text-600 ml-2">vs période précédente</span>
</div>
</div>
</div>
<div class="formgrid grid">
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Revenus Totaux" />
<ui:param name="value" value="#{rapportBean.revenusTotaux}" />
<ui:param name="icon" value="pi-dollar" />
<ui:param name="iconColor" value="blue-600" />
<ui:param name="growthValue" value="#{rapportBean.croissanceRevenus}" />
<ui:param name="growthLabel" value="vs période précédente" />
<ui:param name="showProgress" value="false" />
</ui:include>
<div class="col-12 md:col-3">
<div class="card text-center">
<div class="text-900 text-xl mb-3 font-medium">penses Totales</div>
<div class="text-orange-500 text-5xl font-bold mb-3">#{rapportBean.depensesTotales}</div>
<div class="flex align-items-center justify-content-center">
<i class="pi pi-arrow-down text-red-500 mr-2"></i>
<span class="text-red-500 font-medium">+#{rapportBean.croissanceDepenses}%</span>
<span class="text-600 ml-2">vs période précédente</span>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Dépenses Totales" />
<ui:param name="value" value="#{rapportBean.depensesTotales}" />
<ui:param name="icon" value="pi-shopping-cart" />
<ui:param name="iconColor" value="orange-600" />
<ui:param name="growthValue" value="#{rapportBean.croissanceDepenses}" />
<ui:param name="growthLabel" value="vs période précédente" />
<ui:param name="showProgress" value="false" />
</ui:include>
<div class="col-12 md:col-3">
<div class="card text-center">
<div class="text-900 text-xl mb-3 font-medium">Bénéfice Net</div>
<div class="text-green-500 text-5xl font-bold mb-3">#{rapportBean.beneficeNet}</div>
<div class="flex align-items-center justify-content-center">
<p:progressBar value="#{rapportBean.margePercentage}"
labelTemplate="Marge: {value}%"
styleClass="w-full" />
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Bénéfice Net" />
<ui:param name="value" value="#{rapportBean.beneficeNet}" />
<ui:param name="icon" value="pi-chart-line" />
<ui:param name="iconColor" value="green-600" />
<ui:param name="progressValue" value="#{rapportBean.margePercentage}" />
<ui:param name="showGrowth" value="false" />
</ui:include>
<div class="col-12 md:col-3">
<div class="card text-center">
<div class="text-900 text-xl mb-3 font-medium">Trésorerie</div>
<div class="text-purple-500 text-5xl font-bold mb-3">#{rapportBean.tresorerie}</div>
<div class="flex align-items-center justify-content-center">
<i class="pi pi-info-circle text-blue-500 mr-2"></i>
<span class="text-600">#{rapportBean.joursAutonomie} jours d'autonomie</span>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Trésorerie" />
<ui:param name="value" value="#{rapportBean.tresorerie}" />
<ui:param name="icon" value="pi-wallet" />
<ui:param name="iconColor" value="purple-600" />
<ui:param name="statusIcon" value="pi-info-circle" />
<ui:param name="statusLabel" value="Jours d'autonomie" />
<ui:param name="statusValue" value="#{rapportBean.joursAutonomie}" />
<ui:param name="showProgress" value="false" />
</ui:include>
</div>
</h:panelGroup>
@@ -309,31 +302,42 @@
<!-- Ratios et Indicateurs -->
<div class="card">
<h5>Indicateurs Clés de Performance</h5>
<div class="grid">
<div class="col-12 md:col-3">
<div class="text-center p-3 surface-50 border-round">
<div class="text-3xl font-bold text-primary mb-2">#{rapportBean.tauxRecouvrement}%</div>
<div class="text-600">Taux de Recouvrement</div>
</div>
</div>
<div class="col-12 md:col-3">
<div class="text-center p-3 surface-50 border-round">
<div class="text-3xl font-bold text-green-500 mb-2">#{rapportBean.ratioCouverture}</div>
<div class="text-600">Ratio de Couverture</div>
</div>
</div>
<div class="col-12 md:col-3">
<div class="text-center p-3 surface-50 border-round">
<div class="text-3xl font-bold text-orange-500 mb-2">#{rapportBean.coutMoyenMembre}</div>
<div class="text-600">Coût Moyen/Membre</div>
</div>
</div>
<div class="col-12 md:col-3">
<div class="text-center p-3 surface-50 border-round">
<div class="text-3xl font-bold text-purple-500 mb-2">#{rapportBean.revenuMoyenMembre}</div>
<div class="text-600">Revenu Moyen/Membre</div>
</div>
</div>
<div class="formgrid grid">
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Taux de Recouvrement" />
<ui:param name="value" value="#{rapportBean.tauxRecouvrement}%" />
<ui:param name="icon" value="pi-percentage" />
<ui:param name="iconColor" value="blue-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
</ui:include>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Ratio de Couverture" />
<ui:param name="value" value="#{rapportBean.ratioCouverture}" />
<ui:param name="icon" value="pi-shield" />
<ui:param name="iconColor" value="green-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
</ui:include>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Coût Moyen/Membre" />
<ui:param name="value" value="#{rapportBean.coutMoyenMembre}" />
<ui:param name="icon" value="pi-user-minus" />
<ui:param name="iconColor" value="orange-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
</ui:include>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Revenu Moyen/Membre" />
<ui:param name="value" value="#{rapportBean.revenuMoyenMembre}" />
<ui:param name="icon" value="pi-user-plus" />
<ui:param name="iconColor" value="purple-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
</ui:include>
</div>
</div>
</h:panelGroup>

View File

@@ -377,26 +377,18 @@
<i class="pi pi-chart-bar text-blue-500 mr-2"></i>
Indicateurs Clés de Performance
</h5>
<div class="grid">
<div class="formgrid grid">
<ui:repeat value="#{rapportsBean.kpis}" var="kpi" varStatus="status">
<div class="col-12">
<div class="surface-100 border-round-lg p-3 mb-3 hover:surface-200 transition-colors transition-duration-150">
<div class="flex align-items-center justify-content-between mb-2">
<div class="flex align-items-center">
<i class="pi #{kpi.icon} text-#{kpi.couleur} mr-2"></i>
<span class="text-900 font-medium">#{kpi.libelle}</span>
</div>
<div class="text-#{kpi.couleur} font-bold text-xl">#{kpi.valeur}</div>
</div>
<div class="flex align-items-center justify-content-between">
<p:progressBar value="#{kpi.progression}" style="width: 60%; height: 4px;" />
<div class="flex align-items-center">
<i class="pi #{kpi.tendance == 'UP' ? 'pi-arrow-up text-green-500' : (kpi.tendance == 'DOWN' ? 'pi-arrow-down text-red-500' : 'pi-minus text-600')} mr-1 text-sm"></i>
<span class="text-600 text-sm">#{kpi.variation}%</span>
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="#{kpi.libelle}" />
<ui:param name="value" value="#{kpi.valeur}" />
<ui:param name="icon" value="#{kpi.icon}" />
<ui:param name="iconColor" value="#{kpi.couleur}" />
<ui:param name="growthValue" value="#{kpi.variation}" />
<ui:param name="growthLabel" value="variation" />
<ui:param name="progressValue" value="#{kpi.progression}" />
<ui:param name="colSize" value="col-12" />
</ui:include>
</ui:repeat>
</div>
</div>

View File

@@ -130,100 +130,50 @@
</div>
<!-- 1. MEMBRES : L'humain d'abord - le plus important -->
<div class="col-12 md:col-6 lg:col-3">
<div class="card bg-gradient-blue border-left-4 border-blue-500">
<div class="flex justify-content-between align-items-start mb-3">
<div>
<span class="text-blue-600 font-semibold text-sm uppercase">Communauté</span>
<div class="text-900 font-bold text-3xl mt-2">#{dashboardBean.activeMembers}</div>
<span class="text-700 text-sm">Membres actifs</span>
</div>
<div class="bg-blue-500 text-white border-round-xl flex align-items-center justify-content-center"
style="width:3.5rem;height:3.5rem">
<i class="pi pi-users text-2xl"></i>
</div>
</div>
<div class="flex align-items-center">
<i class="pi pi-arrow-up text-green-500 mr-1"></i>
<span class="text-green-500 font-bold">+#{dashboardBean.membresEvolutionPourcent}%</span>
<span class="text-600 text-sm ml-2">ce mois</span>
</div>
<p:progressBar value="#{dashboardBean.tauxActivite}" style="height: 4px; margin-top: 8px;"
styleClass="bg-blue-200" />
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Membres Actifs" />
<ui:param name="value" value="#{dashboardBean.activeMembers}" />
<ui:param name="icon" value="pi-users" />
<ui:param name="iconColor" value="blue-600" />
<ui:param name="growthValue" value="#{dashboardBean.membresEvolutionPourcent}" />
<ui:param name="growthLabel" value="ce mois" />
<ui:param name="progressValue" value="#{dashboardBean.tauxActivite}" />
</ui:include>
<!-- 2. FINANCES : Santé financière - crucial pour la survie -->
<div class="col-12 md:col-6 lg:col-3">
<div class="card bg-gradient-green border-left-4 border-green-500">
<div class="flex justify-content-between align-items-start mb-3">
<div>
<span class="text-green-600 font-semibold text-sm uppercase">Trésorerie</span>
<div class="text-900 font-bold text-2xl mt-2">#{dashboardBean.totalCotisations}</div>
<span class="text-700 text-sm">FCFA collectés</span>
</div>
<div class="bg-green-500 text-white border-round-xl flex align-items-center justify-content-center"
style="width:3.5rem;height:3.5rem">
<i class="pi pi-dollar text-2xl"></i>
</div>
</div>
<div class="flex align-items-center">
<i class="pi pi-arrow-up text-green-500 mr-1"></i>
<span class="text-green-500 font-bold">+#{dashboardBean.cotisationsEvolutionPourcent}%</span>
<span class="text-600 text-sm ml-2">vs mois dernier</span>
</div>
<p:progressBar value="#{dashboardBean.tauxObjectifCotisations}" style="height: 4px; margin-top: 8px;"
styleClass="bg-green-200" />
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="FCFA Collectés" />
<ui:param name="value" value="#{dashboardBean.totalCotisations}" />
<ui:param name="icon" value="pi-dollar" />
<ui:param name="iconColor" value="green-600" />
<ui:param name="growthValue" value="#{dashboardBean.cotisationsEvolutionPourcent}" />
<ui:param name="growthLabel" value="vs mois dernier" />
<ui:param name="progressValue" value="#{dashboardBean.tauxObjectifCotisations}" />
</ui:include>
<!-- 3. SOLIDARITÉ : Impact social - raison d'être -->
<div class="col-12 md:col-6 lg:col-3">
<div class="card bg-gradient-purple border-left-4 border-purple-500">
<div class="flex justify-content-between align-items-start mb-3">
<div>
<span class="text-purple-600 font-semibold text-sm uppercase">Solidarité</span>
<div class="text-900 font-bold text-2xl mt-2">#{dashboardBean.aidesDistribuees}</div>
<span class="text-700 text-sm">FCFA distribués</span>
</div>
<div class="bg-purple-500 text-white border-round-xl flex align-items-center justify-content-center"
style="width:3.5rem;height:3.5rem">
<i class="pi pi-heart text-2xl"></i>
</div>
</div>
<div class="flex align-items-center">
<div class="bg-orange-500 border-round mr-1" style="width: 8px; height: 8px;"></div>
<span class="text-orange-500 font-bold">#{dashboardBean.pendingAides}</span>
<span class="text-600 text-sm ml-2">demandes en attente</span>
</div>
<p:progressBar value="#{dashboardBean.tauxAidesTraitees}" style="height: 4px; margin-top: 8px;"
styleClass="bg-purple-200" />
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="FCFA Distribués" />
<ui:param name="value" value="#{dashboardBean.aidesDistribuees}" />
<ui:param name="icon" value="pi-heart" />
<ui:param name="iconColor" value="purple-600" />
<ui:param name="statusIcon" value="pi-circle-fill" />
<ui:param name="statusLabel" value="Demandes en attente" />
<ui:param name="statusValue" value="#{dashboardBean.pendingAides}" />
<ui:param name="progressValue" value="#{dashboardBean.tauxAidesTraitees}" />
</ui:include>
<!-- 4. ENGAGEMENT : Vitalité de l'organisation -->
<div class="col-12 md:col-6 lg:col-3">
<div class="card bg-gradient-orange border-left-4 border-orange-500">
<div class="flex justify-content-between align-items-start mb-3">
<div>
<span class="text-orange-600 font-semibold text-sm uppercase">Engagement</span>
<div class="text-900 font-bold text-3xl mt-2">#{dashboardBean.tauxParticipation}%</div>
<span class="text-700 text-sm">Taux de participation</span>
</div>
<div class="bg-orange-500 text-white border-round-xl flex align-items-center justify-content-center"
style="width:3.5rem;height:3.5rem">
<i class="pi pi-chart-line text-2xl"></i>
</div>
</div>
<div class="flex align-items-center">
<i class="pi pi-calendar text-orange-500 mr-1"></i>
<span class="text-orange-500 font-bold">#{dashboardBean.upcomingEvents}</span>
<span class="text-600 text-sm ml-2">événements prévus</span>
</div>
<p:progressBar value="#{dashboardBean.tauxEngagement}" style="height: 4px; margin-top: 8px;"
styleClass="bg-orange-200" />
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Taux de Participation" />
<ui:param name="value" value="#{dashboardBean.tauxParticipation}%" />
<ui:param name="icon" value="pi-chart-line" />
<ui:param name="iconColor" value="orange-600" />
<ui:param name="statusIcon" value="pi-calendar" />
<ui:param name="statusLabel" value="Événements prévus" />
<ui:param name="statusValue" value="#{dashboardBean.upcomingEvents}" />
<ui:param name="progressValue" value="#{dashboardBean.tauxEngagement}" />
</ui:include>
<!-- Tendances financières et analyses -->
<div class="col-12 lg:col-8">

View File

@@ -43,62 +43,45 @@
<!-- Résumé cotisations -->
<div class="grid mb-3">
<div class="col-12 md:col-3">
<div class="card bg-green-100 border-left-3 border-green-500">
<div class="flex justify-content-between">
<div>
<div class="text-green-900 font-bold text-2xl">#{membreCotisationBean.cotisationsPayees}</div>
<div class="text-green-700">Payées</div>
</div>
<div class="bg-green-500 text-white border-round text-center"
style="width: 3rem; height: 3rem; line-height: 3rem;">
<i class="pi pi-check text-xl"></i>
</div>
</div>
</div>
</div>
<div class="col-12 md:col-3">
<div class="card bg-orange-100 border-left-3 border-orange-500">
<div class="flex justify-content-between">
<div>
<div class="text-orange-900 font-bold text-2xl">#{membreCotisationBean.cotisationsEnAttente}</div>
<div class="text-orange-700">En Attente</div>
</div>
<div class="bg-orange-500 text-white border-round text-center"
style="width: 3rem; height: 3rem; line-height: 3rem;">
<i class="pi pi-clock text-xl"></i>
</div>
</div>
</div>
</div>
<div class="col-12 md:col-3">
<div class="card bg-red-100 border-left-3 border-red-500">
<div class="flex justify-content-between">
<div>
<div class="text-red-900 font-bold text-2xl">#{membreCotisationBean.montantDu}</div>
<div class="text-red-700">Montant Dû</div>
</div>
<div class="bg-red-500 text-white border-round text-center"
style="width: 3rem; height: 3rem; line-height: 3rem;">
<i class="pi pi-exclamation-triangle text-xl"></i>
</div>
</div>
</div>
</div>
<div class="col-12 md:col-3">
<div class="card bg-blue-100 border-left-3 border-blue-500">
<div class="flex justify-content-between">
<div>
<div class="text-blue-900 font-bold text-2xl">#{membreCotisationBean.totalVerse}</div>
<div class="text-blue-700">Total Versé</div>
</div>
<div class="bg-blue-500 text-white border-round text-center"
style="width: 3rem; height: 3rem; line-height: 3rem;">
<i class="pi pi-dollar text-xl"></i>
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Payées" />
<ui:param name="value" value="#{membreCotisationBean.cotisationsPayees}" />
<ui:param name="icon" value="pi-check" />
<ui:param name="iconColor" value="green-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
<ui:param name="colSize" value="col-12 md:col-3" />
</ui:include>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="En Attente" />
<ui:param name="value" value="#{membreCotisationBean.cotisationsEnAttente}" />
<ui:param name="icon" value="pi-clock" />
<ui:param name="iconColor" value="orange-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
<ui:param name="colSize" value="col-12 md:col-3" />
</ui:include>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Montant Dû" />
<ui:param name="value" value="#{membreCotisationBean.montantDu}" />
<ui:param name="icon" value="pi-exclamation-triangle" />
<ui:param name="iconColor" value="red-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
<ui:param name="colSize" value="col-12 md:col-3" />
</ui:include>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Total Versé" />
<ui:param name="value" value="#{membreCotisationBean.totalVerse}" />
<ui:param name="icon" value="pi-dollar" />
<ui:param name="iconColor" value="blue-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
<ui:param name="colSize" value="col-12 md:col-3" />
</ui:include>
</div>
<!-- Liste des cotisations -->
@@ -127,11 +110,14 @@
</div>
</p:toolbarGroup>
<p:toolbarGroup align="right">
<p:commandButton icon="pi pi-refresh"
styleClass="ui-button-outlined ui-button-secondary"
action="#{membreCotisationBean.actualiser}"
update="@form"
title="Actualiser"/>
<ui:include src="/templates/components/buttons/button-secondary.xhtml">
<ui:param name="value" value="" />
<ui:param name="icon" value="pi pi-refresh" />
<ui:param name="action" value="#{membreCotisationBean.actualiser}" />
<ui:param name="update" value=":formCotisations:dtCotisations" />
<ui:param name="title" value="Actualiser" />
<ui:param name="outlined" value="true" />
</ui:include>
</p:toolbarGroup>
</p:toolbar>

View File

@@ -378,16 +378,15 @@
</div>
<div class="flex flex-wrap gap-2">
<p:commandButton
value="🎯 Inscrire le membre"
icon="pi pi-user-plus"
action="#{membreInscriptionBean.inscrire}"
update="@form"
process="@form"
onclick="PF('statusDialog').show();"
oncomplete="PF('statusDialog').hide();"
styleClass="ui-button-success"
title="Soumettre l'inscription" />
<ui:include src="/templates/components/buttons/button-success.xhtml">
<ui:param name="value" value="🎯 Inscrire le membre" />
<ui:param name="icon" value="pi pi-user-plus" />
<ui:param name="action" value="#{membreInscriptionBean.inscrire}" />
<ui:param name="update" value="messages" />
<ui:param name="onclick" value="PF('statusDialog').show();" />
<ui:param name="oncomplete" value="PF('statusDialog').hide();" />
<ui:param name="title" value="Soumettre l'inscription" />
</ui:include>
<ui:include src="/templates/components/buttons/button-info.xhtml">
<ui:param name="value" value="💾 Enregistrer brouillon" />

View File

@@ -155,11 +155,15 @@
<div class="col-12 md:col-auto">
<div class="field">
<label class="invisible">Actualiser</label>
<p:commandButton icon="pi pi-refresh"
action="#{membreListeBean.actualiser}"
update="@form"
title="Actualiser"
styleClass="ui-button-outlined ui-button-secondary w-full" />
<ui:include src="/templates/components/buttons/button-secondary.xhtml">
<ui:param name="value" value="" />
<ui:param name="icon" value="pi pi-refresh" />
<ui:param name="action" value="#{membreListeBean.actualiser}" />
<ui:param name="update" value=":formMembres:dtMembres" />
<ui:param name="title" value="Actualiser" />
<ui:param name="outlined" value="true" />
<ui:param name="styleClass" value="w-full" />
</ui:include>
</div>
</div>
@@ -278,7 +282,7 @@
<ui:include src="/templates/components/buttons/button-icon.xhtml">
<ui:param name="icon" value="pi pi-envelope" />
<ui:param name="action" value="#{membreListeBean.contacterMembre(membre)}" />
<ui:param name="update" value="@form" />
<ui:param name="update" value=":formMembres :formContact" />
<ui:param name="oncomplete" value="PF('dlgContact').show();" />
<ui:param name="title" value="Contacter" />
<ui:param name="severity" value="" />
@@ -322,7 +326,7 @@
<ui:param name="value" value="Rappel cotisations" />
<ui:param name="icon" value="pi pi-bell" />
<ui:param name="action" value="#{membreListeBean.rappelCotisationsGroupe}" />
<ui:param name="update" value="@form" />
<ui:param name="update" value=":formMembres" />
<ui:param name="outlined" value="true" />
<ui:param name="disabled" value="#{empty membreListeBean.selectedMembres}" />
</ui:include>
@@ -422,7 +426,7 @@
<ui:param name="value" value="Réinitialiser" />
<ui:param name="icon" value="pi pi-refresh" />
<ui:param name="action" value="#{membreListeBean.reinitialiserFiltres}" />
<ui:param name="update" value="@form :formMembres:dtMembres" />
<ui:param name="update" value=":formFiltresAvances :formMembres:dtMembres" />
<ui:param name="outlined" value="true" />
</ui:include>
<ui:include src="/templates/components/buttons/button-secondary.xhtml">
@@ -478,7 +482,7 @@
<ui:param name="value" value="Envoyer" />
<ui:param name="icon" value="pi pi-send" />
<ui:param name="action" value="#{membreListeBean.envoyerMessageGroupe}" />
<ui:param name="update" value="@form" />
<ui:param name="update" value=":formMessageGroupe :formMembres" />
<ui:param name="onclick" value="if(!args.validationFailed) PF('dlgMessageGroupe').hide();" />
</ui:include>
<ui:include src="/templates/components/buttons/button-secondary.xhtml">
@@ -521,7 +525,7 @@
<ui:param name="value" value="Importer" />
<ui:param name="icon" value="pi pi-upload" />
<ui:param name="action" value="#{membreListeBean.importerMembres}" />
<ui:param name="update" value="@form :formMembres" />
<ui:param name="update" value=":formImportExport :formMembres" />
</ui:include>
<ui:include src="/templates/components/buttons/button-info.xhtml">
<ui:param name="value" value="Télécharger modèle" />
@@ -634,14 +638,14 @@
<ui:param name="value" value="Annuler" />
<ui:param name="icon" value="pi pi-times" />
<ui:param name="action" value="#{membreListeBean.annulerContact}" />
<ui:param name="update" value="@form" />
<ui:param name="update" value=":formContact" />
<ui:param name="oncomplete" value="PF('dlgContact').hide();" />
</ui:include>
<ui:include src="/templates/components/buttons/button-success.xhtml">
<ui:param name="value" value="Envoyer" />
<ui:param name="icon" value="pi pi-send" />
<ui:param name="action" value="#{membreListeBean.envoyerMessageContact}" />
<ui:param name="update" value="@form :formMembres" />
<ui:param name="update" value=":formContact :formMembres" />
<ui:param name="oncomplete" value="if(!args.validationFailed) { PF('dlgContact').hide(); }" />
</ui:include>
</div>

View File

@@ -17,23 +17,15 @@
<div class="flex align-items-start">
<!-- Photo de profil -->
<div class="mr-4">
<div class="border-circle overflow-hidden" style="width: 120px; height: 120px;">
<h:graphicImage value="#{membreProfilBean.membre.photoUrl}"
style="width: 100%; height: 100%; object-fit: cover;"
rendered="#{membreProfilBean.membre.photoUrl != null}" />
<div class="bg-primary text-white flex align-items-center justify-content-center w-full h-full"
rendered="#{membreProfilBean.membre.photoUrl == null}">
<span style="font-size: 2.5rem;">#{membreProfilBean.membre.initiales}</span>
</div>
</div>
<h:form id="formPhoto" enctype="multipart/form-data">
<p:fileUpload mode="simple" skinSimple="true"
label="Changer photo" chooseLabel="Modifier"
accept="image/*" maxFileSize="2000000"
listener="#{membreProfilBean.changerPhoto}"
update="@form"
styleClass="mt-2 w-full" />
</h:form>
<ui:include src="/templates/components/profile-photo.xhtml">
<ui:param name="photoId" value="photoProfil" />
<ui:param name="photoUrl" value="#{membreProfilBean.membre.photoUrl}" />
<ui:param name="initiales" value="#{membreProfilBean.membre.initiales}" />
<ui:param name="formId" value="formPhoto" />
<ui:param name="listener" value="#{membreProfilBean.changerPhoto}" />
<ui:param name="update" value=":photoProfil" />
<ui:param name="size" value="120" />
</ui:include>
</div>
<!-- Informations principales -->
@@ -47,10 +39,11 @@
<div class="grid">
<div class="col-12 md:col-6">
<div class="mb-2">
<span class="font-medium text-900">Numéro membre:</span>
<span class="ml-2 font-bold text-primary">#{membreProfilBean.membre.numeroMembre}</span>
</div>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Numéro membre" />
<ui:param name="value" value="#{membreProfilBean.membre.numeroMembre}" />
<ui:param name="valueClass" value="font-bold text-primary" />
</ui:include>
<div class="mb-2">
<span class="font-medium text-900">Type:</span>
<p:tag value="#{membreProfilBean.membre.typeMembre}"
@@ -58,25 +51,26 @@
icon="pi #{membreProfilBean.membre.typeIcon}"
styleClass="ml-2" />
</div>
<div class="mb-2">
<span class="font-medium text-900">Entité:</span>
<span class="ml-2">#{membreProfilBean.membre.entite}</span>
</div>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Entité" />
<ui:param name="value" value="#{membreProfilBean.membre.entite}" />
</ui:include>
</div>
<div class="col-12 md:col-6">
<div class="mb-2">
<span class="font-medium text-900">Adhésion:</span>
<span class="ml-2">#{membreProfilBean.membre.dateAdhesion}</span>
<small class="text-600 ml-1">(#{membreProfilBean.membre.anciennete})</small>
</div>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Adhésion" />
<ui:param name="value" value="#{membreProfilBean.membre.dateAdhesion}" />
<ui:param name="suffix" value="(#{membreProfilBean.membre.anciennete})" />
</ui:include>
<div class="mb-2">
<span class="font-medium text-900">Cotisations:</span>
<span class="ml-2 #{membreProfilBean.membre.cotisationColor}">#{membreProfilBean.membre.cotisationStatut}</span>
</div>
<div class="mb-2">
<span class="font-medium text-900">Participation:</span>
<span class="ml-2 font-bold text-blue-500">#{membreProfilBean.membre.tauxParticipation}%</span>
</div>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Participation" />
<ui:param name="value" value="#{membreProfilBean.membre.tauxParticipation}%" />
<ui:param name="valueClass" value="font-bold text-blue-500" />
</ui:include>
</div>
</div>
</div>
@@ -118,65 +112,45 @@
<!-- Statistiques et KPIs -->
<div class="grid">
<div class="col-12 md:col-3">
<div class="card bg-blue-100 border-left-3 border-blue-500">
<div class="flex justify-content-between">
<div>
<div class="text-blue-900 font-bold text-xl">#{membreProfilBean.statistiques.evenementsParticipes}</div>
<div class="text-blue-700">Événements</div>
</div>
<div class="bg-blue-500 text-white border-round text-center"
style="width: 2.5rem; height: 2.5rem; line-height: 2.5rem;">
<i class="pi pi-calendar text-lg"></i>
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Événements" />
<ui:param name="value" value="#{membreProfilBean.statistiques.evenementsParticipes}" />
<ui:param name="icon" value="pi-calendar" />
<ui:param name="iconColor" value="blue-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
<ui:param name="colSize" value="col-12 md:col-3" />
</ui:include>
<div class="col-12 md:col-3">
<div class="card bg-green-100 border-left-3 border-green-500">
<div class="flex justify-content-between">
<div>
<div class="text-green-900 font-bold text-xl">#{membreProfilBean.statistiques.cotisationsPayees}</div>
<div class="text-green-700">Cotisations</div>
</div>
<div class="bg-green-500 text-white border-round text-center"
style="width: 2.5rem; height: 2.5rem; line-height: 2.5rem;">
<i class="pi pi-dollar text-lg"></i>
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Cotisations" />
<ui:param name="value" value="#{membreProfilBean.statistiques.cotisationsPayees}" />
<ui:param name="icon" value="pi-dollar" />
<ui:param name="iconColor" value="green-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
<ui:param name="colSize" value="col-12 md:col-3" />
</ui:include>
<div class="col-12 md:col-3">
<div class="card bg-orange-100 border-left-3 border-orange-500">
<div class="flex justify-content-between">
<div>
<div class="text-orange-900 font-bold text-xl">#{membreProfilBean.statistiques.aidesRecues}</div>
<div class="text-orange-700">Aides reçues</div>
</div>
<div class="bg-orange-500 text-white border-round text-center"
style="width: 2.5rem; height: 2.5rem; line-height: 2.5rem;">
<i class="pi pi-heart text-lg"></i>
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Aides reçues" />
<ui:param name="value" value="#{membreProfilBean.statistiques.aidesRecues}" />
<ui:param name="icon" value="pi-heart" />
<ui:param name="iconColor" value="orange-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
<ui:param name="colSize" value="col-12 md:col-3" />
</ui:include>
<div class="col-12 md:col-3">
<div class="card bg-purple-100 border-left-3 border-purple-500">
<div class="flex justify-content-between">
<div>
<div class="text-purple-900 font-bold text-xl">#{membreProfilBean.statistiques.scoreEngagement}</div>
<div class="text-purple-700">Score engagement</div>
</div>
<div class="bg-purple-500 text-white border-round text-center"
style="width: 2.5rem; height: 2.5rem; line-height: 2.5rem;">
<i class="pi pi-star text-lg"></i>
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Score engagement" />
<ui:param name="value" value="#{membreProfilBean.statistiques.scoreEngagement}" />
<ui:param name="icon" value="pi-star" />
<ui:param name="iconColor" value="purple-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
<ui:param name="colSize" value="col-12 md:col-3" />
</ui:include>
</div>
<!-- Contenu principal avec onglets -->
@@ -188,52 +162,52 @@
<div class="col-12 md:col-6">
<h6 class="mb-3">Informations de base</h6>
<div class="surface-50 p-3 border-round">
<div class="flex justify-content-between align-items-center mb-2">
<span class="font-medium">Nom complet:</span>
<span>#{membreProfilBean.membre.nomComplet}</span>
</div>
<div class="flex justify-content-between align-items-center mb-2">
<span class="font-medium">Date de naissance:</span>
<span>#{membreProfilBean.membre.dateNaissance}</span>
</div>
<div class="flex justify-content-between align-items-center mb-2">
<span class="font-medium">Genre:</span>
<span>#{membreProfilBean.membre.genre}</span>
</div>
<div class="flex justify-content-between align-items-center mb-2">
<span class="font-medium">Situation familiale:</span>
<span>#{membreProfilBean.membre.situationFamiliale}</span>
</div>
<div class="flex justify-content-between align-items-center">
<span class="font-medium">Profession:</span>
<span>#{membreProfilBean.membre.profession}</span>
</div>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Nom complet" />
<ui:param name="value" value="#{membreProfilBean.membre.nomComplet}" />
</ui:include>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Date de naissance" />
<ui:param name="value" value="#{membreProfilBean.membre.dateNaissance}" />
</ui:include>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Genre" />
<ui:param name="value" value="#{membreProfilBean.membre.genre}" />
</ui:include>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Situation familiale" />
<ui:param name="value" value="#{membreProfilBean.membre.situationFamiliale}" />
</ui:include>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Profession" />
<ui:param name="value" value="#{membreProfilBean.membre.profession}" />
</ui:include>
</div>
</div>
<div class="col-12 md:col-6">
<h6 class="mb-3">Coordonnées</h6>
<div class="surface-50 p-3 border-round">
<div class="flex justify-content-between align-items-center mb-2">
<span class="font-medium">Email:</span>
<span>#{membreProfilBean.membre.email}</span>
</div>
<div class="flex justify-content-between align-items-center mb-2">
<span class="font-medium">Téléphone:</span>
<span>#{membreProfilBean.membre.telephone}</span>
</div>
<div class="flex justify-content-between align-items-center mb-2">
<span class="font-medium">Adresse:</span>
<span>#{membreProfilBean.membre.adresse}</span>
</div>
<div class="flex justify-content-between align-items-center mb-2">
<span class="font-medium">Ville:</span>
<span>#{membreProfilBean.membre.ville}</span>
</div>
<div class="flex justify-content-between align-items-center">
<span class="font-medium">Pays:</span>
<span>#{membreProfilBean.membre.pays}</span>
</div>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Email" />
<ui:param name="value" value="#{membreProfilBean.membre.email}" />
</ui:include>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Téléphone" />
<ui:param name="value" value="#{membreProfilBean.membre.telephone}" />
</ui:include>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Adresse" />
<ui:param name="value" value="#{membreProfilBean.membre.adresse}" />
</ui:include>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Ville" />
<ui:param name="value" value="#{membreProfilBean.membre.ville}" />
</ui:include>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Pays" />
<ui:param name="value" value="#{membreProfilBean.membre.pays}" />
</ui:include>
</div>
</div>
@@ -272,20 +246,20 @@
<p:tag value="#{membreProfilBean.cotisations.statutActuel}"
severity="#{membreProfilBean.cotisations.statutSeverity}" />
</div>
<div class="flex justify-content-between align-items-center mb-2">
<span class="font-medium">Dernier paiement:</span>
<span>#{membreProfilBean.cotisations.dernierPaiement}</span>
</div>
<div class="flex justify-content-between align-items-center mb-2">
<span class="font-medium">Prochaine échéance:</span>
<span class="#{membreProfilBean.cotisations.prochaineEcheanceClass}">
#{membreProfilBean.cotisations.prochaineEcheance}
</span>
</div>
<div class="flex justify-content-between align-items-center">
<span class="font-medium">Total payé cette année:</span>
<span class="font-bold text-green-500">#{membreProfilBean.cotisations.totalAnnee}</span>
</div>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Dernier paiement" />
<ui:param name="value" value="#{membreProfilBean.cotisations.dernierPaiement}" />
</ui:include>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Prochaine échéance" />
<ui:param name="value" value="#{membreProfilBean.cotisations.prochaineEcheance}" />
<ui:param name="valueClass" value="#{membreProfilBean.cotisations.prochaineEcheanceClass}" />
</ui:include>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Total payé cette année" />
<ui:param name="value" value="#{membreProfilBean.cotisations.totalAnnee}" />
<ui:param name="valueClass" value="font-bold text-green-500" />
</ui:include>
</div>
<h:form id="formCotisationsActions">
@@ -363,29 +337,31 @@
<div class="col-12 md:col-4">
<h6 class="mb-3">Statistiques participation</h6>
<div class="surface-50 p-3 border-round">
<div class="flex justify-content-between align-items-center mb-3">
<span class="font-medium">Taux de participation:</span>
<span class="font-bold text-blue-500">#{membreProfilBean.statistiques.tauxParticipation}%</span>
</div>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Taux de participation" />
<ui:param name="value" value="#{membreProfilBean.statistiques.tauxParticipation}%" />
<ui:param name="valueClass" value="font-bold text-blue-500" />
</ui:include>
<p:progressBar value="#{membreProfilBean.statistiques.tauxParticipation}"
labelTemplate="" styleClass="mb-3" />
<div class="flex justify-content-between align-items-center mb-2">
<span class="font-medium">Cette année:</span>
<span>#{membreProfilBean.statistiques.evenementsAnnee}</span>
</div>
<div class="flex justify-content-between align-items-center mb-2">
<span class="font-medium">Total:</span>
<span>#{membreProfilBean.statistiques.evenementsTotal}</span>
</div>
<div class="flex justify-content-between align-items-center mb-2">
<span class="font-medium">En tant qu'organisateur:</span>
<span>#{membreProfilBean.statistiques.evenementsOrganises}</span>
</div>
<div class="flex justify-content-between align-items-center">
<span class="font-medium">Absences:</span>
<span class="text-red-500">#{membreProfilBean.statistiques.absences}</span>
</div>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Cette année" />
<ui:param name="value" value="#{membreProfilBean.statistiques.evenementsAnnee}" />
</ui:include>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Total" />
<ui:param name="value" value="#{membreProfilBean.statistiques.evenementsTotal}" />
</ui:include>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="En tant qu'organisateur" />
<ui:param name="value" value="#{membreProfilBean.statistiques.evenementsOrganises}" />
</ui:include>
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Absences" />
<ui:param name="value" value="#{membreProfilBean.statistiques.absences}" />
<ui:param name="valueClass" value="text-red-500" />
</ui:include>
</div>
</div>
</div>
@@ -554,7 +530,7 @@
<ui:param name="value" value="Enregistrer" />
<ui:param name="icon" value="pi pi-check" />
<ui:param name="action" value="#{membreProfilBean.sauvegarderModifications}" />
<ui:param name="update" value="@form" />
<ui:param name="update" value=":formModifierProfil :photoProfil" />
<ui:param name="onclick" value="if(!args.validationFailed) PF('dlgModifierProfil').hide();" />
</ui:include>
<ui:include src="/templates/components/buttons/button-secondary.xhtml">
@@ -570,20 +546,24 @@
<p:dialog header="Contacter #{membreProfilBean.membre.prenom}" widgetVar="dlgContacter" modal="true" width="500">
<h:form id="formContacter">
<div class="ui-fluid">
<div class="field">
<p:outputLabel for="sujetContact" value="Sujet" />
<p:inputText id="sujetContact" value="#{membreProfilBean.contact.sujet}" required="true" />
</div>
<ui:include src="/templates/components/forms/form-field-text.xhtml">
<ui:param name="id" value="sujetContact" />
<ui:param name="label" value="Sujet" />
<ui:param name="value" value="#{membreProfilBean.contact.sujet}" />
<ui:param name="required" value="true" />
</ui:include>
<div class="field">
<p:outputLabel for="messageContact" value="Message" />
<p:inputTextarea id="messageContact" value="#{membreProfilBean.contact.message}"
rows="5" required="true" />
</div>
<ui:include src="/templates/components/forms/form-field-textarea.xhtml">
<ui:param name="id" value="messageContact" />
<ui:param name="label" value="Message" />
<ui:param name="value" value="#{membreProfilBean.contact.message}" />
<ui:param name="required" value="true" />
<ui:param name="rows" value="5" />
</ui:include>
<div class="field">
<p:outputLabel for="canalContact" value="Canal de communication" />
<p:selectCheckboxMenu id="canalContact" value="#{membreProfilBean.contact.canaux}" multiple="true">
<p:selectCheckboxMenu id="canalContact" value="#{membreProfilBean.contact.canaux}" multiple="true" styleClass="w-full">
<f:selectItem itemLabel="📧 Email" itemValue="EMAIL" />
<f:selectItem itemLabel="📱 SMS" itemValue="SMS" />
<f:selectItem itemLabel="💬 WhatsApp" itemValue="WHATSAPP" />
@@ -592,14 +572,18 @@
</div>
<div class="flex gap-2 mt-3">
<p:commandButton value="Envoyer" icon="pi pi-send"
styleClass="ui-button-success"
action="#{membreProfilBean.envoyerMessage}"
update="@form"
oncomplete="if(!args.validationFailed) PF('dlgContacter').hide();" />
<p:commandButton value="Annuler" icon="pi pi-times"
styleClass="ui-button-secondary"
onclick="PF('dlgContacter').hide();" type="button" />
<ui:include src="/templates/components/buttons/button-success.xhtml">
<ui:param name="value" value="Envoyer" />
<ui:param name="icon" value="pi pi-send" />
<ui:param name="action" value="#{membreProfilBean.envoyerMessage}" />
<ui:param name="update" value=":formContacter" />
<ui:param name="oncomplete" value="if(!args.validationFailed) PF('dlgContacter').hide();" />
</ui:include>
<ui:include src="/templates/components/buttons/button-secondary.xhtml">
<ui:param name="value" value="Annuler" />
<ui:param name="icon" value="pi pi-times" />
<ui:param name="onclick" value="PF('dlgContacter').hide();" />
</ui:include>
</div>
</h:form>
</p:dialog>
@@ -609,46 +593,65 @@
<h:form id="formActions">
<div class="grid">
<div class="col-12">
<p:commandButton value="Suspendre membre"
icon="pi pi-ban"
styleClass="ui-button-danger w-full mb-2"
action="#{membreProfilBean.suspendre}"
onclick="return confirm('Êtes-vous sûr de vouloir suspendre ce membre ?');"
rendered="#{membreProfilBean.membre.statut == 'ACTIF'}" />
<ui:include src="/templates/components/buttons/button-secondary.xhtml">
<ui:param name="value" value="Suspendre membre" />
<ui:param name="icon" value="pi pi-ban" />
<ui:param name="action" value="#{membreProfilBean.suspendre}" />
<ui:param name="onclick" value="return confirm('Êtes-vous sûr de vouloir suspendre ce membre ?');" />
<ui:param name="styleClass" value="ui-button-danger w-full mb-2" />
<ui:param name="rendered" value="#{membreProfilBean.membre.statut == 'ACTIF'}" />
</ui:include>
<p:commandButton value="Réactiver membre"
icon="pi pi-check"
styleClass="ui-button-success w-full mb-2"
action="#{membreProfilBean.reactiver}"
rendered="#{membreProfilBean.membre.statut == 'SUSPENDU'}" />
<ui:include src="/templates/components/buttons/button-success.xhtml">
<ui:param name="value" value="Réactiver membre" />
<ui:param name="icon" value="pi pi-check" />
<ui:param name="action" value="#{membreProfilBean.reactiver}" />
<ui:param name="styleClass" value="w-full mb-2" />
<ui:param name="rendered" value="#{membreProfilBean.membre.statut == 'SUSPENDU'}" />
</ui:include>
<p:commandButton value="Changer de type"
icon="pi pi-user-edit"
styleClass="ui-button-outlined ui-button-warning w-full mb-2"
onclick="PF('dlgChangerType').show();" />
<ui:include src="/templates/components/buttons/button-warning.xhtml">
<ui:param name="value" value="Changer de type" />
<ui:param name="icon" value="pi pi-user-edit" />
<ui:param name="onclick" value="PF('dlgChangerType').show();" />
<ui:param name="outlined" value="true" />
<ui:param name="styleClass" value="w-full mb-2" />
</ui:include>
<p:commandButton value="Transférer vers entité"
icon="pi pi-arrow-right"
styleClass="ui-button-outlined ui-button-info w-full mb-2"
onclick="PF('dlgTransferer').show();" />
<ui:include src="/templates/components/buttons/button-info.xhtml">
<ui:param name="value" value="Transférer vers entité" />
<ui:param name="icon" value="pi pi-arrow-right" />
<ui:param name="onclick" value="PF('dlgTransferer').show();" />
<ui:param name="outlined" value="true" />
<ui:param name="styleClass" value="w-full mb-2" />
</ui:include>
<p:commandButton value="Exporter données"
icon="pi pi-download"
styleClass="ui-button-outlined ui-button-secondary w-full mb-2"
action="#{membreProfilBean.exporterDonnees}" />
<ui:include src="/templates/components/buttons/button-secondary.xhtml">
<ui:param name="value" value="Exporter données" />
<ui:param name="icon" value="pi pi-download" />
<ui:param name="action" value="#{membreProfilBean.exporterDonnees}" />
<ui:param name="outlined" value="true" />
<ui:param name="styleClass" value="w-full mb-2" />
<ui:param name="update" value="none" />
</ui:include>
<p:commandButton value="Supprimer membre"
icon="pi pi-trash"
styleClass="ui-button-outlined ui-button-danger w-full"
onclick="return confirm('ATTENTION: Cette action est irréversible. Confirmer la suppression ?');"
action="#{membreProfilBean.supprimer}" />
<ui:include src="/templates/components/buttons/button-secondary.xhtml">
<ui:param name="value" value="Supprimer membre" />
<ui:param name="icon" value="pi pi-trash" />
<ui:param name="action" value="#{membreProfilBean.supprimer}" />
<ui:param name="onclick" value="return confirm('ATTENTION: Cette action est irréversible. Confirmer la suppression ?');" />
<ui:param name="outlined" value="true" />
<ui:param name="styleClass" value="ui-button-danger w-full" />
</ui:include>
</div>
</div>
<div class="flex justify-content-end mt-3">
<p:commandButton value="Fermer" icon="pi pi-times"
styleClass="ui-button-secondary"
onclick="PF('dlgActions').hide();" type="button" />
<ui:include src="/templates/components/buttons/button-secondary.xhtml">
<ui:param name="value" value="Fermer" />
<ui:param name="icon" value="pi pi-times" />
<ui:param name="onclick" value="PF('dlgActions').hide();" />
</ui:include>
</div>
</h:form>
</p:dialog>

View File

@@ -36,32 +36,44 @@
<!-- Statistiques de recherche -->
<div class="grid">
<ui:include src="/templates/components/cards/stat-card.xhtml">
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Total Membres" />
<ui:param name="value" value="#{membreRechercheBean.statistiques.totalMembres}" />
<ui:param name="label" value="Total Membres" />
<ui:param name="icon" value="pi pi-users" />
<ui:param name="bgColor" value="blue" />
<ui:param name="icon" value="pi-users" />
<ui:param name="iconColor" value="blue-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
<ui:param name="colSize" value="col-12 md:col-6 lg:col-3" />
</ui:include>
<ui:include src="/templates/components/cards/stat-card.xhtml">
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Résultats trouvés" />
<ui:param name="value" value="#{membreRechercheBean.statistiques.resultatsActuels}" />
<ui:param name="label" value="Résultats trouvés" />
<ui:param name="icon" value="pi pi-check" />
<ui:param name="bgColor" value="green" />
<ui:param name="icon" value="pi-check" />
<ui:param name="iconColor" value="green-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
<ui:param name="colSize" value="col-12 md:col-6 lg:col-3" />
</ui:include>
<ui:include src="/templates/components/cards/stat-card.xhtml">
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Filtres actifs" />
<ui:param name="value" value="#{membreRechercheBean.statistiques.filtresActifs}" />
<ui:param name="label" value="Filtres actifs" />
<ui:param name="icon" value="pi pi-filter" />
<ui:param name="bgColor" value="orange" />
<ui:param name="icon" value="pi-filter" />
<ui:param name="iconColor" value="orange-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
<ui:param name="colSize" value="col-12 md:col-6 lg:col-3" />
</ui:include>
<ui:include src="/templates/components/cards/stat-card.xhtml">
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Temps de recherche" />
<ui:param name="value" value="#{membreRechercheBean.statistiques.tempsRecherche}ms" />
<ui:param name="label" value="Temps de recherche" />
<ui:param name="icon" value="pi pi-clock" />
<ui:param name="bgColor" value="purple" />
<ui:param name="icon" value="pi-clock" />
<ui:param name="iconColor" value="purple-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
<ui:param name="colSize" value="col-12 md:col-6 lg:col-3" />
</ui:include>
</div>
@@ -325,7 +337,7 @@
<ui:param name="value" value="Réinitialiser" />
<ui:param name="icon" value="pi pi-refresh" />
<ui:param name="action" value="#{membreRechercheBean.reinitialiserFiltres}" />
<ui:param name="update" value="@form :formResultats:dtResultats @(.search-summary)" />
<ui:param name="update" value=":formRechercheAvancee :formResultats:dtResultats @(.search-summary)" />
<ui:param name="outlined" value="true" />
</ui:include>
<ui:include src="/templates/components/buttons/button-info.xhtml">
@@ -389,14 +401,14 @@
<div class="flex align-items-center justify-content-between">
<span>Liste des membres</span>
<div class="flex gap-2">
<ui:include src="/templates/components/buttons/button-icon.xhtml">
<ui:include src="/templates/components/buttons/button-secondary.xhtml">
<ui:param name="value" value="" />
<ui:param name="icon" value="pi pi-refresh" />
<ui:param name="action" value="#{membreRechercheBean.actualiserResultats}" />
<ui:param name="update" value="@form" />
<ui:param name="update" value=":formResultats:dtResultats" />
<ui:param name="title" value="Actualiser" />
<ui:param name="rounded" value="true" />
<ui:param name="text" value="false" />
<ui:param name="styleClass" value="ui-button-outlined ui-button-secondary" />
<ui:param name="outlined" value="true" />
<ui:param name="styleClass" value="ui-button-sm" />
</ui:include>
<ui:include src="/templates/components/buttons/button-icon.xhtml">
<ui:param name="icon" value="pi pi-cog" />
@@ -572,7 +584,7 @@
<ui:param name="value" value="Sauvegarder" />
<ui:param name="icon" value="pi pi-check" />
<ui:param name="action" value="#{membreRechercheBean.sauvegarderRecherche}" />
<ui:param name="update" value="@form" />
<ui:param name="update" value=":formSauvegarderRecherche" />
<ui:param name="onclick" value="if(!args.validationFailed) PF('dlgSauvegarderRecherche').hide();" />
</ui:include>
<ui:include src="/templates/components/buttons/button-secondary.xhtml">
@@ -622,7 +634,7 @@
<ui:include src="/templates/components/buttons/button-icon.xhtml">
<ui:param name="icon" value="pi pi-trash" />
<ui:param name="action" value="#{membreRechercheBean.supprimerRecherche(recherche)}" />
<ui:param name="update" value="@form" />
<ui:param name="update" value=":formRecherchesSauvegardees" />
<ui:param name="onclick" value="return confirm('Supprimer cette recherche ?');" />
<ui:param name="title" value="Supprimer" />
<ui:param name="severity" value="danger" />
@@ -685,7 +697,7 @@
<ui:param name="value" value="Envoyer" />
<ui:param name="icon" value="pi pi-send" />
<ui:param name="action" value="#{membreRechercheBean.envoyerMessageGroupe}" />
<ui:param name="update" value="@form" />
<ui:param name="update" value=":formMessageGroupe :formResultats" />
<ui:param name="onclick" value="if(!args.validationFailed) PF('dlgMessageGroupe').hide();" />
</ui:include>
<ui:include src="/templates/components/buttons/button-secondary.xhtml">

View File

@@ -57,23 +57,18 @@
<!-- KPIs Activités -->
<div class="card">
<h5>Indicateurs d'Activité</h5>
<div class="grid">
<div class="formgrid grid">
<ui:repeat value="#{rapportsBean.kpis}" var="kpi">
<div class="col-12 md:col-4">
<div class="surface-100 border-round-lg p-4">
<div class="flex align-items-center justify-content-between mb-3">
<div class="flex align-items-center gap-2">
<i class="#{kpi.icon} text-2xl text-#{kpi.couleur}"></i>
<span class="font-semibold">#{kpi.libelle}</span>
</div>
</div>
<div class="text-3xl font-bold text-#{kpi.couleur} mb-2">#{kpi.valeur}</div>
<div class="flex align-items-center gap-2">
<i class="#{kpi.variation > 0 ? 'pi pi-arrow-up text-green-500' : kpi.variation < 0 ? 'pi pi-arrow-down text-red-500' : 'pi pi-minus text-gray-500'}"></i>
<span class="text-sm">#{kpi.variation}%</span>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="#{kpi.libelle}" />
<ui:param name="value" value="#{kpi.valeur}" />
<ui:param name="icon" value="#{kpi.icon}" />
<ui:param name="iconColor" value="#{kpi.couleur}" />
<ui:param name="growthValue" value="#{kpi.variation}" />
<ui:param name="growthLabel" value="variation" />
<ui:param name="colSize" value="col-12 md:col-4" />
<ui:param name="showProgress" value="false" />
</ui:include>
</ui:repeat>
</div>
</div>

View File

@@ -150,23 +150,18 @@
<!-- KPIs Financiers -->
<div class="card">
<h5>Indicateurs Clés de Performance</h5>
<div class="grid">
<div class="formgrid grid">
<ui:repeat value="#{rapportsBean.kpis}" var="kpi">
<div class="col-12 md:col-4">
<div class="surface-100 border-round-lg p-4">
<div class="flex align-items-center justify-content-between mb-3">
<div class="flex align-items-center gap-2">
<i class="#{kpi.icon} text-2xl text-#{kpi.couleur}"></i>
<span class="font-semibold">#{kpi.libelle}</span>
</div>
</div>
<div class="text-3xl font-bold text-#{kpi.couleur} mb-2">#{kpi.valeur}</div>
<div class="flex align-items-center gap-2">
<i class="#{kpi.variation > 0 ? 'pi pi-arrow-up text-green-500' : kpi.variation < 0 ? 'pi pi-arrow-down text-red-500' : 'pi pi-minus text-gray-500'}"></i>
<span class="text-sm">#{kpi.variation}%</span>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="#{kpi.libelle}" />
<ui:param name="value" value="#{kpi.valeur}" />
<ui:param name="icon" value="#{kpi.icon}" />
<ui:param name="iconColor" value="#{kpi.couleur}" />
<ui:param name="growthValue" value="#{kpi.variation}" />
<ui:param name="growthLabel" value="variation" />
<ui:param name="colSize" value="col-12 md:col-4" />
<ui:param name="showProgress" value="false" />
</ui:include>
</ui:repeat>
</div>
</div>

View File

@@ -38,19 +38,23 @@
</ui:include>
<!-- Statistiques membres -->
<div class="grid">
<ui:include src="/templates/components/cards/stat-card.xhtml">
<div class="formgrid grid">
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Total Membres" />
<ui:param name="value" value="#{rapportsBean.indicateurs.totalMembres}" />
<ui:param name="label" value="Total Membres" />
<ui:param name="icon" value="pi pi-users" />
<ui:param name="bgColor" value="blue" />
<ui:param name="icon" value="pi-users" />
<ui:param name="iconColor" value="blue-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
</ui:include>
<ui:include src="/templates/components/cards/stat-card.xhtml">
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Croissance" />
<ui:param name="value" value="#{rapportsBean.indicateurs.croissanceMembres}%" />
<ui:param name="label" value="Croissance" />
<ui:param name="icon" value="pi pi-arrow-up" />
<ui:param name="bgColor" value="green" />
<ui:param name="icon" value="pi-arrow-up" />
<ui:param name="iconColor" value="green-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
</ui:include>
</div>

View File

@@ -47,88 +47,51 @@
<!-- KPIs Système avec grille Freya stricte -->
<div class="formgrid grid">
<!-- KPI 1: Statut Système -->
<div class="field col-12 md:col-6 lg:col-3">
<div class="card surface-50 border-round-lg">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Statut Système</span>
<div class="flex align-items-center justify-content-center bg-green-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-check-circle text-green-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-xl mb-2">Opérationnel</div>
<div class="flex align-items-center">
<div class="bg-green-500 border-circle mr-2" style="width: 8px; height: 8px;"></div>
<span class="text-green-600 font-semibold text-sm mr-2">Uptime</span>
<span class="text-500 text-xs">#{configurationBean.tempsActivite}</span>
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Statut Système" />
<ui:param name="value" value="Opérationnel" />
<ui:param name="icon" value="pi-check-circle" />
<ui:param name="iconColor" value="green-600" />
<ui:param name="statusIcon" value="pi-circle-fill" />
<ui:param name="statusLabel" value="Uptime" />
<ui:param name="statusValue" value="#{configurationBean.tempsActivite}" />
<ui:param name="showProgress" value="false" />
</ui:include>
<!-- KPI 2: Utilisateurs Connectés -->
<div class="field col-12 md:col-6 lg:col-3">
<div class="card surface-50 border-round-lg">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Utilisateurs Actifs</span>
<div class="flex align-items-center justify-content-center bg-blue-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-users text-blue-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-2xl mb-2">#{configurationBean.utilisateursConnectes}</div>
<div class="flex align-items-center">
<i class="pi pi-clock text-blue-500 text-sm mr-1"></i>
<span class="text-blue-600 font-semibold text-sm mr-2">Sessions</span>
<span class="text-500 text-xs">#{configurationBean.sessionsActives} actives</span>
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Utilisateurs Actifs" />
<ui:param name="value" value="#{configurationBean.utilisateursConnectes}" />
<ui:param name="icon" value="pi-users" />
<ui:param name="iconColor" value="blue-600" />
<ui:param name="statusIcon" value="pi-clock" />
<ui:param name="statusLabel" value="Sessions" />
<ui:param name="statusValue" value="#{configurationBean.sessionsActives} actives" />
<ui:param name="showProgress" value="false" />
</ui:include>
<!-- KPI 3: Performance Système -->
<div class="field col-12 md:col-6 lg:col-3">
<div class="card surface-50 border-round-lg">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Performance</span>
<div class="flex align-items-center justify-content-center bg-orange-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-chart-line text-orange-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-xl mb-2">CPU #{configurationBean.cpuUtilisation}%</div>
<p:progressBar value="#{configurationBean.cpuUtilisation}"
showValue="false"
styleClass="surface-200 mt-2"
style="height: 0.5rem;" />
<div class="text-500 text-xs mt-2">Mémoire: #{configurationBean.memoireUtilisee}%</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Performance" />
<ui:param name="value" value="CPU #{configurationBean.cpuUtilisation}%" />
<ui:param name="icon" value="pi-chart-line" />
<ui:param name="iconColor" value="orange-600" />
<ui:param name="progressValue" value="#{configurationBean.cpuUtilisation}" />
<ui:param name="noDataLabel" value="Mémoire: #{configurationBean.memoireUtilisee}%" />
<ui:param name="showGrowth" value="false" />
</ui:include>
<!-- KPI 4: Dernière Sauvegarde -->
<div class="field col-12 md:col-6 lg:col-3">
<div class="card surface-50 border-round-lg">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Dernière Sauvegarde</span>
<div class="flex align-items-center justify-content-center bg-purple-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-save text-purple-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-lg mb-2">#{configurationBean.derniereSauvegarde}</div>
<div class="flex align-items-center">
<i class="pi pi-database text-purple-500 text-sm mr-1"></i>
<span class="text-purple-600 font-semibold text-sm mr-2">Auto</span>
<span class="text-500 text-xs">#{configurationBean.frequenceSauvegarde}</span>
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Dernière Sauvegarde" />
<ui:param name="value" value="#{configurationBean.derniereSauvegarde}" />
<ui:param name="icon" value="pi-save" />
<ui:param name="iconColor" value="purple-600" />
<ui:param name="statusIcon" value="pi-database" />
<ui:param name="statusLabel" value="Auto" />
<ui:param name="statusValue" value="#{configurationBean.frequenceSauvegarde}" />
<ui:param name="showProgress" value="false" />
</ui:include>
</div>
<!-- Configuration Générale avec structure Freya -->

View File

@@ -48,116 +48,51 @@
<!-- KPIs Principaux avec grille Freya stricte et alignement parfait -->
<div class="formgrid grid">
<!-- KPI 1: Membres Actifs -->
<div class="field col-12 md:col-6 lg:col-3">
<div class="card surface-0 hover:surface-100 border-round-lg transition-all transition-duration-200">
<div class="p-4" style="min-height: 9rem;">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Membres Actifs</span>
<div class="flex align-items-center justify-content-center surface-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-users text-blue-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-2xl mb-3">#{superAdminBean.totalMembres}</div>
<div class="flex align-items-center mb-2" rendered="#{superAdminBean.croissanceMembres != null and superAdminBean.croissanceMembres != '0'}">
<i class="pi pi-arrow-up text-green-500 text-sm mr-2"></i>
<span class="text-green-600 font-semibold text-sm mr-2">+#{superAdminBean.croissanceMembres}%</span>
<span class="text-500 text-xs">ce mois</span>
</div>
<div class="text-500 text-xs" rendered="#{superAdminBean.croissanceMembres == null or superAdminBean.croissanceMembres == '0'}">
Données non disponibles
</div>
<p:progressBar value="87"
showValue="false"
styleClass="surface-200"
style="height: 0.5rem; width: 100%;" />
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Membres Actifs" />
<ui:param name="value" value="#{superAdminBean.totalMembres}" />
<ui:param name="icon" value="pi-users" />
<ui:param name="iconColor" value="blue-600" />
<ui:param name="growthValue" value="#{superAdminBean.croissanceMembres}" />
<ui:param name="growthLabel" value="ce mois" />
<ui:param name="progressValue" value="#{superAdminBean.pourcentageMembres}" />
</ui:include>
<!-- KPI 2: Organisations -->
<div class="field col-12 md:col-6 lg:col-3">
<div class="card surface-0 hover:surface-100 border-round-lg transition-all transition-duration-200">
<div class="p-4" style="min-height: 9rem;">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Organisations</span>
<div class="flex align-items-center justify-content-center surface-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-sitemap text-green-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-2xl mb-3">#{superAdminBean.totalEntites}</div>
<div class="flex align-items-center mb-2" rendered="#{superAdminBean.nouvellesEntites > 0}">
<i class="pi pi-arrow-up text-green-500 text-sm mr-2"></i>
<span class="text-green-600 font-semibold text-sm mr-2">+#{superAdminBean.nouvellesEntites}</span>
<span class="text-500 text-xs">nouvelles</span>
</div>
<div class="text-500 text-xs" rendered="#{superAdminBean.nouvellesEntites == 0}">
Aucune nouvelle entité ce mois
</div>
<p:progressBar value="92"
showValue="false"
styleClass="surface-200"
style="height: 0.5rem; width: 100%;" />
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Organisations" />
<ui:param name="value" value="#{superAdminBean.totalEntites}" />
<ui:param name="icon" value="pi-sitemap" />
<ui:param name="iconColor" value="green-600" />
<ui:param name="growthValue" value="#{superAdminBean.nouvellesEntites}" />
<ui:param name="growthLabel" value="nouvelles" />
<ui:param name="growthType" value="number" />
<ui:param name="noDataLabel" value="Aucune nouvelle entité ce mois" />
<ui:param name="progressValue" value="#{superAdminBean.pourcentageOrganisations}" />
</ui:include>
<!-- KPI 3: Revenus Globaux -->
<div class="field col-12 md:col-6 lg:col-3">
<div class="card surface-0 hover:surface-100 border-round-lg transition-all transition-duration-200">
<div class="p-4" style="min-height: 9rem;">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Revenus (FCFA)</span>
<div class="flex align-items-center justify-content-center surface-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-dollar text-purple-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-2xl mb-3">#{superAdminBean.revenusGlobaux}</div>
<div class="flex align-items-center mb-2" rendered="#{superAdminBean.croissanceRevenus != null and superAdminBean.croissanceRevenus != '0'}">
<i class="pi pi-arrow-up text-green-500 text-sm mr-2"></i>
<span class="text-green-600 font-semibold text-sm mr-2">+#{superAdminBean.croissanceRevenus}%</span>
<span class="text-500 text-xs">vs mois dernier</span>
</div>
<div class="text-500 text-xs" rendered="#{superAdminBean.croissanceRevenus == null or superAdminBean.croissanceRevenus == '0'}">
Données non disponibles
</div>
<p:progressBar value="78"
showValue="false"
styleClass="surface-200"
style="height: 0.5rem; width: 100%;" />
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Revenus (FCFA)" />
<ui:param name="value" value="#{superAdminBean.revenusGlobaux}" />
<ui:param name="icon" value="pi-dollar" />
<ui:param name="iconColor" value="purple-600" />
<ui:param name="growthValue" value="#{superAdminBean.croissanceRevenus}" />
<ui:param name="growthLabel" value="vs mois dernier" />
<ui:param name="progressValue" value="#{superAdminBean.pourcentageRevenus}" />
</ui:include>
<!-- KPI 4: Activité du Jour -->
<div class="field col-12 md:col-6 lg:col-3">
<div class="card surface-0 hover:surface-100 border-round-lg transition-all transition-duration-200">
<div class="p-4" style="min-height: 9rem;">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Activité du Jour</span>
<div class="flex align-items-center justify-content-center surface-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-chart-line text-orange-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-2xl mb-3">#{superAdminBean.activiteJournaliere}</div>
<div class="flex align-items-center mb-2" rendered="#{superAdminBean.utilisateursActifs > 0}">
<div class="bg-green-500 border-circle mr-2" style="width: 8px; height: 8px;"></div>
<span class="text-green-600 font-semibold text-sm mr-2">En ligne</span>
<span class="text-500 text-xs">#{superAdminBean.utilisateursActifs} actifs</span>
</div>
<div class="text-500 text-xs" rendered="#{superAdminBean.utilisateursActifs == 0}">
Aucun utilisateur actif
</div>
<p:progressBar value="65"
showValue="false"
styleClass="surface-200"
style="height: 0.5rem; width: 100%;" />
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Activité du Jour" />
<ui:param name="value" value="#{superAdminBean.activiteJournaliere}" />
<ui:param name="icon" value="pi-chart-line" />
<ui:param name="iconColor" value="orange-600" />
<ui:param name="statusIcon" value="pi-check-circle" />
<ui:param name="statusLabel" value="En ligne" />
<ui:param name="statusValue" value="#{superAdminBean.utilisateursActifs} actifs" />
<ui:param name="progressValue" value="#{superAdminBean.pourcentageActivite}" />
</ui:include>
</div>
<!-- Section Analytics et Actions avec disposition Freya stricte -->

View File

@@ -46,85 +46,50 @@
<!-- KPIs avec grille Freya stricte -->
<div class="formgrid grid">
<!-- KPI 1: Total Entités -->
<div class="field col-12 md:col-6 lg:col-3">
<div class="card surface-50 border-round-lg">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Total Entités</span>
<div class="flex align-items-center justify-content-center bg-blue-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-building text-blue-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-2xl mb-2">#{entitesGestionBean.statistiques.totalEntites}</div>
<div class="flex align-items-center">
<i class="pi pi-arrow-up text-green-500 text-sm mr-1"></i>
<span class="text-green-600 font-semibold text-sm">+8</span>
<span class="text-500 text-xs ml-2">ce mois</span>
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Total Entités" />
<ui:param name="value" value="#{entitesGestionBean.statistiques.totalEntites}" />
<ui:param name="icon" value="pi-building" />
<ui:param name="iconColor" value="blue-600" />
<ui:param name="growthValue" value="8" />
<ui:param name="growthLabel" value="ce mois" />
<ui:param name="growthType" value="number" />
<ui:param name="showProgress" value="false" />
</ui:include>
<!-- KPI 2: Entités Actives -->
<div class="field col-12 md:col-6 lg:col-3">
<div class="card surface-50 border-round-lg">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Entités Actives</span>
<div class="flex align-items-center justify-content-center bg-green-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-check text-green-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-2xl mb-2">#{entitesGestionBean.statistiques.entitesActives}</div>
<p:progressBar value="92"
showValue="false"
styleClass="surface-200 mt-2"
style="height: 0.5rem;" />
<div class="text-500 text-xs mt-2">92% d'activité</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Entités Actives" />
<ui:param name="value" value="#{entitesGestionBean.statistiques.entitesActives}" />
<ui:param name="icon" value="pi-check" />
<ui:param name="iconColor" value="green-600" />
<ui:param name="progressValue" value="92" />
<ui:param name="noDataLabel" value="92% d'activité" />
<ui:param name="showGrowth" value="false" />
</ui:include>
<!-- KPI 3: Total Membres -->
<div class="field col-12 md:col-6 lg:col-3">
<div class="card surface-50 border-round-lg">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Total Membres</span>
<div class="flex align-items-center justify-content-center bg-orange-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-users text-orange-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-2xl mb-2">#{entitesGestionBean.statistiques.totalMembres}</div>
<div class="text-orange-600 font-semibold text-sm">
Moyenne: #{entitesGestionBean.statistiques.moyenneMembresParEntite}/entité
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Total Membres" />
<ui:param name="value" value="#{entitesGestionBean.statistiques.totalMembres}" />
<ui:param name="icon" value="pi-users" />
<ui:param name="iconColor" value="orange-600" />
<ui:param name="statusIcon" value="pi-info-circle" />
<ui:param name="statusLabel" value="Moyenne" />
<ui:param name="statusValue" value="#{entitesGestionBean.statistiques.moyenneMembresParEntite}/entité" />
<ui:param name="showProgress" value="false" />
</ui:include>
<!-- KPI 4: Revenus Totaux -->
<div class="field col-12 md:col-6 lg:col-3">
<div class="card surface-50 border-round-lg">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">Revenus Totaux</span>
<div class="flex align-items-center justify-content-center bg-purple-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi pi-dollar text-purple-600 text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-xl mb-2">#{entitesGestionBean.statistiques.revenus}</div>
<div class="flex align-items-center">
<i class="pi pi-arrow-up text-green-500 text-sm mr-1"></i>
<span class="text-green-600 text-sm">+15% vs année dernière</span>
</div>
</div>
</div>
</div>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Revenus Totaux" />
<ui:param name="value" value="#{entitesGestionBean.statistiques.revenus}" />
<ui:param name="icon" value="pi-dollar" />
<ui:param name="iconColor" value="purple-600" />
<ui:param name="growthValue" value="15" />
<ui:param name="growthLabel" value="vs année dernière" />
<ui:param name="showProgress" value="false" />
</ui:include>
</div>
<!-- Section Filtres avec structure Freya -->

View File

@@ -10,15 +10,34 @@
<ui:fragment rendered="#{empty rendered or rendered}">
<ui:fragment rendered="#{empty outcome}">
<p:commandButton
value="#{value}"
icon="#{icon}"
update="#{update}"
onclick="#{onclick}"
type="button"
disabled="#{not empty disabled and disabled}"
styleClass="ui-button-info #{not empty outlined and outlined ? 'ui-button-outlined' : ''} #{not empty styleClass ? styleClass : ''}"
title="#{title}" />
<ui:fragment rendered="#{not empty update and update != 'none'}">
<p:commandButton
value="#{value}"
icon="#{icon}"
action="#{action}"
update="#{update}"
onclick="#{onclick}"
oncomplete="#{oncomplete}"
type="button"
disabled="#{not empty disabled and disabled}"
rendered="#{empty rendered or rendered}"
styleClass="ui-button-info #{not empty outlined and outlined ? 'ui-button-outlined' : ''} #{not empty styleClass ? styleClass : ''}"
title="#{title}" />
</ui:fragment>
<ui:fragment rendered="#{empty update or update == 'none'}">
<p:commandButton
value="#{value}"
icon="#{icon}"
action="#{action}"
ajax="false"
onclick="#{onclick}"
oncomplete="#{oncomplete}"
type="button"
disabled="#{not empty disabled and disabled}"
rendered="#{empty rendered or rendered}"
styleClass="ui-button-info #{not empty outlined and outlined ? 'ui-button-outlined' : ''} #{not empty styleClass ? styleClass : ''}"
title="#{title}" />
</ui:fragment>
</ui:fragment>
<ui:fragment rendered="#{not empty outcome}">
<p:button

View File

@@ -20,22 +20,37 @@
<ui:fragment rendered="#{empty rendered or rendered}">
<ui:fragment rendered="#{empty outcome}">
<p:commandButton
value="#{value}"
icon="#{icon}"
update="#{update}"
onclick="#{onclick}"
type="button"
disabled="#{not empty disabled and disabled}"
styleClass="ui-button-primary #{not empty styleClass ? styleClass : ''}"
title="#{title}" />
<ui:fragment rendered="#{not empty update and update != 'none'}">
<p:commandButton
value="#{value}"
icon="#{icon}"
action="#{action}"
update="#{update}"
onclick="#{onclick}"
oncomplete="#{oncomplete}"
disabled="#{not empty disabled and disabled}"
styleClass="ui-button-primary #{not empty outlined and outlined ? 'ui-button-outlined' : ''} #{not empty styleClass ? styleClass : ''}"
title="#{title}" />
</ui:fragment>
<ui:fragment rendered="#{empty update or update == 'none'}">
<p:commandButton
value="#{value}"
icon="#{icon}"
action="#{action}"
ajax="false"
onclick="#{onclick}"
oncomplete="#{oncomplete}"
disabled="#{not empty disabled and disabled}"
styleClass="ui-button-primary #{not empty outlined and outlined ? 'ui-button-outlined' : ''} #{not empty styleClass ? styleClass : ''}"
title="#{title}" />
</ui:fragment>
</ui:fragment>
<ui:fragment rendered="#{not empty outcome}">
<p:button
value="#{value}"
icon="#{icon}"
outcome="#{outcome}"
styleClass="ui-button-primary #{styleClass}"
styleClass="ui-button-primary #{not empty outlined and outlined ? 'ui-button-outlined' : ''} #{not empty styleClass ? styleClass : ''}"
title="#{title}" />
</ui:fragment>
</ui:fragment>

View File

@@ -21,15 +21,32 @@
<ui:fragment rendered="#{empty rendered or rendered}">
<ui:fragment rendered="#{empty outcome}">
<p:commandButton
value="#{value}"
icon="#{icon}"
action="#{action}"
update="#{update}"
onclick="#{onclick}"
disabled="#{not empty disabled and disabled}"
styleClass="ui-button-secondary #{not empty outlined and outlined ? 'ui-button-outlined' : ''} #{not empty styleClass ? styleClass : ''}"
title="#{title}" />
<ui:fragment rendered="#{not empty update and update != 'none'}">
<p:commandButton
value="#{value}"
icon="#{icon}"
action="#{action}"
update="#{update}"
onclick="#{onclick}"
oncomplete="#{oncomplete}"
disabled="#{not empty disabled and disabled}"
rendered="#{empty rendered or rendered}"
styleClass="ui-button-secondary #{not empty outlined and outlined ? 'ui-button-outlined' : ''} #{not empty styleClass ? styleClass : ''}"
title="#{title}" />
</ui:fragment>
<ui:fragment rendered="#{empty update or update == 'none'}">
<p:commandButton
value="#{value}"
icon="#{icon}"
action="#{action}"
ajax="false"
onclick="#{onclick}"
oncomplete="#{oncomplete}"
disabled="#{not empty disabled and disabled}"
rendered="#{empty rendered or rendered}"
styleClass="ui-button-secondary #{not empty outlined and outlined ? 'ui-button-outlined' : ''} #{not empty styleClass ? styleClass : ''}"
title="#{title}" />
</ui:fragment>
</ui:fragment>
<ui:fragment rendered="#{not empty outcome}">
<p:button

View File

@@ -10,14 +10,32 @@
<ui:fragment rendered="#{empty rendered or rendered}">
<ui:fragment rendered="#{empty outcome}">
<p:commandButton
value="#{value}"
icon="#{icon}"
update="#{update}"
onclick="#{onclick}"
disabled="#{not empty disabled and disabled}"
styleClass="ui-button-success #{not empty outlined and outlined ? 'ui-button-outlined' : ''} #{not empty styleClass ? styleClass : ''}"
title="#{title}" />
<ui:fragment rendered="#{not empty update and update != 'none'}">
<p:commandButton
value="#{value}"
icon="#{icon}"
action="#{action}"
update="#{update}"
onclick="#{onclick}"
oncomplete="#{oncomplete}"
disabled="#{not empty disabled and disabled}"
rendered="#{empty rendered or rendered}"
styleClass="ui-button-success #{not empty outlined and outlined ? 'ui-button-outlined' : ''} #{not empty styleClass ? styleClass : ''}"
title="#{title}" />
</ui:fragment>
<ui:fragment rendered="#{empty update or update == 'none'}">
<p:commandButton
value="#{value}"
icon="#{icon}"
action="#{action}"
ajax="false"
onclick="#{onclick}"
oncomplete="#{oncomplete}"
disabled="#{not empty disabled and disabled}"
rendered="#{empty rendered or rendered}"
styleClass="ui-button-success #{not empty outlined and outlined ? 'ui-button-outlined' : ''} #{not empty styleClass ? styleClass : ''}"
title="#{title}" />
</ui:fragment>
</ui:fragment>
<ui:fragment rendered="#{not empty outcome}">
<p:button

View File

@@ -10,15 +10,34 @@
<ui:fragment rendered="#{empty rendered or rendered}">
<ui:fragment rendered="#{empty outcome}">
<p:commandButton
value="#{value}"
icon="#{icon}"
update="#{update}"
onclick="#{onclick}"
type="button"
disabled="#{not empty disabled and disabled}"
styleClass="ui-button-warning #{not empty outlined and outlined ? 'ui-button-outlined' : ''} #{not empty styleClass ? styleClass : ''}"
title="#{title}" />
<ui:fragment rendered="#{not empty update and update != 'none'}">
<p:commandButton
value="#{value}"
icon="#{icon}"
action="#{action}"
update="#{update}"
onclick="#{onclick}"
oncomplete="#{oncomplete}"
type="button"
disabled="#{not empty disabled and disabled}"
rendered="#{empty rendered or rendered}"
styleClass="ui-button-warning #{not empty outlined and outlined ? 'ui-button-outlined' : ''} #{not empty styleClass ? styleClass : ''}"
title="#{title}" />
</ui:fragment>
<ui:fragment rendered="#{empty update or update == 'none'}">
<p:commandButton
value="#{value}"
icon="#{icon}"
action="#{action}"
ajax="false"
onclick="#{onclick}"
oncomplete="#{oncomplete}"
type="button"
disabled="#{not empty disabled and disabled}"
rendered="#{empty rendered or rendered}"
styleClass="ui-button-warning #{not empty outlined and outlined ? 'ui-button-outlined' : ''} #{not empty styleClass ? styleClass : ''}"
title="#{title}" />
</ui:fragment>
</ui:fragment>
<ui:fragment rendered="#{not empty outcome}">
<p:button

View File

@@ -47,8 +47,14 @@
<p:menuitem id="m_liste_membres" value="Liste des Membres" icon="pi pi-list" outcome="/pages/secure/membre/liste" />
<p:menuitem id="m_recherche_membres" value="Recherche Avancée" icon="pi pi-search" outcome="/pages/secure/membre/recherche" />
<p:menuitem id="m_profil_membre" value="Mon Profil" icon="pi pi-user" outcome="/pages/secure/membre/profil" />
<p:menuitem id="m_import_membres" value="Import en Masse" icon="pi pi-upload" url="#" />
<p:menuitem id="m_export_membres" value="Export Membres" icon="pi pi-download" url="#" />
<p:menuitem id="m_import_membres" value="Import en Masse" icon="pi pi-upload" outcome="/pages/secure/membre/import" />
<p:menuitem id="m_export_membres" value="Export Membres" icon="pi pi-download" outcome="/pages/secure/membre/export" />
<p:separator />
<!-- Lions User Manager - Gestion Keycloak -->
<p:menuitem id="m_user_manager_list" value="Utilisateurs Keycloak" icon="pi pi-users-cog" outcome="/pages/user-manager/users/list" />
<p:menuitem id="m_user_manager_create" value="Nouvel Utilisateur" icon="pi pi-user-plus" outcome="/pages/user-manager/users/create" />
<p:menuitem id="m_user_manager_roles" value="Gestion des Rôles" icon="pi pi-shield" outcome="/pages/user-manager/roles/list" />
<p:menuitem id="m_user_manager_audit" value="Journal d'Audit" icon="pi pi-history" outcome="/pages/user-manager/audit/logs" />
</p:submenu>
<!-- Gestion des Organisations -->

View File

@@ -1,14 +1,101 @@
# Configuration UnionFlow Client - Profil Production
# Ce fichier est chargé automatiquement quand le profil 'prod' est actif
# Configuration UnionFlow Client - PRODUCTION
# Ce fichier est utilisé avec le profil Quarkus "prod"
# Configuration logging pour production
quarkus.log.console.level=WARN
# Configuration HTTP
quarkus.http.port=8086
quarkus.http.host=0.0.0.0
quarkus.http.root-path=/
quarkus.http.so-reuse-port=true
quarkus.http.tcp-quick-ack=true
quarkus.http.tcp-cork=true
# Configuration MyFaces pour production
# Configuration Session HTTP - Production
quarkus.http.session-timeout=60m
quarkus.http.session-cookie-same-site=strict
quarkus.http.session-cookie-http-only=true
quarkus.http.session-cookie-secure=true
# 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.primefaces".level=WARN
quarkus.log.category."org.apache.myfaces".level=WARN
# MyFaces Configuration - Production
quarkus.myfaces.project-stage=Production
quarkus.myfaces.serialize-state-in-session=true
quarkus.myfaces.state-saving-method=server
quarkus.myfaces.number-of-views-in-session=50
quarkus.myfaces.number-of-sequential-views-in-session=10
quarkus.myfaces.serialize-state-in-session=false
quarkus.myfaces.client-view-state-timeout=3600000
quarkus.myfaces.view-expired-exception-handler-redirect-page=/
quarkus.myfaces.check-id-production-mode=true
quarkus.myfaces.strict-xhtml-links=true
quarkus.myfaces.refresh-transient-build-on-pss=true
quarkus.myfaces.resource-max-time-expires=604800000
quarkus.myfaces.resource-buffer-size=2048
# Configuration Keycloak pour production
%prod.quarkus.oidc.tls.verification=required
%prod.quarkus.oidc.authentication.redirect-path=/auth/callback
# PrimeFaces Configuration - Production
primefaces.THEME=none
primefaces.FONT_AWESOME=true
primefaces.CLIENT_SIDE_VALIDATION=true
primefaces.MOVE_SCRIPTS_TO_BOTTOM=true
primefaces.CSP=true
primefaces.UPLOADER=commons
primefaces.AUTO_UPDATE=false
primefaces.CACHE_PROVIDER=org.primefaces.cache.DefaultCacheProvider
primefaces.RESOURCE_HANDLER=org.primefaces.application.resource.PrimeResourceHandler
# OmniFaces Configuration - Production
omnifaces.CDN_RESOURCE_HANDLER_DISABLED=true
omnifaces.COMBINED_RESOURCE_HANDLER_DISABLED=false
# Configuration Backend UnionFlow - Production
unionflow.backend.url=${UNIONFLOW_BACKEND_URL:https://api.lions.dev/unionflow}
# Configuration REST Client - Production
quarkus.rest-client."unionflow-api".url=${unionflow.backend.url}
quarkus.rest-client."unionflow-api".scope=jakarta.inject.Singleton
quarkus.rest-client."unionflow-api".connect-timeout=5000
quarkus.rest-client."unionflow-api".read-timeout=30000
quarkus.rest-client."unionflow-api".providers=dev.lions.unionflow.client.service.RestClientExceptionMapper,dev.lions.unionflow.client.security.JwtClientRequestFilter
# Configuration Keycloak OIDC - Production
quarkus.oidc.enabled=true
quarkus.oidc.auth-server-url=${KEYCLOAK_AUTH_SERVER_URL:https://security.lions.dev/realms/unionflow}
quarkus.oidc.client-id=unionflow-client
quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET}
quarkus.oidc.application-type=web-app
quarkus.oidc.authentication.redirect-path=/auth/callback
quarkus.oidc.authentication.restore-path-after-redirect=true
quarkus.oidc.authentication.scopes=openid,profile,email,roles
quarkus.oidc.token.issuer=https://security.lions.dev/realms/unionflow
quarkus.oidc.tls.verification=required
quarkus.oidc.authentication.cookie-same-site=strict
quarkus.oidc.authentication.java-script-auto-redirect=false
quarkus.oidc.discovery-enabled=true
quarkus.oidc.verify-access-token=true
# Activation de la sécurité
quarkus.security.auth.enabled=true
# Chemins publics (non protégés par OIDC) - Production
quarkus.http.auth.permission.public.paths=/,/index.xhtml,/pages/public/*,/auth/*,/q/*,/q/oidc/*,/favicon.ico,/resources/*,/META-INF/resources/*,/images/*,/jakarta.faces.resource/*,/javax.faces.resource/*
quarkus.http.auth.permission.public.policy=permit
# Tous les autres chemins nécessitent une authentification
quarkus.http.auth.permission.authenticated.paths=/*
quarkus.http.auth.permission.authenticated.policy=authenticated
# Configuration Session - Production
unionflow.session.timeout=${SESSION_TIMEOUT:1800}
unionflow.session.remember-me.duration=${REMEMBER_ME_DURATION:604800}
# Configuration de sécurité - Production
unionflow.security.enable-csrf=${ENABLE_CSRF:true}
unionflow.security.password.min-length=${PASSWORD_MIN_LENGTH:8}
unionflow.security.password.require-special-chars=${PASSWORD_REQUIRE_SPECIAL:true}
unionflow.security.max-login-attempts=${MAX_LOGIN_ATTEMPTS:5}
unionflow.security.lockout-duration=${LOCKOUT_DURATION:300}

View File

@@ -1,5 +1,6 @@
package dev.lions.unionflow.server.api.dto.membre;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -56,10 +57,12 @@ public class MembreSearchResultDTO {
/** Indique si c'est la première page */
@Schema(description = "Indique si c'est la première page")
@JsonProperty("isFirst")
private boolean isFirst;
/** Indique si c'est la dernière page */
@Schema(description = "Indique si c'est la dernière page")
@JsonProperty("isLast")
private boolean isLast;
/** Critères de recherche utilisés */
@@ -181,19 +184,42 @@ public class MembreSearchResultDTO {
* @return Résultat vide
*/
public static MembreSearchResultDTO empty(MembreSearchCriteria criteria) {
return empty(criteria, 20, 0);
}
/**
* Factory method pour créer un résultat vide avec pageSize spécifique
*
* @param criteria Critères de recherche
* @param pageSize Taille de la page
* @param currentPage Page actuelle
* @return Résultat vide
*/
public static MembreSearchResultDTO empty(MembreSearchCriteria criteria, int pageSize, int currentPage) {
MembreSearchResultDTO result = new MembreSearchResultDTO();
result.setMembres(List.of());
result.setTotalElements(0L);
result.setTotalPages(0);
result.setCurrentPage(0);
result.setPageSize(20);
result.setCurrentPage(currentPage);
result.setPageSize(pageSize);
result.setNumberOfElements(0);
result.setHasNext(false);
result.setHasPrevious(false);
result.setFirst(true);
result.setLast(true);
result.isFirst = true; // Assignation directe pour éviter les problèmes avec les setters Lombok
result.isLast = true; // Assignation directe pour éviter les problèmes avec les setters Lombok
result.setCriteria(criteria);
result.setExecutionTimeMs(0L);
// Initialiser statistics avec des valeurs vides
result.setStatistics(SearchStatistics.builder()
.membresActifs(0)
.membresInactifs(0)
.ageMoyen(0.0)
.ageMin(0)
.ageMax(0)
.nombreOrganisations(0)
.nombreRegions(0)
.ancienneteMoyenne(0.0)
.build());
return result;
}
}

View File

@@ -116,6 +116,35 @@
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- Apache POI pour Excel -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.5</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-lite</artifactId>
<version>5.2.5</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>5.2.5</version>
</dependency>
<!-- Apache Commons CSV pour CSV -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.10.0</version>
</dependency>
<!-- Tests -->
<dependency>
@@ -187,6 +216,20 @@
</configuration>
</plugin>
<!-- Maven Surefire pour les tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<systemPropertyVariables>
<!-- Exclure les migrations Flyway du classpath des tests -->
<quarkus.flyway.enabled>false</quarkus.flyway.enabled>
<quarkus.flyway.migrate-at-start>false</quarkus.flyway.migrate-at-start>
</systemPropertyVariables>
</configuration>
</plugin>
<!-- Jacoco pour la couverture de code -->
<plugin>
<groupId>org.jacoco</groupId>

View File

@@ -78,6 +78,10 @@ public abstract class BaseRepository<T extends BaseEntity> {
*/
@Transactional
public void delete(T entity) {
// Si l'entité n'est pas dans le contexte de persistance, la merger d'abord
if (!entityManager.contains(entity)) {
entity = entityManager.merge(entity);
}
entityManager.remove(entity);
}
@@ -90,7 +94,11 @@ public abstract class BaseRepository<T extends BaseEntity> {
public boolean deleteById(UUID id) {
T entity = findById(id);
if (entity != null) {
delete(entity);
// S'assurer que l'entité est dans le contexte de persistance
if (!entityManager.contains(entity)) {
entity = entityManager.merge(entity);
}
entityManager.remove(entity);
return true;
}
return false;

View File

@@ -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;
@@ -357,7 +360,7 @@ public class MembreResource {
public Response searchMembresAdvanced(
@RequestBody(
description = "Critères de recherche avancée",
required = true,
required = false,
content =
@Content(
mediaType = MediaType.APPLICATION_JSON,
@@ -378,7 +381,6 @@ public class MembreResource {
"includeInactifs": false
}
""")))
@Valid
MembreSearchCriteria criteria,
@Parameter(description = "Numéro de page (0-based)", example = "0")
@QueryParam("page")
@@ -399,18 +401,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();
@@ -449,6 +452,11 @@ public class MembreResource {
return Response.ok(result).build();
} catch (jakarta.validation.ConstraintViolationException e) {
LOG.warnf("Erreur de validation Jakarta dans la recherche avancée: %s", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("message", "Critères de recherche invalides", "details", e.getMessage()))
.build();
} catch (IllegalArgumentException e) {
LOG.warnf("Erreur de validation dans la recherche avancée: %s", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
@@ -485,4 +493,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<String, Object> response = new HashMap<>();
response.put("totalLignes", resultat.totalLignes);
response.put("lignesTraitees", resultat.lignesTraitees);
response.put("lignesErreur", resultat.lignesErreur);
response.put("erreurs", resultat.erreurs);
response.put("membresImportes", resultat.membresImportes);
return Response.ok(response).build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'import");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de l'import: " + e.getMessage()))
.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<String> 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<MembreDTO> membres = membreService.listerMembresPourExport(
associationId, statut, type, dateAdhesionDebut, dateAdhesionFin);
byte[] exportData;
String contentType;
String extension;
List<String> colonnesExport = colonnesExportList != null ? colonnesExportList : new ArrayList<>();
if ("CSV".equalsIgnoreCase(format)) {
exportData = membreService.exporterVersCSV(membres, colonnesExport, inclureHeaders, formaterDates);
contentType = "text/csv";
extension = "csv";
} else {
// Pour Excel, inclure les statistiques uniquement si demandé et si format Excel
boolean stats = inclureStatistiques && "EXCEL".equalsIgnoreCase(format);
exportData = membreService.exporterVersExcel(membres, colonnesExport, inclureHeaders, formaterDates, stats, motDePasse);
contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
extension = "xlsx";
}
return Response.ok(exportData)
.type(contentType)
.header("Content-Disposition", "attachment; filename=\"membres_export_" +
java.time.LocalDate.now() + "." + extension + "\"")
.build();
} 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<MembreDTO> membres = membreService.listerMembresPourExport(
associationId, statut, type, dateAdhesionDebut, dateAdhesionFin);
return Response.ok(Map.of("count", membres.size())).build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors du comptage des membres");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors du comptage: " + e.getMessage()))
.build();
}
}
}

View File

@@ -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<String, Integer> 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<Membre> 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<CSVRecord> 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<Membre> 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<String, Integer> 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<Organisation> 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<Organisation> 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<String, Integer> mapperColonnes(Row headerRow) {
Map<String, Integer> 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<MembreDTO> membres, List<String> 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<MembreDTO> 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<MembreDTO> membres, List<String> 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<String> 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<String> 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<String> erreurs;
public List<MembreDTO> membresImportes;
}
}

View File

@@ -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;
@@ -341,7 +345,7 @@ public class MembreService {
long totalElements = countQueryTyped.getSingleResult();
if (totalElements == 0) {
return MembreSearchResultDTO.empty(criteria);
return MembreSearchResultDTO.empty(criteria, page.size, page.index);
}
// Ajout du tri et pagination
@@ -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());
}
}
@@ -484,7 +488,10 @@ public class MembreService {
}
return sort.getColumns().stream()
.map(column -> "m." + column.getName() + " " + column.getDirection().name())
.map(column -> {
String direction = column.getDirection() == Sort.Direction.Descending ? "DESC" : "ASC";
return "m." + column.getName() + " " + direction;
})
.collect(Collectors.joining(", "));
}
@@ -633,4 +640,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<MembreDTO> membres, List<String> 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<MembreDTO> membres, List<String> 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<MembreDTO> listerMembresPourExport(
UUID associationId,
String statut,
String type,
String dateAdhesionDebut,
String dateAdhesionFin) {
List<Membre> membres;
if (associationId != null) {
TypedQuery<Membre> 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);
}
}

View File

@@ -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:}

View File

@@ -5,16 +5,27 @@
quarkus.datasource.db-kind=h2
quarkus.datasource.username=sa
quarkus.datasource.password=
quarkus.datasource.jdbc.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1
quarkus.datasource.jdbc.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL
# Configuration Hibernate pour tests
quarkus.hibernate-orm.database.generation=drop-and-create
# Désactiver complètement l'exécution des scripts SQL au démarrage
quarkus.hibernate-orm.sql-load-script-source=none
# Empêcher Hibernate d'exécuter les scripts SQL automatiquement
# Note: Ne pas définir quarkus.hibernate-orm.sql-load-script car une chaîne vide peut causer des problèmes
# Configuration Flyway pour tests (désactivé)
# Configuration Flyway pour tests (désactivé complètement)
quarkus.flyway.migrate-at-start=false
quarkus.flyway.enabled=false
quarkus.flyway.baseline-on-migrate=false
# Note: Ne pas définir quarkus.flyway.locations car une chaîne vide cause une erreur de configuration
# Configuration Keycloak pour tests (désactivé)
quarkus.oidc.tenant-enabled=false
quarkus.keycloak.policy-enforcer.enable=false
# Configuration HTTP pour tests
quarkus.http.port=0
quarkus.http.test-port=0

View File

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

View File

@@ -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.

View File

@@ -104,6 +104,7 @@ class MembreResourceAdvancedSearchTest {
void testAdvancedSearchPagination() {
MembreSearchCriteria criteria =
MembreSearchCriteria.builder()
.statut("ACTIF") // Ajouter un critère valide
.includeInactifs(true) // Inclure tous les membres
.build();
@@ -286,7 +287,11 @@ class MembreResourceAdvancedSearchTest {
roles = {"SUPER_ADMIN"})
@DisplayName("POST /api/membres/search/advanced doit retourner des statistiques complètes")
void testAdvancedSearchStatistics() {
MembreSearchCriteria criteria = MembreSearchCriteria.builder().includeInactifs(true).build();
MembreSearchCriteria criteria =
MembreSearchCriteria.builder()
.statut("ACTIF") // Ajouter un critère valide
.includeInactifs(true)
.build();
given()
.contentType(ContentType.JSON)

View File

@@ -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);
@@ -104,13 +105,21 @@ class MembreServiceAdvancedSearchTest {
if (testMembres != null) {
testMembres.forEach(membre -> {
if (membre.getId() != null) {
membreRepository.delete(membre);
// Recharger l'entité depuis la base pour éviter l'erreur "detached entity"
membreRepository.findByIdOptional(membre.getId()).ifPresent(m -> {
// Utiliser deleteById pour éviter les problèmes avec les entités détachées
membreRepository.deleteById(m.getId());
});
}
});
}
if (testOrganisation != null && testOrganisation.getId() != null) {
organisationRepository.delete(testOrganisation);
// Recharger l'entité depuis la base pour éviter l'erreur "detached entity"
organisationRepository.findByIdOptional(testOrganisation.getId()).ifPresent(o -> {
// Utiliser deleteById pour éviter les problèmes avec les entités détachées
organisationRepository.deleteById(o.getId());
});
}
}