diff --git a/.gitignore b/.gitignore
index bdac85e..086aad3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -83,3 +83,31 @@ credentials.json
# Quarkus specific
.quarkus/
quarkus.log
+
+# JSF/Faces specific
+**/META-INF/resources/.faces-config.xml.jsfdia
+**/javax.faces.resource/
+
+# PrimeFaces cache
+**/primefaces_resource_cache/
+
+# Node modules (if using npm/webpack for frontend assets)
+node_modules/
+npm-debug.log
+yarn-error.log
+package-lock.json
+yarn.lock
+
+# Static resources compiled
+src/main/resources/META-INF/resources/dist/
+src/main/resources/META-INF/resources/assets/vendor/
+
+# Database files (dev)
+*.db
+*.sqlite
+*.h2.db
+
+# Application config overrides (local dev)
+application-local.properties
+application-dev-override.properties
+*-secret.properties
diff --git a/assign-roles.sh b/assign-roles.sh
deleted file mode 100644
index ebade29..0000000
--- a/assign-roles.sh
+++ /dev/null
@@ -1,64 +0,0 @@
-#!/bin/bash
-
-KEYCLOAK_URL="http://localhost:8180"
-ADMIN_USER="admin"
-ADMIN_PASS="admin"
-REALM_NAME="unionflow"
-USER_ID="4ebcdfef-960e-4dd2-b89c-028129af906d"
-
-echo "🔧 Attribution des rôles à l'utilisateur test..."
-
-# Obtenir le token
-TOKEN=$(curl -s -X POST "$KEYCLOAK_URL/realms/master/protocol/openid-connect/token" \
- -H "Content-Type: application/x-www-form-urlencoded" \
- -d "username=$ADMIN_USER" \
- -d "password=$ADMIN_PASS" \
- -d "grant_type=password" \
- -d "client_id=admin-cli" | grep -o '"access_token":"[^"]*' | cut -d'"' -f4)
-
-if [ -z "$TOKEN" ]; then
- echo "❌ Impossible d'obtenir le token"
- exit 1
-fi
-
-# Récupérer les rôles
-ROLES_JSON=$(curl -s -X GET "$KEYCLOAK_URL/admin/realms/$REALM_NAME/roles" \
- -H "Authorization: Bearer $TOKEN")
-
-# Extraire les IDs des rôles MEMBRE et ADMIN_ENTITE
-ROLE_MEMBRE_ID=$(echo "$ROLES_JSON" | grep -o '"id":"[^"]*","name":"MEMBRE"' | grep -o '"id":"[^"]*' | cut -d'"' -f4)
-ROLE_ADMIN_ID=$(echo "$ROLES_JSON" | grep -o '"id":"[^"]*","name":"ADMIN_ENTITE"' | grep -o '"id":"[^"]*' | cut -d'"' -f4)
-
-echo "MEMBRE ID: $ROLE_MEMBRE_ID"
-echo "ADMIN_ENTITE ID: $ROLE_ADMIN_ID"
-
-if [ -n "$ROLE_MEMBRE_ID" ]; then
- curl -s -X POST "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users/$USER_ID/role-mappings/realm" \
- -H "Authorization: Bearer $TOKEN" \
- -H "Content-Type: application/json" \
- -d "[{\"id\":\"$ROLE_MEMBRE_ID\",\"name\":\"MEMBRE\"}]" > /dev/null 2>&1
- echo "✅ Rôle MEMBRE assigné"
-fi
-
-if [ -n "$ROLE_ADMIN_ID" ]; then
- curl -s -X POST "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users/$USER_ID/role-mappings/realm" \
- -H "Authorization: Bearer $TOKEN" \
- -H "Content-Type: application/json" \
- -d "[{\"id\":\"$ROLE_ADMIN_ID\",\"name\":\"ADMIN_ENTITE\"}]" > /dev/null 2>&1
- echo "✅ Rôle ADMIN_ENTITE assigné"
-fi
-
-echo ""
-echo "======================================================== "
-echo "✅ Configuration terminée!"
-echo "========================================================"
-echo ""
-echo "📋 Identifiants de connexion:"
-echo " - Username: test@unionflow.dev"
-echo " - Password: test123"
-echo ""
-echo "🚀 Prochaines étapes:"
-echo " 1. Lancez l'application: ./start-local.sh"
-echo " 2. Accédez à: http://localhost:8086"
-echo " 3. Connectez-vous avec les identifiants ci-dessus"
-echo ""
diff --git a/clients.json b/clients.json
deleted file mode 100644
index edc479d..0000000
--- a/clients.json
+++ /dev/null
@@ -1 +0,0 @@
-{"error":"HTTP 401 Unauthorized"}
\ No newline at end of file
diff --git a/Dockerfile b/docker/Dockerfile
similarity index 93%
rename from Dockerfile
rename to docker/Dockerfile
index f21b6b1..5d12bc3 100644
--- a/Dockerfile
+++ b/docker/Dockerfile
@@ -9,7 +9,7 @@ ENV LANGUAGE='en_US:en'
# Configuration Quarkus
ENV QUARKUS_PROFILE=prod
-ENV QUARKUS_HTTP_PORT=8080
+ENV QUARKUS_HTTP_PORT=8086
ENV QUARKUS_HTTP_HOST=0.0.0.0
# Configuration Backend UnionFlow
@@ -39,8 +39,8 @@ COPY --chown=appuser:appuser target/quarkus-app/quarkus/ /app/quarkus/
USER appuser
-# Exposer le port 8080
-EXPOSE 8080
+# Exposer le port 8086
+EXPOSE 8086
# Variables JVM optimisées
ENV JAVA_OPTS="-Xmx1g -Xms512m \
@@ -60,4 +60,4 @@ ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/quarkus-run.jar"]
# Health check sur le bon port
HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 \
- CMD curl -f http://localhost:8080/q/health/ready || exit 1
+ CMD curl -f http://localhost:8086/q/health/ready || exit 1
diff --git a/Dockerfile.prod b/docker/Dockerfile.prod
similarity index 95%
rename from Dockerfile.prod
rename to docker/Dockerfile.prod
index e029475..249d1b7 100644
--- a/Dockerfile.prod
+++ b/docker/Dockerfile.prod
@@ -31,12 +31,13 @@ ENV QUARKUS_PROFILE=prod
ENV QUARKUS_HTTP_PORT=8086
ENV QUARKUS_HTTP_HOST=0.0.0.0
-# Configuration Keycloak/OIDC (production)
+# Configuration Keycloak OIDC (production)
ENV QUARKUS_OIDC_AUTH_SERVER_URL=https://security.lions.dev/realms/unionflow
ENV QUARKUS_OIDC_CLIENT_ID=unionflow-client
ENV QUARKUS_OIDC_ENABLED=true
ENV QUARKUS_OIDC_TLS_VERIFICATION=required
-ENV KEYCLOAK_CLIENT_SECRET=changeme
+# KEYCLOAK_CLIENT_SECRET MUST be injected via Kubernetes Secret at runtime
+ENV KEYCLOAK_CLIENT_SECRET=
# Configuration API Backend
ENV UNIONFLOW_BACKEND_URL=https://api.lions.dev/unionflow
diff --git a/keycloak-config.sh b/keycloak-config.sh
deleted file mode 100644
index 66c419a..0000000
--- a/keycloak-config.sh
+++ /dev/null
@@ -1,208 +0,0 @@
-#!/bin/bash
-
-# Script complet de configuration Keycloak
-
-KEYCLOAK_URL="http://localhost:8180"
-ADMIN_USER="admin"
-ADMIN_PASS="admin"
-REALM_NAME="unionflow"
-CLIENT_ID="unionflow-client"
-
-echo "🔧 Configuration automatique de Keycloak..."
-echo ""
-
-# Obtenir le token
-echo "1. Obtention du token admin..."
-TOKEN_RESPONSE=$(curl -s -X POST "$KEYCLOAK_URL/realms/master/protocol/openid-connect/token" \
- -H "Content-Type: application/x-www-form-urlencoded" \
- -d "username=$ADMIN_USER" \
- -d "password=$ADMIN_PASS" \
- -d "grant_type=password" \
- -d "client_id=admin-cli")
-
-TOKEN=$(echo "$TOKEN_RESPONSE" | grep -o '"access_token":"[^"]*' | cut -d'"' -f4)
-
-if [ -z "$TOKEN" ]; then
- echo "❌ Impossible d'obtenir le token admin"
- exit 1
-fi
-echo "✅ Token obtenu"
-
-# Créer le realm (ignore si existe déjà)
-echo ""
-echo "2. Création du realm..."
-curl -s -X POST "$KEYCLOAK_URL/admin/realms" \
- -H "Authorization: Bearer $TOKEN" \
- -H "Content-Type: application/json" \
- -d "{\"realm\":\"$REALM_NAME\",\"enabled\":true,\"displayName\":\"UnionFlow\"}" > /dev/null 2>&1
-echo "✅ Realm vérifié"
-
-# Créer les rôles
-echo ""
-echo "3. Création des rôles..."
-curl -s -X POST "$KEYCLOAK_URL/admin/realms/$REALM_NAME/roles" \
- -H "Authorization: Bearer $TOKEN" \
- -H "Content-Type: application/json" \
- -d '{"name":"SUPER_ADMIN","description":"Super admin"}' > /dev/null 2>&1
-
-curl -s -X POST "$KEYCLOAK_URL/admin/realms/$REALM_NAME/roles" \
- -H "Authorization: Bearer $TOKEN" \
- -H "Content-Type: application/json" \
- -d '{"name":"ADMIN_ENTITE","description":"Admin entite"}' > /dev/null 2>&1
-
-curl -s -X POST "$KEYCLOAK_URL/admin/realms/$REALM_NAME/roles" \
- -H "Authorization: Bearer $TOKEN" \
- -H "Content-Type: application/json" \
- -d '{"name":"MEMBRE","description":"Membre"}' > /dev/null 2>&1
-
-curl -s -X POST "$KEYCLOAK_URL/admin/realms/$REALM_NAME/roles" \
- -H "Authorization: Bearer $TOKEN" \
- -H "Content-Type: application/json" \
- -d '{"name":"GESTIONNAIRE_MEMBRE","description":"Gestionnaire"}' > /dev/null 2>&1
-echo "✅ Rôles vérifiés"
-
-# Créer le client
-echo ""
-echo "4. Création du client..."
-curl -s -X POST "$KEYCLOAK_URL/admin/realms/$REALM_NAME/clients" \
- -H "Authorization: Bearer $TOKEN" \
- -H "Content-Type: application/json" \
- -d "{\"clientId\":\"$CLIENT_ID\",\"enabled\":true,\"protocol\":\"openid-connect\",\"publicClient\":false,\"directAccessGrantsEnabled\":true,\"standardFlowEnabled\":true,\"implicitFlowEnabled\":false,\"rootUrl\":\"http://localhost:8086\",\"redirectUris\":[\"http://localhost:8086/*\"],\"webOrigins\":[\"http://localhost:8086\"],\"attributes\":{\"post.logout.redirect.uris\":\"http://localhost:8086/*\"}}" > /dev/null 2>&1
-echo "✅ Client vérifié"
-
-# Récupérer l'UUID du client
-echo ""
-echo "5. Récupération du client UUID..."
-curl -s -X GET "$KEYCLOAK_URL/admin/realms/$REALM_NAME/clients" \
- -H "Authorization: Bearer $TOKEN" > clients_temp.json
-
-# Sauvegarder dans un fichier pour debug
-cat clients_temp.json > clients_debug.json
-
-# Extraire seulement l'entrée du client unionflow-client
-# On cherche la ligne complète qui contient notre client
-CLIENT_UUID=$(cat clients_temp.json | tr ',' '\n' | grep -A 10 "\"clientId\":\"$CLIENT_ID\"" | grep "\"id\":" | head -1 | grep -o '"[a-f0-9-]*"' | tr -d '"')
-
-if [ -z "$CLIENT_UUID" ]; then
- echo "❌ Impossible de trouver le client UUID"
- echo "Contenu du fichier (premiers 500 caractères):"
- head -c 500 clients_debug.json
- exit 1
-fi
-echo "✅ Client UUID: $CLIENT_UUID"
-
-# Récupérer le client secret
-echo ""
-echo "6. Récupération du client secret..."
-SECRET_JSON=$(curl -s -X GET "$KEYCLOAK_URL/admin/realms/$REALM_NAME/clients/$CLIENT_UUID/client-secret" \
- -H "Authorization: Bearer $TOKEN")
-
-CLIENT_SECRET=$(echo "$SECRET_JSON" | grep -o '"value":"[^"]*' | cut -d'"' -f4)
-
-if [ -z "$CLIENT_SECRET" ]; then
- echo "❌ Impossible de récupérer le client secret"
- echo "Contenu reçu: $SECRET_JSON"
- exit 1
-fi
-echo "✅ Client Secret: $CLIENT_SECRET"
-
-# Configurer le mapper de rôles
-echo ""
-echo "7. Configuration du mapper de rôles..."
-SCOPES_JSON=$(curl -s -X GET "$KEYCLOAK_URL/admin/realms/$REALM_NAME/clients/$CLIENT_UUID/default-client-scopes" \
- -H "Authorization: Bearer $TOKEN")
-
-SCOPE_ID=$(echo "$SCOPES_JSON" | grep -o '"id":"[^"]*"' | grep -A5 "dedicated" | head -1 | cut -d'"' -f4)
-
-if [ -n "$SCOPE_ID" ]; then
- curl -s -X POST "$KEYCLOAK_URL/admin/realms/$REALM_NAME/client-scopes/$SCOPE_ID/protocol-mappers/models" \
- -H "Authorization: Bearer $TOKEN" \
- -H "Content-Type: application/json" \
- -d '{"name":"realm-roles","protocol":"openid-connect","protocolMapper":"oidc-usermodel-realm-role-mapper","config":{"multivalued":"true","userinfo.token.claim":"true","id.token.claim":"true","access.token.claim":"true","claim.name":"roles","jsonType.label":"String"}}' > /dev/null 2>&1
- echo "✅ Mapper configuré"
-else
- echo "⚠️ Scope non trouvé, mapper à configurer manuellement"
-fi
-
-# Créer l'utilisateur test
-echo ""
-echo "8. Création de l'utilisateur test..."
-curl -s -X POST "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users" \
- -H "Authorization: Bearer $TOKEN" \
- -H "Content-Type: application/json" \
- -d '{"username":"test@unionflow.dev","email":"test@unionflow.dev","firstName":"Test","lastName":"User","enabled":true,"emailVerified":true}' > /dev/null 2>&1
-
-# Récupérer l'ID de l'utilisateur
-USER_JSON=$(curl -s -X GET "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users?username=test@unionflow.dev" \
- -H "Authorization: Bearer $TOKEN")
-
-USER_ID=$(echo "$USER_JSON" | grep -o '"id":"[^"]*' | head -1 | cut -d'"' -f4)
-
-if [ -n "$USER_ID" ]; then
- # Définir le mot de passe
- curl -s -X PUT "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users/$USER_ID/reset-password" \
- -H "Authorization: Bearer $TOKEN" \
- -H "Content-Type: application/json" \
- -d '{"type":"password","value":"test123","temporary":false}' > /dev/null 2>&1
- echo "✅ Utilisateur créé (test@unionflow.dev / test123)"
-
- # Récupérer et assigner les rôles
- ROLES_JSON=$(curl -s -X GET "$KEYCLOAK_URL/admin/realms/$REALM_NAME/roles" \
- -H "Authorization: Bearer $TOKEN")
-
- ROLE_MEMBRE=$(echo "$ROLES_JSON" | grep -B2 '"name":"MEMBRE"' | grep '"id"' | grep -o '"id":"[^"]*' | cut -d'"' -f4)
- ROLE_ADMIN=$(echo "$ROLES_JSON" | grep -B2 '"name":"ADMIN_ENTITE"' | grep '"id"' | grep -o '"id":"[^"]*' | cut -d'"' -f4)
-
- if [ -n "$ROLE_MEMBRE" ]; then
- curl -s -X POST "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users/$USER_ID/role-mappings/realm" \
- -H "Authorization: Bearer $TOKEN" \
- -H "Content-Type: application/json" \
- -d "[{\"id\":\"$ROLE_MEMBRE\",\"name\":\"MEMBRE\"}]" > /dev/null 2>&1
- echo " ✅ Rôle MEMBRE assigné"
- fi
-
- if [ -n "$ROLE_ADMIN" ]; then
- curl -s -X POST "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users/$USER_ID/role-mappings/realm" \
- -H "Authorization: Bearer $TOKEN" \
- -H "Content-Type: application/json" \
- -d "[{\"id\":\"$ROLE_ADMIN\",\"name\":\"ADMIN_ENTITE\"}]" > /dev/null 2>&1
- echo " ✅ Rôle ADMIN_ENTITE assigné"
- fi
-else
- echo "⚠️ Utilisateur non trouvé"
-fi
-
-# Sauvegarder dans .env
-echo ""
-echo "9. Sauvegarde de la configuration..."
-cat > .env << EOF
-# Configuration Keycloak générée automatiquement
-# Date: $(date)
-
-KEYCLOAK_CLIENT_SECRET=$CLIENT_SECRET
-UNIONFLOW_BACKEND_URL=http://localhost:8085
-
-# Informations de connexion pour tests
-# Username: test@unionflow.dev
-# Password: test123
-EOF
-
-echo "✅ Fichier .env créé"
-
-# Résumé
-echo ""
-echo "========================================================"
-echo "✅ Configuration terminée avec succès!"
-echo "========================================================"
-echo ""
-echo "📋 Résumé:"
-echo " - Realm: $REALM_NAME"
-echo " - Client ID: $CLIENT_ID"
-echo " - Client Secret: $CLIENT_SECRET"
-echo " - Utilisateur: test@unionflow.dev / test123"
-echo ""
-echo "🚀 Prochaines étapes:"
-echo " 1. Lancez: ./start-local.sh (ou start-local.bat)"
-echo " 2. Accédez à: http://localhost:8086"
-echo " 3. Connectez-vous avec test@unionflow.dev / test123"
-echo ""
diff --git a/roles.json b/roles.json
deleted file mode 100644
index edc479d..0000000
--- a/roles.json
+++ /dev/null
@@ -1 +0,0 @@
-{"error":"HTTP 401 Unauthorized"}
\ No newline at end of file
diff --git a/scopes.json b/scopes.json
deleted file mode 100644
index 2eeb330..0000000
--- a/scopes.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"id":"ca43f64e-d864-48c9-b969-834468690fbb","name":"web-origins"},{"id":"79b0a5da-b22c-4f42-82e1-17ca3e845e98","name":"acr"},{"id":"630b7e04-b7a8-487e-ab4e-8ef569f2ee30","name":"profile"},{"id":"9706160c-2b0c-4308-af92-b363d9f0d461","name":"roles"},{"id":"eb2f9842-0bba-45b1-9ffa-60b621937d6a","name":"basic"},{"id":"459abd14-dc0c-49d9-8248-445731115816","name":"email"}]
\ No newline at end of file
diff --git a/src/main/java/dev/lions/unionflow/client/api/dto/MembreDashboardResponse.java b/src/main/java/dev/lions/unionflow/client/api/dto/MembreDashboardResponse.java
new file mode 100644
index 0000000..eefab52
--- /dev/null
+++ b/src/main/java/dev/lions/unionflow/client/api/dto/MembreDashboardResponse.java
@@ -0,0 +1,35 @@
+package dev.lions.unionflow.client.api.dto;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+/**
+ * DTO received from the backend for the member dashboard synthesis.
+ */
+public record MembreDashboardResponse(
+ String prenom,
+ String nom,
+ LocalDate dateInscription,
+
+ // Cotisations
+ BigDecimal mesCotisationsPaiement,
+ String statutCotisations,
+ Integer tauxCotisationsPerso,
+
+ // Epargne
+ BigDecimal monSoldeEpargne,
+ BigDecimal evolutionEpargneNombre,
+ String evolutionEpargne,
+ Integer objectifEpargne,
+
+ // Evenements
+ Integer mesEvenementsInscrits,
+ Integer evenementsAVenir,
+ Integer tauxParticipationPerso,
+
+ // Aides
+ Integer mesDemandesAide,
+ Integer aidesEnCours,
+ Integer tauxAidesApprouvees) implements Serializable {
+}
diff --git a/src/main/java/dev/lions/unionflow/client/bean/MenuBean.java b/src/main/java/dev/lions/unionflow/client/bean/MenuBean.java
index a2dce22..513a7ef 100644
--- a/src/main/java/dev/lions/unionflow/client/bean/MenuBean.java
+++ b/src/main/java/dev/lions/unionflow/client/bean/MenuBean.java
@@ -130,12 +130,22 @@ public class MenuBean implements Serializable {
/**
* Annuaire des Membres - Consultation de la liste (pas de modification)
- * Visible à partir de MEMBRE_ACTIF (pour créer du lien social)
+ * Visible pour les responsables et bureau SEULEMENT (PAS pour MEMBRE_ACTIF)
+ *
+ * Raison métier: Un membre simple n'a généralement pas besoin de voir la liste complète
+ * des autres membres. Cela peut poser des problèmes de:
+ * - RGPD: Exposition non justifiée de données personnelles
+ * - Sécurité: Risque de spam/phishing entre membres
+ * - UX: Surcharge du menu pour un usage limité
+ *
+ * Si l'organisation souhaite activer l'annuaire pour MEMBRE_ACTIF, cela doit être
+ * fait via configuration explicite (future Phase 3).
*/
public boolean isAnnuaireMembresVisible() {
return hasAnyRole("SUPER_ADMIN", "ADMIN_ORGANISATION", "SECRETAIRE", "TRESORIER",
"RESPONSABLE_SOCIAL", "RESPONSABLE_EVENEMENTS", "RESPONSABLE_CREDIT",
- "MEMBRE_BUREAU", "MEMBRE_ACTIF");
+ "MEMBRE_BUREAU");
+ // MEMBRE_ACTIF retiré intentionnellement pour raisons UX et RGPD
}
/**
diff --git a/src/main/java/dev/lions/unionflow/client/bean/PageSecurityBean.java b/src/main/java/dev/lions/unionflow/client/bean/PageSecurityBean.java
new file mode 100644
index 0000000..514efdc
--- /dev/null
+++ b/src/main/java/dev/lions/unionflow/client/bean/PageSecurityBean.java
@@ -0,0 +1,179 @@
+package dev.lions.unionflow.client.bean;
+
+import io.quarkus.security.identity.SecurityIdentity;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.faces.context.FacesContext;
+import jakarta.inject.Inject;
+import jakarta.inject.Named;
+import org.jboss.logging.Logger;
+
+import java.io.IOException;
+
+/**
+ * Bean centralisé pour la sécurisation des pages basée sur les rôles.
+ * Fournit des méthodes réutilisables pour vérifier l'accès et rediriger si nécessaire.
+ *
+ *
Principe DRY/WOU : Une seule implémentation de la logique de sécurité,
+ * réutilisée par toutes les pages via un composant Facelet.
+ *
+ * @author UnionFlow Team
+ * @version 1.0
+ * @since 2026-03-02
+ */
+@Named("pageSecurityBean")
+@ApplicationScoped
+public class PageSecurityBean {
+
+ private static final Logger LOG = Logger.getLogger(PageSecurityBean.class);
+ private static final String ACCESS_DENIED_PAGE = "/pages/secure/access-denied.xhtml";
+
+ @Inject
+ SecurityIdentity securityIdentity;
+
+ @Inject
+ MenuBean menuBean;
+
+ /**
+ * Vérifie si l'utilisateur a le droit d'accéder à une page donnée.
+ * Si non autorisé, redirige vers la page access-denied.
+ *
+ * @param allowedRoles Rôles autorisés séparés par des virgules (ex: "ADMIN,TRESORIER")
+ * @return true si autorisé, false sinon (après redirection)
+ */
+ public boolean checkAccessOrRedirect(String allowedRoles) {
+ if (allowedRoles == null || allowedRoles.trim().isEmpty()) {
+ // Aucune restriction = accès autorisé pour tous les utilisateurs authentifiés
+ return !securityIdentity.isAnonymous();
+ }
+
+ String[] roles = allowedRoles.split(",");
+ boolean hasAccess = false;
+
+ for (String role : roles) {
+ String trimmedRole = role.trim();
+ if (hasRole(trimmedRole)) {
+ hasAccess = true;
+ break;
+ }
+ }
+
+ if (!hasAccess) {
+ LOG.warnf("Accès refusé pour l'utilisateur %s à une page nécessitant les rôles: %s",
+ securityIdentity.getPrincipal().getName(), allowedRoles);
+ redirectToAccessDenied();
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Vérifie si l'utilisateur possède un rôle spécifique.
+ *
+ * @param role Le rôle à vérifier
+ * @return true si l'utilisateur a ce rôle
+ */
+ private boolean hasRole(String role) {
+ return switch (role) {
+ case "SUPER_ADMIN" -> menuBean.isSuperAdmin();
+ case "ADMIN_ORGANISATION", "ADMIN" -> menuBean.isAdminOrganisation() || menuBean.isSuperAdmin();
+ case "TRESORIER" -> menuBean.isTresorier() || menuBean.isAdminOrganisation() || menuBean.isSuperAdmin();
+ case "SECRETAIRE" -> menuBean.isSecretaire() || menuBean.isAdminOrganisation() || menuBean.isSuperAdmin();
+ case "RESPONSABLE_SOCIAL" -> menuBean.isResponsableSocial() || menuBean.isAdminOrganisation() || menuBean.isSuperAdmin();
+ case "RESPONSABLE_EVENEMENTS" -> menuBean.isResponsableEvenements() || menuBean.isAdminOrganisation() || menuBean.isSuperAdmin();
+ case "RESPONSABLE_CREDIT" -> menuBean.isResponsableCredit() || menuBean.isAdminOrganisation() || menuBean.isSuperAdmin();
+ case "MEMBRE_BUREAU" -> menuBean.isMembreBureau() || menuBean.isAdminOrganisation() || menuBean.isSuperAdmin();
+ case "MEMBRE_ACTIF" -> menuBean.isMembreActif() || menuBean.isMembreBureau() || menuBean.isAdminOrganisation() || menuBean.isSuperAdmin();
+ case "MEMBRE_SIMPLE" -> menuBean.isMembreSimple() || menuBean.isMembreActif() || menuBean.isMembreBureau() || menuBean.isAdminOrganisation() || menuBean.isSuperAdmin();
+ case "ALL" -> !securityIdentity.isAnonymous(); // Tous les utilisateurs authentifiés
+ default -> {
+ LOG.warnf("Rôle inconnu: %s", role);
+ yield false;
+ }
+ };
+ }
+
+ /**
+ * Redirige vers la page d'accès refusé.
+ */
+ private void redirectToAccessDenied() {
+ try {
+ FacesContext ctx = FacesContext.getCurrentInstance();
+ if (ctx != null && !ctx.getResponseComplete()) {
+ String contextPath = ctx.getExternalContext().getRequestContextPath();
+ ctx.getExternalContext().redirect(contextPath + ACCESS_DENIED_PAGE);
+ ctx.responseComplete();
+ }
+ } catch (IOException e) {
+ LOG.error("Erreur lors de la redirection vers access-denied", e);
+ }
+ }
+
+ // ═══════════════════════════════════════════════════════════════════════
+ // Méthodes helper pour vérifications rapides (utilisées dans les pages)
+ // ═══════════════════════════════════════════════════════════════════════
+
+ /**
+ * Vérifie si l'utilisateur peut gérer les membres.
+ * @return true si SECRETAIRE, ADMIN, ou SUPER_ADMIN
+ */
+ public boolean canManageMembers() {
+ return hasRole("SECRETAIRE");
+ }
+
+ /**
+ * Vérifie si l'utilisateur peut gérer les finances.
+ * @return true si TRESORIER, ADMIN, ou SUPER_ADMIN
+ */
+ public boolean canManageFinances() {
+ return hasRole("TRESORIER");
+ }
+
+ /**
+ * Vérifie si l'utilisateur peut gérer les événements.
+ * @return true si RESPONSABLE_EVENEMENTS, SECRETAIRE, ADMIN, ou SUPER_ADMIN
+ */
+ public boolean canManageEvents() {
+ return hasRole("RESPONSABLE_EVENEMENTS");
+ }
+
+ /**
+ * Vérifie si l'utilisateur peut gérer les aides sociales.
+ * @return true si RESPONSABLE_SOCIAL, ADMIN, ou SUPER_ADMIN
+ */
+ public boolean canManageSocialAid() {
+ return hasRole("RESPONSABLE_SOCIAL");
+ }
+
+ /**
+ * Vérifie si l'utilisateur peut voir les rapports financiers.
+ * @return true si TRESORIER, SECRETAIRE, ADMIN, ou SUPER_ADMIN
+ */
+ public boolean canViewFinancialReports() {
+ return hasRole("TRESORIER") || hasRole("SECRETAIRE");
+ }
+
+ /**
+ * Vérifie si l'utilisateur peut exporter des données.
+ * @return true si TRESORIER, SECRETAIRE, ADMIN, ou SUPER_ADMIN
+ */
+ public boolean canExportData() {
+ return hasRole("TRESORIER") || hasRole("SECRETAIRE");
+ }
+
+ /**
+ * Vérifie si l'utilisateur est un simple membre (MEMBRE_ACTIF uniquement).
+ * @return true si MEMBRE_ACTIF mais pas d'autre rôle administratif
+ */
+ public boolean isSimpleMember() {
+ return menuBean.isMembreActif() &&
+ !menuBean.isSecretaire() &&
+ !menuBean.isTresorier() &&
+ !menuBean.isResponsableSocial() &&
+ !menuBean.isResponsableEvenements() &&
+ !menuBean.isResponsableCredit() &&
+ !menuBean.isMembreBureau() &&
+ !menuBean.isAdminOrganisation() &&
+ !menuBean.isSuperAdmin();
+ }
+}
diff --git a/src/main/java/dev/lions/unionflow/client/converter/UuidConverter.java b/src/main/java/dev/lions/unionflow/client/converter/UuidConverter.java
new file mode 100644
index 0000000..f890da8
--- /dev/null
+++ b/src/main/java/dev/lions/unionflow/client/converter/UuidConverter.java
@@ -0,0 +1,37 @@
+package dev.lions.unionflow.client.converter;
+
+import jakarta.faces.component.UIComponent;
+import jakarta.faces.context.FacesContext;
+import jakarta.faces.convert.Converter;
+import jakarta.faces.convert.ConverterException;
+import jakarta.faces.convert.FacesConverter;
+
+import java.util.UUID;
+
+/**
+ * Convertisseur JSF pour les paramètres de vue et champs liés à {@link UUID}.
+ * Permet la conversion String ↔ UUID dans les f:viewParam et composants d'entrée.
+ */
+@FacesConverter(value = "uuidConverter", managed = true)
+public class UuidConverter implements Converter {
+
+ @Override
+ public UUID getAsObject(FacesContext context, UIComponent component, String value) {
+ if (value == null || value.isBlank()) {
+ return null;
+ }
+ try {
+ return UUID.fromString(value.trim());
+ } catch (IllegalArgumentException e) {
+ throw new ConverterException("Identifiant invalide : " + value, e);
+ }
+ }
+
+ @Override
+ public String getAsString(FacesContext context, UIComponent component, UUID value) {
+ if (value == null) {
+ return "";
+ }
+ return value.toString();
+ }
+}
diff --git a/src/main/java/dev/lions/unionflow/client/exception/ViewExpiredExceptionHandler.java b/src/main/java/dev/lions/unionflow/client/exception/ViewExpiredExceptionHandler.java
index ec6b383..c183704 100644
--- a/src/main/java/dev/lions/unionflow/client/exception/ViewExpiredExceptionHandler.java
+++ b/src/main/java/dev/lions/unionflow/client/exception/ViewExpiredExceptionHandler.java
@@ -1,5 +1,6 @@
package dev.lions.unionflow.client.exception;
+import jakarta.el.PropertyNotFoundException;
import jakarta.faces.FacesException;
import jakarta.faces.application.ViewExpiredException;
import jakarta.faces.context.ExceptionHandler;
@@ -12,28 +13,57 @@ import java.util.logging.Level;
import java.util.logging.Logger;
public class ViewExpiredExceptionHandler extends ExceptionHandlerWrapper {
-
+
private static final Logger LOG = Logger.getLogger(ViewExpiredExceptionHandler.class.getName());
private ExceptionHandler wrapped;
-
+
public ViewExpiredExceptionHandler(ExceptionHandler wrapped) {
this.wrapped = wrapped;
}
-
+
@Override
public ExceptionHandler getWrapped() {
return wrapped;
}
-
+
+ private static boolean isPropertyNotFound(Throwable t) {
+ for (Throwable x = t; x != null; x = x.getCause()) {
+ if (x instanceof PropertyNotFoundException) return true;
+ }
+ return false;
+ }
+
@Override
public void handle() throws FacesException {
Iterator iterator = getUnhandledExceptionQueuedEvents().iterator();
-
+
while (iterator.hasNext()) {
ExceptionQueuedEvent event = iterator.next();
ExceptionQueuedEventContext context = (ExceptionQueuedEventContext) event.getSource();
Throwable throwable = context.getException();
-
+
+ if (isPropertyNotFound(throwable)) {
+ LOG.log(Level.WARNING, "PropertyNotFoundException EL (évite page d''erreur TreeMap): {0}",
+ throwable.getMessage());
+ try {
+ FacesContext fc = FacesContext.getCurrentInstance();
+ if (fc != null && fc.getExternalContext() != null && !fc.getResponseComplete()) {
+ fc.getExternalContext().redirect(
+ fc.getExternalContext().getRequestContextPath() + "/pages/secure/organisation/liste.xhtml");
+ fc.responseComplete();
+ }
+ } catch (Exception e) {
+ String msg = e != null ? e.getMessage() : "";
+ if (msg != null && (msg.contains("already commited") || msg.contains("already committed"))) {
+ LOG.log(Level.WARNING, "Redirection impossible (réponse déjà envoyée): {0}", msg);
+ } else {
+ LOG.log(Level.SEVERE, "Redirection après PropertyNotFoundException: {0}", msg);
+ }
+ }
+ iterator.remove();
+ continue;
+ }
+
if (throwable instanceof ViewExpiredException) {
ViewExpiredException vee = (ViewExpiredException) throwable;
FacesContext facesContext = FacesContext.getCurrentInstance();
diff --git a/src/main/java/dev/lions/unionflow/client/service/AssociationService.java b/src/main/java/dev/lions/unionflow/client/service/AssociationService.java
index 3dbbc9e..163bdf0 100644
--- a/src/main/java/dev/lions/unionflow/client/service/AssociationService.java
+++ b/src/main/java/dev/lions/unionflow/client/service/AssociationService.java
@@ -1,5 +1,6 @@
package dev.lions.unionflow.client.service;
+import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@@ -16,41 +17,17 @@ import java.util.UUID;
public interface AssociationService {
@GET
- PagedResponseDTO listerToutes(
+ PagedResponse listerToutes(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("1000") int size);
- class PagedResponseDTO {
- public List data;
- public Long total;
- public Integer page;
- public Integer size;
- public Integer totalPages;
-
- public List getData() {
- return data;
- }
-
- public void setData(List data) {
- this.data = data;
- }
-
- public Long getTotal() {
- return total;
- }
-
- public void setTotal(Long total) {
- this.total = total;
- }
- }
-
@GET
@Path("/{id}")
OrganisationResponse obtenirParId(@PathParam("id") UUID id);
@GET
@Path("/recherche")
- PagedResponseDTO rechercher(
+ PagedResponse rechercher(
@QueryParam("nom") String nom,
@QueryParam("type") String type,
@QueryParam("statut") String statut,
diff --git a/src/main/java/dev/lions/unionflow/client/service/EvenementService.java b/src/main/java/dev/lions/unionflow/client/service/EvenementService.java
index 5fdfade..2ab73b3 100644
--- a/src/main/java/dev/lions/unionflow/client/service/EvenementService.java
+++ b/src/main/java/dev/lions/unionflow/client/service/EvenementService.java
@@ -1,5 +1,6 @@
package dev.lions.unionflow.client.service;
+import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.evenement.response.EvenementResponse;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@@ -27,7 +28,7 @@ public interface EvenementService {
* Liste tous les événements actifs avec pagination
*/
@GET
- Map listerTous(
+ PagedResponse listerTous(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size,
@QueryParam("sort") @DefaultValue("dateDebut") String sortField,
@@ -66,17 +67,17 @@ public interface EvenementService {
*/
@GET
@Path("/a-venir")
- Map listerAVenir(
+ PagedResponse listerAVenir(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("10") int size
);
-
+
/**
* Recherche d'événements avec filtres
*/
@GET
@Path("/search")
- Map rechercher(
+ PagedResponse rechercher(
@QueryParam("titre") String titre,
@QueryParam("type") String type,
@QueryParam("statut") String statut,
@@ -85,24 +86,24 @@ public interface EvenementService {
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size
);
-
+
/**
* Liste les événements par statut
*/
@GET
@Path("/statut/{statut}")
- Map listerParStatut(
+ PagedResponse listerParStatut(
@PathParam("statut") String statut,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size
);
-
+
/**
* Liste les événements par association
*/
@GET
@Path("/association/{associationId}")
- Map listerParAssociation(
+ PagedResponse listerParAssociation(
@PathParam("associationId") UUID associationId,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size
diff --git a/src/main/java/dev/lions/unionflow/client/service/MembreDashboardRestClient.java b/src/main/java/dev/lions/unionflow/client/service/MembreDashboardRestClient.java
new file mode 100644
index 0000000..779ab0f
--- /dev/null
+++ b/src/main/java/dev/lions/unionflow/client/service/MembreDashboardRestClient.java
@@ -0,0 +1,23 @@
+package dev.lions.unionflow.client.service;
+
+import dev.lions.unionflow.client.api.dto.MembreDashboardResponse;
+import dev.lions.unionflow.client.security.AuthHeaderFactory;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
+import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
+
+@RegisterRestClient(configKey = "unionflow-api")
+@RegisterClientHeaders(AuthHeaderFactory.class)
+@Path("/api/dashboard/membre")
+@Consumes(MediaType.APPLICATION_JSON)
+@Produces(MediaType.APPLICATION_JSON)
+public interface MembreDashboardRestClient {
+
+ @GET
+ @Path("/me")
+ MembreDashboardResponse getMonDashboard();
+}
diff --git a/src/main/java/dev/lions/unionflow/client/service/MembreService.java b/src/main/java/dev/lions/unionflow/client/service/MembreService.java
index db97681..d63a986 100644
--- a/src/main/java/dev/lions/unionflow/client/service/MembreService.java
+++ b/src/main/java/dev/lions/unionflow/client/service/MembreService.java
@@ -1,5 +1,6 @@
package dev.lions.unionflow.client.service;
+import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.membre.response.MembreResponse;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@@ -16,7 +17,7 @@ import java.util.UUID;
public interface MembreService {
@GET
- List listerTous();
+ PagedResponse listerTous();
@GET
@Path("/{id}")
@@ -26,6 +27,10 @@ public interface MembreService {
@Path("/numero/{numeroMembre}")
MembreResponse obtenirParNumero(@PathParam("numeroMembre") String numeroMembre);
+ @GET
+ @Path("/me")
+ MembreResponse obtenirMembreConnecte();
+
@GET
@Path("/search")
List rechercher(
diff --git a/src/main/java/dev/lions/unionflow/client/view/AdhesionsBean.java b/src/main/java/dev/lions/unionflow/client/view/AdhesionsBean.java
index 45ae445..e3e6656 100644
--- a/src/main/java/dev/lions/unionflow/client/view/AdhesionsBean.java
+++ b/src/main/java/dev/lions/unionflow/client/view/AdhesionsBean.java
@@ -13,9 +13,13 @@ import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
import dev.lions.unionflow.server.api.dto.membre.response.MembreResponse;
+import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse;
+import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.finance.request.*;
+import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.finance.response.*;
+import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.client.service.AdhesionService;
import dev.lions.unionflow.client.service.AssociationService;
import dev.lions.unionflow.client.service.ErrorHandlerService;
@@ -109,7 +113,7 @@ public class AdhesionsBean implements Serializable {
}
try {
- AssociationService.PagedResponseDTO response = retryService.executeWithRetrySupplier(
+ PagedResponse response = retryService.executeWithRetrySupplier(
() -> associationService.listerToutes(0, 1000),
"chargement des associations");
listeAssociations = (response != null && response.getData() != null) ? response.getData()
diff --git a/src/main/java/dev/lions/unionflow/client/view/CotisationsGestionBean.java b/src/main/java/dev/lions/unionflow/client/view/CotisationsGestionBean.java
index 3fcb39b..6118581 100644
--- a/src/main/java/dev/lions/unionflow/client/view/CotisationsGestionBean.java
+++ b/src/main/java/dev/lions/unionflow/client/view/CotisationsGestionBean.java
@@ -1,10 +1,15 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.server.api.dto.cotisation.request.CreateCotisationRequest;
+import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationResponse;
+import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.membre.response.MembreResponse;
+import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse;
+import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.paiement.WaveCheckoutSessionDTO;
+import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.client.service.CotisationService;
import dev.lions.unionflow.client.service.AssociationService;
import dev.lions.unionflow.client.service.MembreService;
@@ -304,7 +309,7 @@ public class CotisationsGestionBean implements Serializable {
this.filtres = new FiltresCotisations();
this.listeOrganisations = new ArrayList<>();
try {
- AssociationService.PagedResponseDTO response = associationService.listerToutes(0, 1000);
+ PagedResponse response = associationService.listerToutes(0, 1000);
if (response != null && response.getData() != null) {
for (OrganisationResponse assoc : response.getData()) {
Organisation org = new Organisation();
@@ -340,7 +345,7 @@ public class CotisationsGestionBean implements Serializable {
private void chargerTopOrganisations() {
this.topOrganisations = new ArrayList<>();
try {
- AssociationService.PagedResponseDTO response = associationService.listerToutes(0, 1000);
+ PagedResponse response = associationService.listerToutes(0, 1000);
List associations = response != null && response.getData() != null ? response.getData()
: new ArrayList<>();
List cotisationsDTO = cotisationService.listerToutes(0, 1000);
diff --git a/src/main/java/dev/lions/unionflow/client/view/DashboardBean.java b/src/main/java/dev/lions/unionflow/client/view/DashboardBean.java
index 85a39d9..5aaea6d 100644
--- a/src/main/java/dev/lions/unionflow/client/view/DashboardBean.java
+++ b/src/main/java/dev/lions/unionflow/client/view/DashboardBean.java
@@ -8,10 +8,12 @@ import dev.lions.unionflow.server.api.dto.dashboard.DashboardStatsResponse;
import dev.lions.unionflow.server.api.dto.dashboard.RecentActivityResponse;
import dev.lions.unionflow.server.api.dto.dashboard.UpcomingEventResponse;
import jakarta.annotation.PostConstruct;
+import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.eclipse.microprofile.rest.client.inject.RestClient;
+import java.io.IOException;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
@@ -51,10 +53,13 @@ public class DashboardBean implements Serializable {
@Inject
private UserSession userSession;
-
+
+ @Inject
+ private dev.lions.unionflow.client.bean.MenuBean menuBean;
+
@Inject
ErrorHandlerService errorHandler;
-
+
@Inject
RetryService retryService;
@@ -134,8 +139,30 @@ public class DashboardBean implements Serializable {
@PostConstruct
public void init() {
+ // Charger les données pour les rôles administratifs
chargerDonneesBackend();
}
+
+ /**
+ * Méthode appelée par f:viewAction pour rediriger les MEMBRE_ACTIF.
+ * S'exécute AVANT le rendu de la page (phase INVOKE_APPLICATION).
+ */
+ public void checkAccessAndRedirect() {
+ if (menuBean != null && menuBean.isMembreActif() &&
+ !menuBean.isSecretaire() && !menuBean.isTresorier() &&
+ !menuBean.isResponsableSocial() && !menuBean.isResponsableEvenements() &&
+ !menuBean.isAdminOrganisation() && !menuBean.isSuperAdmin()) {
+ try {
+ FacesContext ctx = FacesContext.getCurrentInstance();
+ String redirectUrl = ctx.getExternalContext().getRequestContextPath() +
+ "/pages/secure/dashboard-membre.xhtml?faces-redirect=true";
+ ctx.getExternalContext().redirect(redirectUrl);
+ ctx.responseComplete();
+ } catch (IOException e) {
+ LOG.error("Erreur lors de la redirection vers dashboard-membre", e);
+ }
+ }
+ }
/**
* Charge toutes les données depuis le service Dashboard (DRY/WOU - un seul appel)
diff --git a/src/main/java/dev/lions/unionflow/client/view/DashboardMembreBean.java b/src/main/java/dev/lions/unionflow/client/view/DashboardMembreBean.java
new file mode 100644
index 0000000..b10a8ab
--- /dev/null
+++ b/src/main/java/dev/lions/unionflow/client/view/DashboardMembreBean.java
@@ -0,0 +1,386 @@
+package dev.lions.unionflow.client.view;
+
+import dev.lions.unionflow.server.api.dto.dashboard.UpcomingEventResponse;
+import dev.lions.unionflow.server.api.dto.evenement.response.EvenementResponse;
+import dev.lions.unionflow.client.api.dto.MembreDashboardResponse;
+import dev.lions.unionflow.client.service.MembreDashboardRestClient;
+import dev.lions.unionflow.client.service.ErrorHandlerService;
+import dev.lions.unionflow.client.service.RetryService;
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+import io.quarkus.security.identity.SecurityIdentity;
+import jakarta.annotation.PostConstruct;
+import jakarta.faces.view.ViewScoped;
+import jakarta.inject.Inject;
+import jakarta.inject.Named;
+import org.jboss.logging.Logger;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Bean de gestion du dashboard personnel pour les membres actifs
+ * (MEMBRE_ACTIF).
+ * Affiche uniquement les données personnelles du membre connecté, pas les
+ * statistiques globales.
+ *
+ * @author UnionFlow Team
+ * @version 1.0
+ * @since 2026-03-02
+ */
+@Named("dashboardMembreBean")
+@ViewScoped
+public class DashboardMembreBean implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+ private static final Logger LOG = Logger.getLogger(DashboardMembreBean.class);
+
+ @Inject
+ SecurityIdentity securityIdentity;
+
+ @Inject
+ ErrorHandlerService errorHandler;
+
+ @Inject
+ RetryService retryService;
+
+ @Inject
+ @RestClient
+ MembreDashboardRestClient dashboardClient;
+
+ // Informations personnelles du membre
+ private String prenomMembre;
+ private String nomMembre;
+ private LocalDate dateInscription;
+
+ // KPI personnels - TOUTES LES VALEURS DOIVENT ÊTRE CALCULÉES DEPUIS LES DONNÉES
+ // RÉELLES
+ // IMPORTANT: Ces valeurs par défaut (0, "", null) sont TEMPORAIRES en attendant
+ // l'implémentation des endpoints REST
+ // Une fois les endpoints implémentés, ces valeurs seront REMPLACÉES par les
+ // données calculées depuis PostgreSQL
+
+ // Cotisations
+ private String mesCotisationsPaiement = "0"; // TEMPORAIRE - Sera remplacé par le montant réel depuis API
+ private String statutCotisations = "Non disponible"; // TEMPORAIRE - Sera remplacé par "À jour"/"En retard" depuis
+ // API
+ private Integer tauxCotisationsPerso = null; // null = pas de jauge affichée en attendant les données réelles
+
+ // Épargne
+ private String monSoldeEpargne = "0"; // TEMPORAIRE - Sera remplacé par le solde réel depuis API
+ private String evolutionEpargne = "+0%"; // TEMPORAIRE - Sera remplacé par l'évolution réelle depuis API
+ private String evolutionEpargneNombre = "0"; // TEMPORAIRE - Sera remplacé par l'évolution en FCFA depuis API
+ private Integer objectifEpargne = null; // null = pas de jauge affichée en attendant les données réelles
+
+ // Événements
+ private Integer mesEvenementsInscrits = 0; // TEMPORAIRE - Sera remplacé par le nombre réel depuis API
+ private Integer evenementsAVenir = 0; // TEMPORAIRE - Sera remplacé par le nombre réel depuis API
+ private Integer tauxParticipationPerso = null; // null = pas de jauge affichée en attendant les données réelles
+
+ // Aides
+ private Integer mesDemandesAide = 0; // TEMPORAIRE - Sera remplacé par le nombre réel depuis API
+ private Integer aidesEnCours = 0; // TEMPORAIRE - Sera remplacé par le nombre réel depuis API
+ private Integer tauxAidesApprouvees = null; // null = pas de jauge affichée en attendant les données réelles
+
+ // Collections
+ private List historiqueCotisations = new ArrayList<>();
+ private List mesNotifications = new ArrayList<>();
+ private List evenementsPublics = new ArrayList<>();
+
+ @PostConstruct
+ public void init() {
+ LOG.info("Initialisation du dashboard personnel membre");
+ chargerDonneesPersonnelles();
+ }
+
+ /**
+ * Charge les données personnelles du membre connecté depuis les endpoints REST.
+ * Les données de synthèse sont récupérées via l'API membre/me.
+ */
+ private void chargerDonneesPersonnelles() {
+ try {
+ LOG.info("Chargement des données du dashboard depuis l'API...");
+ MembreDashboardResponse data = dashboardClient.getMonDashboard();
+
+ if (data != null) {
+ this.prenomMembre = data.prenom();
+ this.nomMembre = data.nom();
+ this.dateInscription = data.dateInscription();
+
+ this.mesCotisationsPaiement = formatMontant(data.mesCotisationsPaiement());
+ this.statutCotisations = data.statutCotisations() != null ? data.statutCotisations() : "Non disponible";
+ this.tauxCotisationsPerso = data.tauxCotisationsPerso();
+
+ this.monSoldeEpargne = formatMontant(data.monSoldeEpargne());
+ this.evolutionEpargneNombre = formatMontant(data.evolutionEpargneNombre());
+ this.evolutionEpargne = data.evolutionEpargne() != null ? data.evolutionEpargne() : "+0%";
+ this.objectifEpargne = data.objectifEpargne();
+
+ this.mesEvenementsInscrits = data.mesEvenementsInscrits() != null ? data.mesEvenementsInscrits() : 0;
+ this.evenementsAVenir = data.evenementsAVenir() != null ? data.evenementsAVenir() : 0;
+ this.tauxParticipationPerso = data.tauxParticipationPerso();
+
+ this.mesDemandesAide = data.mesDemandesAide() != null ? data.mesDemandesAide() : 0;
+ this.aidesEnCours = data.aidesEnCours() != null ? data.aidesEnCours() : 0;
+ this.tauxAidesApprouvees = data.tauxAidesApprouvees();
+ }
+
+ // Pour l'historique et événements, on mock en attendant les endpoints détaillés
+ // si nécessaires
+ // ou on laissera vide vu que le dashboard principal est fonctionnel avec les
+ // KPI
+ historiqueCotisations = new ArrayList<>();
+ mesNotifications = new ArrayList<>();
+ evenementsPublics = new ArrayList<>();
+
+ } catch (Exception e) {
+ LOG.error("Erreur lors du chargement des données de synthèse du dashboard", e);
+ errorHandler.handleException(e, "lors du chargement de votre dashboard", null);
+ }
+ }
+
+ private String formatMontant(BigDecimal montant) {
+ if (montant == null)
+ return "0";
+ // Format simple, on pourrait rajouter des espaces pour les milliers
+ return String.format("%,d", montant.longValue()).replace(',', ' ');
+ }
+
+ // ═══════════════════════════════════════════════════════════════════════
+ // Actions
+ // ═══════════════════════════════════════════════════════════════════════
+
+ public void payerCotisation() {
+ try {
+ // TODO: Rediriger vers la page de paiement des cotisations
+ LOG.info("Redirection vers paiement cotisation");
+ } catch (Exception e) {
+ errorHandler.handleException(e, "lors de la redirection", null);
+ }
+ }
+
+ public void inscrireEvenement() {
+ try {
+ // TODO: Rediriger vers /pages/secure/evenement/calendrier.xhtml
+ // Liste des événements publics où le membre peut s'inscrire
+ LOG.info("Redirection vers calendrier des événements disponibles");
+ } catch (Exception e) {
+ errorHandler.handleException(e, "lors de la redirection", null);
+ }
+ }
+
+ public void demanderAide() {
+ try {
+ // TODO: Rediriger vers le formulaire de demande d'aide
+ LOG.info("Redirection vers demande d'aide");
+ } catch (Exception e) {
+ errorHandler.handleException(e, "lors de la redirection", null);
+ }
+ }
+
+ public void allerAMonProfil() {
+ try {
+ // TODO: Rediriger vers le profil personnel
+ LOG.info("Redirection vers mon profil");
+ } catch (Exception e) {
+ errorHandler.handleException(e, "lors de la redirection", null);
+ }
+ }
+
+ public void allerAuxEvenements() {
+ try {
+ // TODO: Rediriger vers la liste complète des événements
+ LOG.info("Redirection vers liste événements");
+ } catch (Exception e) {
+ errorHandler.handleException(e, "lors de la redirection", null);
+ }
+ }
+
+ public void inscrireAEvenement(String evenementId) {
+ try {
+ // TODO: Appeler API pour inscrire le membre à l'événement
+ LOG.infof("Inscription à l'événement %s", evenementId);
+ errorHandler.showSuccess("Inscription confirmée", "Vous êtes inscrit à cet événement");
+ } catch (Exception e) {
+ errorHandler.handleException(e, "lors de l'inscription à l'événement", null);
+ }
+ }
+
+ // ═══════════════════════════════════════════════════════════════════════
+ // Helpers
+ // ═══════════════════════════════════════════════════════════════════════
+
+ private String extractPrenomFromUsername(String username) {
+ // Extraction basique depuis le username en attendant l'API
+ if (username != null && username.contains("@")) {
+ return username.split("@")[0];
+ }
+ return username != null ? username : "Membre";
+ }
+
+ private String extractNomFromUsername(String username) {
+ // TODO: Appeler GET /api/membres/mon-profil pour récupérer le nom complet
+ return "";
+ }
+
+ // ═══════════════════════════════════════════════════════════════════════
+ // DTOs internes
+ // ═══════════════════════════════════════════════════════════════════════
+
+ public static class CotisationPerso implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private LocalDate datePaiement;
+ private LocalDate periode;
+ private String montant;
+ private String modePaiement;
+ private String statut;
+
+ public CotisationPerso(LocalDate datePaiement, LocalDate periode, String montant,
+ String modePaiement, String statut) {
+ this.datePaiement = datePaiement;
+ this.periode = periode;
+ this.montant = montant;
+ this.modePaiement = modePaiement;
+ this.statut = statut;
+ }
+
+ // Getters
+ public LocalDate getDatePaiement() {
+ return datePaiement;
+ }
+
+ public LocalDate getPeriode() {
+ return periode;
+ }
+
+ public String getMontant() {
+ return montant;
+ }
+
+ public String getModePaiement() {
+ return modePaiement;
+ }
+
+ public String getStatut() {
+ return statut;
+ }
+ }
+
+ public static class NotificationPerso implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private String icon;
+ private String titre;
+ private String message;
+ private LocalDateTime date;
+
+ public NotificationPerso(String icon, String titre, String message, LocalDateTime date) {
+ this.icon = icon;
+ this.titre = titre;
+ this.message = message;
+ this.date = date;
+ }
+
+ // Getters
+ public String getIcon() {
+ return icon;
+ }
+
+ public String getTitre() {
+ return titre;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public LocalDateTime getDate() {
+ return date;
+ }
+ }
+
+ // ═══════════════════════════════════════════════════════════════════════
+ // Getters pour JSF
+ // ═══════════════════════════════════════════════════════════════════════
+
+ public String getPrenomMembre() {
+ return prenomMembre;
+ }
+
+ public String getNomMembre() {
+ return nomMembre;
+ }
+
+ public LocalDate getDateInscription() {
+ return dateInscription;
+ }
+
+ public String getMesCotisationsPaiement() {
+ return mesCotisationsPaiement;
+ }
+
+ public String getStatutCotisations() {
+ return statutCotisations;
+ }
+
+ public Integer getTauxCotisationsPerso() {
+ return tauxCotisationsPerso;
+ }
+
+ public String getMonSoldeEpargne() {
+ return monSoldeEpargne;
+ }
+
+ public String getEvolutionEpargne() {
+ return evolutionEpargne;
+ }
+
+ public String getEvolutionEpargneNombre() {
+ return evolutionEpargneNombre;
+ }
+
+ public Integer getObjectifEpargne() {
+ return objectifEpargne;
+ }
+
+ public Integer getMesEvenementsInscrits() {
+ return mesEvenementsInscrits;
+ }
+
+ public Integer getEvenementsAVenir() {
+ return evenementsAVenir;
+ }
+
+ public Integer getTauxParticipationPerso() {
+ return tauxParticipationPerso;
+ }
+
+ public Integer getMesDemandesAide() {
+ return mesDemandesAide;
+ }
+
+ public Integer getAidesEnCours() {
+ return aidesEnCours;
+ }
+
+ public Integer getTauxAidesApprouvees() {
+ return tauxAidesApprouvees;
+ }
+
+ public List getHistoriqueCotisations() {
+ return historiqueCotisations;
+ }
+
+ public List getMesNotifications() {
+ return mesNotifications;
+ }
+
+ public List getEvenementsPublics() {
+ return evenementsPublics;
+ }
+}
diff --git a/src/main/java/dev/lions/unionflow/client/view/DemandesAideBean.java b/src/main/java/dev/lions/unionflow/client/view/DemandesAideBean.java
index 469d2ff..5ff754d 100644
--- a/src/main/java/dev/lions/unionflow/client/view/DemandesAideBean.java
+++ b/src/main/java/dev/lions/unionflow/client/view/DemandesAideBean.java
@@ -109,7 +109,7 @@ public class DemandesAideBean implements Serializable {
try {
// Charger toutes les demandes depuis le backend pour calculer les étapes
- List demandesDTO = demandeAideService.listerToutes(0, 10000);
+ List demandesDTO = demandeAideService.listerToutes(0, 1000);
// Calculer le nombre de demandes par statut depuis les données réelles
long enAttenteCount = demandesDTO.stream().filter(d -> StatutAide.EN_ATTENTE.equals(d.getStatut())).count();
diff --git a/src/main/java/dev/lions/unionflow/client/view/DemandesBean.java b/src/main/java/dev/lions/unionflow/client/view/DemandesBean.java
index e9a9b51..babe657 100644
--- a/src/main/java/dev/lions/unionflow/client/view/DemandesBean.java
+++ b/src/main/java/dev/lions/unionflow/client/view/DemandesBean.java
@@ -65,7 +65,7 @@ public class DemandesBean implements Serializable {
private void initializeDemandes() {
demandes = new ArrayList<>();
try {
- List dtos = demandeAideService.listerToutes(0, 10000);
+ List dtos = demandeAideService.listerToutes(0, 1000);
if (dtos != null) {
for (DemandeAideResponse dto : dtos) {
demandes.add(mapToDemande(dto));
diff --git a/src/main/java/dev/lions/unionflow/client/view/EntitesGestionBean.java b/src/main/java/dev/lions/unionflow/client/view/EntitesGestionBean.java
index 6bd3440..571e8a3 100644
--- a/src/main/java/dev/lions/unionflow/client/view/EntitesGestionBean.java
+++ b/src/main/java/dev/lions/unionflow/client/view/EntitesGestionBean.java
@@ -1,7 +1,9 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse;
+import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.abonnement.response.AbonnementResponse;
+import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.enums.abonnement.StatutAbonnement;
import dev.lions.unionflow.client.service.AssociationService;
import dev.lions.unionflow.client.service.CotisationService;
@@ -105,7 +107,7 @@ public class EntitesGestionBean implements Serializable {
private void initializeStatistiques() {
statistiques = new Statistiques();
try {
- AssociationService.PagedResponseDTO response = associationService.listerToutes(0, 1000);
+ PagedResponse response = associationService.listerToutes(0, 1000);
List associations = new ArrayList<>();
if (response != null && response.getData() != null) {
associations = response.getData();
@@ -138,7 +140,7 @@ public class EntitesGestionBean implements Serializable {
private void initializeEntites() {
toutesLesEntites = new ArrayList<>();
try {
- AssociationService.PagedResponseDTO response = associationService.listerToutes(0, 1000);
+ PagedResponse response = associationService.listerToutes(0, 1000);
if (response != null && response.getData() != null) {
for (OrganisationResponse dto : response.getData()) {
Entite entite = convertToEntite(dto);
diff --git a/src/main/java/dev/lions/unionflow/client/view/EvenementsBean.java b/src/main/java/dev/lions/unionflow/client/view/EvenementsBean.java
index 0535b5a..b093712 100644
--- a/src/main/java/dev/lions/unionflow/client/view/EvenementsBean.java
+++ b/src/main/java/dev/lions/unionflow/client/view/EvenementsBean.java
@@ -114,44 +114,16 @@ public class EvenementsBean implements Serializable {
public void chargerEvenements() {
try {
LOG.info("Chargement des événements depuis le backend");
- Map response = retryService.executeWithRetrySupplier(
+ var response = retryService.executeWithRetrySupplier(
() -> evenementService.listerTous(0, 1000, "dateDebut", "asc"),
"chargement de tous les événements"
);
-
+
tousLesEvenements = new ArrayList<>();
-
- // Le backend peut retourner soit une liste de DTOs, soit une Map avec "data"
- if (response.containsKey("data")) {
- @SuppressWarnings("unchecked")
- List