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> <artifactId>unionflow-server-api</artifactId>
<version>1.0.0</version> <version>1.0.0</version>
</dependency> </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 --> <!-- Scheduler pour le rafraîchissement des tokens -->
<dependency> <dependency>

View File

@@ -82,20 +82,42 @@ public interface MembreService {
@GET @GET
@Path("/export") @Path("/export")
@Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") @Produces({"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "text/csv", "application/pdf", "application/json"})
byte[] exporterExcel( byte[] exporterExcel(
@QueryParam("format") @DefaultValue("EXCEL") String format, @QueryParam("format") @DefaultValue("EXCEL") String format,
@QueryParam("associationId") UUID associationId, @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 @POST
@Path("/import") @Path("/import")
@Consumes(MediaType.MULTIPART_FORM_DATA) @Consumes(MediaType.MULTIPART_FORM_DATA)
ResultatImportDTO importerDonnees( @Produces(MediaType.APPLICATION_JSON)
@FormParam("file") java.io.InputStream fileInputStream, ResultatImportDTO importerDonnees(MembreImportMultipartForm form);
@FormParam("associationId") UUID associationId
); @GET
@Path("/import/modele")
@Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
byte[] telechargerModeleImport();
@GET @GET
@Path("/autocomplete/villes") @Path("/autocomplete/villes")

View File

@@ -114,7 +114,7 @@ public class AuditBean implements Serializable {
} catch (Exception e) { } catch (Exception e) {
LOGGER.severe("Erreur lors du chargement des logs: " + e.getMessage()); 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<>(); tousLesLogs = new ArrayList<>();
ajouterMessage(FacesMessage.SEVERITY_ERROR, "Erreur", ajouterMessage(FacesMessage.SEVERITY_ERROR, "Erreur",
"Erreur lors du chargement des logs: " + e.getMessage()); "Erreur lors du chargement des logs: " + e.getMessage());

View File

@@ -5,8 +5,6 @@ import jakarta.inject.Named;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -42,51 +40,58 @@ public class DemandesBean implements Serializable {
private String commentaireAssignation; private String commentaireAssignation;
// Statistiques // Statistiques
private int enAttente = 12; private int enAttente = 0;
private int urgentes = 5; private int urgentes = 0;
private int traitees = 143; private int traitees = 0;
private int delaiMoyenTraitement = 3; private int delaiMoyenTraitement = 0;
@PostConstruct @PostConstruct
public void init() { public void init() {
initializeDemandes(); initializeDemandes();
initializeGestionnaires(); initializeGestionnaires();
initializeNouvelleDemande(); initializeNouvelleDemande();
calculerStatistiques();
selectedDemandes = new ArrayList<>(); selectedDemandes = new ArrayList<>();
} }
private void initializeDemandes() { private void initializeDemandes() {
demandes = new ArrayList<>(); demandes = new ArrayList<>();
// TODO: Charger depuis le backend via DemandeAideService
String[] objets = { // Pour l'instant, liste vide - les données viendront du backend
"Demande d'aide financière urgente", "Certificat de membership", }
"Mutation vers club Dakar", "Réclamation cotisation",
"Aide médicale famille", "Certificat de bonne conduite" private void calculerStatistiques() {
}; if (demandes == null || demandes.isEmpty()) {
enAttente = 0;
String[] types = {"AIDE_FINANCIERE", "CERTIFICAT", "MUTATION", "RECLAMATION", "AIDE_FINANCIERE", "CERTIFICAT"}; urgentes = 0;
String[] statuts = {"EN_ATTENTE", "EN_COURS", "APPROUVEE", "EN_ATTENTE", "URGENTE", "EN_COURS"}; traitees = 0;
String[] priorites = {"URGENTE", "NORMALE", "NORMALE", "BASSE", "URGENTE", "NORMALE"}; delaiMoyenTraitement = 0;
String[] demandeurs = {"Marie Kouassi", "Paul Traoré", "Fatou Sanogo", "Jean Ouattara", "Aissata Koné", "Ibrahim Touré"}; return;
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);
} }
// 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 // Initialiser les sous-listes
demandesUrgentes = demandes.stream() demandesUrgentes = demandes.stream()
.filter(d -> "URGENTE".equals(d.getPriorite()) || "EN_ATTENTE".equals(d.getStatut())) .filter(d -> "URGENTE".equals(d.getPriorite()) || "EN_ATTENTE".equals(d.getStatut()))
@@ -94,6 +99,11 @@ public class DemandesBean implements Serializable {
.collect(Collectors.toList()); .collect(Collectors.toList());
dernieresDemandes = demandes.stream() 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) .limit(4)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@@ -181,6 +191,8 @@ public class DemandesBean implements Serializable {
public void actualiser() { public void actualiser() {
LOGGER.info("Actualisation des données"); LOGGER.info("Actualisation des données");
initializeDemandes();
calculerStatistiques();
} }
public void filtrerUrgentes() { public void filtrerUrgentes() {

View File

@@ -147,7 +147,7 @@ public class EvenementsBean implements Serializable {
} catch (Exception e) { } catch (Exception e) {
LOGGER.severe("Erreur lors du chargement des événements: " + e.getMessage()); 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<>(); tousLesEvenements = new ArrayList<>();
ajouterMessage(FacesMessage.SEVERITY_ERROR, "Erreur", ajouterMessage(FacesMessage.SEVERITY_ERROR, "Erreur",
"Erreur lors du chargement des événements: " + e.getMessage()); "Erreur lors du chargement des événements: " + e.getMessage());
@@ -522,7 +522,7 @@ public class EvenementsBean implements Serializable {
} catch (Exception e) { } catch (Exception e) {
LOGGER.severe("Erreur lors de la création de l'événement: " + e.getMessage()); 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", ajouterMessage(FacesMessage.SEVERITY_ERROR, "Erreur",
"Erreur lors de la création de l'événement: " + e.getMessage()); "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 int getUtilisateursActifs() { return utilisateursActifs; }
public void setUtilisateursActifs(int utilisateursActifs) { this.utilisateursActifs = 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 // Classes internes
public static class Alerte { public static class Alerte {
private UUID id; private UUID id;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -377,26 +377,18 @@
<i class="pi pi-chart-bar text-blue-500 mr-2"></i> <i class="pi pi-chart-bar text-blue-500 mr-2"></i>
Indicateurs Clés de Performance Indicateurs Clés de Performance
</h5> </h5>
<div class="grid"> <div class="formgrid grid">
<ui:repeat value="#{rapportsBean.kpis}" var="kpi" varStatus="status"> <ui:repeat value="#{rapportsBean.kpis}" var="kpi" varStatus="status">
<div class="col-12"> <ui:include src="/templates/components/cards/kpi-card.xhtml">
<div class="surface-100 border-round-lg p-3 mb-3 hover:surface-200 transition-colors transition-duration-150"> <ui:param name="title" value="#{kpi.libelle}" />
<div class="flex align-items-center justify-content-between mb-2"> <ui:param name="value" value="#{kpi.valeur}" />
<div class="flex align-items-center"> <ui:param name="icon" value="#{kpi.icon}" />
<i class="pi #{kpi.icon} text-#{kpi.couleur} mr-2"></i> <ui:param name="iconColor" value="#{kpi.couleur}" />
<span class="text-900 font-medium">#{kpi.libelle}</span> <ui:param name="growthValue" value="#{kpi.variation}" />
</div> <ui:param name="growthLabel" value="variation" />
<div class="text-#{kpi.couleur} font-bold text-xl">#{kpi.valeur}</div> <ui:param name="progressValue" value="#{kpi.progression}" />
</div> <ui:param name="colSize" value="col-12" />
<div class="flex align-items-center justify-content-between"> </ui:include>
<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:repeat> </ui:repeat>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -38,19 +38,23 @@
</ui:include> </ui:include>
<!-- Statistiques membres --> <!-- Statistiques membres -->
<div class="grid"> <div class="formgrid 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="#{rapportsBean.indicateurs.totalMembres}" /> <ui:param name="value" value="#{rapportsBean.indicateurs.totalMembres}" />
<ui:param name="label" value="Total Membres" /> <ui:param name="icon" value="pi-users" />
<ui:param name="icon" value="pi pi-users" /> <ui:param name="iconColor" value="blue-600" />
<ui:param name="bgColor" value="blue" /> <ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
</ui:include> </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="value" value="#{rapportsBean.indicateurs.croissanceMembres}%" />
<ui:param name="label" value="Croissance" /> <ui:param name="icon" value="pi-arrow-up" />
<ui:param name="icon" value="pi pi-arrow-up" /> <ui:param name="iconColor" value="green-600" />
<ui:param name="bgColor" value="green" /> <ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
</ui:include> </ui:include>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,15 +10,34 @@
<ui:fragment rendered="#{empty rendered or rendered}"> <ui:fragment rendered="#{empty rendered or rendered}">
<ui:fragment rendered="#{empty outcome}"> <ui:fragment rendered="#{empty outcome}">
<p:commandButton <ui:fragment rendered="#{not empty update and update != 'none'}">
value="#{value}" <p:commandButton
icon="#{icon}" value="#{value}"
update="#{update}" icon="#{icon}"
onclick="#{onclick}" action="#{action}"
type="button" update="#{update}"
disabled="#{not empty disabled and disabled}" onclick="#{onclick}"
styleClass="ui-button-warning #{not empty outlined and outlined ? 'ui-button-outlined' : ''} #{not empty styleClass ? styleClass : ''}" oncomplete="#{oncomplete}"
title="#{title}" /> 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>
<ui:fragment rendered="#{not empty outcome}"> <ui:fragment rendered="#{not empty outcome}">
<p:button <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_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_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_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_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" url="#" /> <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> </p:submenu>
<!-- Gestion des Organisations --> <!-- Gestion des Organisations -->

View File

@@ -1,14 +1,101 @@
# Configuration UnionFlow Client - Profil Production # Configuration UnionFlow Client - PRODUCTION
# Ce fichier est chargé automatiquement quand le profil 'prod' est actif # Ce fichier est utilisé avec le profil Quarkus "prod"
# Configuration logging pour production # Configuration HTTP
quarkus.log.console.level=WARN 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.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 # PrimeFaces Configuration - Production
%prod.quarkus.oidc.tls.verification=required primefaces.THEME=none
%prod.quarkus.oidc.authentication.redirect-path=/auth/callback 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; package dev.lions.unionflow.server.api.dto.membre;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List; import java.util.List;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
@@ -56,10 +57,12 @@ public class MembreSearchResultDTO {
/** Indique si c'est la première page */ /** Indique si c'est la première page */
@Schema(description = "Indique si c'est la première page") @Schema(description = "Indique si c'est la première page")
@JsonProperty("isFirst")
private boolean isFirst; private boolean isFirst;
/** Indique si c'est la dernière page */ /** Indique si c'est la dernière page */
@Schema(description = "Indique si c'est la dernière page") @Schema(description = "Indique si c'est la dernière page")
@JsonProperty("isLast")
private boolean isLast; private boolean isLast;
/** Critères de recherche utilisés */ /** Critères de recherche utilisés */
@@ -181,19 +184,42 @@ public class MembreSearchResultDTO {
* @return Résultat vide * @return Résultat vide
*/ */
public static MembreSearchResultDTO empty(MembreSearchCriteria criteria) { 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(); MembreSearchResultDTO result = new MembreSearchResultDTO();
result.setMembres(List.of()); result.setMembres(List.of());
result.setTotalElements(0L); result.setTotalElements(0L);
result.setTotalPages(0); result.setTotalPages(0);
result.setCurrentPage(0); result.setCurrentPage(currentPage);
result.setPageSize(20); result.setPageSize(pageSize);
result.setNumberOfElements(0); result.setNumberOfElements(0);
result.setHasNext(false); result.setHasNext(false);
result.setHasPrevious(false); result.setHasPrevious(false);
result.setFirst(true); result.isFirst = true; // Assignation directe pour éviter les problèmes avec les setters Lombok
result.setLast(true); result.isLast = true; // Assignation directe pour éviter les problèmes avec les setters Lombok
result.setCriteria(criteria); result.setCriteria(criteria);
result.setExecutionTimeMs(0L); 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; return result;
} }
} }

View File

@@ -116,6 +116,35 @@
<version>1.18.30</version> <version>1.18.30</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </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 --> <!-- Tests -->
<dependency> <dependency>
@@ -187,6 +216,20 @@
</configuration> </configuration>
</plugin> </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 --> <!-- Jacoco pour la couverture de code -->
<plugin> <plugin>
<groupId>org.jacoco</groupId> <groupId>org.jacoco</groupId>

View File

@@ -78,6 +78,10 @@ public abstract class BaseRepository<T extends BaseEntity> {
*/ */
@Transactional @Transactional
public void delete(T entity) { 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); entityManager.remove(entity);
} }
@@ -90,7 +94,11 @@ public abstract class BaseRepository<T extends BaseEntity> {
public boolean deleteById(UUID id) { public boolean deleteById(UUID id) {
T entity = findById(id); T entity = findById(id);
if (entity != null) { 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 true;
} }
return false; return false;

View File

@@ -15,6 +15,9 @@ import jakarta.validation.Valid;
import jakarta.ws.rs.*; import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; 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.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@@ -357,7 +360,7 @@ public class MembreResource {
public Response searchMembresAdvanced( public Response searchMembresAdvanced(
@RequestBody( @RequestBody(
description = "Critères de recherche avancée", description = "Critères de recherche avancée",
required = true, required = false,
content = content =
@Content( @Content(
mediaType = MediaType.APPLICATION_JSON, mediaType = MediaType.APPLICATION_JSON,
@@ -378,7 +381,6 @@ public class MembreResource {
"includeInactifs": false "includeInactifs": false
} }
"""))) """)))
@Valid
MembreSearchCriteria criteria, MembreSearchCriteria criteria,
@Parameter(description = "Numéro de page (0-based)", example = "0") @Parameter(description = "Numéro de page (0-based)", example = "0")
@QueryParam("page") @QueryParam("page")
@@ -399,18 +401,19 @@ public class MembreResource {
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
LOG.infof(
"Recherche avancée de membres - critères: %s, page: %d, size: %d",
criteria.getDescription(), page, size);
try { try {
// Validation des critères // Validation des critères
if (criteria == null) { if (criteria == null) {
LOG.warn("Recherche avancée de membres - critères null rejetés");
return Response.status(Response.Status.BAD_REQUEST) return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("message", "Les critères de recherche sont requis")) .entity(Map.of("message", "Les critères de recherche sont requis"))
.build(); .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 // Nettoyage et validation des critères
criteria.sanitize(); criteria.sanitize();
@@ -449,6 +452,11 @@ public class MembreResource {
return Response.ok(result).build(); 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) { } catch (IllegalArgumentException e) {
LOG.warnf("Erreur de validation dans la recherche avancée: %s", e.getMessage()); LOG.warnf("Erreur de validation dans la recherche avancée: %s", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST) return Response.status(Response.Status.BAD_REQUEST)
@@ -485,4 +493,151 @@ public class MembreResource {
.build(); .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.PersistenceContext;
import jakarta.persistence.TypedQuery; import jakarta.persistence.TypedQuery;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import java.io.InputStream;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.Period; import java.time.Period;
@@ -34,6 +35,9 @@ public class MembreService {
@Inject MembreRepository membreRepository; @Inject MembreRepository membreRepository;
@Inject
MembreImportExportService membreImportExportService;
@PersistenceContext @PersistenceContext
EntityManager entityManager; EntityManager entityManager;
@@ -341,7 +345,7 @@ public class MembreService {
long totalElements = countQueryTyped.getSingleResult(); long totalElements = countQueryTyped.getSingleResult();
if (totalElements == 0) { if (totalElements == 0) {
return MembreSearchResultDTO.empty(criteria); return MembreSearchResultDTO.empty(criteria, page.size, page.index);
} }
// Ajout du tri et pagination // Ajout du tri et pagination
@@ -464,16 +468,16 @@ public class MembreService {
parameters.put("organisationIds", criteria.getOrganisationIds()); 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()) { if (criteria.getRoles() != null && !criteria.getRoles().isEmpty()) {
StringBuilder roleCondition = new StringBuilder(" AND ("); // Utiliser EXISTS avec une sous-requête pour vérifier les rôles
for (int i = 0; i < criteria.getRoles().size(); i++) { queryBuilder.append(" AND EXISTS (");
if (i > 0) roleCondition.append(" OR "); queryBuilder.append(" SELECT 1 FROM MembreRole mr WHERE mr.membre = m");
roleCondition.append("m.roles LIKE :role").append(i); queryBuilder.append(" AND mr.actif = true");
parameters.put("role" + i, "%" + criteria.getRoles().get(i) + "%"); queryBuilder.append(" AND mr.role.code IN :roleCodes");
} queryBuilder.append(")");
roleCondition.append(")"); // Convertir les noms de rôles en codes (supposant que criteria.getRoles() contient des codes)
queryBuilder.append(roleCondition); parameters.put("roleCodes", criteria.getRoles());
} }
} }
@@ -484,7 +488,10 @@ public class MembreService {
} }
return sort.getColumns().stream() 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(", ")); .collect(Collectors.joining(", "));
} }
@@ -633,4 +640,101 @@ public class MembreService {
return csv.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); 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 # Configuration UnionFlow Server - PRODUCTION
# Ce fichier est chargé automatiquement quand le profil 'prod' est actif # Ce fichier est utilisé avec le profil Quarkus "prod"
# Configuration Hibernate pour production # Configuration HTTP
quarkus.hibernate-orm.database.generation=validate 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.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 # Configuration Flyway - Production (ACTIVÉ)
quarkus.log.console.level=WARN quarkus.flyway.migrate-at-start=true
quarkus.log.category."dev.lions.unionflow".level=INFO quarkus.flyway.baseline-on-migrate=true
quarkus.log.category.root.level=WARN quarkus.flyway.baseline-version=1.0.0
# Configuration Keycloak pour production # Configuration Keycloak OIDC - Production
quarkus.oidc.auth-server-url=${KEYCLOAK_SERVER_URL:http://localhost:8180/realms/unionflow} quarkus.oidc.auth-server-url=${KEYCLOAK_AUTH_SERVER_URL:https://security.lions.dev/realms/unionflow}
quarkus.oidc.client-id=${KEYCLOAK_CLIENT_ID:unionflow-server} quarkus.oidc.client-id=unionflow-server
quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET} quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET}
quarkus.oidc.tls.verification=required 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.db-kind=h2
quarkus.datasource.username=sa quarkus.datasource.username=sa
quarkus.datasource.password= 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 # Configuration Hibernate pour tests
quarkus.hibernate-orm.database.generation=drop-and-create 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.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é) # Configuration Keycloak pour tests (désactivé)
quarkus.oidc.tenant-enabled=false quarkus.oidc.tenant-enabled=false
quarkus.keycloak.policy-enforcer.enable=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 IF;
END $$; END $$;
-- Insertion de données de test pour le développement -- IMPORTANT: Aucune donnée fictive n'est insérée dans ce script de migration.
INSERT INTO organisations ( -- Les données doivent être insérées manuellement via l'interface d'administration
nom, nom_court, type_organisation, statut, description, -- ou via des scripts de migration séparés si nécessaire pour la production.
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'
);
-- Mise à jour des statistiques de la base de données -- Mise à jour des statistiques de la base de données
ANALYZE organisations; 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 -- 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) -- 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 --
-- IMPORTANT: Ce fichier ne doit PAS contenir de données fictives pour la production.
-- Insertion d'organisations initiales avec UUIDs générés -- Les données doivent être insérées manuellement via l'interface d'administration
INSERT INTO organisations (id, nom, nom_court, type_organisation, statut, description, -- ou via des scripts de migration Flyway si nécessaire.
email, telephone, adresse, ville, region, pays, --
objectifs, activites_principales, nombre_membres, -- Ce fichier est laissé vide intentionnellement pour éviter l'insertion automatique
organisation_publique, accepte_nouveaux_membres, cree_par, -- de données fictives lors du démarrage du serveur.
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.

View File

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

View File

@@ -45,6 +45,7 @@ class MembreServiceAdvancedSearchTest {
.nom("Organisation Test") .nom("Organisation Test")
.typeOrganisation("ASSOCIATION") .typeOrganisation("ASSOCIATION")
.statut("ACTIF") .statut("ACTIF")
.email("test@organisation.com")
.build(); .build();
testOrganisation.setDateCreation(LocalDateTime.now()); testOrganisation.setDateCreation(LocalDateTime.now());
testOrganisation.setActif(true); testOrganisation.setActif(true);
@@ -104,13 +105,21 @@ class MembreServiceAdvancedSearchTest {
if (testMembres != null) { if (testMembres != null) {
testMembres.forEach(membre -> { testMembres.forEach(membre -> {
if (membre.getId() != null) { 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) { 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());
});
} }
} }