diff --git a/k8s/patch-unionflow-server-secrets.yaml b/k8s/patch-unionflow-server-secrets.yaml new file mode 100644 index 0000000..87a1737 --- /dev/null +++ b/k8s/patch-unionflow-server-secrets.yaml @@ -0,0 +1,17 @@ +# Patch pour ajouter les secrets SMTP et Wave au deployment UnionFlow Server +# Usage: kubectl patch deployment unionflow-server-impl-quarkus -n applications --patch-file patch-unionflow-server-secrets.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: unionflow-server-impl-quarkus + namespace: applications +spec: + template: + spec: + containers: + - name: unionflow-server-impl-quarkus + envFrom: + - secretRef: + name: unionflow-smtp-secret + - secretRef: + name: unionflow-wave-secret diff --git a/k8s/unionflow-smtp-secret.yaml b/k8s/unionflow-smtp-secret.yaml new file mode 100644 index 0000000..7aeb5b0 --- /dev/null +++ b/k8s/unionflow-smtp-secret.yaml @@ -0,0 +1,16 @@ +# Secret K8s pour la configuration SMTP d'UnionFlow +# Usage: kubectl apply -f unionflow-smtp-secret.yaml -n applications +# IMPORTANT: Remplacer les valeurs avant d'appliquer +apiVersion: v1 +kind: Secret +metadata: + name: unionflow-smtp-secret + namespace: applications +type: Opaque +stringData: + QUARKUS_MAILER_HOST: "smtp.lions.dev" + QUARKUS_MAILER_PORT: "587" + QUARKUS_MAILER_USERNAME: "" + QUARKUS_MAILER_PASSWORD: "" + QUARKUS_MAILER_FROM: "noreply@lions.dev" + QUARKUS_MAILER_STARTTLS: "REQUIRED" diff --git a/k8s/unionflow-wave-secret.yaml b/k8s/unionflow-wave-secret.yaml new file mode 100644 index 0000000..5f3f993 --- /dev/null +++ b/k8s/unionflow-wave-secret.yaml @@ -0,0 +1,13 @@ +# Secret K8s pour l'intégration Wave Money d'UnionFlow +# Usage: kubectl apply -f unionflow-wave-secret.yaml -n applications +# IMPORTANT: Remplacer les valeurs avant d'appliquer +apiVersion: v1 +kind: Secret +metadata: + name: unionflow-wave-secret + namespace: applications +type: Opaque +stringData: + WAVE_API_KEY: "" + WAVE_API_SECRET: "" + WAVE_WEBHOOK_SECRET: "" diff --git a/setup-keycloak.sh b/setup-keycloak.sh index 34f5a15..2ad6897 100644 --- a/setup-keycloak.sh +++ b/setup-keycloak.sh @@ -154,8 +154,34 @@ fi echo "✅ Client Secret récupéré: $CLIENT_SECRET" echo "" -# Étape 6: Configurer le client scope mapper pour les rôles -echo "📝 Étape 6/7: Configuration du mapper de rôles..." +# Étape 6: Ajouter l'audience mapper pour unionflow-server +echo "📝 Étape 6/8: Ajout de l'audience mapper 'unionflow-server'..." +HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + "$KEYCLOAK_URL/admin/realms/$REALM_NAME/clients/$CLIENT_UUID/protocol-mappers/models" \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "unionflow-server-audience", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-mapper", + "consentRequired": false, + "config": { + "included.client.audience": "unionflow-server", + "id.token.claim": "false", + "access.token.claim": "true" + } + }') +if [ "$HTTP_STATUS" = "201" ]; then + echo "✅ Audience mapper 'unionflow-server' ajouté" +elif [ "$HTTP_STATUS" = "409" ]; then + echo "⚠️ Audience mapper 'unionflow-server-audience' existe déjà" +else + echo "⚠️ Échec ajout audience mapper (HTTP ${HTTP_STATUS}) — à configurer manuellement" +fi +echo "" + +# Étape 7: Configurer le client scope mapper pour les rôles +echo "📝 Étape 7/8: Configuration du mapper de rôles..." SCOPES=$(curl -s -X GET "$KEYCLOAK_URL/admin/realms/$REALM_NAME/clients/$CLIENT_UUID/default-client-scopes" \ -H "Authorization: Bearer $ADMIN_TOKEN") @@ -177,8 +203,8 @@ else fi echo "" -# Étape 7: Créer un utilisateur test -echo "📝 Étape 7/7: Création de l'utilisateur test..." +# Étape 8: Créer un utilisateur test +echo "📝 Étape 8/8: Création de l'utilisateur test..." curl -s -X POST "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users" \ -H "Authorization: Bearer $ADMIN_TOKEN" \ 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 513a7ef..fb54b23 100644 --- a/src/main/java/dev/lions/unionflow/client/bean/MenuBean.java +++ b/src/main/java/dev/lions/unionflow/client/bean/MenuBean.java @@ -1,5 +1,6 @@ package dev.lions.unionflow.client.bean; +import dev.lions.unionflow.client.view.UserSession; import jakarta.enterprise.context.SessionScoped; import jakarta.inject.Named; import io.quarkus.security.identity.SecurityIdentity; @@ -30,6 +31,9 @@ public class MenuBean implements Serializable { @Inject SecurityIdentity securityIdentity; + @Inject + UserSession userSession; + /** * Vérifie si l'utilisateur a au moins un des rôles spécifiés. * @@ -548,4 +552,117 @@ public class MenuBean implements Serializable { public boolean isMembreSimple() { return hasAnyRole("MEMBRE_SIMPLE"); } + + // ======================================================================== + // MENUS PAR MODULE (conditionnels selon le type d'organisation active) + // Chaque méthode combine : rôle requis + module actif dans l'org active. + // Un SUPERADMIN voit tout sans restriction de module. + // ======================================================================== + + /** + * Vérifie si un module est actif pour l'organisation courante. + * Les SUPER_ADMIN voient tous les modules. + */ + private boolean hasModule(String module) { + if (hasAnyRole("SUPER_ADMIN")) return true; + if (userSession == null) return false; + return userSession.hasModuleActif(module); + } + + /** Menu Tontine — visible si module TONTINE actif + rôle adéquat. */ + public boolean isTontineMenuVisible() { + return hasModule("TONTINE") && + hasAnyRole("SUPER_ADMIN", "ADMIN_ORGANISATION", "TRESORIER", + "TONTINE_RESP", "TONTINE_MANAGER", "TONTINE_COLLECTOR"); + } + + /** Menu Tontine (membres) — consultation de ses propres tontines. */ + public boolean isTontineMemberVisible() { + return hasModule("TONTINE") && !securityIdentity.isAnonymous(); + } + + /** Menu Épargne — visible si module EPARGNE actif. */ + public boolean isEpargneMenuVisible() { + return hasModule("EPARGNE") && + hasAnyRole("SUPER_ADMIN", "ADMIN_ORGANISATION", "TRESORIER", + "MUTUELLE_RESP", "EPARGNE_MANAGER"); + } + + /** Menu Épargne (membres). */ + public boolean isEpargneMemberVisible() { + return hasModule("EPARGNE") && !securityIdentity.isAnonymous(); + } + + /** Menu Crédit — visible si module CREDIT actif. */ + public boolean isCreditMenuVisible() { + return hasModule("CREDIT") && + hasAnyRole("SUPER_ADMIN", "ADMIN_ORGANISATION", "TRESORIER", + "MUTUELLE_RESP", "CREDIT_ANALYSTE", "RESPONSABLE_CREDIT"); + } + + /** Menu Crédit (membres). */ + public boolean isCreditMemberVisible() { + return hasModule("CREDIT") && !securityIdentity.isAnonymous(); + } + + /** Menu Agriculture / Campagnes — visible si module AGRICULTURE actif. */ + public boolean isAgricoleMenuVisible() { + return hasModule("AGRICULTURE") && + hasAnyRole("SUPER_ADMIN", "ADMIN_ORGANISATION", "COOP_RESP"); + } + + /** Menu Agriculture (membres). */ + public boolean isAgricoleMemberVisible() { + return hasModule("AGRICULTURE") && !securityIdentity.isAnonymous(); + } + + /** Menu Collecte de fonds — visible si module COLLECTE_FONDS actif. */ + public boolean isCollecteFondsMenuVisible() { + return hasModule("COLLECTE_FONDS") && + hasAnyRole("SUPER_ADMIN", "ADMIN_ORGANISATION", "COLLECTE_RESP"); + } + + /** Menu Projets ONG — visible si module PROJETS_ONG actif. */ + public boolean isProjetOngMenuVisible() { + return hasModule("PROJETS_ONG") && + hasAnyRole("SUPER_ADMIN", "ADMIN_ORGANISATION", "ONG_RESP", "PROJET_MANAGER"); + } + + /** Menu Culte / Dons religieux — visible si module CULTE_DONS actif. */ + public boolean isCulteMenuVisible() { + return hasModule("CULTE_DONS") && + hasAnyRole("SUPER_ADMIN", "ADMIN_ORGANISATION", "CULTE_RESP", "PASTEUR", "DIACRE"); + } + + /** Menu Vote — visible si module VOTES actif. */ + public boolean isVoteMenuVisible() { + return hasModule("VOTES") && + hasAnyRole("SUPER_ADMIN", "ADMIN_ORGANISATION", "VOTE_RESP", "SCRUTATEUR"); + } + + /** Menu Registre / Agrément — visible si module REGISTRE_AGREMENT actif. */ + public boolean isRegistreAgrementMenuVisible() { + return hasModule("REGISTRE_AGREMENT") && + hasAnyRole("SUPER_ADMIN", "ADMIN_ORGANISATION", "REGISTRE_RESP"); + } + + /** Menu Gouvernance / Organigramme — visible si module GOUVERNANCE actif. */ + public boolean isGouvernanceMenuVisible() { + return hasModule("GOUVERNANCE") && + hasAnyRole("SUPER_ADMIN", "ADMIN_ORGANISATION"); + } + + /** + * Retourne true si l'organisation active dispose d'au moins un module métier spécifique + * (au-delà des modules communs toujours disponibles). + * Utilisé pour afficher/masquer la section "Modules métier" dans la navigation. + */ + public boolean hasAnyBusinessModule() { + if (hasAnyRole("SUPER_ADMIN")) return true; + return isTontineMenuVisible() || isCreditMenuVisible() || isEpargneMenuVisible() + || isAgricoleMenuVisible() || isCollecteFondsMenuVisible() + || isProjetOngMenuVisible() || isCulteMenuVisible() + || isVoteMenuVisible() || isRegistreAgrementMenuVisible() + || isGouvernanceMenuVisible(); + } } diff --git a/src/main/java/dev/lions/unionflow/client/interceptor/LogBackendCall.java b/src/main/java/dev/lions/unionflow/client/interceptor/LogBackendCall.java index eb5095a..422eaba 100644 --- a/src/main/java/dev/lions/unionflow/client/interceptor/LogBackendCall.java +++ b/src/main/java/dev/lions/unionflow/client/interceptor/LogBackendCall.java @@ -19,9 +19,9 @@ import jakarta.interceptor.InterceptorBinding; *

Usage: *

{@code
  * @LogBackendCall
- * public interface AssociationService {
+ * public interface OrganisationService {
  *     @LogBackendCall
- *     OrganisationResponse creer(OrganisationResponse association);
+ *     OrganisationResponse creer(OrganisationResponse organisation);
  * }
  * }
* diff --git a/src/main/java/dev/lions/unionflow/client/security/AuthHeaderFactory.java b/src/main/java/dev/lions/unionflow/client/security/AuthHeaderFactory.java index ebce05d..d93e807 100644 --- a/src/main/java/dev/lions/unionflow/client/security/AuthHeaderFactory.java +++ b/src/main/java/dev/lions/unionflow/client/security/AuthHeaderFactory.java @@ -1,5 +1,6 @@ package dev.lions.unionflow.client.security; +import dev.lions.unionflow.client.view.UserSession; import io.quarkus.oidc.AccessTokenCredential; import io.quarkus.oidc.IdToken; import io.quarkus.security.identity.SecurityIdentity; @@ -10,6 +11,7 @@ import jakarta.ws.rs.core.MultivaluedMap; import org.eclipse.microprofile.jwt.JsonWebToken; import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory; +import java.util.UUID; import java.util.logging.Logger; @ApplicationScoped @@ -17,6 +19,8 @@ public class AuthHeaderFactory implements ClientHeadersFactory { private static final Logger LOGGER = Logger.getLogger(AuthHeaderFactory.class.getName()); + public static final String HEADER_ACTIVE_ORG = "X-Active-Organisation-Id"; + @Inject SecurityIdentity securityIdentity; @@ -24,6 +28,9 @@ public class AuthHeaderFactory implements ClientHeadersFactory { @IdToken JsonWebToken idToken; + @Inject + UserSession userSession; + @Override public MultivaluedMap update( MultivaluedMap incomingHeaders, @@ -36,19 +43,34 @@ public class AuthHeaderFactory implements ClientHeadersFactory { if (credential != null && credential.getToken() != null && !credential.getToken().isEmpty()) { result.add("Authorization", "Bearer " + credential.getToken()); LOGGER.fine("Access token Bearer ajouté via AccessTokenCredential"); - return result; - } - String rawIdToken = idToken.getRawToken(); - if (rawIdToken != null && !rawIdToken.isEmpty()) { - result.add("Authorization", "Bearer " + rawIdToken); - LOGGER.warning("Fallback sur l'ID token — l'access token n'était pas disponible"); } else { - LOGGER.warning("Aucun token disponible pour la requête REST Client"); + String rawIdToken = idToken.getRawToken(); + if (rawIdToken != null && !rawIdToken.isEmpty()) { + result.add("Authorization", "Bearer " + rawIdToken); + LOGGER.warning("Fallback sur l'ID token — l'access token n'était pas disponible"); + } else { + LOGGER.warning("Aucun token disponible pour la requête REST Client"); + } } } catch (Exception e) { LOGGER.severe("Erreur lors de l'ajout du token Bearer: " + e.getMessage()); } + // Injecter l'organisation active si définie en session. + // Guard : ne pas résoudre le proxy CDI si UserSession est encore en cours d'initialisation + // sur ce thread (évite StackOverflowError par réentrance CDI dans @PostConstruct). + try { + if (userSession != null && !Boolean.TRUE.equals(UserSession.INITIALIZING_ON_THREAD.get())) { + UUID activeOrgId = userSession.getActiveOrganisationId(); + if (activeOrgId != null) { + result.add(HEADER_ACTIVE_ORG, activeOrgId.toString()); + LOGGER.fine("Header " + HEADER_ACTIVE_ORG + " ajouté : " + activeOrgId); + } + } + } catch (Exception e) { + LOGGER.fine("Impossible d'ajouter le header org (contexte non disponible) : " + e.getMessage()); + } + return result; } } diff --git a/src/main/java/dev/lions/unionflow/client/security/AuthenticationFilter.java b/src/main/java/dev/lions/unionflow/client/security/AuthenticationFilter.java index e0d0b04..576b748 100644 --- a/src/main/java/dev/lions/unionflow/client/security/AuthenticationFilter.java +++ b/src/main/java/dev/lions/unionflow/client/security/AuthenticationFilter.java @@ -35,14 +35,25 @@ public class AuthenticationFilter implements Filter { String requestURI = httpRequest.getRequestURI(); - // Laisser Quarkus OIDC appliquer l'authentification (rediriger vers Keycloak si - // nécessaire) - // Ici, si l'utilisateur n'est pas encore authentifié, on ne force PAS une - // redirection custom - // pour éviter les boucles / conflits. On délègue au mécanisme Quarkus défini - // via - // quarkus.http.auth.permission.* et quarkus.oidc.* + // Vérifier l'authentification if (!isAuthenticated()) { + // Requête AJAX PrimeFaces (partial/ajax) : on ne peut pas rediriger avec un 302 HTTP. + // On retourne une réponse XML partial-response qui force PrimeFaces à recharger la page. + if ("partial/ajax".equals(httpRequest.getHeader("Faces-Request"))) { + String loginUrl = httpRequest.getContextPath() + "/login"; + httpResponse.setContentType("text/xml;charset=UTF-8"); + httpResponse.setHeader("Cache-Control", "no-cache"); + try { + httpResponse.getWriter().printf( + "" + + "", + loginUrl); + } catch (IOException e) { + LOGGER.warning("Impossible d'écrire la réponse AJAX de redirection: " + e.getMessage()); + } + return; + } + // Requête pleine page : Quarkus OIDC gère la redirection vers Keycloak LOGGER.fine("Requête non authentifiée sur " + requestURI + ", délégation à Quarkus OIDC."); chain.doFilter(request, response); return; @@ -81,11 +92,16 @@ public class AuthenticationFilter implements Filter { return userSession.isSuperAdmin(); } - // Pages admin : nécessitent ADMIN_ENTITE ou SUPER_ADMIN + // Pages admin : nécessitent ADMIN_ORGANISATION ou SUPER_ADMIN if (requestURI.contains("/pages/admin/")) { return userSession.isAdmin(); } + // Pages admin dans le dossier secure : nécessitent également ADMIN_ORGANISATION ou SUPER_ADMIN + if (requestURI.contains("/pages/secure/admin/")) { + return userSession.isAdmin(); + } + // Pages membre : nécessitent le rôle MEMBRE ou tout utilisateur authentifié if (requestURI.contains("/pages/membre/")) { return userSession.isMembre(); diff --git a/src/main/java/dev/lions/unionflow/client/security/PermissionChecker.java b/src/main/java/dev/lions/unionflow/client/security/PermissionChecker.java index 943f663..c15a8e6 100644 --- a/src/main/java/dev/lions/unionflow/client/security/PermissionChecker.java +++ b/src/main/java/dev/lions/unionflow/client/security/PermissionChecker.java @@ -28,18 +28,27 @@ public class PermissionChecker implements Serializable { @Inject private UserSession userSession; - // Hiérarchie des rôles (de plus privilégié à moins privilégié) + // Hiérarchie des rôles plateforme (de plus privilégié à moins privilégié) + // Aligné avec la constitution UnionFlow v2.11 — codes Keycloak private static final List ROLE_HIERARCHY = Arrays.asList( - "SUPER_ADMIN", - "ADMIN_ENTITE", - "ADMIN", - "GESTIONNAIRE_MEMBRE", - "GESTIONNAIRE_EVENEMENT", - "GESTIONNAIRE_AIDE", - "GESTIONNAIRE_FINANCE", - "TRESORIER", - "MEMBRE", - "MEMBER" + "SUPERADMIN", // niveau 10 — plateforme + "SUPER_ADMIN", // alias rétro-compatible + "ORGADMIN", // niveau 20 — plateforme + "ADMIN_ORGANISATION", // alias rétro-compatible + "ADMIN", // alias rétro-compatible + "MODERATOR", // niveau 30 — plateforme + "MODERATEUR", // alias français + "CONSULTANT", // niveau 45 — lecture étendue + "GESTIONNAIRE_RH", // niveau 30 — RH + "ACTIVEMEMBER", // niveau 40 — plateforme + "MEMBRE_ACTIF", // alias + "TRESORIER", // rôle fonctionnel + "SECRETAIRE", // rôle fonctionnel + "SIMPLEMEMBER", // niveau 50 — plateforme + "MEMBRE", // alias + "MEMBER", // alias anglais + "VISITOR", // niveau 60 — lecture seule + "VISITEUR" // alias français ); /** @@ -133,41 +142,53 @@ public class PermissionChecker implements Serializable { return false; } - // Vérifications basées sur les permissions + // ── Vérifications basées sur les rôles plateforme ────────────────────────── + public boolean canManageMembers() { - return hasAnyRole("ADMIN", "GESTIONNAIRE_MEMBRE"); + return userSession != null && (userSession.isAdmin() || userSession.isModerateur()); } - + public boolean canValidateMembers() { - return hasAnyRole("ADMIN", "GESTIONNAIRE_MEMBRE"); + return userSession != null && userSession.isAdmin(); } - + public boolean canManageFinances() { - return hasAnyRole("ADMIN", "TRESORIER", "GESTIONNAIRE_FINANCE"); + return userSession != null && (userSession.isAdmin() || hasAnyRole("TRESORIER", "TRESORIER_ADJOINT")); } - + public boolean canManageEvents() { - return hasAnyRole("ADMIN", "GESTIONNAIRE_EVENEMENT"); + return userSession != null && (userSession.isAdmin() || userSession.isModerateur() + || hasRole("RESPONSABLE_EVENEMENTS")); } - + public boolean canManageAides() { - return hasAnyRole("ADMIN", "GESTIONNAIRE_AIDE"); + return userSession != null && (userSession.isAdmin() || userSession.isModerateur()); } - + public boolean canViewReports() { - return hasAnyRole("ADMIN", "GESTIONNAIRE_MEMBRE", "TRESORIER"); + return userSession != null && (userSession.isAdmin() || userSession.isModerateur() + || userSession.isConsultant() || hasRole("TRESORIER")); } - + public boolean canManageSubscription() { - return hasRole("ADMIN"); + return userSession != null && userSession.isAdmin(); } - + public boolean canManageOrganization() { - return hasRole("ADMIN"); + return userSession != null && userSession.isAdmin(); } - + public boolean canAccessSuperAdmin() { - return hasRole("SUPER_ADMIN"); + return userSession != null && userSession.isSuperAdmin(); + } + + public boolean canInviteMembers() { + return userSession != null && (userSession.isAdmin() || userSession.isModerateur() + || hasRole("RESPONSABLE_MEMBRES")); + } + + public boolean canManageRoles() { + return userSession != null && (userSession.isSuperAdmin() || userSession.isAdmin()); } // Vérifications basées sur les fonctionnalités du forfait @@ -289,7 +310,7 @@ public class PermissionChecker implements Serializable { // Déterminer le style basé sur le rôle le plus privilégié if (hasRole("SUPER_ADMIN")) { return "super-admin-mode"; - } else if (hasAnyRole("ADMIN", "ADMIN_ENTITE")) { + } else if (hasAnyRole("ADMIN", "ADMIN_ORGANISATION")) { return "admin-mode"; } else if (hasAnyRole("GESTIONNAIRE_MEMBRE", "GESTIONNAIRE_EVENEMENT", "GESTIONNAIRE_AIDE", "GESTIONNAIRE_FINANCE")) { return "gestionnaire-mode"; @@ -343,14 +364,49 @@ public class PermissionChecker implements Serializable { } public boolean isMember() { - return hasRole("MEMBER"); + return hasAnyRole("MEMBRE", "MEMBER", "SIMPLEMEMBER", "ACTIVEMEMBER"); } - + + public boolean isModerateur() { + return userSession != null && userSession.isModerateur(); + } + + public boolean isConsultant() { + return userSession != null && userSession.isConsultant(); + } + + public boolean isGestionnaireRh() { + return userSession != null && userSession.isGestionnaireRh(); + } + + public boolean isVisiteur() { + return userSession != null && userSession.isVisiteur(); + } + public boolean isGestionnaire() { - return hasAnyRole("GESTIONNAIRE_MEMBRE", "GESTIONNAIRE_EVENEMENT", "GESTIONNAIRE_AIDE", "GESTIONNAIRE_FINANCE"); + return isModerateur() || isConsultant() || isGestionnaireRh(); } - + public boolean isTresorier() { return hasRole("TRESORIER"); } + + /** Retourne true si l'utilisateur a accès à une fonctionnalité métier selon son rôle. */ + public boolean canAccessBusinessFeature(String feature) { + if (userSession == null || !userSession.isAuthenticated()) { + return false; + } + return switch (feature.toUpperCase()) { + case "TONTINE" -> hasAnyRole("TONTINE_MANAGER", "TONTINE_COLLECTOR") || userSession.isAdmin(); + case "CREDIT" -> hasAnyRole("MUTUELLE_RESP", "CREDIT_ANALYSTE") || userSession.isAdmin(); + case "EPARGNE" -> hasAnyRole("MUTUELLE_RESP", "EPARGNE_MANAGER") || userSession.isAdmin(); + case "AGRICULTURE" -> hasAnyRole("COOP_RESP") || userSession.isAdmin(); + case "PROJETS_ONG" -> hasAnyRole("ONG_RESP", "PROJET_MANAGER") || userSession.isAdmin(); + case "CULTE_DONS" -> hasAnyRole("CULTE_RESP", "PASTEUR", "DIACRE") || userSession.isAdmin(); + case "VOTES" -> hasAnyRole("VOTE_RESP") || userSession.isAdmin(); + case "COLLECTE_FONDS" -> hasAnyRole("COLLECTE_RESP") || userSession.isAdmin(); + case "REGISTRE_AGREMENT"-> hasAnyRole("REGISTRE_RESP") || userSession.isAdmin(); + default -> userSession.isAdmin() || userSession.isModerateur(); + }; + } } diff --git a/src/main/java/dev/lions/unionflow/client/service/AuthenticationService.java b/src/main/java/dev/lions/unionflow/client/service/AuthenticationService.java index 34c9b06..398820d 100644 --- a/src/main/java/dev/lions/unionflow/client/service/AuthenticationService.java +++ b/src/main/java/dev/lions/unionflow/client/service/AuthenticationService.java @@ -125,8 +125,8 @@ public class AuthenticationService { userInfo.setPrenom("Fatou"); userInfo.setEmail("fatou.traore@association-example.sn"); userInfo.setUsername("admin"); - userInfo.setTypeCompte("ADMIN_ENTITE"); - userInfo.setRoles(java.util.Arrays.asList("ADMIN_ENTITE")); + userInfo.setTypeCompte("ADMIN_ORGANISATION"); + userInfo.setRoles(java.util.Arrays.asList("ADMIN_ORGANISATION")); // Entité de démonstration LoginResponse.EntiteInfo entite = new LoginResponse.EntiteInfo(); diff --git a/src/main/java/dev/lions/unionflow/client/service/BackupService.java b/src/main/java/dev/lions/unionflow/client/service/BackupService.java new file mode 100644 index 0000000..bc6179d --- /dev/null +++ b/src/main/java/dev/lions/unionflow/client/service/BackupService.java @@ -0,0 +1,55 @@ +package dev.lions.unionflow.client.service; + +import dev.lions.unionflow.server.api.dto.backup.request.CreateBackupRequest; +import dev.lions.unionflow.server.api.dto.backup.request.RestoreBackupRequest; +import dev.lions.unionflow.server.api.dto.backup.request.UpdateBackupConfigRequest; +import dev.lions.unionflow.server.api.dto.backup.response.BackupConfigResponse; +import dev.lions.unionflow.server.api.dto.backup.response.BackupResponse; +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +import java.util.List; +import java.util.UUID; + +@RegisterRestClient(configKey = "unionflow-api") +@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class) +@Path("/api/backups") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public interface BackupService { + + @GET + List listerSauvegardes( + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("size") @DefaultValue("20") int size + ); + + @GET + @Path("/{id}") + BackupResponse obtenirParId(@PathParam("id") UUID id); + + @POST + BackupResponse creerSauvegarde(CreateBackupRequest request); + + @POST + @Path("/restore") + BackupResponse restaurer(RestoreBackupRequest request); + + @DELETE + @Path("/{id}") + void supprimer(@PathParam("id") UUID id); + + @GET + @Path("/config") + BackupConfigResponse obtenirConfiguration(); + + @PUT + @Path("/config") + BackupConfigResponse mettreAJourConfiguration(UpdateBackupConfigRequest request); + + @POST + @Path("/restore-point") + BackupResponse creerPointDeRestauration(CreateBackupRequest request); +} diff --git a/src/main/java/dev/lions/unionflow/client/service/ConversationService.java b/src/main/java/dev/lions/unionflow/client/service/ConversationService.java new file mode 100644 index 0000000..8a91afe --- /dev/null +++ b/src/main/java/dev/lions/unionflow/client/service/ConversationService.java @@ -0,0 +1,51 @@ +package dev.lions.unionflow.client.service; + +import dev.lions.unionflow.server.api.dto.communication.request.CreateConversationRequest; +import dev.lions.unionflow.server.api.dto.communication.response.ConversationResponse; +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +import java.util.List; +import java.util.UUID; + +@RegisterRestClient(configKey = "unionflow-api") +@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class) +@Path("/api/conversations") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public interface ConversationService { + + @GET + List listerConversations( + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("size") @DefaultValue("20") int size + ); + + @GET + @Path("/{id}") + ConversationResponse obtenirParId(@PathParam("id") UUID id); + + @POST + ConversationResponse creer(CreateConversationRequest request); + + @PUT + @Path("/{id}/archive") + ConversationResponse archiver( + @PathParam("id") UUID id, + @QueryParam("archive") @DefaultValue("true") boolean archive + ); + + @PUT + @Path("/{id}/mark-read") + void marquerCommeLue(@PathParam("id") UUID id); + + @PUT + @Path("/{id}/toggle-mute") + ConversationResponse basculerSilence(@PathParam("id") UUID id); + + @PUT + @Path("/{id}/toggle-pin") + ConversationResponse basculerEpinglage(@PathParam("id") UUID id); +} diff --git a/src/main/java/dev/lions/unionflow/client/service/CotisationService.java b/src/main/java/dev/lions/unionflow/client/service/CotisationService.java index 701f0dc..806d14d 100644 --- a/src/main/java/dev/lions/unionflow/client/service/CotisationService.java +++ b/src/main/java/dev/lions/unionflow/client/service/CotisationService.java @@ -1,6 +1,7 @@ package dev.lions.unionflow.client.service; import dev.lions.unionflow.server.api.dto.cotisation.request.CreateCotisationRequest; +import dev.lions.unionflow.server.api.dto.cotisation.request.UpdateCotisationRequest; import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationResponse; import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; @@ -112,7 +113,7 @@ public interface CotisationService { */ @PUT @Path("/{id}") - CotisationResponse modifier(@PathParam("id") UUID id, CotisationResponse cotisation); + CotisationResponse modifier(@PathParam("id") UUID id, UpdateCotisationRequest request); /** * Supprime une cotisation @@ -121,11 +122,34 @@ public interface CotisationService { @Path("/{id}") void supprimer(@PathParam("id") UUID id); + /** + * Enregistre le paiement d'une cotisation (endpoint dédié backend). + * Corps attendu : { montantPaye, datePaiement, modePaiement, reference } + */ + @PUT + @Path("/{id}/payer") + @Consumes(MediaType.APPLICATION_JSON) + CotisationResponse payer(@PathParam("id") UUID id, Map paiementData); + + /** + * Récupère les cotisations du membre connecté (authentification JWT). + */ + @GET + @Path("/mes-cotisations") + List getMesCotisations( + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("size") @DefaultValue("20") int size + ); + + /** + * Récupère les cotisations en attente du membre connecté. + */ + @GET + @Path("/mes-cotisations/en-attente") + List getMesCotisationsEnAttente(); + /** * Envoie des rappels de cotisations groupés à plusieurs membres (WOU/DRY) - * - * @param membreIds Liste des IDs des membres destinataires - * @return Nombre de rappels envoyés */ @POST @Path("/rappels/groupes") diff --git a/src/main/java/dev/lions/unionflow/client/service/DocumentService.java b/src/main/java/dev/lions/unionflow/client/service/DocumentService.java index e99281a..17f587e 100644 --- a/src/main/java/dev/lions/unionflow/client/service/DocumentService.java +++ b/src/main/java/dev/lions/unionflow/client/service/DocumentService.java @@ -22,6 +22,13 @@ import java.util.UUID; @Produces(MediaType.APPLICATION_JSON) public interface DocumentService { + /** + * Liste les documents de l'utilisateur connecté + */ + @GET + @Path("/mes-documents") + List mesDocuments(); + /** * Crée un nouveau document */ diff --git a/src/main/java/dev/lions/unionflow/client/service/EpargneService.java b/src/main/java/dev/lions/unionflow/client/service/EpargneService.java new file mode 100644 index 0000000..6492f59 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/client/service/EpargneService.java @@ -0,0 +1,71 @@ +package dev.lions.unionflow.client.service; + +import dev.lions.unionflow.server.api.dto.mutuelle.epargne.CompteEpargneRequest; +import dev.lions.unionflow.server.api.dto.mutuelle.epargne.CompteEpargneResponse; +import dev.lions.unionflow.server.api.dto.mutuelle.epargne.TransactionEpargneRequest; +import dev.lions.unionflow.server.api.dto.mutuelle.epargne.TransactionEpargneResponse; +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +import java.util.List; + +@RegisterRestClient(configKey = "unionflow-api") +@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class) +@Path("/api/v1/epargne") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public interface EpargneService { + + // --- Comptes --- + + @POST + @Path("/comptes") + CompteEpargneResponse ouvrirCompte(CompteEpargneRequest request); + + @GET + @Path("/comptes/{id}") + CompteEpargneResponse obtenirCompte(@PathParam("id") String id); + + @GET + @Path("/comptes/mes-comptes") + List obtenirMesComptes(); + + @GET + @Path("/comptes/membre/{membreId}") + List obtenirComptesParMembre(@PathParam("membreId") String membreId); + + @GET + @Path("/comptes/organisation/{organisationId}") + List obtenirComptesParOrganisation( + @PathParam("organisationId") String organisationId, + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("size") @DefaultValue("20") int size + ); + + @PATCH + @Path("/comptes/{id}/statut") + CompteEpargneResponse changerStatut( + @PathParam("id") String id, + @QueryParam("statut") String statut + ); + + // --- Transactions --- + + @POST + @Path("/transactions") + TransactionEpargneResponse executerTransaction(TransactionEpargneRequest request); + + @POST + @Path("/transactions/transfert") + TransactionEpargneResponse effectuerTransfert(TransactionEpargneRequest request); + + @GET + @Path("/transactions/compte/{compteId}") + List obtenirTransactions( + @PathParam("compteId") String compteId, + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("size") @DefaultValue("20") int size + ); +} 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 2ab73b3..68d8aa3 100644 --- a/src/main/java/dev/lions/unionflow/client/service/EvenementService.java +++ b/src/main/java/dev/lions/unionflow/client/service/EvenementService.java @@ -73,38 +73,23 @@ public interface EvenementService { ); /** - * Recherche d'événements avec filtres + * Recherche d'événements — backend: GET /api/evenements/recherche */ @GET - @Path("/search") - PagedResponse rechercher( - @QueryParam("titre") String titre, - @QueryParam("type") String type, - @QueryParam("statut") String statut, - @QueryParam("dateDebut") String dateDebut, - @QueryParam("dateFin") String dateFin, + @Path("/recherche") + List rechercher( + @QueryParam("q") String q, @QueryParam("page") @DefaultValue("0") int page, @QueryParam("size") @DefaultValue("20") int size ); /** - * Liste les événements par statut + * Événements par type — backend: GET /api/evenements/type/{type} */ @GET - @Path("/statut/{statut}") - 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}") - PagedResponse listerParAssociation( - @PathParam("associationId") UUID associationId, + @Path("/type/{type}") + List listerParType( + @PathParam("type") String type, @QueryParam("page") @DefaultValue("0") int page, @QueryParam("size") @DefaultValue("20") int size ); @@ -117,19 +102,21 @@ public interface EvenementService { Map compter(); /** - * Inscrit un participant à un événement + * Inscrit le membre connecté à un événement + * Backend: POST /api/evenements/{id}/inscriptions (membre résolu via SecurityIdentity) */ @POST - @Path("/{evenementId}/participants/{membreId}") - void inscrireParticipant(@PathParam("evenementId") UUID evenementId, @PathParam("membreId") UUID membreId); - + @Path("/{evenementId}/inscriptions") + void inscrireParticipant(@PathParam("evenementId") UUID evenementId); + /** - * Désinscrit un participant d'un événement + * Désinscrit le membre connecté d'un événement + * Backend: DELETE /api/evenements/{id}/inscriptions (membre résolu via SecurityIdentity) */ @DELETE - @Path("/{evenementId}/participants/{membreId}") - void desinscrireParticipant(@PathParam("evenementId") UUID evenementId, @PathParam("membreId") UUID membreId); - + @Path("/{evenementId}/inscriptions") + void desinscrireParticipant(@PathParam("evenementId") UUID evenementId); + /** * Liste les participants d'un événement */ diff --git a/src/main/java/dev/lions/unionflow/client/service/FinanceApprovalService.java b/src/main/java/dev/lions/unionflow/client/service/FinanceApprovalService.java new file mode 100644 index 0000000..f31e6bb --- /dev/null +++ b/src/main/java/dev/lions/unionflow/client/service/FinanceApprovalService.java @@ -0,0 +1,102 @@ +package dev.lions.unionflow.client.service; + +import dev.lions.unionflow.server.api.dto.finance_workflow.request.ApproveTransactionRequest; +import dev.lions.unionflow.server.api.dto.finance_workflow.request.CreateBudgetRequest; +import dev.lions.unionflow.server.api.dto.finance_workflow.request.RejectTransactionRequest; +import dev.lions.unionflow.server.api.dto.finance_workflow.response.BudgetResponse; +import dev.lions.unionflow.server.api.dto.finance_workflow.response.TransactionApprovalResponse; +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@RegisterRestClient(configKey = "unionflow-api") +@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class) +@Path("/api/finance") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public interface FinanceApprovalService { + + // --- Approbations --- + + @GET + @Path("/approvals/pending") + List obtenirApprobationsEnAttente( + @QueryParam("organizationId") UUID organizationId, + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("size") @DefaultValue("20") int size + ); + + @GET + @Path("/approvals/{approvalId}") + TransactionApprovalResponse obtenirApprobation(@PathParam("approvalId") UUID approvalId); + + @POST + @Path("/approvals/{approvalId}/approve") + TransactionApprovalResponse approuver( + @PathParam("approvalId") UUID approvalId, + ApproveTransactionRequest request + ); + + @POST + @Path("/approvals/{approvalId}/reject") + TransactionApprovalResponse rejeter( + @PathParam("approvalId") UUID approvalId, + RejectTransactionRequest request + ); + + @GET + @Path("/approvals/history") + List obtenirHistorique( + @QueryParam("organizationId") UUID organizationId, + @QueryParam("startDate") String startDate, + @QueryParam("endDate") String endDate, + @QueryParam("status") String status, + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("size") @DefaultValue("20") int size + ); + + @GET + @Path("/approvals/count/pending") + Map compterEnAttente(@QueryParam("organizationId") UUID organizationId); + + // --- Budgets --- + + @GET + @Path("/budgets") + List listerBudgets( + @QueryParam("organizationId") UUID organizationId, + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("size") @DefaultValue("20") int size + ); + + @GET + @Path("/budgets/{budgetId}") + BudgetResponse obtenirBudget(@PathParam("budgetId") UUID budgetId); + + @POST + @Path("/budgets") + BudgetResponse creerBudget(CreateBudgetRequest request); + + @PUT + @Path("/budgets/{budgetId}") + BudgetResponse modifierBudget(@PathParam("budgetId") UUID budgetId, CreateBudgetRequest request); + + @DELETE + @Path("/budgets/{budgetId}") + void supprimerBudget(@PathParam("budgetId") UUID budgetId); + + // --- Statistiques workflow --- + + @GET + @Path("/stats") + Map obtenirStatistiques( + @QueryParam("organizationId") UUID organizationId, + @QueryParam("startDate") String startDate, + @QueryParam("endDate") String endDate + ); +} diff --git a/src/main/java/dev/lions/unionflow/client/service/LogsService.java b/src/main/java/dev/lions/unionflow/client/service/LogsService.java new file mode 100644 index 0000000..1224b99 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/client/service/LogsService.java @@ -0,0 +1,63 @@ +package dev.lions.unionflow.client.service; + +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@RegisterRestClient(configKey = "unionflow-api") +@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class) +@Path("/api") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public interface LogsService { + + @GET + @Path("/logs/search") + List> rechercherLogs( + @QueryParam("level") String level, + @QueryParam("source") String source, + @QueryParam("userId") String userId, + @QueryParam("startDate") String startDate, + @QueryParam("endDate") String endDate, + @QueryParam("keyword") String keyword, + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("size") @DefaultValue("50") int size + ); + + @GET + @Path("/finance/audit-logs") + List> obtenirLogsAuditFinance( + @QueryParam("organizationId") UUID organizationId, + @QueryParam("startDate") String startDate, + @QueryParam("endDate") String endDate, + @QueryParam("operation") String operation, + @QueryParam("entityType") String entityType, + @QueryParam("severity") String severity, + @QueryParam("limit") @DefaultValue("100") int limit + ); + + @GET + @Path("/finance/audit-logs/anomalies") + List> obtenirAnomalies( + @QueryParam("organizationId") UUID organizationId, + @QueryParam("startDate") String startDate, + @QueryParam("endDate") String endDate + ); + + @POST + @Path("/finance/audit-logs/export") + byte[] exporterLogsAudit(Map request); + + @GET + @Path("/monitoring/metrics") + Map obtenirMetriques(); + + @GET + @Path("/v1/dashboard/health") + Map obtenirSanteSysteme(); +} 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 d63a986..5305888 100644 --- a/src/main/java/dev/lions/unionflow/client/service/MembreService.java +++ b/src/main/java/dev/lions/unionflow/client/service/MembreService.java @@ -1,7 +1,9 @@ package dev.lions.unionflow.client.service; import dev.lions.unionflow.server.api.dto.common.PagedResponse; +import dev.lions.unionflow.server.api.dto.membre.request.CreateMembreRequest; import dev.lions.unionflow.server.api.dto.membre.response.MembreResponse; +import dev.lions.unionflow.server.api.dto.membre.response.MembreSummaryResponse; import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import jakarta.ws.rs.*; @@ -17,7 +19,14 @@ import java.util.UUID; public interface MembreService { @GET - PagedResponse listerTous(); + PagedResponse listerTous( + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("size") @DefaultValue("20") int size); + + /** Surcharge sans pagination — retourne jusqu'à 1000 membres (usage hors DataTable). */ + default PagedResponse listerTous() { + return listerTous(0, 1000); + } @GET @Path("/{id}") @@ -53,8 +62,8 @@ public interface MembreService { @QueryParam("direction") @DefaultValue("asc") String sortDirection); @GET - @Path("/association/{associationId}") - List listerParAssociation(@PathParam("associationId") UUID associationId); + @Path("/organisation/{organisationId}") + List listerParOrganisation(@PathParam("organisationId") UUID organisationId); @GET @Path("/actifs") @@ -65,7 +74,7 @@ public interface MembreService { List listerInactifs(); @POST - MembreResponse creer(MembreResponse membre); + MembreResponse creer(CreateMembreRequest membre); @PUT @Path("/{id}") @@ -137,6 +146,46 @@ public interface MembreService { @Path("/autocomplete/villes") List obtenirVilles(@QueryParam("query") String query); + @GET + @Path("/mes-organisations") + List mesOrganisations(); + + // ── Cycle de vie des adhésions ───────────────────────────────────────── + + @GET + @Path("/{membreId}/adhesion") + @SuppressWarnings("unchecked") + java.util.Map getAdhesionStatut(@PathParam("membreId") UUID membreId, + @QueryParam("organisationId") UUID organisationId); + + @PUT + @Path("/{membreId}/inviter-organisation") + java.util.Map inviterMembreOrg(@PathParam("membreId") UUID membreId, + @QueryParam("organisationId") UUID organisationId, + @QueryParam("roleOrg") String roleOrg); + + @POST + @Path("/accepter-invitation/{token}") + java.util.Map accepterInvitation(@PathParam("token") String token); + + @PUT + @Path("/{membreId}/adhesion/activer") + java.util.Map activerAdhesion(@PathParam("membreId") UUID membreId, + @QueryParam("organisationId") UUID organisationId, + java.util.Map body); + + @PUT + @Path("/{membreId}/adhesion/suspendre") + java.util.Map suspendrAdhesion(@PathParam("membreId") UUID membreId, + @QueryParam("organisationId") UUID organisationId, + java.util.Map body); + + @PUT + @Path("/{membreId}/adhesion/radier") + java.util.Map radierAdhesion(@PathParam("membreId") UUID membreId, + @QueryParam("organisationId") UUID organisationId, + java.util.Map body); + @GET @Path("/autocomplete/professions") List obtenirProfessions(@QueryParam("query") String query); @@ -150,6 +199,52 @@ public interface MembreService { @QueryParam("format") @DefaultValue("EXCEL") String format); // Classes DTO internes pour les réponses spécialisées + + /** + * DTO pour le sélecteur d'organisation (multi-org switcher). + * Mappé depuis GET /api/membres/mes-organisations. + */ + class OrganisationSwitcherDTO implements java.io.Serializable { + public String organisationId; + public String nom; + public String nomCourt; + public String typeOrganisation; + public String categorieType; + public String modulesActifs; + public String statut; + public String statutMembre; + public String roleOrg; + public String dateAdhesion; + + public String getOrganisationId() { return organisationId; } + public void setOrganisationId(String v) { this.organisationId = v; } + public String getNom() { return nom; } + public void setNom(String v) { this.nom = v; } + public String getNomCourt() { return nomCourt; } + public void setNomCourt(String v) { this.nomCourt = v; } + public String getTypeOrganisation() { return typeOrganisation; } + public void setTypeOrganisation(String v) { this.typeOrganisation = v; } + public String getCategorieType() { return categorieType; } + public void setCategorieType(String v) { this.categorieType = v; } + public String getModulesActifs() { return modulesActifs; } + public void setModulesActifs(String v) { this.modulesActifs = v; } + public String getStatut() { return statut; } + public void setStatut(String v) { this.statut = v; } + public String getStatutMembre() { return statutMembre; } + public void setStatutMembre(String v) { this.statutMembre = v; } + public String getRoleOrg() { return roleOrg; } + public void setRoleOrg(String v) { this.roleOrg = v; } + public String getDateAdhesion() { return dateAdhesion; } + public void setDateAdhesion(String v) { this.dateAdhesion = v; } + + /** Retourne le libellé court à afficher dans le switcher. */ + public String getLibelleCourt() { + if (nomCourt != null && !nomCourt.isBlank()) return nomCourt; + if (nom != null && nom.length() > 25) return nom.substring(0, 22) + "…"; + return nom; + } + } + class StatistiquesMembreDTO { public Long totalMembres; public Long membresActifs; diff --git a/src/main/java/dev/lions/unionflow/client/service/MessageService.java b/src/main/java/dev/lions/unionflow/client/service/MessageService.java new file mode 100644 index 0000000..bc010e5 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/client/service/MessageService.java @@ -0,0 +1,41 @@ +package dev.lions.unionflow.client.service; + +import dev.lions.unionflow.server.api.dto.communication.request.SendMessageRequest; +import dev.lions.unionflow.server.api.dto.communication.response.MessageResponse; +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@RegisterRestClient(configKey = "unionflow-api") +@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class) +@Path("/api/messages") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public interface MessageService { + + @GET + List listerMessages( + @QueryParam("conversationId") UUID conversationId, + @QueryParam("limit") @DefaultValue("50") int limit + ); + + @POST + MessageResponse envoyer(SendMessageRequest request); + + @PUT + @Path("/{id}") + MessageResponse modifier(@PathParam("id") UUID id, Map body); + + @DELETE + @Path("/{id}") + void supprimer(@PathParam("id") UUID id); + + @POST + @Path("/broadcast") + MessageResponse diffuser(SendMessageRequest request); +} diff --git a/src/main/java/dev/lions/unionflow/client/service/NotificationClientService.java b/src/main/java/dev/lions/unionflow/client/service/NotificationClientService.java index 374a5ce..b897014 100644 --- a/src/main/java/dev/lions/unionflow/client/service/NotificationClientService.java +++ b/src/main/java/dev/lions/unionflow/client/service/NotificationClientService.java @@ -4,11 +4,13 @@ import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; -import java.util.List; import java.util.Map; /** - * Service REST client pour les notifications + * Client REST pour l'envoi de notifications groupées. + * Seul endpoint utilisé côté web : POST /api/notifications/groupees. + * + * Pour les opérations de lecture/marquage, utiliser {@link NotificationService}. */ @RegisterRestClient(configKey = "unionflow-api") @RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class) @@ -16,40 +18,13 @@ import java.util.Map; @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public interface NotificationClientService { - - @POST - @Path("/groupe") - List> envoyerNotificationGroupe( - @QueryParam("type") String type, - @QueryParam("titre") String titre, - @QueryParam("message") String message, - List destinatairesIds - ); - - @GET - @Path("/utilisateur/{utilisateurId}") - List> obtenirNotifications( - @PathParam("utilisateurId") String utilisateurId, - @QueryParam("includeArchivees") @DefaultValue("false") boolean includeArchivees, - @QueryParam("limite") @DefaultValue("50") int limite - ); - - @PUT - @Path("/{notificationId}/lue") - Map marquerCommeLue( - @PathParam("notificationId") String notificationId, - @QueryParam("utilisateurId") String utilisateurId - ); - - @GET - @Path("/stats") - Map obtenirStatistiques(); - - @POST - @Path("/test/{utilisateurId}") - Map envoyerNotificationTest( - @PathParam("utilisateurId") String utilisateurId, - @QueryParam("type") @DefaultValue("SYSTEME") String type - ); -} + /** + * Envoie des notifications groupées à plusieurs membres. + * Body attendu : {@code { sujet, corps, membreIds: [UUID], canaux: [String] (optionnel) }} + * Retourne : {@code { notificationsCreees: int }} + */ + @POST + @Path("/groupees") + Map envoyerNotificationGroupe(Map request); +} diff --git a/src/main/java/dev/lions/unionflow/client/service/AssociationService.java b/src/main/java/dev/lions/unionflow/client/service/OrganisationService.java similarity index 86% rename from src/main/java/dev/lions/unionflow/client/service/AssociationService.java rename to src/main/java/dev/lions/unionflow/client/service/OrganisationService.java index 163bdf0..7f475a9 100644 --- a/src/main/java/dev/lions/unionflow/client/service/AssociationService.java +++ b/src/main/java/dev/lions/unionflow/client/service/OrganisationService.java @@ -2,6 +2,7 @@ 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 dev.lions.unionflow.server.api.dto.organisation.response.OrganisationSummaryResponse; import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import jakarta.ws.rs.*; @@ -14,10 +15,10 @@ import java.util.UUID; @Path("/api/organisations") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) -public interface AssociationService { +public interface OrganisationService { @GET - PagedResponse listerToutes( + PagedResponse listerToutes( @QueryParam("page") @DefaultValue("0") int page, @QueryParam("size") @DefaultValue("1000") int size); @@ -27,7 +28,7 @@ public interface AssociationService { @GET @Path("/recherche") - PagedResponse rechercher( + PagedResponse rechercher( @QueryParam("nom") String nom, @QueryParam("type") String type, @QueryParam("statut") String statut, @@ -38,18 +39,18 @@ public interface AssociationService { @GET @Path("/type/{type}") - List listerParType(@PathParam("type") String type); + List listerParType(@PathParam("type") String type); @GET @Path("/region/{region}") - List listerParRegion(@PathParam("region") String region); + List listerParRegion(@PathParam("region") String region); @POST - OrganisationResponse creer(OrganisationResponse association); + OrganisationResponse creer(OrganisationResponse organisation); @PUT @Path("/{id}") - OrganisationResponse modifier(@PathParam("id") UUID id, OrganisationResponse association); + OrganisationResponse modifier(@PathParam("id") UUID id, OrganisationResponse organisation); @DELETE @Path("/{id}") @@ -71,7 +72,7 @@ public interface AssociationService { @GET @Path("/statistiques") - StatistiquesAssociationDTO obtenirStatistiques(); + StatistiquesOrganisationDTO obtenirStatistiques(); @GET @Path("/{id}/membres/count") @@ -79,10 +80,10 @@ public interface AssociationService { @GET @Path("/{id}/performance") - PerformanceAssociationDTO obtenirPerformance(@PathParam("id") UUID id); + PerformanceOrganisationDTO obtenirPerformance(@PathParam("id") UUID id); // Classes DTO internes - class StatistiquesAssociationDTO { + class StatistiquesOrganisationDTO { public Long totalAssociations; public Long associationsActives; public Long associationsInactives; @@ -94,7 +95,7 @@ public interface AssociationService { public java.util.Map repartitionParRegion; // Constructeurs - public StatistiquesAssociationDTO() { + public StatistiquesOrganisationDTO() { } // Getters et setters @@ -171,8 +172,8 @@ public interface AssociationService { } } - class PerformanceAssociationDTO { - public UUID associationId; + class PerformanceOrganisationDTO { + public UUID organisationId; public String nom; public Integer scoreGlobal; public Integer scoreMembres; @@ -182,16 +183,16 @@ public interface AssociationService { public java.time.LocalDateTime derniereMiseAJour; // Constructeurs - public PerformanceAssociationDTO() { + public PerformanceOrganisationDTO() { } // Getters et setters - public UUID getAssociationId() { - return associationId; + public UUID getOrganisationId() { + return organisationId; } - public void setAssociationId(UUID associationId) { - this.associationId = associationId; + public void setOrganisationId(UUID organisationId) { + this.organisationId = organisationId; } public String getNom() { diff --git a/src/main/java/dev/lions/unionflow/client/service/PaiementClientService.java b/src/main/java/dev/lions/unionflow/client/service/PaiementClientService.java new file mode 100644 index 0000000..e447d82 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/client/service/PaiementClientService.java @@ -0,0 +1,39 @@ +package dev.lions.unionflow.client.service; + +import dev.lions.unionflow.server.api.dto.paiement.request.InitierPaiementEnLigneRequest; +import dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse; +import dev.lions.unionflow.server.api.dto.paiement.response.PaiementGatewayResponse; +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import java.util.UUID; + +/** + * Client REST pour les paiements en ligne (Wave Checkout QR). + */ +@RegisterRestClient(configKey = "unionflow-api") +@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class) +@Path("/api/paiements") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public interface PaiementClientService { + + /** + * Initie un paiement Wave et retourne le wave_launch_url à encoder en QR code. + * Sur le web, laisser {@code numeroTelephone} null → pas de restriction de payeur. + */ + @POST + @Path("/initier-paiement-en-ligne") + PaiementGatewayResponse initierPaiementEnLigne(InitierPaiementEnLigneRequest request); + + /** + * Polling du statut d'une intention Wave. + * Si Wave a confirmé, retourne statut=COMPLETEE et la cotisation est marquée PAYEE. + * + * @param intentionId UUID retourné dans {@code clientReference} de {@link PaiementGatewayResponse} + */ + @GET + @Path("/statut-intention/{intentionId}") + IntentionStatutResponse getStatutIntention(@PathParam("intentionId") UUID intentionId); +} diff --git a/src/main/java/dev/lions/unionflow/client/service/RetryService.java b/src/main/java/dev/lions/unionflow/client/service/RetryService.java index f04847b..658bfdd 100644 --- a/src/main/java/dev/lions/unionflow/client/service/RetryService.java +++ b/src/main/java/dev/lions/unionflow/client/service/RetryService.java @@ -24,7 +24,7 @@ import java.util.function.Supplier; * RetryService retryService; * * OrganisationResponse org = retryService.executeWithRetry( - * () -> associationService.creer(nouvelleOrganisation), + * () -> organisationService.creer(nouvelleOrganisation), * "création d'une organisation" * ); * } diff --git a/src/main/java/dev/lions/unionflow/client/service/SouscriptionService.java b/src/main/java/dev/lions/unionflow/client/service/SouscriptionService.java index c542ed8..1a5c741 100644 --- a/src/main/java/dev/lions/unionflow/client/service/SouscriptionService.java +++ b/src/main/java/dev/lions/unionflow/client/service/SouscriptionService.java @@ -1,6 +1,6 @@ package dev.lions.unionflow.client.service; -import dev.lions.unionflow.server.api.dto.abonnement.response.AbonnementResponse; +import dev.lions.unionflow.server.api.dto.souscription.SouscriptionStatutResponse; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; import jakarta.ws.rs.*; @@ -14,35 +14,29 @@ import java.util.UUID; @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public interface SouscriptionService { - + + /** Liste toutes les souscriptions — SUPER_ADMIN. Filtre par org si organisationId non null. */ @GET - List listerToutes( + @Path("/admin/toutes") + List listerToutes( @QueryParam("organisationId") UUID organisationId, @QueryParam("page") @DefaultValue("0") int page, - @QueryParam("size") @DefaultValue("20") int size + @QueryParam("size") @DefaultValue("1000") int size ); - + + /** Souscription active d'une organisation spécifique — SUPER_ADMIN. */ @GET - @Path("/{id}") - AbonnementResponse obtenirParId(@PathParam("id") UUID id); - + @Path("/admin/organisation/{organisationId}/active") + SouscriptionStatutResponse obtenirActive(@PathParam("organisationId") UUID organisationId); + + /** Souscriptions en attente de validation — SUPER_ADMIN. */ @GET - @Path("/organisation/{organisationId}/active") - AbonnementResponse obtenirActive(@PathParam("organisationId") UUID organisationId); - - @POST - AbonnementResponse creer(AbonnementResponse souscription); - - @PUT - @Path("/{id}") - AbonnementResponse modifier(@PathParam("id") UUID id, AbonnementResponse souscription); - - @DELETE - @Path("/{id}") - void supprimer(@PathParam("id") UUID id); - - @PUT - @Path("/{id}/renouveler") - AbonnementResponse renouveler(@PathParam("id") UUID id); + @Path("/admin/en-attente") + List listerEnAttente(); + + /** Souscription de l'organisation du membre connecté. */ + @GET + @Path("/ma-souscription") + SouscriptionStatutResponse getMaSouscription(); } 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 e3e6656..1327d71 100644 --- a/src/main/java/dev/lions/unionflow/client/view/AdhesionsBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/AdhesionsBean.java @@ -15,13 +15,14 @@ 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.organisation.response.OrganisationSummaryResponse; 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.OrganisationService; import dev.lions.unionflow.client.service.ErrorHandlerService; import dev.lions.unionflow.client.service.MembreService; import dev.lions.unionflow.client.service.RetryService; @@ -55,7 +56,7 @@ public class AdhesionsBean implements Serializable { @Inject @RestClient - private AssociationService associationService; + private OrganisationService organisationService; @Inject ErrorHandlerService errorHandler; @@ -65,7 +66,7 @@ public class AdhesionsBean implements Serializable { // Listes de référence pour les select private List listeMembres; - private List listeAssociations; + private List listeAssociations; // Données principales private List toutesLesAdhesions; @@ -113,11 +114,11 @@ public class AdhesionsBean implements Serializable { } try { - PagedResponse response = retryService.executeWithRetrySupplier( - () -> associationService.listerToutes(0, 1000), + PagedResponse response = retryService.executeWithRetrySupplier( + () -> organisationService.listerToutes(0, 1000), "chargement des associations"); listeAssociations = (response != null && response.getData() != null) ? response.getData() - : new ArrayList<>(); + : new ArrayList(); LOG.infof("Associations chargées: %d associations", listeAssociations.size()); } catch (Exception e) { LOG.warnf(e, "Impossible de charger les associations"); @@ -150,12 +151,12 @@ public class AdhesionsBean implements Serializable { List items = new ArrayList<>(); items.add(new SelectItem(null, "Sélectionner une organisation")); if (listeAssociations != null) { - for (OrganisationResponse assoc : listeAssociations) { - String label = assoc.getNom(); - if (assoc.getTypeOrganisation() != null) { - label += " (" + assoc.getTypeOrganisation() + ")"; + for (OrganisationSummaryResponse assoc : listeAssociations) { + String label = assoc.nom(); + if (assoc.typeOrganisation() != null) { + label += " (" + assoc.typeOrganisation() + ")"; } - items.add(new SelectItem(assoc.getId(), label)); + items.add(new SelectItem(assoc.id(), label)); } } return items; diff --git a/src/main/java/dev/lions/unionflow/client/view/ApprobationsFinanceBean.java b/src/main/java/dev/lions/unionflow/client/view/ApprobationsFinanceBean.java new file mode 100644 index 0000000..d3e0700 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/client/view/ApprobationsFinanceBean.java @@ -0,0 +1,230 @@ +package dev.lions.unionflow.client.view; + +import dev.lions.unionflow.client.service.ErrorHandlerService; +import dev.lions.unionflow.client.service.FinanceApprovalService; +import dev.lions.unionflow.server.api.dto.finance_workflow.request.ApproveTransactionRequest; +import dev.lions.unionflow.server.api.dto.finance_workflow.request.CreateBudgetRequest; +import dev.lions.unionflow.server.api.dto.finance_workflow.request.RejectTransactionRequest; +import dev.lions.unionflow.server.api.dto.finance_workflow.response.BudgetResponse; +import dev.lions.unionflow.server.api.dto.finance_workflow.response.TransactionApprovalResponse; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.SessionScoped; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.FacesContext; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import lombok.Data; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Logger; + +@Named("approbationsFinanceBean") +@SessionScoped +@Data +public class ApprobationsFinanceBean implements Serializable { + + private static final Logger LOGGER = Logger.getLogger(ApprobationsFinanceBean.class.getName()); + + @Inject + @RestClient + private FinanceApprovalService financeApprovalService; + + @Inject + private ErrorHandlerService errorHandler; + + @Inject + private UserSession userSession; + + private List approbationsEnAttente = new ArrayList<>(); + private List historiqueApprobations = new ArrayList<>(); + private TransactionApprovalResponse approbationSelectionnee; + + private List budgets = new ArrayList<>(); + private BudgetResponse budgetSelectionne; + private NouveauBudget nouveauBudget = new NouveauBudget(); + + private String commentaireApprobation; + private String raisonRejet; + + private Map statistiques; + private long nombreEnAttente = 0; + + private String filtreStatut; + private String dateDebut; + private String dateFin; + + private int page = 0; + private int taille = 20; + + @PostConstruct + public void init() { + chargerApprobationsEnAttente(); + chargerBudgets(); + chargerStatistiques(); + } + + private UUID getOrgId() { + return userSession.getEntite() != null ? userSession.getEntite().getId() : null; + } + + public void chargerApprobationsEnAttente() { + try { + UUID orgId = getOrgId(); + approbationsEnAttente = financeApprovalService.obtenirApprobationsEnAttente(orgId, page, taille); + Map compteur = financeApprovalService.compterEnAttente(orgId); + nombreEnAttente = compteur.getOrDefault("count", 0L); + } catch (Exception e) { + LOGGER.severe("Erreur chargement approbations: " + e.getMessage()); + approbationsEnAttente = new ArrayList<>(); + } + } + + public void chargerHistorique() { + try { + UUID orgId = getOrgId(); + historiqueApprobations = financeApprovalService.obtenirHistorique(orgId, dateDebut, dateFin, filtreStatut, page, taille); + } catch (Exception e) { + LOGGER.severe("Erreur chargement historique: " + e.getMessage()); + historiqueApprobations = new ArrayList<>(); + ajouterMessageErreur("Impossible de charger l'historique."); + } + } + + public void chargerBudgets() { + try { + UUID orgId = getOrgId(); + budgets = financeApprovalService.listerBudgets(orgId, 0, 50); + } catch (Exception e) { + LOGGER.severe("Erreur chargement budgets: " + e.getMessage()); + budgets = new ArrayList<>(); + } + } + + public void chargerStatistiques() { + try { + statistiques = financeApprovalService.obtenirStatistiques( + getOrgId(), null, null); + } catch (Exception e) { + LOGGER.warning("Statistiques finance indisponibles: " + e.getMessage()); + } + } + + public void selectionnerApprobation(TransactionApprovalResponse approbation) { + this.approbationSelectionnee = approbation; + this.commentaireApprobation = null; + this.raisonRejet = null; + } + + public void approuver() { + if (approbationSelectionnee == null) return; + try { + ApproveTransactionRequest request = ApproveTransactionRequest.builder() + .comment(commentaireApprobation) + .build(); + financeApprovalService.approuver(approbationSelectionnee.getId(), request); + approbationsEnAttente.removeIf(a -> a.getId().equals(approbationSelectionnee.getId())); + nombreEnAttente = Math.max(0, nombreEnAttente - 1); + approbationSelectionnee = null; + ajouterMessageSucces("Transaction approuvée avec succès."); + } catch (Exception e) { + LOGGER.severe("Erreur approbation: " + e.getMessage()); + ajouterMessageErreur("Impossible d'approuver la transaction."); + } + } + + public void rejeter() { + if (approbationSelectionnee == null || raisonRejet == null || raisonRejet.isBlank()) { + ajouterMessageErreur("La raison du rejet est obligatoire."); + return; + } + try { + RejectTransactionRequest request = RejectTransactionRequest.builder() + .reason(raisonRejet.trim()) + .build(); + financeApprovalService.rejeter(approbationSelectionnee.getId(), request); + approbationsEnAttente.removeIf(a -> a.getId().equals(approbationSelectionnee.getId())); + nombreEnAttente = Math.max(0, nombreEnAttente - 1); + approbationSelectionnee = null; + ajouterMessageSucces("Transaction rejetée."); + } catch (Exception e) { + LOGGER.severe("Erreur rejet: " + e.getMessage()); + ajouterMessageErreur("Impossible de rejeter la transaction."); + } + } + + public void creerBudget() { + try { + CreateBudgetRequest request = CreateBudgetRequest.builder() + .name(nouveauBudget.nom) + .description(nouveauBudget.description) + .organizationId(getOrgId()) + .period(nouveauBudget.periode) + .year(nouveauBudget.annee) + .month(nouveauBudget.mois) + .lines(new ArrayList<>()) + .build(); + BudgetResponse cree = financeApprovalService.creerBudget(request); + budgets.add(0, cree); + nouveauBudget = new NouveauBudget(); + ajouterMessageSucces("Budget créé avec succès."); + } catch (Exception e) { + LOGGER.severe("Erreur création budget: " + e.getMessage()); + ajouterMessageErreur("Impossible de créer le budget."); + } + } + + public void supprimerBudget(BudgetResponse budget) { + try { + financeApprovalService.supprimerBudget(budget.getId()); + budgets.remove(budget); + ajouterMessageSucces("Budget supprimé."); + } catch (Exception e) { + ajouterMessageErreur("Impossible de supprimer le budget."); + } + } + + public void appliquerFiltres() { + page = 0; + chargerHistorique(); + } + + public void actualiser() { + chargerApprobationsEnAttente(); + chargerBudgets(); + chargerStatistiques(); + } + + public String getStatusSeverity(String status) { + if (status == null) return "info"; + return switch (status) { + case "APPROVED", "VALIDATED", "ACTIVE" -> "success"; + case "REJECTED", "EXPIRED", "CANCELLED" -> "danger"; + case "PENDING", "DRAFT" -> "warning"; + default -> "info"; + }; + } + + private void ajouterMessageSucces(String detail) { + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", detail)); + } + + private void ajouterMessageErreur(String detail) { + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", detail)); + } + + @Data + public static class NouveauBudget { + private String nom; + private String description; + private String periode = "MONTHLY"; + private Integer annee = java.time.Year.now().getValue(); + private Integer mois; + } +} diff --git a/src/main/java/dev/lions/unionflow/client/view/AuditBean.java b/src/main/java/dev/lions/unionflow/client/view/AuditBean.java index aeb68c4..da00cef 100644 --- a/src/main/java/dev/lions/unionflow/client/view/AuditBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/AuditBean.java @@ -375,12 +375,14 @@ public class AuditBean implements Serializable { retryService.executeWithRetrySupplier( () -> { - notificationService.envoyerNotificationGroupe( - "SYSTEME", - "Signalement d'un événement d'audit", - message, - List.of(signaleurId) // Envoyer aux admins (à adapter selon votre logique) - ); + if (!"anonyme".equals(signaleurId)) { + notificationService.envoyerNotificationGroupe( + java.util.Map.of( + "sujet", "Signalement d'un événement d'audit", + "corps", message, + "membreIds", List.of(java.util.UUID.fromString(signaleurId)) + )); + } return null; }, "envoi d'une notification de signalement" diff --git a/src/main/java/dev/lions/unionflow/client/view/CotisationsBean.java b/src/main/java/dev/lions/unionflow/client/view/CotisationsBean.java index 0db5816..bce70db 100644 --- a/src/main/java/dev/lions/unionflow/client/view/CotisationsBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/CotisationsBean.java @@ -7,6 +7,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.Month; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; @@ -17,13 +18,19 @@ import org.jboss.logging.Logger; import dev.lions.unionflow.server.api.dto.cotisation.request.CreateCotisationRequest; import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationResponse; +import dev.lions.unionflow.server.api.dto.paiement.request.InitierPaiementEnLigneRequest; +import dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse; +import dev.lions.unionflow.server.api.dto.paiement.response.PaiementGatewayResponse; import dev.lions.unionflow.client.service.CotisationService; import dev.lions.unionflow.client.service.ErrorHandlerService; import dev.lions.unionflow.client.service.ExportClientService; +import dev.lions.unionflow.client.service.MembreService; import dev.lions.unionflow.client.service.NotificationClientService; +import dev.lions.unionflow.client.service.PaiementClientService; import dev.lions.unionflow.client.service.RetryService; +import io.quarkus.security.identity.SecurityIdentity; import jakarta.annotation.PostConstruct; -import jakarta.enterprise.context.SessionScoped; +import jakarta.faces.view.ViewScoped; import jakarta.faces.context.ExternalContext; import jakarta.inject.Inject; import jakarta.inject.Named; @@ -36,7 +43,7 @@ import jakarta.inject.Named; * @version 2.0 */ @Named("cotisationsBean") -@SessionScoped +@ViewScoped public class CotisationsBean implements Serializable { private static final long serialVersionUID = 1L; @@ -45,27 +52,57 @@ public class CotisationsBean implements Serializable { @Inject @RestClient private CotisationService cotisationService; - + + @Inject + @RestClient + private MembreService membreService; + @Inject @RestClient private NotificationClientService notificationService; - + @Inject @RestClient private ExportClientService exportService; - + @Inject ErrorHandlerService errorHandler; - + @Inject RetryService retryService; - + + @Inject + @RestClient + private PaiementClientService paiementClientService; + + @Inject + SecurityIdentity securityIdentity; + + // null = vue admin (toutes les cotisations) ; non-null = vue membre (cotisations du membre) + private UUID membreConnecteId; + + // ── État du paiement Wave QR ────────────────────────────────────────────── + /** UUID de l'IntentionPaiement en cours (retourné par clientReference) */ + private UUID waveIntentionId; + /** wave_launch_url à encoder en QR code (JavaScript côté client) */ + private String waveLaunchUrl; + /** Statut de l'intention : INITIEE | EN_COURS | COMPLETEE | EXPIREE | ECHOUEE */ + private String waveStatut; + /** Indique si le dialog QR Wave est ouvert (contrôle le p:poll) */ + private boolean waveDialogOuvert = false; + // Données principales - Utilisation directe de CotisationResponse private List toutesLesCotisations; private List cotisationsFiltrees; private List cotisationsSelectionnees; private CotisationResponse cotisationSelectionnee; + // Champs du dialogue paiement (évite de polluer CotisationResponse) + private String methodePaiementDialog; + private String referencePaiementDialog; + private String observationsDialog; + private BigDecimal montantPaiementPartiel; + // Formulaire nouvelle cotisation private NouvelleCotisation nouvelleCotisation; @@ -83,6 +120,7 @@ public class CotisationsBean implements Serializable { @PostConstruct public void init() { initializeFiltres(); + resoudreMembreConnecte(); chargerCotisations(); chargerStatistiques(); initializeNouvelleCotisation(); @@ -96,17 +134,62 @@ public class CotisationsBean implements Serializable { filtres = new Filtres(); cotisationsSelectionnees = new ArrayList<>(); } + + /** + * Détermine si l'utilisateur est admin/trésorier (vue globale) ou un membre ordinaire (vue personnelle). + * Positionne {@code membreConnecteId} : null = vue admin, UUID = vue membre. + */ + private void resoudreMembreConnecte() { + if (isAdminOuTresorier()) { + membreConnecteId = null; // vue globale + return; + } + try { + var moi = retryService.executeWithRetrySupplier( + () -> membreService.obtenirMembreConnecte(), + "résolution du membre connecté" + ); + membreConnecteId = (moi != null) ? moi.getId() : null; + LOG.infof("Mode membre: chargement des cotisations pour le membre %s", membreConnecteId); + } catch (Exception e) { + LOG.warnf(e, "Impossible de récupérer le membre connecté — mode admin par défaut"); + membreConnecteId = null; + } + } + + private boolean isAdminOuTresorier() { + if (securityIdentity == null || securityIdentity.isAnonymous()) return false; + var roles = securityIdentity.getRoles(); + return roles != null && (roles.contains("SUPER_ADMIN") + || roles.contains("ADMIN_ORGANISATION") + || roles.contains("TRESORIER")); + } + + /** Exposé en EL pour adapter l'affichage (titres, colonnes admin). */ + public boolean isVueAdmin() { + return membreConnecteId == null; + } /** - * Charge les cotisations depuis le backend + * Charge les cotisations depuis le backend. + * Mode admin : toutes les cotisations. Mode membre : uniquement les siennes. */ private void chargerCotisations() { toutesLesCotisations = new ArrayList<>(); try { - toutesLesCotisations = retryService.executeWithRetrySupplier( - () -> cotisationService.listerToutes(0, 1000), - "chargement des cotisations" - ); + if (membreConnecteId != null) { + // Vue membre — uniquement ses propres cotisations + toutesLesCotisations = retryService.executeWithRetrySupplier( + () -> cotisationService.obtenirParMembre(membreConnecteId, 0, 200), + "chargement des cotisations du membre" + ); + } else { + // Vue admin/trésorier — toutes les cotisations + toutesLesCotisations = retryService.executeWithRetrySupplier( + () -> cotisationService.listerToutes(0, 1000), + "chargement de toutes les cotisations" + ); + } LOG.infof("Cotisations chargées: %d cotisations", toutesLesCotisations.size()); } catch (Exception e) { LOG.errorf(e, "Erreur lors du chargement des cotisations"); @@ -201,52 +284,40 @@ public class CotisationsBean implements Serializable { */ private void chargerRepartitionMethodes() { repartitionMethodes = new ArrayList<>(); - // Note: CotisationResponse n'a pas de champ methodePaiement - // Cette fonctionnalité est temporairement désactivée - /* try { - // Calculer le total des paiements BigDecimal totalPaiements = toutesLesCotisations.stream() - .filter(c -> ("PAYEE".equals(c.getStatut()) || "PARTIELLEMENT_PAYEE".equals(c.getStatut()))) + .filter(c -> "PAYEE".equals(c.getStatut()) || "PARTIELLEMENT_PAYEE".equals(c.getStatut())) .map(c -> c.getMontantPaye() != null ? c.getMontantPaye() : BigDecimal.ZERO) .reduce(BigDecimal.ZERO, BigDecimal::add); - if (totalPaiements.compareTo(BigDecimal.ZERO) == 0) { - return; // Pas de paiements - } + if (totalPaiements.compareTo(BigDecimal.ZERO) == 0) return; - // Grouper par méthode de paiement + // Grouper par modePaiementLibelle (champ disponible dans CotisationResponse) Map parMethode = toutesLesCotisations.stream() - .filter(c -> ("PAYEE".equals(c.getStatut()) || "PARTIELLEMENT_PAYEE".equals(c.getStatut()))) + .filter(c -> "PAYEE".equals(c.getStatut()) || "PARTIELLEMENT_PAYEE".equals(c.getStatut())) .collect(Collectors.groupingBy( - c -> "N/A", // methodePaiement non disponible + c -> c.getModePaiementLibelle() != null ? c.getModePaiementLibelle() : "Non défini", Collectors.reducing(BigDecimal.ZERO, c -> c.getMontantPaye() != null ? c.getMontantPaye() : BigDecimal.ZERO, BigDecimal::add))); - // Créer les objets RepartitionMethode for (Map.Entry entry : parMethode.entrySet()) { - String methode = entry.getKey(); BigDecimal montant = entry.getValue(); double pourcentage = montant.multiply(BigDecimal.valueOf(100)) .divide(totalPaiements, 2, java.math.RoundingMode.HALF_UP) .doubleValue(); - - RepartitionMethode repartition = new RepartitionMethode(); - repartition.setMethode(getMethodeLibelle(methode)); - repartition.setMontant(montant); - repartition.setPourcentage(pourcentage); - repartition.setCouleur(getCouleurMethode(methode)); - repartition.setIcon(getIconMethode(methode)); - repartitionMethodes.add(repartition); + RepartitionMethode r = new RepartitionMethode(); + r.setMethode(entry.getKey()); + r.setMontant(montant); + r.setPourcentage(pourcentage); + r.setCouleur("primary-500"); + r.setIcon("pi pi-credit-card"); + repartitionMethodes.add(r); } - - // Trier par montant décroissant repartitionMethodes.sort((a, b) -> b.getMontant().compareTo(a.getMontant())); } catch (Exception e) { - LOG.errorf(e, "Erreur lors du calcul de la répartition des méthodes"); + LOG.warnf("Impossible de calculer la répartition des méthodes: %s", e.getMessage()); } - */ } /** @@ -262,6 +333,7 @@ public class CotisationsBean implements Serializable { for (CotisationResponse cotisation : enRetard) { RappelCotisation rappel = new RappelCotisation(); + rappel.setMembreId(cotisation.getMembreId()); rappel.setNomMembre(cotisation.getNomMembre()); rappel.setClub(cotisation.getNomOrganisation()); // Corrigé: getNomOrganisation au lieu de getNomAssociation rappel.setMontantDu(cotisation.getMontantDu()); @@ -346,10 +418,11 @@ public class CotisationsBean implements Serializable { */ private void appliquerFiltres() { try { - // Utiliser la recherche backend au lieu du filtrage côté client + // Utiliser la recherche backend ; restreindre au membre connecté si vue membre + final UUID filtreMembreId = membreConnecteId; // effectively final pour lambda cotisationsFiltrees = retryService.executeWithRetrySupplier( () -> cotisationService.rechercher( - null, // membreId - peut être ajouté si nécessaire + filtreMembreId, filtres.getStatut(), filtres.getTypeCotisation(), null, // annee @@ -452,127 +525,196 @@ public class CotisationsBean implements Serializable { } /** - * Marque une cotisation comme payée via le backend (DRY/WOU - réutilise CotisationService) - * @param cotisation La cotisation à marquer comme payée (peut être null, utilise alors cotisationSelectionnee) + * Enregistre le paiement complet de la cotisation sélectionnée via PUT /{id}/payer. */ public void marquerCommePaye(CotisationResponse cotisation) { - // Si aucun paramètre, utiliser la cotisation sélectionnée (pour compatibilité avec la page paiement.xhtml) - final CotisationResponse cotisationFinale = (cotisation != null) ? cotisation : cotisationSelectionnee; - - if (cotisationFinale == null || cotisationFinale.getId() == null) { - errorHandler.showWarning("Attention", "Aucune cotisation sélectionnée"); - return; - } - - try { - // Note: CotisationResponse n'a pas de champ methodePaiement - // Mettre à jour les informations de paiement depuis le dialogue + this.cotisationSelectionnee = cotisation; + marquerCommePaye(); + } - cotisationFinale.setStatut("PAYEE"); - cotisationFinale.setMontantPaye(cotisationFinale.getMontantDu() != null ? cotisationFinale.getMontantDu() : BigDecimal.ZERO); - cotisationFinale.setDatePaiement(LocalDateTime.now()); - - // Appeler le backend pour persister (DRY/WOU - utilise CotisationService) - retryService.executeWithRetrySupplier( - () -> { - cotisationService.modifier(cotisationFinale.getId(), cotisationFinale); - return null; - }, - "marquage d'une cotisation comme payée" - ); - - // Recharger les données - chargerCotisations(); - chargerStatistiques(); - chargerRepartitionMethodes(); - appliquerFiltres(); - - LOG.infof("Cotisation marquée comme payée: %s", cotisationFinale.getId()); - errorHandler.showSuccess("Succès", "Cotisation marquée comme payée"); - } catch (Exception e) { - LOG.errorf(e, "Erreur lors du marquage de la cotisation"); - errorHandler.handleException(e, "lors du marquage d'une cotisation comme payée", null); - } - } - - /** - * Marque la cotisation sélectionnée comme payée (surcharge sans paramètre pour compatibilité XHTML) - */ public void marquerCommePaye() { - marquerCommePaye(cotisationSelectionnee); - } - - /** - * Enregistre un paiement partiel via le backend (DRY/WOU - réutilise CotisationService) - * @param montantPaye Le montant payé - * @param methodePaiement La méthode de paiement - * @param referencePaiement La référence du paiement - */ - public void enregistrerPaiementPartiel(BigDecimal montantPaye, String methodePaiement, String referencePaiement) { if (cotisationSelectionnee == null || cotisationSelectionnee.getId() == null) { errorHandler.showWarning("Attention", "Aucune cotisation sélectionnée"); return; } - - // Validation du montant - if (montantPaye == null || montantPaye.compareTo(BigDecimal.ZERO) <= 0) { - errorHandler.showWarning("Attention", "Le montant payé doit être supérieur à zéro"); - return; - } - - BigDecimal montantDu = cotisationSelectionnee.getMontantDu() != null - ? cotisationSelectionnee.getMontantDu() : BigDecimal.ZERO; - - if (montantPaye.compareTo(montantDu) > 0) { - errorHandler.showWarning("Attention", "Le montant payé ne peut pas être supérieur au montant dû"); - return; - } - try { - // Calculer le nouveau montant payé (additionner si paiement partiel précédent) - BigDecimal ancienMontantPaye = cotisationSelectionnee.getMontantPaye() != null - ? cotisationSelectionnee.getMontantPaye() : BigDecimal.ZERO; - BigDecimal nouveauMontantPaye = ancienMontantPaye.add(montantPaye); - - // Déterminer le statut - String nouveauStatut = nouveauMontantPaye.compareTo(montantDu) >= 0 - ? "PAYEE" : "PARTIELLEMENT_PAYEE"; - - cotisationSelectionnee.setStatut(nouveauStatut); - cotisationSelectionnee.setMontantPaye(nouveauMontantPaye); - cotisationSelectionnee.setMethodePaiement(methodePaiement); - cotisationSelectionnee.setReferencePaiement(referencePaiement); - cotisationSelectionnee.setDatePaiement(LocalDateTime.now()); - - // Appeler le backend pour persister (DRY/WOU - utilise CotisationService) + BigDecimal montant = cotisationSelectionnee.getMontantDu() != null + ? cotisationSelectionnee.getMontantDu() : BigDecimal.ZERO; + Map paiementData = new java.util.HashMap<>(); + paiementData.put("montantPaye", montant.toPlainString()); + paiementData.put("datePaiement", LocalDate.now().toString()); + if (methodePaiementDialog != null) paiementData.put("modePaiement", methodePaiementDialog); + if (referencePaiementDialog != null) paiementData.put("reference", referencePaiementDialog); + retryService.executeWithRetrySupplier( - () -> { - cotisationService.modifier(cotisationSelectionnee.getId(), cotisationSelectionnee); - return null; - }, - "enregistrement d'un paiement partiel" + () -> cotisationService.payer(cotisationSelectionnee.getId(), paiementData), + "marquage comme payée" ); - - // Recharger les données + chargerCotisations(); chargerStatistiques(); chargerRepartitionMethodes(); appliquerFiltres(); - - LOG.infof("Paiement partiel enregistré: %s - Montant: %s", cotisationSelectionnee.getId(), montantPaye); + LOG.infof("Cotisation payée: %s", cotisationSelectionnee.getId()); + errorHandler.showSuccess("Succès", "Paiement enregistré avec succès"); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors du paiement"); + errorHandler.handleException(e, "lors de l'enregistrement du paiement", null); + } + } + + /** + * Enregistre un paiement partiel via PUT /{id}/payer. + */ + public void enregistrerPaiementPartiel() { + if (cotisationSelectionnee == null || cotisationSelectionnee.getId() == null) { + errorHandler.showWarning("Attention", "Aucune cotisation sélectionnée"); + return; + } + if (montantPaiementPartiel == null || montantPaiementPartiel.compareTo(BigDecimal.ZERO) <= 0) { + errorHandler.showWarning("Attention", "Le montant doit être supérieur à zéro"); + return; + } + BigDecimal montantRestant = cotisationSelectionnee.getMontantRestant() != null + ? cotisationSelectionnee.getMontantRestant() + : cotisationSelectionnee.getMontantDu(); + if (montantPaiementPartiel.compareTo(montantRestant) > 0) { + errorHandler.showWarning("Attention", "Le montant ne peut pas dépasser le montant restant"); + return; + } + try { + Map paiementData = new java.util.HashMap<>(); + paiementData.put("montantPaye", montantPaiementPartiel.toPlainString()); + paiementData.put("datePaiement", LocalDate.now().toString()); + if (methodePaiementDialog != null) paiementData.put("modePaiement", methodePaiementDialog); + if (referencePaiementDialog != null) paiementData.put("reference", referencePaiementDialog); + + retryService.executeWithRetrySupplier( + () -> cotisationService.payer(cotisationSelectionnee.getId(), paiementData), + "paiement partiel" + ); + + chargerCotisations(); + chargerStatistiques(); + chargerRepartitionMethodes(); + appliquerFiltres(); + LOG.infof("Paiement partiel: %s — %s", cotisationSelectionnee.getId(), montantPaiementPartiel); errorHandler.showSuccess("Succès", "Paiement partiel enregistré"); } catch (Exception e) { - LOG.errorf(e, "Erreur lors de l'enregistrement du paiement partiel"); - errorHandler.handleException(e, "lors de l'enregistrement d'un paiement partiel", null); + LOG.errorf(e, "Erreur paiement partiel"); + errorHandler.handleException(e, "lors de l'enregistrement du paiement partiel", null); } } /** - * Sélectionne une cotisation pour afficher ses détails + * Sélectionne une cotisation et réinitialise les champs du dialogue de paiement. */ public void selectionnerCotisation(CotisationResponse cotisation) { this.cotisationSelectionnee = cotisation; + this.methodePaiementDialog = null; + this.referencePaiementDialog = null; + this.observationsDialog = null; + this.montantPaiementPartiel = cotisation != null ? cotisation.getMontantRestant() : null; + // Réinitialiser l'état Wave + this.waveIntentionId = null; + this.waveLaunchUrl = null; + this.waveStatut = null; + this.waveDialogOuvert = false; } + + // ── Paiement Wave QR ────────────────────────────────────────────────────── + + /** + * Initie un paiement Wave pour la cotisation sélectionnée. + * Appelle POST /api/paiements/initier-paiement-en-ligne sans numéro de téléphone (QR web). + * Le backend crée une session Wave Checkout et retourne le wave_launch_url. + * Le dialog QR s'ouvre ensuite via oncomplete JS. + */ + /** + * Sélectionne la cotisation ET initie le paiement Wave en une seule action + * (pour le bouton "Wave QR" de la DataTable). + */ + public void initierPaiementWavePour(CotisationResponse cotisation) { + selectionnerCotisation(cotisation); + initierPaiementWave(); + } + + public void initierPaiementWave() { + if (cotisationSelectionnee == null || cotisationSelectionnee.getId() == null) { + errorHandler.showWarning("Attention", "Sélectionnez d'abord une cotisation"); + return; + } + try { + InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder() + .cotisationId(cotisationSelectionnee.getId()) + .methodePaiement("WAVE") + // Pas de numéro de téléphone → QR non restreint (tout abonné Wave peut payer) + .build(); + + PaiementGatewayResponse response = retryService.executeWithRetrySupplier( + () -> paiementClientService.initierPaiementEnLigne(request), + "initiation paiement Wave" + ); + + this.waveLaunchUrl = response.getWaveLaunchUrl(); + this.waveIntentionId = response.getClientReference() != null + ? UUID.fromString(response.getClientReference()) : null; + this.waveStatut = "EN_COURS"; + this.waveDialogOuvert = true; + + LOG.infof("Wave QR initié: intention=%s, url=%s", waveIntentionId, waveLaunchUrl); + + } catch (Exception e) { + LOG.errorf(e, "Erreur initiation Wave QR"); + errorHandler.handleException(e, "lors de l'initiation du paiement Wave", null); + } + } + + /** + * Polling Wave — appelé par p:poll toutes les 3 secondes tant que le dialog est ouvert. + * Vérifie le statut côté backend ; si COMPLETEE, recharge les cotisations et ferme le dialog. + */ + public void pollStatutWave() { + if (!waveDialogOuvert || waveIntentionId == null) return; + if ("COMPLETEE".equals(waveStatut) || "EXPIREE".equals(waveStatut) || "ECHOUEE".equals(waveStatut)) return; + + try { + IntentionStatutResponse statut = paiementClientService.getStatutIntention(waveIntentionId); + this.waveStatut = statut.statut(); + + if ("COMPLETEE".equals(waveStatut)) { + waveDialogOuvert = false; + chargerCotisations(); + chargerStatistiques(); + chargerRepartitionMethodes(); + appliquerFiltres(); + errorHandler.showSuccess("Paiement confirmé !", + "Votre paiement Wave a été enregistré avec succès."); + LOG.infof("Wave QR complété: intention=%s", waveIntentionId); + + } else if ("EXPIREE".equals(waveStatut) || "ECHOUEE".equals(waveStatut)) { + waveDialogOuvert = false; + errorHandler.showWarning("Paiement annulé", + "La session Wave a expiré. Veuillez recommencer."); + } + } catch (Exception e) { + LOG.warnf(e, "Erreur polling Wave — sera retentée au prochain tick"); + } + } + + /** Ferme le dialog Wave QR et stoppe le polling. */ + public void annulerPaiementWave() { + this.waveDialogOuvert = false; + this.waveIntentionId = null; + this.waveLaunchUrl = null; + this.waveStatut = null; + } + + // Getters Wave + public String getWaveLaunchUrl() { return waveLaunchUrl; } + public String getWaveStatut() { return waveStatut; } + public boolean isWaveDialogOuvert() { return waveDialogOuvert; } + public UUID getWaveIntentionId() { return waveIntentionId; } /** * Envoie un rappel pour une cotisation (DRY/WOU - réutilise NotificationClientService) @@ -590,12 +732,9 @@ public class CotisationsBean implements Serializable { retryService.executeWithRetrySupplier( () -> { - notificationService.envoyerNotificationGroupe( - "RAPPEL_COTISATION", - "Rappel de cotisation", - message, - List.of(cotisation.getMembreId().toString()) - ); + notificationService.envoyerNotificationGroupe(buildNotifRequest( + "Rappel de cotisation", message, + List.of(cotisation.getMembreId().toString()))); return null; }, "envoi d'un rappel de cotisation" @@ -609,6 +748,34 @@ public class CotisationsBean implements Serializable { } } + /** + * Envoie un rappel depuis un objet RappelCotisation (vue relances) + */ + public void envoyerRappelPourRappel(RappelCotisation rappel) { + if (rappel == null || rappel.getMembreId() == null) { + errorHandler.showWarning("Attention", "Membre introuvable pour ce rappel"); + return; + } + try { + String message = "Rappel: Votre cotisation de " + formatMontant(rappel.getMontantDu()) + + " est en attente de paiement."; + retryService.executeWithRetrySupplier( + () -> { + notificationService.envoyerNotificationGroupe(buildNotifRequest( + "Rappel de cotisation", message, + List.of(rappel.getMembreId().toString()))); + return null; + }, + "envoi d'un rappel de cotisation" + ); + LOG.infof("Rappel envoyé à: %s", rappel.getNomMembre()); + errorHandler.showSuccess("Rappel", "Rappel envoyé à " + rappel.getNomMembre()); + } catch (Exception e) { + LOG.errorf(e, "Erreur envoi rappel"); + errorHandler.handleException(e, "lors de l'envoi d'un rappel", null); + } + } + /** * Envoie des rappels groupés */ @@ -631,12 +798,10 @@ public class CotisationsBean implements Serializable { retryService.executeWithRetrySupplier( () -> { - notificationService.envoyerNotificationGroupe( - "RAPPEL_COTISATION", + notificationService.envoyerNotificationGroupe(buildNotifRequest( "Rappel de paiement", "Vous avez des cotisations en attente. Montant total: " + formatMontant(montantTotal), - destinataires - ); + destinataires)); return null; }, "envoi de rappels groupés de cotisation" @@ -736,6 +901,14 @@ public class CotisationsBean implements Serializable { if (montant == null) return "0 FCFA"; return String.format("%,.0f FCFA", montant.doubleValue()); } + + private Map buildNotifRequest(String sujet, String corps, List ids) { + Map req = new HashMap<>(); + req.put("sujet", sujet); + req.put("corps", corps); + req.put("membreIds", ids.stream().map(UUID::fromString).collect(Collectors.toList())); + return req; + } /** * Actualise les données depuis le backend @@ -796,13 +969,25 @@ public class CotisationsBean implements Serializable { this.cotisationsSelectionnees = cotisationsSelectionnees; } - public CotisationResponse getCotisationSelectionnee() { - return cotisationSelectionnee; + public CotisationResponse getCotisationSelectionnee() { + return cotisationSelectionnee; } - - public void setCotisationSelectionnee(CotisationResponse cotisationSelectionnee) { - this.cotisationSelectionnee = cotisationSelectionnee; + + public void setCotisationSelectionnee(CotisationResponse cotisationSelectionnee) { + this.cotisationSelectionnee = cotisationSelectionnee; } + + public String getMethodePaiementDialog() { return methodePaiementDialog; } + public void setMethodePaiementDialog(String v) { this.methodePaiementDialog = v; } + + public String getReferencePaiementDialog() { return referencePaiementDialog; } + public void setReferencePaiementDialog(String v) { this.referencePaiementDialog = v; } + + public String getObservationsDialog() { return observationsDialog; } + public void setObservationsDialog(String v) { this.observationsDialog = v; } + + public BigDecimal getMontantPaiementPartiel() { return montantPaiementPartiel; } + public void setMontantPaiementPartiel(BigDecimal v) { this.montantPaiementPartiel = v; } public NouvelleCotisation getNouvelleCotisation() { return nouvelleCotisation; @@ -1063,14 +1248,18 @@ public class CotisationsBean implements Serializable { */ public static class RappelCotisation implements Serializable { private static final long serialVersionUID = 1L; - + + private UUID membreId; private String nomMembre; private String club; private BigDecimal montantDu; private int joursRetard; private String priorite; - + // Getters et setters + public UUID getMembreId() { return membreId; } + public void setMembreId(UUID membreId) { this.membreId = membreId; } + public String getNomMembre() { return nomMembre; } public void setNomMembre(String nomMembre) { this.nomMembre = nomMembre; } 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 6118581..d5617f5 100644 --- a/src/main/java/dev/lions/unionflow/client/view/CotisationsGestionBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/CotisationsGestionBean.java @@ -7,15 +7,17 @@ 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.organisation.response.OrganisationSummaryResponse; 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.OrganisationService; import dev.lions.unionflow.client.service.MembreService; import dev.lions.unionflow.client.service.WaveService; import dev.lions.unionflow.client.service.NotificationClientService; import dev.lions.unionflow.client.service.ExportClientService; +import org.primefaces.PrimeFaces; import jakarta.enterprise.context.SessionScoped; import jakarta.inject.Inject; import jakarta.inject.Named; @@ -62,7 +64,7 @@ public class CotisationsGestionBean implements Serializable { @Inject @RestClient - private AssociationService associationService; + private OrganisationService organisationService; @Inject @RestClient @@ -105,7 +107,7 @@ public class CotisationsGestionBean implements Serializable { private String montantImpayes; private int joursRetardMoyen; private String revenus2024; - private String croissanceAnnuelle; + private BigDecimal croissanceAnnuelle; private String prelevementsActifs; private String montantPrelevementsPrevu; @@ -124,6 +126,13 @@ public class CotisationsGestionBean implements Serializable { private List cotisationsFiltrees; private List cotisationsSelectionnees; private String montantTotalSelectionne; + private CotisationResponse cotisationDetail; + // Dialog paiement + private CotisationResponse cotisationAPayer; + private BigDecimal montantPaiement; + private String modePaiement; + private LocalDate datePaiement; + private String referencePaiement; // Wave Money private int membresPrelevementActif; @@ -190,14 +199,28 @@ public class CotisationsGestionBean implements Serializable { this.tauxRecouvrement = BigDecimal.valueOf(tauxPaiement); this.totalMembresActifs = totalCotisations.intValue(); this.montantCollecte = formatMontant(totalCollecte); - this.objectifMensuel = formatMontant(totalCollecte.multiply(new BigDecimal("1.15"))); + // Objectif mensuel = moyenne des montants dus sur les 3 derniers mois + int moisCourant = LocalDate.now().getMonthValue(); + int anneeCourante = LocalDate.now().getYear(); + BigDecimal moyenneMensuelle = cotisationsDTO.stream() + .filter(c -> c.getAnnee() != null && c.getAnnee() == anneeCourante) + .filter(c -> c.getMois() != null && c.getMois() >= Math.max(1, moisCourant - 2) && c.getMois() <= moisCourant) + .map(c -> c.getMontantDu() != null ? c.getMontantDu() : BigDecimal.ZERO) + .reduce(BigDecimal.ZERO, BigDecimal::add); + this.objectifMensuel = formatMontant(moyenneMensuelle); this.progressionMensuelle = tauxPaiement.intValue(); this.membresAJour = String.valueOf(cotisationsPayees); this.pourcentageMembresAJour = tauxPaiement.intValue(); this.montantEnAttente = formatMontant(montantAttente); this.nombreCotisationsEnAttente = (int) enAttente; this.montantImpayes = formatMontant(montantImpayes); - this.revenus2024 = formatMontant(totalCollecte.multiply(new BigDecimal("12"))); + // Revenus année courante = somme des montants réellement payés (statut PAYEE ou PARTIELLEMENT_PAYEE) + BigDecimal revenusAnneeCourante = cotisationsDTO.stream() + .filter(c -> c.getAnnee() != null && c.getAnnee() == anneeCourante) + .filter(c -> "PAYEE".equals(c.getStatut()) || "PARTIELLEMENT_PAYEE".equals(c.getStatut())) + .map(c -> c.getMontantPaye() != null ? c.getMontantPaye() : BigDecimal.ZERO) + .reduce(BigDecimal.ZERO, BigDecimal::add); + this.revenus2024 = formatMontant(revenusAnneeCourante); this.croissanceAnnuelle = calculerCroissanceAnnuelle(cotisationsDTO); // Charger les informations Wave @@ -226,7 +249,7 @@ public class CotisationsGestionBean implements Serializable { this.montantImpayes = "0 FCFA"; this.joursRetardMoyen = 0; this.revenus2024 = "0 FCFA"; - this.croissanceAnnuelle = "0%"; + this.croissanceAnnuelle = BigDecimal.ZERO; this.prelevementsActifs = "0"; this.montantPrelevementsPrevu = "0"; this.membresPrelevementActif = 0; @@ -237,7 +260,7 @@ public class CotisationsGestionBean implements Serializable { /** * Calcule la croissance annuelle depuis les données historiques */ - private String calculerCroissanceAnnuelle(List cotisationsDTO) { + private BigDecimal calculerCroissanceAnnuelle(List cotisationsDTO) { try { int anneeActuelle = LocalDate.now().getYear(); int anneePrecedente = anneeActuelle - 1; @@ -255,15 +278,14 @@ public class CotisationsGestionBean implements Serializable { .reduce(BigDecimal.ZERO, BigDecimal::add); if (montantAnneePrecedente.compareTo(BigDecimal.ZERO) > 0) { - BigDecimal croissance = montantAnneeActuelle.subtract(montantAnneePrecedente) + return montantAnneeActuelle.subtract(montantAnneePrecedente) .multiply(BigDecimal.valueOf(100)) .divide(montantAnneePrecedente, 1, java.math.RoundingMode.HALF_UP); - return (croissance.compareTo(BigDecimal.ZERO) >= 0 ? "+" : "") + croissance + "%"; } - return montantAnneeActuelle.compareTo(BigDecimal.ZERO) > 0 ? "+100%" : "0%"; + return montantAnneeActuelle.compareTo(BigDecimal.ZERO) > 0 ? BigDecimal.valueOf(100) : BigDecimal.ZERO; } catch (Exception e) { LOG.errorf(e, "Erreur calcul croissance"); - return "N/A"; + return BigDecimal.ZERO; } } @@ -305,16 +327,33 @@ public class CotisationsGestionBean implements Serializable { return String.format("%,.0f FCFA", montant.doubleValue()); } + /** + * Construit le body attendu par POST /api/notifications/groupees. + * @param sujet titre de la notification + * @param corps corps du message + * @param ids liste des IDs membres (chaînes UUID) + */ + private Map buildNotificationRequest(String sujet, String corps, List ids) { + List membreIds = ids.stream() + .map(UUID::fromString) + .collect(Collectors.toList()); + Map req = new HashMap<>(); + req.put("sujet", sujet); + req.put("corps", corps); + req.put("membreIds", membreIds); + return req; + } + private void initializeFiltres() { this.filtres = new FiltresCotisations(); this.listeOrganisations = new ArrayList<>(); try { - PagedResponse response = associationService.listerToutes(0, 1000); + PagedResponse response = organisationService.listerToutes(0, 1000); if (response != null && response.getData() != null) { - for (OrganisationResponse assoc : response.getData()) { + for (OrganisationSummaryResponse assoc : response.getData()) { Organisation org = new Organisation(); - org.setId(assoc.getId()); - org.setNom(assoc.getNom()); + org.setId(assoc.id()); + org.setNom(assoc.nom()); listeOrganisations.add(org); } } @@ -345,14 +384,14 @@ public class CotisationsGestionBean implements Serializable { private void chargerTopOrganisations() { this.topOrganisations = new ArrayList<>(); try { - PagedResponse response = associationService.listerToutes(0, 1000); - List associations = response != null && response.getData() != null ? response.getData() + PagedResponse response = organisationService.listerToutes(0, 1000); + List associations = response != null && response.getData() != null ? response.getData() : new ArrayList<>(); List cotisationsDTO = cotisationService.listerToutes(0, 1000); - for (OrganisationResponse assoc : associations.stream().limit(5).collect(Collectors.toList())) { + for (OrganisationSummaryResponse assoc : associations.stream().limit(5).collect(Collectors.toList())) { List cotisationsOrg = cotisationsDTO.stream() - .filter(c -> c.getOrganisationId() != null && c.getOrganisationId().equals(assoc.getId())) + .filter(c -> c.getOrganisationId() != null && c.getOrganisationId().equals(assoc.id())) .collect(Collectors.toList()); long total = cotisationsOrg.size(); @@ -365,7 +404,7 @@ public class CotisationsGestionBean implements Serializable { .reduce(BigDecimal.ZERO, BigDecimal::add); OrganisationPerformante org = new OrganisationPerformante(); - org.setNom(assoc.getNom()); + org.setNom(assoc.nom()); org.setTauxRecouvrement(taux); org.setMontantCollecte(formatMontant(montantCollecte)); org.setNombreMembresAJour((int) payees); @@ -470,8 +509,8 @@ public class CotisationsGestionBean implements Serializable { } else if (nouvelleCampagne.getScope() != null && !nouvelleCampagne.getScope().isEmpty()) { // Membres d'une association spécifique try { - UUID associationId = UUID.fromString(nouvelleCampagne.getScope()); - membres = membreService.listerParAssociation(associationId); + UUID organisationId = UUID.fromString(nouvelleCampagne.getScope()); + membres = membreService.listerParOrganisation(organisationId); } catch (IllegalArgumentException e) { membres = membreService.listerActifs(); } @@ -479,6 +518,10 @@ public class CotisationsGestionBean implements Serializable { int cotisationsCreees = 0; for (MembreResponse membre : membres) { + if (membre.getOrganisationId() == null) { + LOG.warnf("Membre %s ignoré (pas d'organisation associée)", membre.getId()); + continue; + } CreateCotisationRequest request = CreateCotisationRequest.builder() .membreId(membre.getId()) .organisationId(membre.getOrganisationId()) @@ -504,14 +547,13 @@ public class CotisationsGestionBean implements Serializable { .collect(Collectors.toList()); try { - notificationService.envoyerNotificationGroupe( - "RAPPEL_COTISATION", + notificationService.envoyerNotificationGroupe(buildNotificationRequest( "Nouvelle cotisation: " + nouvelleCampagne.getNom(), "Une nouvelle cotisation de " + formatMontant(nouvelleCampagne.getMontant()) + " est due pour le " + nouvelleCampagne.getDateEcheance() .format(DateTimeFormatter.ofPattern("dd/MM/yyyy")), - destinataires); + destinataires)); } catch (Exception e) { LOG.warnf(e, "Impossible d'envoyer les notifications"); } @@ -558,13 +600,12 @@ public class CotisationsGestionBean implements Serializable { retryService.executeWithRetrySupplier( () -> { - notificationService.envoyerNotificationGroupe( - "RAPPEL_COTISATION", + notificationService.envoyerNotificationGroupe(buildNotificationRequest( "Rappel: Cotisation(s) en retard", "Vous avez des cotisations en retard. Montant total dû: " + formatMontant(montantTotalRetard) + ". Veuillez régulariser votre situation.", - destinataires); + destinataires)); return null; }, "envoi de relances groupées"); @@ -608,17 +649,17 @@ public class CotisationsGestionBean implements Serializable { public void appliquerFiltres() { try { // Trouver l'ID de l'organisation - UUID associationId = null; + UUID organisationId = null; if (filtres.getOrganisation() != null && !filtres.getOrganisation().isEmpty()) { for (Organisation org : listeOrganisations) { if (org.getId().toString().equals(filtres.getOrganisation())) { - associationId = org.getId(); + organisationId = org.getId(); break; } } } - final UUID associationIdFinal = associationId; // Variable final pour la lambda + final UUID organisationIdFinal = organisationId; // Variable final pour la lambda // Utiliser la recherche backend cotisationsFiltrees = cotisationService.rechercher( @@ -631,9 +672,9 @@ public class CotisationsGestionBean implements Serializable { 1000); // Filtrer par association si nécessaire - if (associationIdFinal != null) { + if (organisationIdFinal != null) { cotisationsFiltrees = cotisationsFiltrees.stream() - .filter(c -> c.getOrganisationId() != null && c.getOrganisationId().equals(associationIdFinal)) + .filter(c -> c.getOrganisationId() != null && c.getOrganisationId().equals(organisationIdFinal)) .collect(Collectors.toList()); } @@ -720,29 +761,37 @@ public class CotisationsGestionBean implements Serializable { /** * Enregistre un paiement pour une cotisation via le backend */ - public void enregistrerPaiement(CotisationResponse cotisation) { - if (cotisation == null) { - return; - } + public void ouvrirDialogPaiement(CotisationResponse cotisation) { + if (cotisation == null) return; + this.cotisationAPayer = cotisation; + this.montantPaiement = cotisation.getMontantDu(); + this.modePaiement = "ESPECES"; + this.datePaiement = LocalDate.now(); + this.referencePaiement = ""; + PrimeFaces.current().executeScript("PF('dlgPaiement').show();"); + } + public void confirmerPaiement() { + if (cotisationAPayer == null) return; try { - cotisation.setStatut("PAYEE"); - cotisation.setMontantPaye(cotisation.getMontantDu()); - cotisation.setDatePaiement(LocalDateTime.now()); + Map data = new HashMap<>(); + data.put("montantPaye", montantPaiement != null ? montantPaiement : cotisationAPayer.getMontantDu()); + data.put("datePaiement", datePaiement != null ? datePaiement.toString() : LocalDate.now().toString()); + data.put("modePaiement", modePaiement != null ? modePaiement : "ESPECES"); + data.put("reference", referencePaiement != null ? referencePaiement : ""); retryService.executeWithRetrySupplier( - () -> { - cotisationService.modifier(cotisation.getId(), cotisation); - return null; - }, + () -> cotisationService.payer(cotisationAPayer.getId(), data), "enregistrement d'un paiement"); chargerCotisations(); chargerKPIs(); appliquerFiltres(); - LOG.infof("Paiement enregistré pour: %s", cotisation.getNumeroMembre()); - errorHandler.showSuccess("Succès", "Paiement enregistré"); + PrimeFaces.current().executeScript("PF('dlgPaiement').hide();"); + LOG.infof("Paiement enregistré pour: %s", cotisationAPayer.getNumeroMembre()); + errorHandler.showSuccess("Succès", "Paiement enregistré avec succès"); + cotisationAPayer = null; } catch (Exception e) { LOG.errorf(e, "Erreur lors de l'enregistrement du paiement"); errorHandler.handleException(e, "lors de l'enregistrement d'un paiement", null); @@ -797,11 +846,10 @@ public class CotisationsGestionBean implements Serializable { retryService.executeWithRetrySupplier( () -> { - notificationService.envoyerNotificationGroupe( - "RAPPEL_COTISATION", + notificationService.envoyerNotificationGroupe(buildNotificationRequest( "Rappel de cotisation", messageFinal, - List.of(cotisation.getMembreId().toString())); + List.of(cotisation.getMembreId().toString()))); return null; }, "envoi d'un rappel de cotisation"); @@ -817,10 +865,29 @@ public class CotisationsGestionBean implements Serializable { * Affiche les détails d'une cotisation */ public void voirDetails(CotisationResponse cotisation) { - // Navigation vers la page de détails - LOG.infof("Affichage détails pour: %s", cotisation.getNumeroMembre()); + this.cotisationDetail = cotisation; + PrimeFaces.current().executeScript("PF('dlgDetailCotisation').show();"); } + public CotisationResponse getCotisationDetail() { + return cotisationDetail; + } + + public void setCotisationDetail(CotisationResponse cotisationDetail) { + this.cotisationDetail = cotisationDetail; + } + + public CotisationResponse getCotisationAPayer() { return cotisationAPayer; } + public void setCotisationAPayer(CotisationResponse c) { this.cotisationAPayer = c; } + public BigDecimal getMontantPaiement() { return montantPaiement; } + public void setMontantPaiement(BigDecimal montantPaiement) { this.montantPaiement = montantPaiement; } + public String getModePaiement() { return modePaiement; } + public void setModePaiement(String modePaiement) { this.modePaiement = modePaiement; } + public LocalDate getDatePaiement() { return datePaiement; } + public void setDatePaiement(LocalDate datePaiement) { this.datePaiement = datePaiement; } + public String getReferencePaiement() { return referencePaiement; } + public void setReferencePaiement(String referencePaiement) { this.referencePaiement = referencePaiement; } + // Actions groupées /** @@ -835,14 +902,13 @@ public class CotisationsGestionBean implements Serializable { try { int compteur = 0; for (CotisationResponse cotisation : cotisationsSelectionnees) { - cotisation.setStatut("PAYEE"); - cotisation.setMontantPaye(cotisation.getMontantDu()); - cotisation.setDatePaiement(LocalDateTime.now()); retryService.executeWithRetrySupplier( - () -> { - cotisationService.modifier(cotisation.getId(), cotisation); - return null; - }, + () -> cotisationService.payer(cotisation.getId(), Map.of( + "montantPaye", cotisation.getMontantDu(), + "datePaiement", LocalDateTime.now().toLocalDate().toString(), + "modePaiement", "MANUEL", + "reference", cotisation.getNumeroReference() != null + ? cotisation.getNumeroReference() : "")), "marquage d'une cotisation comme payée"); compteur++; } @@ -885,12 +951,11 @@ public class CotisationsGestionBean implements Serializable { retryService.executeWithRetrySupplier( () -> { - notificationService.envoyerNotificationGroupe( - "RAPPEL_COTISATION", + notificationService.envoyerNotificationGroupe(buildNotificationRequest( "Rappel de paiement", "Vous avez des cotisations en attente de paiement. Montant: " + formatMontant(montantTotal), - destinataires); + destinataires)); return null; }, "envoi de relances groupées"); @@ -1013,15 +1078,8 @@ public class CotisationsGestionBean implements Serializable { "création d'une session de paiement Wave"); if (session != null && session.getWaveSessionId() != null) { - // Mettre à jour la cotisation avec l'ID de session Wave - cotisation.setWaveSessionId(session.getWaveSessionId()); - cotisation.setMethodePaiement("WAVE_MONEY"); - retryService.executeWithRetrySupplier( - () -> { - cotisationService.modifier(cotisation.getId(), cotisation); - return null; - }, - "mise à jour d'une cotisation avec session Wave"); + // La session Wave est liée côté backend via IntentionPaiement. + // Pas de mise à jour de la cotisation ici (waveSessionId non géré par UpdateCotisationRequest). prelevementsLances++; } } catch (Exception e) { @@ -1179,18 +1237,18 @@ public class CotisationsGestionBean implements Serializable { int annee = LocalDate.now().getYear(); int mois = LocalDate.now().getMonthValue(); - UUID associationIdTemp = null; + UUID organisationIdTemp = null; if (filtres.getOrganisation() != null && !filtres.getOrganisation().isEmpty()) { try { - associationIdTemp = UUID.fromString(filtres.getOrganisation()); + organisationIdTemp = UUID.fromString(filtres.getOrganisation()); } catch (IllegalArgumentException e) { // Ignorer si pas un UUID valide } } - final UUID associationIdFinal = associationIdTemp; + final UUID organisationIdFinal = organisationIdTemp; byte[] rapport = retryService.executeWithRetrySupplier( - () -> exportService.genererRapportMensuel(annee, mois, associationIdFinal), + () -> exportService.genererRapportMensuel(annee, mois, organisationIdFinal), "génération d'un rapport mensuel"); telechargerFichier(rapport, "rapport-mensuel-" + annee + "-" + String.format("%02d", mois) + ".txt", @@ -1372,11 +1430,11 @@ public class CotisationsGestionBean implements Serializable { this.revenus2024 = revenus2024; } - public String getCroissanceAnnuelle() { + public BigDecimal getCroissanceAnnuelle() { return croissanceAnnuelle; } - public void setCroissanceAnnuelle(String croissanceAnnuelle) { + public void setCroissanceAnnuelle(BigDecimal croissanceAnnuelle) { this.croissanceAnnuelle = croissanceAnnuelle; } 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 5aaea6d..9c07ced 100644 --- a/src/main/java/dev/lions/unionflow/client/view/DashboardBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/DashboardBean.java @@ -148,14 +148,29 @@ public class DashboardBean implements Serializable { * 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()) { + if (menuBean == null) return; + // SUPER_ADMIN → tableau de bord dédié avec vue globale toutes organisations + if (menuBean.isSuperAdmin()) { try { FacesContext ctx = FacesContext.getCurrentInstance(); String redirectUrl = ctx.getExternalContext().getRequestContextPath() + - "/pages/secure/dashboard-membre.xhtml?faces-redirect=true"; + "/pages/super-admin/dashboard.xhtml"; + ctx.getExternalContext().redirect(redirectUrl); + ctx.responseComplete(); + return; + } catch (IOException e) { + LOG.error("Erreur lors de la redirection vers le dashboard super-admin", e); + } + } + // MEMBRE_ACTIF sans rôle fonctionnel → tableau de bord personnel + if (menuBean.isMembreActif() && + !menuBean.isSecretaire() && !menuBean.isTresorier() && + !menuBean.isResponsableSocial() && !menuBean.isResponsableEvenements() && + !menuBean.isAdminOrganisation()) { + try { + FacesContext ctx = FacesContext.getCurrentInstance(); + String redirectUrl = ctx.getExternalContext().getRequestContextPath() + + "/pages/secure/dashboard-membre.xhtml"; ctx.getExternalContext().redirect(redirectUrl); ctx.responseComplete(); } catch (IOException e) { @@ -187,14 +202,19 @@ public class DashboardBean implements Serializable { userId = null; } - // Si pas d'organisation/user, utiliser des valeurs par défaut ou retourner - if (organizationId == null || userId == null) { - LOG.warn("OrganizationId ou UserId manquant - utilisation de valeurs par défaut"); + // userId et organizationId sont obligatoires pour ce dashboard + // Le SUPER_ADMIN est redirigé vers /pages/super-admin/dashboard.xhtml (pas ce bean) + if (userId == null) { + LOG.warn("UserId manquant — utilisateur non initialisé, abandon du chargement dashboard"); initValuesParDefaut(); return; } - - // Appel unique au service Dashboard (DRY/WOU) + if (organizationId == null) { + LOG.debug("OrganisationId absent (SUPER_ADMIN) — ce dashboard ne s'applique pas, valeurs par défaut"); + initValuesParDefaut(); + return; + } + DashboardDataResponse dashboardData = retryService.executeWithRetrySupplier( () -> dashboardService.getDashboardData(organizationId, userId), "chargement des données du dashboard" @@ -251,7 +271,7 @@ public class DashboardBean implements Serializable { activeMembers = stats.getActiveMembers().intValue(); } if (stats.getTotalContributionAmount() != null) { - totalCotisations = String.format("%,d", stats.getTotalContributionAmount().longValue()); + totalCotisations = String.format("%,.0f", stats.getTotalContributionAmount()); tresorerie = totalCotisations; // Approximation } if (stats.getPendingRequests() != null) { @@ -263,7 +283,7 @@ public class DashboardBean implements Serializable { evenementsAPlanifier = upcomingEvents; } if (stats.getEngagementRate() != null) { - tauxParticipation = stats.getEngagementRate().intValue() * 100; // Convertir de 0-1 à 0-100 + tauxParticipation = (int)(stats.getEngagementRate() * 100); // engagementRate est 0-1 } // Note: Les champs suivants ne sont pas dans DashboardStatsResponse actuel // Ils seront chargés depuis d'autres sources ou laissés à 0 diff --git a/src/main/java/dev/lions/unionflow/client/view/DashboardMembreBean.java b/src/main/java/dev/lions/unionflow/client/view/DashboardMembreBean.java index b10a8ab..e53a6aa 100644 --- a/src/main/java/dev/lions/unionflow/client/view/DashboardMembreBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/DashboardMembreBean.java @@ -1,36 +1,30 @@ 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.server.api.dto.cotisation.response.CotisationResponse; import dev.lions.unionflow.client.api.dto.MembreDashboardResponse; +import dev.lions.unionflow.client.service.CotisationService; 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 org.primefaces.PrimeFaces; 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.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** - * 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 + * Bean de gestion du dashboard personnel pour les membres actifs (MEMBRE_ACTIF). */ @Named("dashboardMembreBean") @ViewScoped @@ -39,9 +33,6 @@ 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; @@ -52,44 +43,46 @@ public class DashboardMembreBean implements Serializable { @RestClient MembreDashboardRestClient dashboardClient; - // Informations personnelles du membre + @Inject + @RestClient + CotisationService cotisationService; + + // Informations personnelles 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 + // KPI cotisations + private String mesCotisationsPaiement = "0"; + private String statutCotisations = "Non disponible"; + private Integer tauxCotisationsPerso = null; - // 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 + // KPI épargne + private String monSoldeEpargne = "0"; + private String evolutionEpargne = "+0%"; + private String evolutionEpargneNombre = "0"; + private Integer objectifEpargne = null; - // É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 + // KPI événements + private Integer mesEvenementsInscrits = 0; + private Integer evenementsAVenir = 0; + private Integer tauxParticipationPerso = null; - // É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 + // KPI aides + private Integer mesDemandesAide = 0; + private Integer aidesEnCours = 0; + private Integer tauxAidesApprouvees = null; - // 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 + // Listes réelles + private List mesCotisations = new ArrayList<>(); + private List mesCotisationsEnAttente = new ArrayList<>(); - // Collections - private List historiqueCotisations = new ArrayList<>(); - private List mesNotifications = new ArrayList<>(); - private List evenementsPublics = new ArrayList<>(); + // Dialog paiement (même pattern que CotisationsGestionBean) + private CotisationResponse cotisationAPayer; + private BigDecimal montantPaiement; + private String modePaiement = "ESPECES"; + private LocalDate datePaiement; + private String referencePaiement; @PostConstruct public void init() { @@ -97,290 +90,150 @@ public class DashboardMembreBean implements Serializable { 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); + LOG.error("Erreur chargement KPI 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(',', ' '); + try { + List cotisations = cotisationService.getMesCotisations(0, 20); + this.mesCotisations = cotisations != null ? cotisations : new ArrayList<>(); + } catch (Exception e) { + LOG.error("Erreur chargement historique cotisations", e); + this.mesCotisations = new ArrayList<>(); + } + + try { + List enAttente = cotisationService.getMesCotisationsEnAttente(); + this.mesCotisationsEnAttente = enAttente != null ? enAttente : new ArrayList<>(); + } catch (Exception e) { + LOG.error("Erreur chargement cotisations en attente", e); + this.mesCotisationsEnAttente = new ArrayList<>(); + } } // ═══════════════════════════════════════════════════════════════════════ - // Actions + // Paiement (dialog — même pattern que CotisationsGestionBean) // ═══════════════════════════════════════════════════════════════════════ - public void payerCotisation() { + public void ouvrirDialogPaiement(CotisationResponse cotisation) { + if (cotisation == null) return; + this.cotisationAPayer = cotisation; + this.montantPaiement = cotisation.getMontantDu(); + this.modePaiement = "ESPECES"; + this.datePaiement = LocalDate.now(); + this.referencePaiement = ""; + PrimeFaces.current().executeScript("PF('dlgPaiementMembre').show();"); + } + + public void confirmerPaiement() { + if (cotisationAPayer == null) return; try { - // TODO: Rediriger vers la page de paiement des cotisations - LOG.info("Redirection vers paiement cotisation"); + Map data = new HashMap<>(); + data.put("montantPaye", montantPaiement != null ? montantPaiement : cotisationAPayer.getMontantDu()); + data.put("datePaiement", datePaiement != null ? datePaiement.toString() : LocalDate.now().toString()); + data.put("modePaiement", modePaiement != null ? modePaiement : "ESPECES"); + data.put("reference", referencePaiement != null ? referencePaiement : ""); + + retryService.executeWithRetrySupplier( + () -> cotisationService.payer(cotisationAPayer.getId(), data), + "paiement cotisation"); + + chargerDonneesPersonnelles(); + PrimeFaces.current().executeScript("PF('dlgPaiementMembre').hide();"); + errorHandler.showSuccess("Succès", "Paiement enregistré avec succès"); + cotisationAPayer = null; } catch (Exception e) { - errorHandler.handleException(e, "lors de la redirection", null); + LOG.errorf(e, "Erreur lors du paiement"); + errorHandler.handleException(e, "lors du paiement", 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); - } + // ═══════════════════════════════════════════════════════════════════════ + // Navigation + // ═══════════════════════════════════════════════════════════════════════ + + public String allerAuxCotisations() { + return "/pages/secure/membre/paiement-mes-cotisations.xhtml?faces-redirect=true"; } - 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 String inscrireEvenement() { + return "/pages/secure/evenement/calendrier.xhtml?faces-redirect=true"; } - 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 String demanderAide() { + return "/pages/secure/aide/demande.xhtml?faces-redirect=true"; } - 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 String allerAMonProfil() { + return "/pages/secure/membre/profil.xhtml?faces-redirect=true"; } - 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); - } + public String allerAuxEvenements() { + return "/pages/secure/evenement/calendrier.xhtml?faces-redirect=true"; } // ═══════════════════════════════════════════════════════════════════════ // 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 ""; + private String formatMontant(BigDecimal montant) { + if (montant == null) return "0"; + return String.format("%,d", montant.longValue()).replace(',', ' '); } // ═══════════════════════════════════════════════════════════════════════ - // DTOs internes + // Getters / Setters // ═══════════════════════════════════════════════════════════════════════ - public static class CotisationPerso implements Serializable { - private static final long serialVersionUID = 1L; + 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 getMesCotisations() { return mesCotisations; } + public List getMesCotisationsEnAttente() { return mesCotisationsEnAttente; } - 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; - } + public CotisationResponse getCotisationAPayer() { return cotisationAPayer; } + public void setCotisationAPayer(CotisationResponse c) { this.cotisationAPayer = c; } + public BigDecimal getMontantPaiement() { return montantPaiement; } + public void setMontantPaiement(BigDecimal v) { this.montantPaiement = v; } + public String getModePaiement() { return modePaiement; } + public void setModePaiement(String v) { this.modePaiement = v; } + public LocalDate getDatePaiement() { return datePaiement; } + public void setDatePaiement(LocalDate v) { this.datePaiement = v; } + public String getReferencePaiement() { return referencePaiement; } + public void setReferencePaiement(String v) { this.referencePaiement = v; } } 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 5ff754d..abd875b 100644 --- a/src/main/java/dev/lions/unionflow/client/view/DemandesAideBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/DemandesAideBean.java @@ -38,12 +38,15 @@ public class DemandesAideBean implements Serializable { @Inject @RestClient private DemandeAideService demandeAideService; - + @Inject ErrorHandlerService errorHandler; - + @Inject RetryService retryService; + + @Inject + UserSession userSession; private List toutesLesDemandes; private List demandesFiltrees; @@ -340,8 +343,8 @@ public class DemandesAideBean implements Serializable { .description(nouvelleDemande.getDescription()) .montantDemande(nouvelleDemande.getMontantDemande()) .devise("XOF") - .membreDemandeurId(UUID.randomUUID()) // TODO: récupérer depuis session utilisateur - .associationId(UUID.randomUUID()) // TODO: récupérer depuis session utilisateur + .membreDemandeurId(userSession.getCurrentUser() != null ? userSession.getCurrentUser().getId() : null) + .associationId(userSession.getEntite() != null ? userSession.getEntite().getId() : null) .priorite(parsePrioriteAide(nouvelleDemande.getUrgence())) .localisation(localisationDTO) .dateLimite(nouvelleDemande.getDateLimite()) diff --git a/src/main/java/dev/lions/unionflow/client/view/DocumentBean.java b/src/main/java/dev/lions/unionflow/client/view/DocumentBean.java index b80df5f..f380821 100644 --- a/src/main/java/dev/lions/unionflow/client/view/DocumentBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/DocumentBean.java @@ -64,9 +64,13 @@ public class DocumentBean implements Serializable { public void chargerDocuments() { try { - // Note: Le backend ne fournit pas de méthode pour lister les documents d'un utilisateur - // On utilisera une liste vide pour l'instant - documents = new ArrayList<>(); + documents = retryService.executeWithRetrySupplier( + () -> documentService.mesDocuments(), + "chargement des documents" + ); + if (documents == null) { + documents = new ArrayList<>(); + } LOG.infof("Documents chargés: %d", documents.size()); } catch (Exception e) { LOG.errorf(e, "Erreur lors du chargement des documents"); 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 571e8a3..0acc25d 100644 --- a/src/main/java/dev/lions/unionflow/client/view/EntitesGestionBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/EntitesGestionBean.java @@ -1,11 +1,11 @@ package dev.lions.unionflow.client.view; import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse; +import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationSummaryResponse; 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.souscription.SouscriptionStatutResponse; 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.OrganisationService; import dev.lions.unionflow.client.service.CotisationService; import dev.lions.unionflow.client.service.ErrorHandlerService; import dev.lions.unionflow.client.service.SouscriptionService; @@ -43,7 +43,7 @@ public class EntitesGestionBean implements Serializable { @Inject @RestClient - private AssociationService associationService; + private OrganisationService organisationService; @Inject @RestClient @@ -107,16 +107,16 @@ public class EntitesGestionBean implements Serializable { private void initializeStatistiques() { statistiques = new Statistiques(); try { - PagedResponse response = associationService.listerToutes(0, 1000); - List associations = new ArrayList<>(); + PagedResponse response = organisationService.listerToutes(0, 1000); + List associations = new ArrayList<>(); if (response != null && response.getData() != null) { associations = response.getData(); } statistiques.setTotalEntites(associations.size()); - long actives = associations.stream().filter(a -> "ACTIVE".equals(a.getStatut())).count(); + long actives = associations.stream().filter(a -> "ACTIVE".equals(a.statut())).count(); statistiques.setEntitesActives((int) actives); int totalMembres = associations.stream() - .mapToInt(a -> a.getNombreMembres() != null ? a.getNombreMembres() : 0) + .mapToInt(a -> a.nombreMembres() != null ? a.nombreMembres() : 0) .sum(); statistiques.setTotalMembres(totalMembres); double moyenne = associations.isEmpty() ? 0 : (double) totalMembres / associations.size(); @@ -140,9 +140,9 @@ public class EntitesGestionBean implements Serializable { private void initializeEntites() { toutesLesEntites = new ArrayList<>(); try { - PagedResponse response = associationService.listerToutes(0, 1000); + PagedResponse response = organisationService.listerToutes(0, 1000); if (response != null && response.getData() != null) { - for (OrganisationResponse dto : response.getData()) { + for (OrganisationSummaryResponse dto : response.getData()) { Entite entite = convertToEntite(dto); toutesLesEntites.add(entite); } @@ -152,37 +152,86 @@ public class EntitesGestionBean implements Serializable { } } + private Entite convertToEntite(OrganisationSummaryResponse dto) { + Entite entite = new Entite(); + entite.setId(dto.id()); + entite.setNom(dto.nom()); + entite.setCodeEntite(dto.nomCourt()); // Using nomCourt as code + entite.setType(dto.typeOrganisation()); + entite.setTypeLibelle(typeCatalogueService.resolveLibelle(dto.typeOrganisation())); + entite.setRegion(null); // Not available in Summary + entite.setStatut(dto.statut()); + entite.setNombreMembres(dto.nombreMembres() != null ? dto.nombreMembres() : 0); + entite.setMembresUtilises(dto.nombreMembres() != null ? dto.nombreMembres() : 0); + entite.setAdresse(null); // Not available in Summary + entite.setTelephone(null); // Not available in Summary + entite.setEmail(null); // Not available in Summary + entite.setDescription(null); // Not available in Summary + entite.setDerniereActivite(null); // Not available in Summary + + try { + SouscriptionStatutResponse souscription = souscriptionService.obtenirActive(dto.id()); + if (souscription != null) { + entite.setForfaitSouscrit( + souscription.getPlanCommercial() != null ? souscription.getPlanCommercial() : "Non défini"); + entite.setMembresQuota( + souscription.getQuotaMax() != null ? souscription.getQuotaMax() : 0); + entite.setMontantMensuel(souscription.getMontantTotal() != null + ? String.format("%,.0f FCFA", souscription.getMontantTotal().doubleValue()) + : "0 FCFA"); + entite.setDateExpirationSouscription(souscription.getDateFin()); + entite.setStatutSouscription( + souscription.getStatut() != null ? souscription.getStatut() : "NON_DEFINI"); + } else { + entite.setForfaitSouscrit("Non défini"); + entite.setMembresQuota(0); + entite.setMontantMensuel("0 FCFA"); + entite.setDateExpirationSouscription(null); + entite.setStatutSouscription("NON_DEFINI"); + } + } catch (Exception e) { + entite.setForfaitSouscrit("Non défini"); + entite.setMembresQuota(0); + entite.setMontantMensuel("0 FCFA"); + entite.setDateExpirationSouscription(null); + entite.setStatutSouscription("NON_DEFINI"); + } + + return entite; + } + + // Overload for full OrganisationResponse (CRUD operations) private Entite convertToEntite(OrganisationResponse dto) { Entite entite = new Entite(); entite.setId(dto.getId()); entite.setNom(dto.getNom()); - entite.setCodeEntite(dto.getNumeroEnregistrement()); + entite.setCodeEntite(dto.getNomCourt()); entite.setType(dto.getTypeOrganisation()); entite.setTypeLibelle(typeCatalogueService.resolveLibelle(dto.getTypeOrganisation())); - entite.setRegion(null); // Champ non disponible dans OrganisationResponse + entite.setRegion(dto.getRegion()); + entite.setVille(dto.getVille()); entite.setStatut(dto.getStatut()); entite.setNombreMembres(dto.getNombreMembres() != null ? dto.getNombreMembres() : 0); entite.setMembresUtilises(dto.getNombreMembres() != null ? dto.getNombreMembres() : 0); - entite.setAdresse(null); // Champ non disponible dans OrganisationResponse + entite.setAdresse(dto.getAdresse()); entite.setTelephone(dto.getTelephone()); entite.setEmail(dto.getEmail()); entite.setDescription(dto.getDescription()); - entite.setDerniereActivite(null); // Champ non disponible dans OrganisationResponse + entite.setDerniereActivite(null); try { - AbonnementResponse souscription = souscriptionService.obtenirActive(dto.getId()); + SouscriptionStatutResponse souscription = souscriptionService.obtenirActive(dto.getId()); if (souscription != null) { entite.setForfaitSouscrit( - souscription.getNomFormule() != null ? souscription.getNomFormule() : "Non défini"); + souscription.getPlanCommercial() != null ? souscription.getPlanCommercial() : "Non défini"); entite.setMembresQuota( - souscription.getMaxMembres() != null ? souscription.getMaxMembres() : 0); - entite.setMontantMensuel(souscription.getMontantFinal() != null - ? String.format("%,.0f FCFA", souscription.getMontantFinal().doubleValue()) + souscription.getQuotaMax() != null ? souscription.getQuotaMax() : 0); + entite.setMontantMensuel(souscription.getMontantTotal() != null + ? String.format("%,.0f FCFA", souscription.getMontantTotal().doubleValue()) : "0 FCFA"); entite.setDateExpirationSouscription(souscription.getDateFin()); - entite.setStatutSouscription(souscription.getStatut() != null - ? souscription.getStatut().name() - : "NON_DEFINI"); + entite.setStatutSouscription( + souscription.getStatut() != null ? souscription.getStatut() : "NON_DEFINI"); } else { entite.setForfaitSouscrit("Non défini"); entite.setMembresQuota(0); @@ -309,7 +358,7 @@ public class EntitesGestionBean implements Serializable { dto.setDescription(nouvelleEntite.getDescription()); dto.setStatut("ACTIVE"); - OrganisationResponse creee = associationService.creer(dto); + OrganisationResponse creee = organisationService.creer(dto); LOG.infof("Entité créée avec succès : id=%s, nom=%s", creee.getId(), creee.getNom()); toutesLesEntites.add(convertToEntite(creee)); @@ -326,7 +375,7 @@ public class EntitesGestionBean implements Serializable { public void preparerModification(Entite entite) { this.entiteSelectionne = entite; try { - entiteEnEdition = associationService.obtenirParId(entite.getId()); + entiteEnEdition = organisationService.obtenirParId(entite.getId()); } catch (Exception e) { LOG.errorf(e, "Impossible de charger les détails de l'entité %s pour modification", entite.getId()); entiteEnEdition = new OrganisationResponse(); @@ -345,7 +394,7 @@ public class EntitesGestionBean implements Serializable { public void modifierEntite() { if (entiteEnEdition == null || entiteEnEdition.getId() == null) return; try { - OrganisationResponse maj = associationService.modifier(entiteEnEdition.getId(), entiteEnEdition); + OrganisationResponse maj = organisationService.modifier(entiteEnEdition.getId(), entiteEnEdition); Entite entiteMaj = convertToEntite(maj); int idx = -1; for (int i = 0; i < toutesLesEntites.size(); i++) { @@ -385,7 +434,7 @@ public class EntitesGestionBean implements Serializable { public void suspendreEntite() { if (entiteSelectionne == null) return; try { - OrganisationResponse maj = associationService.suspendre(entiteSelectionne.getId()); + OrganisationResponse maj = organisationService.suspendre(entiteSelectionne.getId()); mettreAJourStatutEntite(maj); errorHandler.showSuccess("Suspendues", "Entité '" + entiteSelectionne.getNom() + "' suspendue."); } catch (Exception e) { @@ -397,7 +446,7 @@ public class EntitesGestionBean implements Serializable { public void reactiverEntite() { if (entiteSelectionne == null) return; try { - OrganisationResponse maj = associationService.activer(entiteSelectionne.getId()); + OrganisationResponse maj = organisationService.activer(entiteSelectionne.getId()); mettreAJourStatutEntite(maj); errorHandler.showSuccess("Réactivée", "Entité '" + entiteSelectionne.getNom() + "' réactivée."); } catch (Exception e) { @@ -419,7 +468,7 @@ public class EntitesGestionBean implements Serializable { public void supprimerEntite() { if (entiteSelectionne == null) return; try { - associationService.supprimer(entiteSelectionne.getId()); + organisationService.supprimer(entiteSelectionne.getId()); toutesLesEntites.removeIf(e -> e.getId().equals(entiteSelectionne.getId())); appliquerFiltres(); errorHandler.showSuccess("Supprimée", "Entité supprimée avec succès."); @@ -513,7 +562,7 @@ public class EntitesGestionBean implements Serializable { } } - private void initializeSouscriptionStats(List associations) { + private void initializeSouscriptionStats(List associations) { try { int expirantes = 0; int totalSouscriptions = 0; @@ -521,13 +570,12 @@ public class EntitesGestionBean implements Serializable { String forfaitLePlusPopulaire = "N/A"; Map forfaitCount = new java.util.HashMap<>(); - for (OrganisationResponse assoc : associations) { + for (OrganisationSummaryResponse assoc : associations) { try { - AbonnementResponse souscription = souscriptionService.obtenirActive(assoc.getId()); + SouscriptionStatutResponse souscription = souscriptionService.obtenirActive(assoc.id()); if (souscription != null) { totalSouscriptions++; - // Corrigé: StatutAbonnement.ACTIF au lieu de ACTIVE - if (souscription.getStatut() == StatutAbonnement.ACTIF) { + if ("ACTIVE".equals(souscription.getStatut())) { souscriptionsActives++; } if (souscription.getDateFin() != null @@ -535,7 +583,7 @@ public class EntitesGestionBean implements Serializable { && souscription.getDateFin().isBefore(LocalDate.now().plusDays(30))) { expirantes++; } - String forfait = souscription.getNomFormule() != null ? souscription.getNomFormule() + String forfait = souscription.getPlanCommercial() != null ? souscription.getPlanCommercial() : "Inconnu"; forfaitCount.merge(forfait, 1, Integer::sum); } diff --git a/src/main/java/dev/lions/unionflow/client/view/EpargneBean.java b/src/main/java/dev/lions/unionflow/client/view/EpargneBean.java new file mode 100644 index 0000000..8ec5262 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/client/view/EpargneBean.java @@ -0,0 +1,226 @@ +package dev.lions.unionflow.client.view; + +import dev.lions.unionflow.client.service.EpargneService; +import dev.lions.unionflow.server.api.dto.mutuelle.epargne.CompteEpargneRequest; +import dev.lions.unionflow.server.api.dto.mutuelle.epargne.CompteEpargneResponse; +import dev.lions.unionflow.server.api.dto.mutuelle.epargne.TransactionEpargneRequest; +import dev.lions.unionflow.server.api.dto.mutuelle.epargne.TransactionEpargneResponse; +import dev.lions.unionflow.server.api.enums.mutuelle.epargne.TypeCompteEpargne; +import dev.lions.unionflow.server.api.enums.mutuelle.epargne.TypeTransactionEpargne; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.SessionScoped; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.FacesContext; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import lombok.Data; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +@Named("epargneBean") +@SessionScoped +@Data +public class EpargneBean implements Serializable { + + private static final Logger LOGGER = Logger.getLogger(EpargneBean.class.getName()); + + @Inject + @RestClient + private EpargneService epargneService; + + @Inject + private UserSession userSession; + + private List mesComptes = new ArrayList<>(); + private List comptesOrganisation = new ArrayList<>(); + private CompteEpargneResponse compteSelectionne; + + private List transactions = new ArrayList<>(); + private int pageTransactions = 0; + private int tailleTransactions = 20; + private boolean plusDeTransactions = true; + + private OuvertureCompte ouvertureCompte = new OuvertureCompte(); + private NouvelleTransaction nouvelleTransaction = new NouvelleTransaction(); + + private int pageComptes = 0; + private int tailleComptes = 20; + + private String getOrgId() { + return userSession.getEntite() != null && userSession.getEntite().getId() != null + ? userSession.getEntite().getId().toString() : null; + } + + @PostConstruct + public void init() { + chargerMesComptes(); + } + + public void chargerMesComptes() { + try { + mesComptes = epargneService.obtenirMesComptes(); + } catch (Exception e) { + LOGGER.severe("Erreur chargement comptes épargne: " + e.getMessage()); + mesComptes = new ArrayList<>(); + ajouterMessageErreur("Impossible de charger vos comptes d'épargne."); + } + } + + public void chargerComptesOrganisation() { + try { + String orgId = getOrgId(); + if (orgId != null) { + comptesOrganisation = epargneService.obtenirComptesParOrganisation(orgId, pageComptes, tailleComptes); + } + } catch (Exception e) { + LOGGER.severe("Erreur chargement comptes organisation: " + e.getMessage()); + comptesOrganisation = new ArrayList<>(); + ajouterMessageErreur("Impossible de charger les comptes de l'organisation."); + } + } + + public void selectionnerCompte(CompteEpargneResponse compte) { + this.compteSelectionne = compte; + this.pageTransactions = 0; + this.transactions = new ArrayList<>(); + chargerTransactions(); + } + + public void chargerTransactions() { + if (compteSelectionne == null) return; + try { + List nouvelles = epargneService.obtenirTransactions( + compteSelectionne.getNumeroCompte(), pageTransactions, tailleTransactions); + transactions.addAll(nouvelles); + plusDeTransactions = nouvelles.size() == tailleTransactions; + } catch (Exception e) { + LOGGER.severe("Erreur chargement transactions: " + e.getMessage()); + ajouterMessageErreur("Impossible de charger l'historique des transactions."); + } + } + + public void chargerPlusDeTransactions() { + pageTransactions++; + chargerTransactions(); + } + + public void ouvrirCompte() { + try { + CompteEpargneRequest request = CompteEpargneRequest.builder() + .membreId(ouvertureCompte.membreId) + .organisationId(getOrgId()) + .typeCompte(TypeCompteEpargne.valueOf(ouvertureCompte.typeCompte)) + .notesOuverture(ouvertureCompte.notes) + .build(); + CompteEpargneResponse cree = epargneService.ouvrirCompte(request); + mesComptes.add(0, cree); + ouvertureCompte = new OuvertureCompte(); + ajouterMessageSucces("Compte d'épargne ouvert : " + cree.getNumeroCompte()); + } catch (Exception e) { + LOGGER.severe("Erreur ouverture compte: " + e.getMessage()); + ajouterMessageErreur("Impossible d'ouvrir le compte d'épargne."); + } + } + + public void executerTransaction() { + if (compteSelectionne == null) { + ajouterMessageErreur("Sélectionnez un compte."); + return; + } + if (nouvelleTransaction.montant == null || nouvelleTransaction.montant.compareTo(BigDecimal.ZERO) <= 0) { + ajouterMessageErreur("Le montant doit être supérieur à 0."); + return; + } + try { + TransactionEpargneRequest request = TransactionEpargneRequest.builder() + .compteId(compteSelectionne.getNumeroCompte()) + .typeTransaction(TypeTransactionEpargne.valueOf(nouvelleTransaction.type)) + .montant(nouvelleTransaction.montant) + .motif(nouvelleTransaction.motif) + .origineFonds(nouvelleTransaction.origineFonds) + .build(); + TransactionEpargneResponse result = epargneService.executerTransaction(request); + transactions.add(0, result); + compteSelectionne = epargneService.obtenirCompte(compteSelectionne.getNumeroCompte()); + nouvelleTransaction = new NouvelleTransaction(); + ajouterMessageSucces("Transaction effectuée avec succès."); + } catch (Exception e) { + LOGGER.severe("Erreur transaction épargne: " + e.getMessage()); + ajouterMessageErreur("Impossible d'effectuer la transaction."); + } + } + + public void changerStatutCompte(CompteEpargneResponse compte, String nouveauStatut) { + try { + epargneService.changerStatut(compte.getNumeroCompte(), nouveauStatut); + chargerComptesOrganisation(); + ajouterMessageSucces("Statut mis à jour : " + nouveauStatut); + } catch (Exception e) { + ajouterMessageErreur("Impossible de modifier le statut."); + } + } + + public void actualiser() { + chargerMesComptes(); + if (compteSelectionne != null) { + pageTransactions = 0; + transactions = new ArrayList<>(); + chargerTransactions(); + } + } + + public String getStatutSeverity(String statut) { + if (statut == null) return "info"; + return switch (statut) { + case "ACTIF" -> "success"; + case "BLOQUE", "EN_CLOTURE" -> "warning"; + case "CLOTURE" -> "danger"; + default -> "info"; + }; + } + + public String getTypeTransactionIcon(String type) { + if (type == null) return "pi-exchange text-600"; + return switch (type) { + case "DEPOT", "TRANSFERT_ENTRANT", "LIBERATION_GARANTIE" -> "pi-arrow-down text-green-500"; + case "RETRAIT", "TRANSFERT_SORTANT", "PRELEVEMENT_FRAIS", "RETENUE_GARANTIE" -> "pi-arrow-up text-red-500"; + case "PAIEMENT_INTERETS" -> "pi-percentage text-blue-500"; + case "REMBOURSEMENT_CREDIT" -> "pi-credit-card text-orange-500"; + default -> "pi-exchange text-600"; + }; + } + + public boolean isRequiertOrigineFonds(BigDecimal montant) { + return montant != null && montant.compareTo(new BigDecimal("500000")) >= 0; + } + + private void ajouterMessageSucces(String detail) { + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", detail)); + } + + private void ajouterMessageErreur(String detail) { + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", detail)); + } + + @Data + public static class OuvertureCompte { + private String membreId; + private String typeCompte = "EPARGNE_LIBRE"; + private String notes; + } + + @Data + public static class NouvelleTransaction { + private String type = "DEPOT"; + private BigDecimal montant; + private String motif; + private String origineFonds; + } +} 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 b093712..e289b6c 100644 --- a/src/main/java/dev/lions/unionflow/client/view/EvenementsBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/EvenementsBean.java @@ -177,9 +177,9 @@ public class EvenementsBean implements Serializable { statistiques.setTotalEvenements(tousLesEvenements.size()); long actifs = tousLesEvenements.stream() - .filter(e -> "PLANIFIE".equals(e.getStatut()) || - "CONFIRME".equals(e.getStatut()) || - "EN_COURS".equals(e.getStatut())) + .filter(e -> e.getStatut() == StatutEvenement.PLANIFIE || + e.getStatut() == StatutEvenement.CONFIRME || + e.getStatut() == StatutEvenement.EN_COURS) .count(); statistiques.setEvenementsActifs((int) actifs); @@ -442,17 +442,19 @@ public class EvenementsBean implements Serializable { } if (filtres.getType() != null && !filtres.getType().trim().isEmpty()) { - if (!filtres.getType().equals(evenement.getTypeEvenement())) { + if (evenement.getTypeEvenement() == null || + !filtres.getType().equals(evenement.getTypeEvenement().name())) { return false; } } - + if (filtres.getStatut() != null && !filtres.getStatut().trim().isEmpty()) { - if (!filtres.getStatut().equals(evenement.getStatut())) { + if (evenement.getStatut() == null || + !filtres.getStatut().equals(evenement.getStatut().name())) { return false; } } - + if (filtres.getOrganisateur() != null && !filtres.getOrganisateur().trim().isEmpty()) { if (evenement.getOrganisateur() == null || !evenement.getOrganisateur().toLowerCase().contains(filtres.getOrganisateur().toLowerCase())) { @@ -461,7 +463,8 @@ public class EvenementsBean implements Serializable { } if (filtres.getPriorite() != null && !filtres.getPriorite().trim().isEmpty()) { - if (!filtres.getPriorite().equals(evenement.getPriorite())) { + if (evenement.getPriorite() == null || + !filtres.getPriorite().equals(evenement.getPriorite().name())) { return false; } } @@ -652,7 +655,7 @@ public class EvenementsBean implements Serializable { // Appeler le service backend pour l'inscription retryService.executeWithRetrySupplier( () -> { - evenementService.inscrireParticipant(evenement.getId(), userId); + evenementService.inscrireParticipant(evenement.getId()); return null; }, "inscription à un événement" diff --git a/src/main/java/dev/lions/unionflow/client/view/ExportMasseBean.java b/src/main/java/dev/lions/unionflow/client/view/ExportMasseBean.java index b362417..e41a9af 100644 --- a/src/main/java/dev/lions/unionflow/client/view/ExportMasseBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/ExportMasseBean.java @@ -41,7 +41,7 @@ public class ExportMasseBean implements Serializable { private String typeExport = "cotisations"; // cotisations, rapports private String formatExport = "CSV"; // CSV, PDF, EXCEL private String statutFiltre; - private UUID associationIdFiltre; + private UUID organisationIdFiltre; // Export cotisations private List cotisationsSelectionnees = new ArrayList<>(); @@ -67,7 +67,7 @@ public class ExportMasseBean implements Serializable { ); } else { csvData = retryService.executeWithRetrySupplier( - () -> exportService.exporterCotisationsCSV(statutFiltre, typeCotisation, associationIdFiltre), + () -> exportService.exporterCotisationsCSV(statutFiltre, typeCotisation, organisationIdFiltre), "export CSV de cotisations" ); } @@ -116,7 +116,7 @@ public class ExportMasseBean implements Serializable { public void genererRapportMensuel() { try { byte[] rapportData = retryService.executeWithRetrySupplier( - () -> exportService.genererRapportMensuel(anneeRapport, moisRapport, associationIdFiltre), + () -> exportService.genererRapportMensuel(anneeRapport, moisRapport, organisationIdFiltre), "génération d'un rapport mensuel" ); String nomFichier = String.format("rapport-%d-%02d.txt", anneeRapport, moisRapport); @@ -159,8 +159,8 @@ public class ExportMasseBean implements Serializable { public String getStatutFiltre() { return statutFiltre; } public void setStatutFiltre(String statutFiltre) { this.statutFiltre = statutFiltre; } - public UUID getAssociationIdFiltre() { return associationIdFiltre; } - public void setAssociationIdFiltre(UUID associationIdFiltre) { this.associationIdFiltre = associationIdFiltre; } + public UUID getAssociationIdFiltre() { return organisationIdFiltre; } + public void setAssociationIdFiltre(UUID organisationIdFiltre) { this.organisationIdFiltre = organisationIdFiltre; } public List getCotisationsSelectionnees() { return cotisationsSelectionnees; } public void setCotisationsSelectionnees(List cotisationsSelectionnees) { this.cotisationsSelectionnees = cotisationsSelectionnees; } diff --git a/src/main/java/dev/lions/unionflow/client/view/LogsSystemeBean.java b/src/main/java/dev/lions/unionflow/client/view/LogsSystemeBean.java new file mode 100644 index 0000000..81d4e63 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/client/view/LogsSystemeBean.java @@ -0,0 +1,158 @@ +package dev.lions.unionflow.client.view; + +import dev.lions.unionflow.client.service.LogsService; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.SessionScoped; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.FacesContext; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import lombok.Data; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Logger; + +@Named("logsSystemeBean") +@SessionScoped +@Data +public class LogsSystemeBean implements Serializable { + + private static final Logger LOGGER = Logger.getLogger(LogsSystemeBean.class.getName()); + + @Inject + @RestClient + private LogsService logsService; + + @Inject + private UserSession userSession; + + // Logs système + private List> logsSysteme = new ArrayList<>(); + private List> logsAuditFinance = new ArrayList<>(); + private List> anomalies = new ArrayList<>(); + + // Santé système + private Map santeSysteme; + private Map metriques; + + // Filtres logs système + private String filtreLevel; + private String filtreSource; + private String filtreUserId; + private String dateDebut; + private String dateFin; + private String motCle; + + // Filtres audit finance + private String filtreOperation; + private String filtreEntityType; + private String filtreSeverity; + + // Pagination + private int page = 0; + private int taille = 50; + + private UUID getOrgId() { + return userSession.getEntite() != null ? userSession.getEntite().getId() : null; + } + + @PostConstruct + public void init() { + chargerSanteSysteme(); + chargerLogsSysteme(); + } + + public void chargerSanteSysteme() { + try { + santeSysteme = logsService.obtenirSanteSysteme(); + metriques = logsService.obtenirMetriques(); + } catch (Exception e) { + LOGGER.warning("Santé système indisponible: " + e.getMessage()); + } + } + + public void chargerLogsSysteme() { + try { + logsSysteme = logsService.rechercherLogs( + filtreLevel, filtreSource, filtreUserId, + dateDebut, dateFin, motCle, page, taille); + } catch (Exception e) { + LOGGER.severe("Erreur chargement logs: " + e.getMessage()); + logsSysteme = new ArrayList<>(); + ajouterMessageErreur("Impossible de charger les logs système."); + } + } + + public void chargerLogsAuditFinance() { + try { + UUID orgId = getOrgId(); + logsAuditFinance = logsService.obtenirLogsAuditFinance( + orgId, dateDebut, dateFin, + filtreOperation, filtreEntityType, filtreSeverity, taille); + } catch (Exception e) { + LOGGER.severe("Erreur chargement audit finance: " + e.getMessage()); + logsAuditFinance = new ArrayList<>(); + ajouterMessageErreur("Impossible de charger les logs d'audit finance."); + } + } + + public void chargerAnomalies() { + try { + UUID orgId = getOrgId(); + anomalies = logsService.obtenirAnomalies(orgId, dateDebut, dateFin); + } catch (Exception e) { + LOGGER.warning("Impossible de charger les anomalies: " + e.getMessage()); + anomalies = new ArrayList<>(); + } + } + + public void appliquerFiltres() { + page = 0; + chargerLogsSysteme(); + } + + public void reinitialiserFiltres() { + filtreLevel = null; + filtreSource = null; + filtreUserId = null; + dateDebut = null; + dateFin = null; + motCle = null; + page = 0; + chargerLogsSysteme(); + } + + public void actualiser() { + chargerSanteSysteme(); + chargerLogsSysteme(); + } + + public String getLevelSeverity(String level) { + if (level == null) return "info"; + return switch (level.toUpperCase()) { + case "ERROR", "SEVERE" -> "danger"; + case "WARN", "WARNING" -> "warning"; + case "INFO" -> "info"; + default -> "secondary"; + }; + } + + public String getSanteSeverity(String status) { + if (status == null) return "warning"; + return switch (status.toUpperCase()) { + case "UP" -> "success"; + case "DEGRADED" -> "warning"; + default -> "danger"; + }; + } + + private void ajouterMessageErreur(String detail) { + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", detail)); + } +} diff --git a/src/main/java/dev/lions/unionflow/client/view/MembreDashboardBean.java b/src/main/java/dev/lions/unionflow/client/view/MembreDashboardBean.java index 5b54e84..b69eb4b 100644 --- a/src/main/java/dev/lions/unionflow/client/view/MembreDashboardBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/MembreDashboardBean.java @@ -1,33 +1,61 @@ package dev.lions.unionflow.client.view; +import dev.lions.unionflow.server.api.dto.membre.response.MembreResponse; +import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationResponse; +import dev.lions.unionflow.server.api.dto.evenement.response.EvenementResponse; +import dev.lions.unionflow.client.service.MembreService; +import dev.lions.unionflow.client.service.CotisationService; +import dev.lions.unionflow.client.service.EvenementService; +import dev.lions.unionflow.client.service.ErrorHandlerService; import jakarta.enterprise.context.SessionScoped; +import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.annotation.PostConstruct; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.logging.Logger; import java.io.Serializable; import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; +import java.util.Locale; @Named("membreDashboardBean") @SessionScoped public class MembreDashboardBean implements Serializable { - + private static final long serialVersionUID = 1L; - - // Constantes de navigation outcomes (WOU/DRY - réutilisables) - private static final String OUTCOME_MEMBRE_EVENEMENT = "membreEvenementPage"; - private static final String OUTCOME_MEMBRE_COTISATIONS = "membreCotisationsPage"; - - // Membre actuel - private Membre membre; - - // Statistiques + private static final Logger LOG = Logger.getLogger(MembreDashboardBean.class); + + @Inject + private UserSession userSession; + + @Inject + @RestClient + private MembreService membreService; + + @Inject + @RestClient + private CotisationService cotisationService; + + @Inject + @RestClient + private EvenementService evenementService; + + @Inject + ErrorHandlerService errorHandler; + + // Membre actuel (DTO backend) + private MembreResponse membre; + + // Statistiques calculées private String statutCotisations; private int evenementsInscrits; private int aidesRecues; private int messagesNonLus; - - // Progression + + // Progression cotisations private int cotisationsPayees; private int cotisationsTotales; private int progressionCotisations; @@ -35,254 +63,383 @@ public class MembreDashboardBean implements Serializable { private int evenementsAssistes; private String anciennete; private String dateAdhesionFormatee; - + // Listes private List alertes = new ArrayList<>(); private List prochainsEvenements = new ArrayList<>(); private List rappels = new ArrayList<>(); private List activiteRecente = new ArrayList<>(); - + // État private boolean peutPayerCotisations; - + @PostConstruct public void init() { - initializeMembre(); - initializeStatistiques(); - initializeAlertes(); - initializeEvenements(); - initializeRappels(); - initializeActivite(); + chargerMembre(); + chargerCotisations(); + chargerEvenements(); + construireRappels(); } - - private void initializeMembre() { - membre = new Membre(); - membre.setPrenom("Jean"); - membre.setNom("Dupont"); - membre.setNumeroMembre("M240001"); - membre.setTypeMembre("Membre Actif"); - membre.setDateAdhesion("15/06/2020"); - membre.setPhotoUrl(null); // Pas de photo par défaut + + // ── Chargement des données réelles ────────────────────────────────────── + + /** + * Charge le membre connecté depuis le backend. + */ + private void chargerMembre() { + try { + membre = membreService.obtenirMembreConnecte(); + if (membre == null) { + LOG.warn("Aucun profil membre retourné par /api/membres/me"); + } else { + LOG.infof("Dashboard membre chargé pour: %s", membre.getNomComplet()); + // Date d'adhésion formatée + if (membre.getDateAdhesion() != null) { + dateAdhesionFormatee = membre.getDateAdhesion() + .format(DateTimeFormatter.ofPattern("d MMMM yyyy", Locale.FRENCH)); + anciennete = calculerAnciennete(membre.getDateAdhesion()); + } + } + } catch (Exception e) { + LOG.errorf(e, "Erreur lors du chargement du membre connecté"); + errorHandler.handleException(e, "lors du chargement du tableau de bord membre", null); + } } - - private void initializeStatistiques() { - this.statutCotisations = "À jour"; - this.evenementsInscrits = 3; - this.aidesRecues = 2; - this.messagesNonLus = 5; - this.cotisationsPayees = 10; - this.cotisationsTotales = 12; - this.progressionCotisations = 83; - this.tauxParticipation = 75; - this.evenementsAssistes = 15; - this.anciennete = "4 ans"; - this.dateAdhesionFormatee = "15 juin 2020"; - this.peutPayerCotisations = true; + + /** + * Charge et calcule les statistiques de cotisations. + */ + private void chargerCotisations() { + alertes = new ArrayList<>(); + activiteRecente = new ArrayList<>(); + cotisationsPayees = 0; + cotisationsTotales = 0; + progressionCotisations = 0; + statutCotisations = "Inconnu"; + peutPayerCotisations = false; + + if (membre == null) return; + + try { + List cotisations = cotisationService.obtenirParMembre(membre.getId(), 0, 50); + if (cotisations == null || cotisations.isEmpty()) { + statutCotisations = "Aucune cotisation"; + return; + } + + cotisationsTotales = cotisations.size(); + long payees = cotisations.stream().filter(c -> "PAYEE".equals(c.getStatut())).count(); + long enAttente = cotisations.stream().filter(c -> "EN_ATTENTE".equals(c.getStatut()) || "PARTIELLE".equals(c.getStatut())).count(); + cotisationsPayees = (int) payees; + + progressionCotisations = cotisationsTotales > 0 + ? (int) Math.round(payees * 100.0 / cotisationsTotales) + : 0; + + if (enAttente == 0) { + statutCotisations = "À jour"; + } else { + statutCotisations = enAttente + " en attente"; + peutPayerCotisations = true; + } + + // Activité récente : 5 dernières cotisations + int limite = Math.min(5, cotisations.size()); + for (int i = 0; i < limite; i++) { + CotisationResponse cot = cotisations.get(i); + Activite act = new Activite(); + String libelleStatut = cot.getStatutLibelle() != null ? cot.getStatutLibelle() : cot.getStatut(); + act.setTitre("Cotisation " + libelleStatut); + act.setDescription(cot.getMontantDuFormatte() + + (cot.getPeriode() != null ? " – " + cot.getPeriode() : "")); + act.setDateRelative(cot.getDatePaiement() != null + ? formatDateRelative(cot.getDatePaiement().toLocalDate()) + : (cot.getDateCreation() != null + ? formatDateRelative(cot.getDateCreation().toLocalDate()) + : "Récemment")); + act.setIcone("pi-dollar"); + act.setCouleurCategorie("bg-green-500"); + activiteRecente.add(act); + } + + // Alertes pour cotisations en retard ou en attente + cotisations.stream() + .filter(c -> "EN_RETARD".equals(c.getStatut()) || "EN_ATTENTE".equals(c.getStatut())) + .limit(3) + .forEach(cot -> { + Alerte alerte = new Alerte(); + alerte.setTitre("Cotisation " + (cot.getStatutLibelle() != null ? cot.getStatutLibelle() : cot.getStatut())); + alerte.setMessage("Montant : " + cot.getMontantDuFormatte() + + (cot.getDateEcheance() != null + ? " – Échéance : " + cot.getDateEcheance().format(DateTimeFormatter.ofPattern("dd/MM/yyyy")) + : "")); + alerte.setDateRelative(cot.getDateEcheance() != null + ? formatDateRelative(cot.getDateEcheance()) + : ""); + alerte.setIcone("pi-dollar"); + alerte.setCouleurIcone("text-orange-500"); + alerte.setCouleurFond("rgba(255, 193, 7, 0.1)"); + alerte.setCouleurBordure("border-orange-500"); + alertes.add(alerte); + }); + + } catch (Exception e) { + LOG.errorf(e, "Erreur lors du chargement des cotisations pour le dashboard"); + errorHandler.handleException(e, "lors du chargement des cotisations", null); + } } - - private void initializeAlertes() { - Alerte alerte1 = new Alerte(); - alerte1.setTitre("Cotisation de décembre"); - alerte1.setMessage("Votre cotisation mensuelle de décembre est due le 15/12/2024"); - alerte1.setDateRelative("Il y a 2 jours"); - alerte1.setIcone("pi-dollar"); - alerte1.setCouleurIcone("text-orange-500"); - alerte1.setCouleurFond("rgba(255, 193, 7, 0.1)"); - alerte1.setCouleurBordure("border-orange-500"); - alertes.add(alerte1); - - Alerte alerte2 = new Alerte(); - alerte2.setTitre("Nouvel événement"); - alerte2.setMessage("Assemblée générale prévue le 28 décembre 2024"); - alerte2.setDateRelative("Hier"); - alerte2.setIcone("pi-calendar"); - alerte2.setCouleurIcone("text-blue-500"); - alerte2.setCouleurFond("rgba(13, 110, 253, 0.1)"); - alerte2.setCouleurBordure("border-blue-500"); - alertes.add(alerte2); + + /** + * Charge les prochains événements depuis le backend. + */ + private void chargerEvenements() { + prochainsEvenements = new ArrayList<>(); + evenementsInscrits = 0; + + try { + var response = evenementService.listerAVenir(0, 5); + if (response == null || response.getData() == null) return; + + evenementsInscrits = response.getData().size(); + + for (EvenementResponse evt : response.getData()) { + Evenement evenement = new Evenement(); + evenement.setTitre(evt.getTitre() != null ? evt.getTitre() : "Événement"); + + // Date formatée + String dateComplete = ""; + if (evt.getDateDebut() != null) { + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("EEEE d MMMM yyyy", Locale.FRENCH); + dateComplete = evt.getDateDebut().format(fmt); + if (evt.getHeureDebut() != null) { + dateComplete += " – " + evt.getHeureDebut().format(DateTimeFormatter.ofPattern("HH'h'mm")); + } + } + evenement.setDateComplete(dateComplete); + evenement.setLieu(evt.getLieu() != null ? evt.getLieu() : (evt.getVille() != null ? evt.getVille() : "À définir")); + + // Prix + if (evt.getBudget() == null || evt.getBudget().signum() == 0) { + evenement.setPrixFormate("Gratuit"); + } else { + evenement.setPrixFormate(String.format(Locale.US, "%,.0f %s", + evt.getBudget(), + evt.getCodeDevise() != null ? evt.getCodeDevise() : "FCFA")); + } + + // Participants + int nb = evt.getParticipantsInscrits() != null ? evt.getParticipantsInscrits() : 0; + evenement.setNombreParticipants(nb + " inscrits"); + + // Statut d'inscription par défaut + evenement.setStatutInscription("Ouvert"); + evenement.setSeverityInscription("info"); + evenement.setPeutAnnuler(false); + + // Icône et couleur selon type + if (evt.getTypeEvenement() != null) { + switch (evt.getTypeEvenement().name()) { + case "ASSEMBLEE_GENERALE": + evenement.setIconeType("pi-users"); + evenement.setCouleurCategorie("bg-blue-500"); + evenement.setCouleurBordure("border-blue-500"); + break; + case "FORMATION": + evenement.setIconeType("pi-book"); + evenement.setCouleurCategorie("bg-purple-500"); + evenement.setCouleurBordure("border-purple-500"); + break; + case "SORTIE": + case "ACTIVITE_SOCIALE": + evenement.setIconeType("pi-car"); + evenement.setCouleurCategorie("bg-green-500"); + evenement.setCouleurBordure("border-green-500"); + break; + default: + evenement.setIconeType("pi-calendar"); + evenement.setCouleurCategorie("bg-blue-500"); + evenement.setCouleurBordure("border-blue-500"); + } + } else { + evenement.setIconeType("pi-calendar"); + evenement.setCouleurCategorie("bg-blue-500"); + evenement.setCouleurBordure("border-blue-500"); + } + + prochainsEvenements.add(evenement); + } + + // Ajouter les événements à venir comme activité récente + for (EvenementResponse evt : response.getData()) { + Activite act = new Activite(); + act.setTitre("Événement à venir : " + (evt.getTitre() != null ? evt.getTitre() : "")); + act.setDescription(evt.getLieu() != null ? evt.getLieu() : ""); + act.setDateRelative(evt.getDateDebut() != null + ? formatDateRelative(evt.getDateDebut()) + : "Bientôt"); + act.setIcone("pi-calendar"); + act.setCouleurCategorie("bg-blue-500"); + activiteRecente.add(act); + } + + // Nombre d'événements assistés (statistique du membre si disponible) + if (membre != null) { + evenementsAssistes = membre.getNombreEvenementsParticipes(); + tauxParticipation = cotisationsTotales > 0 + ? progressionCotisations + : 0; + } + + } catch (Exception e) { + LOG.errorf(e, "Erreur lors du chargement des événements pour le dashboard"); + errorHandler.handleException(e, "lors du chargement des événements", null); + } } - - private void initializeEvenements() { - Evenement event1 = new Evenement(); - event1.setTitre("Assemblée Générale Ordinaire"); - event1.setDateComplete("Samedi 28 décembre 2024 - 09h00"); - event1.setLieu("Siège de l'association"); - event1.setPrixFormate("Gratuit"); - event1.setNombreParticipants("45 inscrits"); - event1.setStatutInscription("Inscrit"); - event1.setSeverityInscription("success"); - event1.setIconeType("pi-users"); - event1.setCouleurCategorie("bg-blue-500"); - event1.setCouleurBordure("border-blue-500"); - event1.setPeutAnnuler(true); - prochainsEvenements.add(event1); - - Evenement event2 = new Evenement(); - event2.setTitre("Formation premiers secours"); - event2.setDateComplete("Dimanche 15 janvier 2025 - 14h00"); - event2.setLieu("Centre de formation"); - event2.setPrixFormate("2,500 FCFA"); - event2.setNombreParticipants("12 inscrits"); - event2.setStatutInscription("En attente"); - event2.setSeverityInscription("warning"); - event2.setIconeType("pi-heart"); - event2.setCouleurCategorie("bg-red-500"); - event2.setCouleurBordure("border-red-500"); - event2.setPeutAnnuler(false); - prochainsEvenements.add(event2); + + /** + * Construit les rappels à partir des cotisations en attente et des événements imminents. + */ + private void construireRappels() { + rappels = new ArrayList<>(); + + // Rappel cotisations si en attente + if (peutPayerCotisations) { + Rappel rappel = new Rappel(); + rappel.setTitre("Cotisation(s) en attente"); + rappel.setEcheance("À régler dès que possible"); + rappel.setIcone("pi-dollar"); + rappel.setCouleurIcone("text-orange-500"); + rappel.setCouleurFond("surface-100"); + rappels.add(rappel); + } + + // Rappels événements imminents (dans les 7 prochains jours) + for (Evenement evt : prochainsEvenements) { + // On essaie d'ajouter un rappel pour chaque événement proche + Rappel rappel = new Rappel(); + rappel.setTitre(evt.getTitre()); + rappel.setEcheance(evt.getDateComplete()); + rappel.setIcone("pi-calendar"); + rappel.setCouleurIcone("text-blue-500"); + rappel.setCouleurFond("surface-100"); + rappels.add(rappel); + if (rappels.size() >= 3) break; + } } - - private void initializeRappels() { - Rappel rappel1 = new Rappel(); - rappel1.setTitre("Cotisation décembre"); - rappel1.setEcheance("Dans 3 jours"); - rappel1.setIcone("pi-dollar"); - rappel1.setCouleurIcone("text-orange-500"); - rappel1.setCouleurFond("surface-100"); - rappels.add(rappel1); - - Rappel rappel2 = new Rappel(); - rappel2.setTitre("Renouvellement adhésion"); - rappel2.setEcheance("Dans 2 mois"); - rappel2.setIcone("pi-id-card"); - rappel2.setCouleurIcone("text-blue-500"); - rappel2.setCouleurFond("surface-100"); - rappels.add(rappel2); + + // ── Utilitaires ───────────────────────────────────────────────────────── + + private String calculerAnciennete(LocalDate dateAdhesion) { + long annees = ChronoUnit.YEARS.between(dateAdhesion, LocalDate.now()); + if (annees == 0) { + long mois = ChronoUnit.MONTHS.between(dateAdhesion, LocalDate.now()); + return mois <= 1 ? "Moins d'un mois" : mois + " mois"; + } + return annees + " an" + (annees > 1 ? "s" : ""); } - - private void initializeActivite() { - Activite activite1 = new Activite(); - activite1.setTitre("Cotisation payée"); - activite1.setDescription("Cotisation de novembre 2024 - 5,000 FCFA"); - activite1.setDateRelative("Il y a 5 jours"); - activite1.setIcone("pi-check"); - activite1.setCouleurCategorie("bg-green-500"); - activiteRecente.add(activite1); - - Activite activite2 = new Activite(); - activite2.setTitre("Participation événement"); - activite2.setDescription("Sortie culturelle au musée"); - activite2.setDateRelative("Il y a 1 semaine"); - activite2.setIcone("pi-calendar"); - activite2.setCouleurCategorie("bg-blue-500"); - activiteRecente.add(activite2); - - Activite activite3 = new Activite(); - activite3.setTitre("Inscription événement"); - activite3.setDescription("Assemblée générale ordinaire"); - activite3.setDateRelative("Il y a 2 semaines"); - activite3.setIcone("pi-user-plus"); - activite3.setCouleurCategorie("bg-purple-500"); - activiteRecente.add(activite3); + + private String formatDateRelative(LocalDate date) { + if (date == null) return "Récemment"; + long days = ChronoUnit.DAYS.between(date, LocalDate.now()); + if (days < 0) { + long futur = Math.abs(days); + if (futur == 1) return "Demain"; + if (futur < 7) return "Dans " + futur + " jours"; + if (futur < 30) return "Dans " + (futur / 7) + " semaine" + (futur / 7 > 1 ? "s" : ""); + return "Dans " + (futur / 30) + " mois"; + } + if (days == 0) return "Aujourd'hui"; + if (days == 1) return "Hier"; + if (days < 7) return "Il y a " + days + " jour" + (days > 1 ? "s" : ""); + if (days < 30) return "Il y a " + (days / 7) + " semaine" + (days / 7 > 1 ? "s" : ""); + long mois = days / 30; + return "Il y a " + mois + " mois"; } - - // Actions + + // ── Actions ───────────────────────────────────────────────────────────── + public void marquerLue(Alerte alerte) { alertes.remove(alerte); } - + public String voirEvenement(Evenement evenement) { - // Utilisation de navigation outcome au lieu de chemin direct (WOU/DRY) - return OUTCOME_MEMBRE_EVENEMENT + "?id=" + evenement.getTitre() + "&faces-redirect=true"; + return "/pages/secure/evenement/gestion.xhtml?faces-redirect=true"; } - + public void annulerInscription(Evenement evenement) { evenement.setStatutInscription("Annulé"); evenement.setSeverityInscription("danger"); evenement.setPeutAnnuler(false); } - + public String payerCotisations() { - // Utilisation de navigation outcome au lieu de chemin direct (WOU/DRY) - return OUTCOME_MEMBRE_COTISATIONS + "?faces-redirect=true"; + return "/pages/secure/cotisation/paiement.xhtml?faces-redirect=true"; } - - // Getters et Setters - public Membre getMembre() { return membre; } - public void setMembre(Membre membre) { this.membre = membre; } - + + public void actualiser() { + chargerMembre(); + chargerCotisations(); + chargerEvenements(); + construireRappels(); + errorHandler.showSuccess("Succès", "Données actualisées."); + } + + // ── Getters et Setters ─────────────────────────────────────────────────── + + public MembreResponse getMembre() { return membre; } + public void setMembre(MembreResponse membre) { this.membre = membre; } + public String getStatutCotisations() { return statutCotisations; } public void setStatutCotisations(String statutCotisations) { this.statutCotisations = statutCotisations; } - + public int getEvenementsInscrits() { return evenementsInscrits; } public void setEvenementsInscrits(int evenementsInscrits) { this.evenementsInscrits = evenementsInscrits; } - + public int getAidesRecues() { return aidesRecues; } public void setAidesRecues(int aidesRecues) { this.aidesRecues = aidesRecues; } - + public int getMessagesNonLus() { return messagesNonLus; } public void setMessagesNonLus(int messagesNonLus) { this.messagesNonLus = messagesNonLus; } - + public int getCotisationsPayees() { return cotisationsPayees; } public void setCotisationsPayees(int cotisationsPayees) { this.cotisationsPayees = cotisationsPayees; } - + public int getCotisationsTotales() { return cotisationsTotales; } public void setCotisationsTotales(int cotisationsTotales) { this.cotisationsTotales = cotisationsTotales; } - + public int getProgressionCotisations() { return progressionCotisations; } public void setProgressionCotisations(int progressionCotisations) { this.progressionCotisations = progressionCotisations; } - + public int getTauxParticipation() { return tauxParticipation; } public void setTauxParticipation(int tauxParticipation) { this.tauxParticipation = tauxParticipation; } - + public int getEvenementsAssistes() { return evenementsAssistes; } public void setEvenementsAssistes(int evenementsAssistes) { this.evenementsAssistes = evenementsAssistes; } - + public String getAnciennete() { return anciennete; } public void setAnciennete(String anciennete) { this.anciennete = anciennete; } - + public String getDateAdhesionFormatee() { return dateAdhesionFormatee; } public void setDateAdhesionFormatee(String dateAdhesionFormatee) { this.dateAdhesionFormatee = dateAdhesionFormatee; } - + public List getAlertes() { return alertes; } public void setAlertes(List alertes) { this.alertes = alertes; } - + public List getProchainsEvenements() { return prochainsEvenements; } public void setProchainsEvenements(List prochainsEvenements) { this.prochainsEvenements = prochainsEvenements; } - + public List getRappels() { return rappels; } public void setRappels(List rappels) { this.rappels = rappels; } - + public List getActiviteRecente() { return activiteRecente; } public void setActiviteRecente(List activiteRecente) { this.activiteRecente = activiteRecente; } - + public boolean isPeutPayerCotisations() { return peutPayerCotisations; } public void setPeutPayerCotisations(boolean peutPayerCotisations) { this.peutPayerCotisations = peutPayerCotisations; } - - // Classes internes - public static class Membre { - private String prenom; - private String nom; - private String numeroMembre; - private String typeMembre; - private String dateAdhesion; - private String photoUrl; - - public String getPrenom() { return prenom; } - public void setPrenom(String prenom) { this.prenom = prenom; } - - public String getNom() { return nom; } - public void setNom(String nom) { this.nom = nom; } - - public String getNumeroMembre() { return numeroMembre; } - public void setNumeroMembre(String numeroMembre) { this.numeroMembre = numeroMembre; } - - public String getTypeMembre() { return typeMembre; } - public void setTypeMembre(String typeMembre) { this.typeMembre = typeMembre; } - - public String getDateAdhesion() { return dateAdhesion; } - public void setDateAdhesion(String dateAdhesion) { this.dateAdhesion = dateAdhesion; } - - public String getPhotoUrl() { return photoUrl; } - public void setPhotoUrl(String photoUrl) { this.photoUrl = photoUrl; } - - public String getInitiales() { - return (prenom != null ? prenom.substring(0, 1) : "") + - (nom != null ? nom.substring(0, 1) : ""); - } - } - - public static class Alerte { + + // ── Classes internes ──────────────────────────────────────────────────── + + public static class Alerte implements Serializable { private String titre; private String message; private String dateRelative; @@ -290,31 +447,30 @@ public class MembreDashboardBean implements Serializable { private String couleurIcone; private String couleurFond; private String couleurBordure; - - // Getters et setters + public String getTitre() { return titre; } public void setTitre(String titre) { this.titre = titre; } - + public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } - + public String getDateRelative() { return dateRelative; } public void setDateRelative(String dateRelative) { this.dateRelative = dateRelative; } - + public String getIcone() { return icone; } public void setIcone(String icone) { this.icone = icone; } - + public String getCouleurIcone() { return couleurIcone; } public void setCouleurIcone(String couleurIcone) { this.couleurIcone = couleurIcone; } - + public String getCouleurFond() { return couleurFond; } public void setCouleurFond(String couleurFond) { this.couleurFond = couleurFond; } - + public String getCouleurBordure() { return couleurBordure; } public void setCouleurBordure(String couleurBordure) { this.couleurBordure = couleurBordure; } } - - public static class Evenement { + + public static class Evenement implements Serializable { private String titre; private String dateComplete; private String lieu; @@ -326,90 +482,87 @@ public class MembreDashboardBean implements Serializable { private String couleurCategorie; private String couleurBordure; private boolean peutAnnuler; - - // Getters et setters + public String getTitre() { return titre; } public void setTitre(String titre) { this.titre = titre; } - + public String getDateComplete() { return dateComplete; } public void setDateComplete(String dateComplete) { this.dateComplete = dateComplete; } - + public String getLieu() { return lieu; } public void setLieu(String lieu) { this.lieu = lieu; } - + public String getPrixFormate() { return prixFormate; } public void setPrixFormate(String prixFormate) { this.prixFormate = prixFormate; } - + public String getNombreParticipants() { return nombreParticipants; } public void setNombreParticipants(String nombreParticipants) { this.nombreParticipants = nombreParticipants; } - + public String getStatutInscription() { return statutInscription; } public void setStatutInscription(String statutInscription) { this.statutInscription = statutInscription; } - + public String getSeverityInscription() { return severityInscription; } public void setSeverityInscription(String severityInscription) { this.severityInscription = severityInscription; } - + public String getIconeType() { return iconeType; } public void setIconeType(String iconeType) { this.iconeType = iconeType; } - + public String getCouleurCategorie() { return couleurCategorie; } public void setCouleurCategorie(String couleurCategorie) { this.couleurCategorie = couleurCategorie; } - + public String getCouleurBordure() { return couleurBordure; } public void setCouleurBordure(String couleurBordure) { this.couleurBordure = couleurBordure; } - + public boolean isPeutAnnuler() { return peutAnnuler; } public void setPeutAnnuler(boolean peutAnnuler) { this.peutAnnuler = peutAnnuler; } - + public String getCouleurPrix() { - return prixFormate.equals("Gratuit") ? "text-green-500" : "text-blue-500"; + return "Gratuit".equals(prixFormate) ? "text-green-500" : "text-blue-500"; } } - - public static class Rappel { + + public static class Rappel implements Serializable { private String titre; private String echeance; private String icone; private String couleurIcone; private String couleurFond; - - // Getters et setters + public String getTitre() { return titre; } public void setTitre(String titre) { this.titre = titre; } - + public String getEcheance() { return echeance; } public void setEcheance(String echeance) { this.echeance = echeance; } - + public String getIcone() { return icone; } public void setIcone(String icone) { this.icone = icone; } - + public String getCouleurIcone() { return couleurIcone; } public void setCouleurIcone(String couleurIcone) { this.couleurIcone = couleurIcone; } - + public String getCouleurFond() { return couleurFond; } public void setCouleurFond(String couleurFond) { this.couleurFond = couleurFond; } } - - public static class Activite { + + public static class Activite implements Serializable { private String titre; private String description; private String dateRelative; private String icone; private String couleurCategorie; - - // Getters et setters + public String getTitre() { return titre; } public void setTitre(String titre) { this.titre = titre; } - + public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } - + public String getDateRelative() { return dateRelative; } public void setDateRelative(String dateRelative) { this.dateRelative = dateRelative; } - + public String getIcone() { return icone; } public void setIcone(String icone) { this.icone = icone; } - + public String getCouleurCategorie() { return couleurCategorie; } public void setCouleurCategorie(String couleurCategorie) { this.couleurCategorie = couleurCategorie; } } diff --git a/src/main/java/dev/lions/unionflow/client/view/MembreExportBean.java b/src/main/java/dev/lions/unionflow/client/view/MembreExportBean.java index aaf9584..89f37b4 100644 --- a/src/main/java/dev/lions/unionflow/client/view/MembreExportBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/MembreExportBean.java @@ -1,7 +1,7 @@ package dev.lions.unionflow.client.view; import dev.lions.unionflow.client.service.MembreService; -import dev.lions.unionflow.client.service.AssociationService; +import dev.lions.unionflow.client.service.OrganisationService; import dev.lions.unionflow.client.service.ErrorHandlerService; import dev.lions.unionflow.client.service.RetryService; import dev.lions.unionflow.server.api.dto.organisation.request.*; @@ -42,7 +42,7 @@ public class MembreExportBean implements Serializable { @Inject @RestClient - AssociationService associationService; + OrganisationService organisationService; @Inject ErrorHandlerService errorHandler; @@ -91,16 +91,16 @@ public class MembreExportBean implements Serializable { private void chargerOrganisations() { organisationsDisponibles = new ArrayList<>(); try { - PagedResponse response = retryService.executeWithRetrySupplier( - () -> associationService.listerToutes(0, 1000), + PagedResponse response = retryService.executeWithRetrySupplier( + () -> organisationService.listerToutes(0, 1000), "chargement des organisations pour export"); - List associations = (response != null && response.getData() != null) ? response.getData() + List associations = (response != null && response.getData() != null) ? response.getData() : new ArrayList<>(); - for (OrganisationResponse assoc : associations) { + for (OrganisationSummaryResponse assoc : associations) { OrganisationResponse org = new OrganisationResponse(); - org.setId(assoc.getId()); - org.setNom(assoc.getNom()); - org.setVille(assoc.getVille()); + org.setId(assoc.id()); + org.setNom(assoc.nom()); + org.setVille(null /* ville not in Summary */); organisationsDisponibles.add(org); } LOG.infof("Organisations chargées: %d organisations", organisationsDisponibles.size()); diff --git a/src/main/java/dev/lions/unionflow/client/view/MembreImportBean.java b/src/main/java/dev/lions/unionflow/client/view/MembreImportBean.java index 261d4f8..2201d3c 100644 --- a/src/main/java/dev/lions/unionflow/client/view/MembreImportBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/MembreImportBean.java @@ -2,7 +2,7 @@ package dev.lions.unionflow.client.view; import dev.lions.unionflow.client.service.MembreService; import dev.lions.unionflow.client.service.MembreImportMultipartForm; -import dev.lions.unionflow.client.service.AssociationService; +import dev.lions.unionflow.client.service.OrganisationService; import dev.lions.unionflow.client.service.ErrorHandlerService; import dev.lions.unionflow.client.service.RetryService; import dev.lions.unionflow.server.api.dto.organisation.request.*; @@ -43,7 +43,7 @@ public class MembreImportBean implements Serializable { @Inject @RestClient - AssociationService associationService; + OrganisationService organisationService; @Inject ErrorHandlerService errorHandler; @@ -74,16 +74,16 @@ public class MembreImportBean implements Serializable { private void chargerOrganisations() { organisationsDisponibles = new ArrayList<>(); try { - PagedResponse response = retryService.executeWithRetrySupplier( - () -> associationService.listerToutes(0, 1000), + PagedResponse response = retryService.executeWithRetrySupplier( + () -> organisationService.listerToutes(0, 1000), "chargement des organisations pour import"); - List associations = (response != null && response.getData() != null) ? response.getData() + List associations = (response != null && response.getData() != null) ? response.getData() : new ArrayList<>(); - for (OrganisationResponse assoc : associations) { + for (OrganisationSummaryResponse assoc : associations) { OrganisationResponse org = new OrganisationResponse(); - org.setId(assoc.getId()); - org.setNom(assoc.getNom()); - org.setVille(assoc.getVille()); + org.setId(assoc.id()); + org.setNom(assoc.nom()); + org.setVille(null /* ville not in Summary */); organisationsDisponibles.add(org); } LOG.infof("Organisations chargées: %d organisations", organisationsDisponibles.size()); diff --git a/src/main/java/dev/lions/unionflow/client/view/MembreInscriptionBean.java b/src/main/java/dev/lions/unionflow/client/view/MembreInscriptionBean.java index 1f0d7ba..a556e51 100644 --- a/src/main/java/dev/lions/unionflow/client/view/MembreInscriptionBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/MembreInscriptionBean.java @@ -1,14 +1,20 @@ package dev.lions.unionflow.client.view; import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse; +import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationSummaryResponse; 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.document.request.CreateDocumentRequest; +import dev.lions.unionflow.server.api.dto.document.request.CreatePieceJointeRequest; +import dev.lions.unionflow.server.api.dto.membre.request.CreateMembreRequest; +import dev.lions.unionflow.server.api.enums.document.TypeDocument; +import dev.lions.unionflow.client.service.DocumentService; import dev.lions.unionflow.client.service.MembreService; -import dev.lions.unionflow.client.service.AssociationService; +import dev.lions.unionflow.client.service.OrganisationService; import dev.lions.unionflow.client.service.ErrorHandlerService; import dev.lions.unionflow.client.service.RetryService; import dev.lions.unionflow.client.service.ValidationService; +import java.util.UUID; import jakarta.faces.view.ViewScoped; import jakarta.inject.Named; import jakarta.inject.Inject; @@ -38,7 +44,7 @@ public class MembreInscriptionBean implements Serializable { @Inject @RestClient - AssociationService associationService; + OrganisationService organisationService; @Inject ValidationService validationService; @@ -49,6 +55,10 @@ public class MembreInscriptionBean implements Serializable { @Inject RetryService retryService; + @Inject + @RestClient + DocumentService documentService; + @Inject SouscriptionBean souscriptionBean; @@ -90,7 +100,7 @@ public class MembreInscriptionBean implements Serializable { private String motifAdhesion; private String organisationId; // ID de l'organisation choisie private String organisationNom; // Nom de l'organisation affichée - private List organisationsDisponibles = new ArrayList<>(); // Liste des organisations + private List organisationsDisponibles = new ArrayList<>(); // Liste des organisations private boolean accepteReglement = false; private boolean acceptePrelevement = false; private boolean autorisationMarketing = false; @@ -120,13 +130,13 @@ public class MembreInscriptionBean implements Serializable { // génération) this.numeroGenere = "À générer automatiquement"; - // Charger les organisations actives (DRY/WOU - utilise AssociationService) + // Charger les organisations actives (DRY/WOU - utilise OrganisationService) try { - PagedResponse response = retryService.executeWithRetrySupplier( - () -> associationService.listerToutes(0, 1000), + PagedResponse response = retryService.executeWithRetrySupplier( + () -> organisationService.listerToutes(0, 1000), "chargement des associations"); organisationsDisponibles = (response != null && response.getData() != null) ? response.getData() - : new ArrayList<>(); + : new ArrayList(); LOG.infof("Associations chargées: %d", organisationsDisponibles.size()); } catch (Exception e) { LOG.errorf(e, "Erreur lors du chargement des organisations"); @@ -150,34 +160,30 @@ public class MembreInscriptionBean implements Serializable { return null; } - // Créer le DTO membre (DRY/WOU - utilise MembreResponse du client) - MembreResponse nouveauMembre = new MembreResponse(); - // Note: numeroMembre sera généré automatiquement par le backend si null - nouveauMembre.setNumeroMembre(null); // Le backend génère automatiquement - nouveauMembre.setNom(nom); - nouveauMembre.setPrenom(prenom); - nouveauMembre.setEmail(email); - // Utiliser telephoneMobile si disponible, sinon telephone - nouveauMembre.setTelephone(telephoneMobile != null && !telephoneMobile.trim().isEmpty() - ? telephoneMobile - : telephone); - nouveauMembre.setDateNaissance(dateNaissance); - // Note: MembreResponse n'a pas de champs adresse ni ville - nouveauMembre.setProfession(profession); - nouveauMembre.setStatutMatrimonial(situationMatrimoniale); - nouveauMembre.setNationalite(nationalite); - nouveauMembre.setStatutCompte("ACTIF"); // Statut actif par défaut pour nouveaux membres - // Note: setDateInscription n'existe probablement pas non plus, c'est géré automatiquement + // Convertir l'organisationId String → UUID + UUID orgUuid = null; + if (organisationId != null && !organisationId.trim().isEmpty()) { + try { + orgUuid = UUID.fromString(organisationId.trim()); + } catch (IllegalArgumentException e) { + errorHandler.showWarning("Erreur", "Identifiant d'organisation invalide."); + return null; + } + } - // Note: MembreResponse n'a pas de champ associationId/organisationId - // L'organisation est déterminée par le contexte de session côté backend - // Conversion de l'organisationId String vers UUID - // try { - // nouveauMembre.setAssociationId(java.util.UUID.fromString(organisationId)); - // } catch (IllegalArgumentException e) { - // errorHandler.showWarning("Erreur", "Identifiant d'organisation invalide."); - // return null; - // } + // Construire la requête de création (DTO correct attendu par le backend) + CreateMembreRequest nouveauMembre = CreateMembreRequest.builder() + .prenom(prenom) + .nom(nom) + .email(email) + .telephone(telephoneMobile != null && !telephoneMobile.trim().isEmpty() + ? telephoneMobile : telephone) + .dateNaissance(dateNaissance) + .profession(profession) + .statutMatrimonial(situationMatrimoniale) + .nationalite(nationalite) + .organisationId(orgUuid) + .build(); // Validation des données ValidationService.ValidationResult validationResult = validationService.validate(nouveauMembre); @@ -195,20 +201,63 @@ public class MembreInscriptionBean implements Serializable { () -> membreService.creer(nouveauMembre), "inscription d'un membre"); - // Gestion de la photo si disponible (DRY/WOU - à implémenter via - // DocumentService) + // Upload de la photo via DocumentService (non bloquant : le membre est déjà créé) if (photoBase64 != null && !photoBase64.trim().isEmpty()) { - LOG.infof("Photo cadrée reçue: %d caractères", photoBase64.length()); - // Note: La sauvegarde de la photo sera implémentée via DocumentService - // qui appellera l'API backend pour stocker la photo associée au membre. - // Pour l'instant, on stocke juste l'info dans les logs. + try { + // Le contenu base64 est stocké dans cheminStockage ; + // taille approximée en octets = nb_chars_base64 * 3/4 + long tailleApprox = (long) (photoBase64.length() * 0.75); + String nomFichierPhoto = "photo-" + membreCreee.getNumeroMembre() + ".jpg"; + CreateDocumentRequest photoRequest = CreateDocumentRequest.builder() + .nomFichier(nomFichierPhoto) + .nomOriginal(nomFichierPhoto) + .cheminStockage(photoBase64) + .typeMime("image/jpeg") + .tailleOctets(tailleApprox) + .typeDocument(TypeDocument.PHOTO) + .description("Photo du membre " + membreCreee.getNomComplet()) + .build(); + documentService.creerDocument(photoRequest); + LOG.infof("Photo uploadée pour le membre %s", membreCreee.getNumeroMembre()); + } catch (Exception ex) { + LOG.warnf(ex, "Impossible d'uploader la photo du membre %s — le membre reste créé", + membreCreee.getNumeroMembre()); + } } - // Gestion des documents joints (DRY/WOU - à implémenter via DocumentService) + // Upload des documents joints via DocumentService (non bloquant) if (documentsJoints != null && !documentsJoints.isEmpty()) { - LOG.infof("Documents joints: %d fichier(s)", documentsJoints.size()); - // Note: Les documents seront uploadés via DocumentService après la création du - // membre + LOG.infof("Documents joints à uploader: %d fichier(s)", documentsJoints.size()); + for (int i = 0; i < documentsJoints.size(); i++) { + String nomDoc = documentsJoints.get(i); + try { + CreateDocumentRequest docRequest = CreateDocumentRequest.builder() + .nomFichier(nomDoc) + .nomOriginal(nomDoc) + .cheminStockage("membres/" + membreCreee.getNumeroMembre() + "/" + nomDoc) + .tailleOctets(0L) + .typeDocument(TypeDocument.PIECE_JUSTIFICATIVE) + .description("Document joint — " + nomDoc) + .build(); + dev.lions.unionflow.server.api.dto.document.response.DocumentResponse docReponse = + documentService.creerDocument(docRequest); + // Rattacher le document créé en tant que pièce jointe du membre + if (docReponse != null && docReponse.getId() != null) { + CreatePieceJointeRequest pjRequest = CreatePieceJointeRequest.builder() + .ordre(i + 1) + .libelle(nomDoc) + .documentId(docReponse.getId()) + .typeEntiteRattachee("MEMBRE") + .entiteRattacheeId(membreCreee.getId()) + .build(); + documentService.creerPieceJointe(pjRequest); + } + LOG.infof("Document '%s' uploadé pour le membre %s", nomDoc, membreCreee.getNumeroMembre()); + } catch (Exception ex) { + LOG.warnf(ex, "Impossible d'uploader le document '%s' pour le membre %s — ignoré", + nomDoc, membreCreee.getNumeroMembre()); + } + } } LOG.infof("Membre inscrit avec succès: %s (N° %s)", @@ -701,11 +750,11 @@ public class MembreInscriptionBean implements Serializable { this.organisationNom = organisationNom; } - public List getOrganisationsDisponibles() { + public List getOrganisationsDisponibles() { return organisationsDisponibles; } - public void setOrganisationsDisponibles(List organisationsDisponibles) { + public void setOrganisationsDisponibles(List organisationsDisponibles) { this.organisationsDisponibles = organisationsDisponibles; } diff --git a/src/main/java/dev/lions/unionflow/client/view/MembreLazyDataModel.java b/src/main/java/dev/lions/unionflow/client/view/MembreLazyDataModel.java index 34d92c6..44ccfcf 100644 --- a/src/main/java/dev/lions/unionflow/client/view/MembreLazyDataModel.java +++ b/src/main/java/dev/lions/unionflow/client/view/MembreLazyDataModel.java @@ -31,6 +31,8 @@ public class MembreLazyDataModel extends LazyDataModelBase lastLoadedPage = new ArrayList<>(); /** * Constructeur avec injection du service REST et des critères de recherche. @@ -53,28 +55,18 @@ public class MembreLazyDataModel extends LazyDataModelBase all = membreService.listerTous().getData(); - if (all == null || all.isEmpty()) return List.of(); - int toIndex = Math.min(first + pageSize, all.size()); - // Conversion MembreResponse → MembreSummaryResponse - List page = first < all.size() ? all.subList(first, toIndex) : List.of(); - return page.stream() - .map(m -> new MembreSummaryResponse( - m.getId(), - m.getNumeroMembre(), - m.getPrenom(), - m.getNom(), - m.getEmail(), - m.getTelephone(), - m.getProfession(), - m.getStatutCompte(), - m.getStatutCompteLibelle(), - m.getStatutCompteSeverity(), - "ACTIF".equals(m.getStatutCompte()), // actif - m.getRoles(), - m.getOrganisationId(), - m.getAssociationNom())) - .collect(java.util.stream.Collectors.toList()); + int pageNum = first / pageSize; + dev.lions.unionflow.server.api.dto.common.PagedResponse pagedResult = + membreService.listerTous(pageNum, pageSize); + if (pagedResult == null || pagedResult.getData() == null || pagedResult.getData().isEmpty()) { + return List.of(); + } + lastLoadedPage = pagedResult.getData(); + // Mettre à jour le rowCount avec le total réel du backend + long total = pagedResult.getTotal() != null + ? pagedResult.getTotal() : lastLoadedPage.size(); + setRowCount((int) total); + return lastLoadedPage; } // Extraire tri @@ -91,25 +83,44 @@ public class MembreLazyDataModel extends LazyDataModelBase filters) { try { applyFiltersToCriteria(filters); searchCriteria.sanitize(); - // Sans critère, compter via listerTous() + // Sans critère, compter via listerTous() (page 0, taille 1 pour avoir le total) if (!searchCriteria.hasAnyCriteria()) { - List all = membreService.listerTous().getData(); - return all != null ? all.size() : 0; + dev.lions.unionflow.server.api.dto.common.PagedResponse r = + membreService.listerTous(0, 1); + if (r == null) return 0; + return r.getTotal() != null ? r.getTotal().intValue() : 0; } MembreSearchResultDTO result = membreService.rechercherAvance( diff --git a/src/main/java/dev/lions/unionflow/client/view/MembreListeBean.java b/src/main/java/dev/lions/unionflow/client/view/MembreListeBean.java index 2b3d281..a3bf939 100644 --- a/src/main/java/dev/lions/unionflow/client/view/MembreListeBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/MembreListeBean.java @@ -3,15 +3,13 @@ package dev.lions.unionflow.client.view; import dev.lions.unionflow.server.api.dto.membre.response.MembreResponse; import dev.lions.unionflow.server.api.dto.common.PagedResponse; import dev.lions.unionflow.client.service.MembreService; -import dev.lions.unionflow.client.service.AssociationService; +import dev.lions.unionflow.client.service.OrganisationService; import dev.lions.unionflow.client.service.NotificationService; import dev.lions.unionflow.client.service.CotisationService; import dev.lions.unionflow.client.service.ErrorHandlerService; import dev.lions.unionflow.client.service.RetryService; import dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria; -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.organisation.response.OrganisationSummaryResponse; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; @@ -60,7 +58,7 @@ public class MembreListeBean implements Serializable { @Inject @RestClient - AssociationService associationService; + OrganisationService organisationService; @Inject @RestClient @@ -76,6 +74,9 @@ public class MembreListeBean implements Serializable { @Inject RetryService retryService; + @Inject + UserSession userSession; + // === Statistiques === @Getter(AccessLevel.NONE) @@ -88,6 +89,14 @@ public class MembreListeBean implements Serializable { @Setter(AccessLevel.NONE) private MembreSearchCriteria searchCriteria = MembreSearchCriteria.builder().build(); + /** + * Org ID imposé pour les non-SUPER_ADMIN. Jamais effacé par les filtres UI. + * Garantit que setEntiteFilter("") ne supprime pas le scope organisationnel. + */ + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private UUID enforcedOrgId; + // === Lazy Data Model pour la DataTable === @Getter(AccessLevel.NONE) @@ -100,7 +109,7 @@ public class MembreListeBean implements Serializable { // === Organisations pour le filtre === - private List organisationsDisponibles = new ArrayList<>(); + private List organisationsDisponibles = new ArrayList<>(); // === Messages groupés === @@ -131,7 +140,9 @@ public class MembreListeBean implements Serializable { @PostConstruct public void init() { try { - // Créer le LazyDataModel avec injection du service + // Pré-filtrer par organisation pour les non-SUPER_ADMIN + initialiserFiltreOrganisation(); + // Créer le LazyDataModel avec les critères (potentiellement avec org filter) this.lazyModel = new MembreLazyDataModel(membreService, searchCriteria); chargerStatistiques(); chargerOrganisations(); @@ -142,6 +153,62 @@ public class MembreListeBean implements Serializable { } } + /** + * Pré-initialise le filtre d'organisation si l'utilisateur n'est pas SUPER_ADMIN. + * Fallback : appelle /api/membres/me si l'entité n'est pas encore en session. + */ + private void initialiserFiltreOrganisation() { + if (userSession.isSuperAdmin()) { + return; // SUPER_ADMIN voit tous les membres, pas de filtre imposé + } + + UUID orgId = null; + + // Cas 0 : organisation active déjà sélectionnée (source la plus fiable) + try { + orgId = userSession.getActiveOrganisationId(); + if (orgId != null) { + LOG.infof("Filtre org depuis activeOrganisationId: %s", orgId); + } + } catch (Exception e) { + LOG.warnf("userSession.getActiveOrganisationId() a échoué: %s", e.getMessage()); + } + + // Cas 1 : entité chargée en session (lazy-load depuis /api/membres/me) + if (orgId == null) { + try { + if (userSession.getEntite() != null && userSession.getEntite().getId() != null) { + orgId = userSession.getEntite().getId(); + LOG.infof("Filtre org depuis userSession.getEntite(): %s", orgId); + } + } catch (Exception e) { + LOG.warnf("userSession.getEntite() a échoué: %s", e.getMessage()); + } + } + + // Cas 2 : appel direct à /api/membres/me + if (orgId == null) { + try { + MembreResponse moi = retryService.executeWithRetrySupplier( + () -> membreService.obtenirMembreConnecte(), + "chargement profil membre connecté"); + if (moi != null && moi.getOrganisationId() != null) { + orgId = moi.getOrganisationId(); + LOG.infof("Filtre org depuis /api/membres/me: %s", orgId); + } + } catch (Exception e) { + LOG.warnf("Impossible de récupérer l'org depuis /api/membres/me: %s", e.getMessage()); + } + } + + if (orgId != null) { + searchCriteria.setOrganisationIds(List.of(orgId)); + this.enforcedOrgId = orgId; + } else { + LOG.warn("Impossible de déterminer l'organisation pour cet utilisateur — aucune donnée ne sera affichée"); + } + } + // ======================================================================== // LAZY DATA MODEL // ======================================================================== @@ -160,11 +227,25 @@ public class MembreListeBean implements Serializable { private void chargerStatistiques() { try { - this.statistiques = retryService.executeWithRetrySupplier( - () -> membreService.obtenirStatistiques(), - "chargement des statistiques membres"); - LOG.infof("Statistiques chargées: %d membres", - statistiques != null ? statistiques.getTotalMembres() : 0); + // Récupérer l'orgId depuis les critères de recherche (déjà initialisé dans init()) + UUID orgId = searchCriteria.getOrganisationIds() != null + && !searchCriteria.getOrganisationIds().isEmpty() + ? searchCriteria.getOrganisationIds().get(0) + : null; + + if (!userSession.isSuperAdmin() && orgId != null) { + // Pour ADMIN_ORGANISATION/MODERATEUR : stats filtrées via rechercherAvance + this.statistiques = chargerStatistiquesParOrg(orgId); + LOG.infof("Statistiques org [%s] calculées: %d membres", + orgId, statistiques != null ? statistiques.getTotalMembres() : 0); + } else { + // SUPER_ADMIN ou aucun org filter : stats globales depuis l'API + this.statistiques = retryService.executeWithRetrySupplier( + () -> membreService.obtenirStatistiques(), + "chargement des statistiques membres"); + LOG.infof("Statistiques globales chargées: %d membres", + statistiques != null ? statistiques.getTotalMembres() : 0); + } } catch (Exception e) { LOG.errorf(e, "Impossible de charger les statistiques"); errorHandler.handleException(e, "lors du chargement des statistiques", null); @@ -172,6 +253,46 @@ public class MembreListeBean implements Serializable { } } + /** + * Calcule les statistiques pour une organisation via rechercherAvance (3 appels légers). + */ + private MembreService.StatistiquesMembreDTO chargerStatistiquesParOrg(UUID orgId) { + MembreService.StatistiquesMembreDTO dto = new MembreService.StatistiquesMembreDTO(); + + // Total membres de l'org + dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO total = + membreService.rechercherAvance( + MembreSearchCriteria.builder() + .organisationIds(List.of(orgId)).build(), + 0, 1, "nom", "asc"); + long totalCount = total != null ? total.getTotalElements() : 0L; + + // Membres actifs de l'org + dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO actifs = + membreService.rechercherAvance( + MembreSearchCriteria.builder() + .organisationIds(List.of(orgId)) + .statut("ACTIF").build(), + 0, 1, "nom", "asc"); + long actifsCount = actifs != null ? actifs.getTotalElements() : 0L; + + // Membres ayant adhéré dans les 30 derniers jours + dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO nouveaux = + membreService.rechercherAvance( + MembreSearchCriteria.builder() + .organisationIds(List.of(orgId)) + .dateAdhesionMin(LocalDate.now().minusDays(30)).build(), + 0, 1, "nom", "asc"); + long nouveauxCount = nouveaux != null ? nouveaux.getTotalElements() : 0L; + + dto.setTotalMembres(totalCount); + dto.setMembresActifs(actifsCount); + dto.setMembresInactifs(totalCount - actifsCount); + dto.setNouveauxMembres30Jours(nouveauxCount); + dto.setTauxActivite(totalCount > 0 ? (actifsCount * 100.0 / totalCount) : 0.0); + return dto; + } + public int getTotalMembres() { return statistiques != null && statistiques.getTotalMembres() != null ? statistiques.getTotalMembres().intValue() @@ -196,6 +317,12 @@ public class MembreListeBean implements Serializable { : 0; } + public int getTauxActivite() { + int total = getTotalMembres(); + if (total == 0) return 0; + return (int) Math.round(getMembresActifs() * 100.0 / total); + } + // ======================================================================== // ORGANISATIONS // ======================================================================== @@ -203,8 +330,8 @@ public class MembreListeBean implements Serializable { private void chargerOrganisations() { organisationsDisponibles = new ArrayList<>(); try { - PagedResponse response = retryService.executeWithRetrySupplier( - () -> associationService.listerToutes(0, 1000), + PagedResponse response = retryService.executeWithRetrySupplier( + () -> organisationService.listerToutes(0, 1000), "chargement des organisations"); if (response != null && response.getData() != null) { organisationsDisponibles.addAll(response.getData()); @@ -274,7 +401,12 @@ public class MembreListeBean implements Serializable { LOG.warnf("ID d'organisation invalide: %s", value); } } else { - searchCriteria.setOrganisationIds(null); + // Si non-SUPER_ADMIN, maintenir le filtre d'organisation imposé + if (enforcedOrgId != null) { + searchCriteria.setOrganisationIds(List.of(enforcedOrgId)); + } else { + searchCriteria.setOrganisationIds(null); + } } } @@ -356,6 +488,8 @@ public class MembreListeBean implements Serializable { public void reinitialiserFiltres() { searchCriteria = MembreSearchCriteria.builder().build(); + // Préserver le filtre d'organisation pour les non-SUPER_ADMIN + initialiserFiltreOrganisation(); lazyModel.setSearchCriteria(searchCriteria); } diff --git a/src/main/java/dev/lions/unionflow/client/view/MembreProfilBean.java b/src/main/java/dev/lions/unionflow/client/view/MembreProfilBean.java index 13b7d3a..c984d11 100644 --- a/src/main/java/dev/lions/unionflow/client/view/MembreProfilBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/MembreProfilBean.java @@ -7,6 +7,7 @@ import dev.lions.unionflow.client.service.ErrorHandlerService; import dev.lions.unionflow.client.service.EvenementService; import dev.lions.unionflow.client.service.NotificationService; import dev.lions.unionflow.client.service.RetryService; +import java.util.Map; import jakarta.faces.view.ViewScoped; import jakarta.inject.Inject; import jakarta.inject.Named; @@ -53,7 +54,10 @@ public class MembreProfilBean implements Serializable { @Inject RetryService retryService; - + + @Inject + UserSession userSession; + private Membre membre; private Membre membreEdit; private Statistiques statistiques; @@ -64,7 +68,17 @@ public class MembreProfilBean implements Serializable { private HistoriqueData historique; private ContactData contact; private UUID membreId; - + + // ── Cycle de vie de l'adhésion ───────────────────────────────────────── + /** UUID du lien MembreOrganisation (chargé en lazy depuis le backend) */ + private UUID membreOrgId; + /** Statut d'adhésion dans l'organisation active (INVITE, EN_ATTENTE_VALIDATION, ACTIF, SUSPENDU…) */ + private String statutAdhesion; + /** Motif saisi dans les dialogs de suspension / radiation */ + private String motifAction; + /** Rôle à proposer lors d'une invitation */ + private String roleOrgInvit; + @PostConstruct public void init() { // Récupérer membreId depuis les paramètres de requête (DRY/WOU - réutilisable) @@ -124,142 +138,135 @@ public class MembreProfilBean implements Serializable { membre.setEmail(dto.getEmail()); membre.setTelephone(dto.getTelephone()); membre.setDateNaissance(dto.getDateNaissance()); - // Note: Genre, situation familiale, ville, pays, type membre ne sont pas disponibles dans MembreResponse client - // Ces champs seront ajoutés ultérieurement si nécessaire membre.setProfession(dto.getProfession()); - // Note: MembreResponse n'a pas de champ adresse - membre.setAdresse(null); - membre.setStatut(dto.getStatutCompte() != null ? dto.getStatutCompte() : "ACTIF"); // Corrigé: getStatutCompte - membre.setDateAdhesion(dto.getDateAdhesion()); // Corrigé: getDateAdhesion + membre.setAdresse(dto.getAdresse()); + membre.setVille(dto.getVille()); + membre.setStatut(dto.getStatutCompte() != null ? dto.getStatutCompte() : "ACTIF"); + membre.setDateAdhesion(dto.getDateAdhesion()); + membre.setEntite(dto.getOrganisationNom()); + membre.setPhotoUrl(dto.getPhotoUrl()); + membre.setStatutMatrimonial(dto.getStatutMatrimonial()); + membre.setNationalite(dto.getNationalite()); + // typeMembre calculé depuis les rôles (cohérent avec MembreSummaryResponse.typeMembre()) + membre.setTypeMembre(calculerTypeMembre(dto.getRoles())); return membre; } + + private String calculerTypeMembre(List roles) { + if (roles == null || roles.isEmpty()) return "ACTIF"; + if (roles.contains("PRESIDENT")) return "PRESIDENT"; + if (roles.contains("VICE_PRESIDENT")) return "VICE_PRESIDENT"; + if (roles.contains("SECRETAIRE")) return "SECRETAIRE"; + if (roles.contains("TRESORIER")) return "TRESORIER"; + if (roles.contains("ADMIN_ORGANISATION")) return "ADMIN"; + if (roles.contains("MODERATEUR")) return "MODERATEUR"; + return "ACTIF"; + } private void chargerStatistiques() { statistiques = new Statistiques(); - statistiques.setEvenementsParticipes(24); - statistiques.setCotisationsPayees(12); - statistiques.setAidesRecues(2); - statistiques.setScoreEngagement(95); - statistiques.setTauxParticipation(85); - statistiques.setEvenementsAnnee(8); - statistiques.setEvenementsTotal(24); - statistiques.setEvenementsOrganises(3); - statistiques.setAbsences(2); + try { + // Cotisations payées pour ce membre + List cotisationsList = + cotisationService.obtenirParMembre(membreId, 0, 100); + long cotisationsPayees = cotisationsList != null + ? cotisationsList.stream().filter(c -> "PAYEE".equals(c.getStatut()) || "VALIDE".equals(c.getStatut())).count() + : 0L; + statistiques.setCotisationsPayees((int) cotisationsPayees); + + // Événements depuis MembreResponse (champ déjà chargé) + if (membre != null) { + statistiques.setEvenementsTotal(0); // pas d'API par membre + } + } catch (Exception e) { + LOG.warnf("Impossible de charger les statistiques détaillées: %s", e.getMessage()); + } } private void chargerCotisations() { cotisations = new CotisationsData(); - cotisations.setStatutActuel("À jour"); - cotisations.setStatutSeverity("success"); - cotisations.setDernierPaiement("15/07/2024"); - cotisations.setProchaineEcheance("15/08/2024"); - cotisations.setProchaineEcheanceClass("text-green-500"); - cotisations.setTotalAnnee("120 000 FCFA"); - - // Historique des paiements - List historique = new ArrayList<>(); - for (int i = 1; i <= 6; i++) { - PaiementCotisation paiement = new PaiementCotisation(); - paiement.setDate(LocalDate.now().minusMonths(i)); - paiement.setMontant(10000.0); - paiement.setModePaiement(i % 2 == 0 ? "Wave Money" : "Espèces"); - paiement.setModeIcon(i % 2 == 0 ? "pi-mobile" : "pi-money-bill"); - paiement.setStatut("Validé"); - paiement.setStatutSeverity("success"); - historique.add(paiement); + try { + List liste = + retryService.executeWithRetrySupplier( + () -> cotisationService.obtenirParMembre(membreId, 0, 12), + "chargement des cotisations du membre"); + + List historiqueCot = new ArrayList<>(); + if (liste != null) { + // Trouver la dernière cotisation payée pour le statut global + liste.stream() + .filter(c -> "PAYEE".equals(c.getStatut()) || "VALIDE".equals(c.getStatut())) + .findFirst() + .ifPresent(derniere -> { + cotisations.setStatutActuel("À jour"); + cotisations.setStatutSeverity("success"); + cotisations.setDernierPaiement( + derniere.getDatePaiementFormattee() != null + ? derniere.getDatePaiementFormattee() + : (derniere.getPeriode() != null ? derniere.getPeriode() : "—")); + }); + + // Prochaine échéance : la plus proche non payée + liste.stream() + .filter(c -> !"PAYEE".equals(c.getStatut()) && !"VALIDE".equals(c.getStatut())) + .findFirst() + .ifPresent(prochaine -> { + cotisations.setStatutActuel("En attente"); + cotisations.setStatutSeverity("warning"); + cotisations.setProchaineEcheance( + prochaine.getDateEcheanceFormattee() != null + ? prochaine.getDateEcheanceFormattee() : "—"); + cotisations.setProchaineEcheanceClass( + "EN_RETARD".equals(prochaine.getStatut()) ? "text-red-500" : "text-orange-500"); + }); + + // Historique + for (var c : liste) { + PaiementCotisation p = new PaiementCotisation(); + p.setDate(c.getDateEcheance()); + p.setMontant(c.getMontant() != null ? c.getMontant().doubleValue() : 0.0); + p.setModePaiement(c.getModePaiementLibelle() != null ? c.getModePaiementLibelle() : "—"); + p.setModeIcon(c.getModePaiementIcon() != null ? c.getModePaiementIcon() : "pi-money-bill"); + p.setStatut(c.getStatutLibelle() != null ? c.getStatutLibelle() : c.getStatut()); + p.setStatutSeverity(c.getStatutSeverity() != null ? c.getStatutSeverity() : "secondary"); + historiqueCot.add(p); + } + } + if (cotisations.getStatutActuel() == null) { + cotisations.setStatutActuel("Aucune cotisation"); + cotisations.setStatutSeverity("secondary"); + } + cotisations.setHistorique(historiqueCot); + } catch (Exception e) { + LOG.warnf("Impossible de charger les cotisations: %s", e.getMessage()); + cotisations.setStatutActuel("Indisponible"); + cotisations.setStatutSeverity("secondary"); + cotisations.setHistorique(new ArrayList<>()); } - cotisations.setHistorique(historique); } private void chargerEvenements() { + // Pas d'endpoint /api/evenements/membre/{id} — liste vide pour l'instant evenements = new EvenementsData(); - List recents = new ArrayList<>(); - - String[] titres = {"Réunion mensuelle", "Action humanitaire", "Formation leadership", "Collecte de fonds"}; - String[] lieux = {"Hôtel Radisson", "École Primaire", "Centre de formation", "Stade Léopold Sédar Senghor"}; - String[] types = {"REUNION", "ACTION", "FORMATION", "COLLECTE"}; - - for (int i = 0; i < 4; i++) { - EvenementParticipation evt = new EvenementParticipation(); - evt.setTitre(titres[i]); - evt.setDate(LocalDate.now().minusDays(i * 7).toString()); - evt.setLieu(lieux[i]); - evt.setParticipation(i < 3 ? "Présent" : "Absent"); - evt.setParticipationSeverity(i < 3 ? "success" : "danger"); - evt.setRole(i == 0 ? "Organisateur" : "Participant"); - evt.setTypeIcon("pi-calendar"); - evt.setTypeColorClass("bg-blue-500"); - recents.add(evt); - } - evenements.setRecents(recents); + evenements.setRecents(new ArrayList<>()); } - + private void chargerAides() { + // Pas d'endpoint /api/demandes-aide?demandeurId — liste vide pour l'instant aides = new AidesData(); - List recues = new ArrayList<>(); - - Aide aide1 = new Aide(); - aide1.setType("Aide médicale"); - aide1.setMontant(50000.0); - aide1.setDate(LocalDate.of(2024, 5, 10)); - aide1.setStatut("Validée"); - aide1.setStatutSeverity("success"); - aide1.setTypeIcon("pi-heart"); - aide1.setTypeColor("text-red-500"); - recues.add(aide1); - - Aide aide2 = new Aide(); - aide2.setType("Aide scolaire"); - aide2.setMontant(25000.0); - aide2.setDate(LocalDate.of(2024, 1, 15)); - aide2.setStatut("Validée"); - aide2.setStatutSeverity("success"); - aide2.setTypeIcon("pi-book"); - aide2.setTypeColor("text-blue-500"); - recues.add(aide2); - - aides.setRecues(recues); + aides.setRecues(new ArrayList<>()); } - + private void chargerDemandes() { + // Pas d'endpoint /api/demandes-aide?demandeurId — liste vide pour l'instant demandes = new DemandesData(); - List enCours = new ArrayList<>(); - - Demande demande1 = new Demande(); - demande1.setType("Certificat"); - demande1.setObjet("Certificat d'adhésion"); - demande1.setDateDepot(LocalDate.now().minusDays(3)); - demande1.setStatut("En cours"); - demande1.setStatutSeverity("warning"); - enCours.add(demande1); - - demandes.setEnCours(enCours); + demandes.setEnCours(new ArrayList<>()); } - + private void chargerHistorique() { + // Pas d'endpoint d'historique par membre — liste vide pour l'instant historique = new HistoriqueData(); - List activites = new ArrayList<>(); - - String[] descriptions = { - "Profil mis à jour", - "Participation à la réunion mensuelle", - "Paiement de cotisation validé", - "Nouvelle adhésion enregistrée" - }; - - for (int i = 0; i < 4; i++) { - Activite activite = new Activite(); - activite.setDescription(descriptions[i]); - activite.setDate(LocalDateTime.now().minusDays(i * 2).toString()); - activite.setAuteur(i == 0 ? "Jean DIALLO" : "Admin"); - activite.setIcone(i % 2 == 0 ? "pi-user" : "pi-calendar"); - activite.setCouleur(i % 2 == 0 ? "text-blue-500" : "text-green-500"); - if (i == 2) { - activite.setDetails("Montant: 10 000 FCFA via Wave Money"); - } - activites.add(activite); - } - historique.setActivites(activites); + historique.setActivites(new ArrayList<>()); } private void initContact() { @@ -397,6 +404,103 @@ public class MembreProfilBean implements Serializable { } } + // ── Cycle de vie de l'adhésion ───────────────────────────────────────── + + /** Charge le statut d'adhésion du membre dans l'organisation active. */ + public void chargerStatutAdhesion() { + UUID activeOrgId = userSession.getActiveOrganisationId(); + if (membreId == null || activeOrgId == null) return; + try { + Map info = retryService.executeWithRetrySupplier( + () -> membreService.getAdhesionStatut(membreId, activeOrgId), + "chargement statut adhésion" + ); + statutAdhesion = info != null ? String.valueOf(info.get("statut")) : null; + Object oid = info != null ? info.get("membreOrgId") : null; + if (oid != null) membreOrgId = UUID.fromString(oid.toString()); + } catch (Exception e) { + statutAdhesion = null; + LOG.debugf("Aucune adhésion trouvée pour membre %s dans l'org active", membreId); + } + } + + /** Invite le membre dans l'organisation active (crée un lien INVITE). */ + public void inviterDansOrgActive() { + UUID activeOrgId = userSession.getActiveOrganisationId(); + if (membreId == null || activeOrgId == null) { + errorHandler.showWarning("Attention", "Organisation active non définie"); + return; + } + try { + retryService.executeWithRetrySupplier( + () -> membreService.inviterMembreOrg(membreId, activeOrgId, roleOrgInvit), + "invitation d'un membre" + ); + statutAdhesion = "INVITE"; + errorHandler.showSuccess("Succès", "Invitation envoyée au membre"); + } catch (Exception e) { + errorHandler.handleException(e, "lors de l'invitation du membre", null); + } + } + + /** Active l'adhésion du membre dans l'organisation active. */ + public void activerAdhesion() { + UUID activeOrgId = userSession.getActiveOrganisationId(); + if (membreId == null || activeOrgId == null) return; + try { + retryService.executeWithRetrySupplier( + () -> membreService.activerAdhesion(membreId, activeOrgId, Map.of("motif", motifAction != null ? motifAction : "")), + "activation adhésion" + ); + statutAdhesion = "ACTIF"; + motifAction = null; + errorHandler.showSuccess("Succès", "Adhésion activée"); + } catch (Exception e) { + errorHandler.handleException(e, "lors de l'activation de l'adhésion", null); + } + } + + /** Suspend l'adhésion du membre dans l'organisation active. */ + public void suspendrAdhesion() { + UUID activeOrgId = userSession.getActiveOrganisationId(); + if (membreId == null || activeOrgId == null) return; + try { + retryService.executeWithRetrySupplier( + () -> membreService.suspendrAdhesion(membreId, activeOrgId, Map.of("motif", motifAction != null ? motifAction : "")), + "suspension adhésion" + ); + statutAdhesion = "SUSPENDU"; + motifAction = null; + errorHandler.showSuccess("Succès", "Adhésion suspendue"); + } catch (Exception e) { + errorHandler.handleException(e, "lors de la suspension de l'adhésion", null); + } + } + + /** Radie le membre de l'organisation active. */ + public void radierAdhesion() { + UUID activeOrgId = userSession.getActiveOrganisationId(); + if (membreId == null || activeOrgId == null) return; + try { + retryService.executeWithRetrySupplier( + () -> membreService.radierAdhesion(membreId, activeOrgId, Map.of("motif", motifAction != null ? motifAction : "")), + "radiation adhésion" + ); + statutAdhesion = "RADIE"; + motifAction = null; + errorHandler.showSuccess("Succès", "Le membre a été radié de l'organisation"); + } catch (Exception e) { + errorHandler.handleException(e, "lors de la radiation du membre", null); + } + } + + // Getters/setters cycle de vie + public String getStatutAdhesion() { return statutAdhesion; } + public String getMotifAction() { return motifAction; } + public void setMotifAction(String motifAction) { this.motifAction = motifAction; } + public String getRoleOrgInvit() { return roleOrgInvit; } + public void setRoleOrgInvit(String roleOrgInvit) { this.roleOrgInvit = roleOrgInvit; } + public void exporterDonnees() { try { // Exporter les données du membre via le service (DRY/WOU) @@ -518,6 +622,8 @@ public class MembreProfilBean implements Serializable { private LocalDate dateNaissance; private String genre; private String situationFamiliale; + private String statutMatrimonial; + private String nationalite; private String profession; private String adresse; private String ville; @@ -558,7 +664,13 @@ public class MembreProfilBean implements Serializable { public String getSituationFamiliale() { return situationFamiliale; } public void setSituationFamiliale(String situationFamiliale) { this.situationFamiliale = situationFamiliale; } - + + public String getStatutMatrimonial() { return statutMatrimonial; } + public void setStatutMatrimonial(String statutMatrimonial) { this.statutMatrimonial = statutMatrimonial; } + + public String getNationalite() { return nationalite; } + public void setNationalite(String nationalite) { this.nationalite = nationalite; } + public String getProfession() { return profession; } public void setProfession(String profession) { this.profession = profession; } @@ -606,6 +718,7 @@ public class MembreProfilBean implements Serializable { } public String getStatutSeverity() { + if (statut == null) return "secondary"; return switch (statut) { case "ACTIF" -> "success"; case "INACTIF" -> "warning"; @@ -614,8 +727,9 @@ public class MembreProfilBean implements Serializable { default -> "secondary"; }; } - + public String getStatutIcon() { + if (statut == null) return "pi-circle"; return switch (statut) { case "ACTIF" -> "pi-check"; case "INACTIF" -> "pi-pause"; @@ -624,8 +738,9 @@ public class MembreProfilBean implements Serializable { default -> "pi-circle"; }; } - + public String getTypeSeverity() { + if (typeMembre == null) return "info"; return switch (typeMembre) { case "ACTIF" -> "info"; case "ASSOCIE" -> "success"; @@ -634,8 +749,9 @@ public class MembreProfilBean implements Serializable { default -> "info"; }; } - + public String getTypeIcon() { + if (typeMembre == null) return "pi-user"; return switch (typeMembre) { case "ACTIF" -> "pi-user"; case "ASSOCIE" -> "pi-users"; diff --git a/src/main/java/dev/lions/unionflow/client/view/MembreRechercheBean.java b/src/main/java/dev/lions/unionflow/client/view/MembreRechercheBean.java index 097bf9b..5279934 100644 --- a/src/main/java/dev/lions/unionflow/client/view/MembreRechercheBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/MembreRechercheBean.java @@ -3,7 +3,8 @@ package dev.lions.unionflow.client.view; import dev.lions.unionflow.server.api.dto.membre.response.MembreResponse; import dev.lions.unionflow.server.api.dto.common.PagedResponse; import dev.lions.unionflow.client.service.MembreService; -import dev.lions.unionflow.client.service.AssociationService; +import dev.lions.unionflow.client.service.OrganisationService; +import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationSummaryResponse; import jakarta.enterprise.context.SessionScoped; import jakarta.inject.Inject; import jakarta.inject.Named; @@ -33,7 +34,7 @@ public class MembreRechercheBean implements Serializable { @Inject @RestClient - private AssociationService associationService; + private OrganisationService organisationService; private Filtres filtres; private Statistiques statistiques; @@ -60,7 +61,7 @@ public class MembreRechercheBean implements Serializable { filtres = new Filtres(); filtres.setStatuts(new ArrayList<>()); filtres.setTypesMembre(new ArrayList<>()); - filtres.setEntites(new ArrayList<>()); + filtres.setEntites(new ArrayList()); filtres.setStatutsCotisation(new ArrayList<>()); filtres.setGenres(new ArrayList<>()); } @@ -113,19 +114,20 @@ public class MembreRechercheBean implements Serializable { membre.setStatut("ACTIF"); } membre.setDateAdhesion(dto.getDateAdhesion()); // Corrigé: getDateAdhesion + membre.setCotisationStatut("N/A"); return membre; } private void initializeEntites() { entitesDisponibles = new ArrayList<>(); try { - PagedResponse response = associationService + PagedResponse response = organisationService .listerToutes(0, 1000); if (response != null && response.getData() != null) { - for (dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse assoc : response.getData()) { + for (dev.lions.unionflow.server.api.dto.organisation.response.OrganisationSummaryResponse assoc : response.getData()) { Entite entite = new Entite(); - entite.setId(assoc.getId()); - entite.setNom(assoc.getNom()); + entite.setId(assoc.id()); + entite.setNom(assoc.nom()); entitesDisponibles.add(entite); } } @@ -202,7 +204,7 @@ public class MembreRechercheBean implements Serializable { // Filtre téléphone if (filtres.getTelephone() != null && !filtres.getTelephone().trim().isEmpty()) { - if (!membre.getTelephone().contains(filtres.getTelephone())) { + if (membre.getTelephone() == null || !membre.getTelephone().contains(filtres.getTelephone())) { return false; } } @@ -279,11 +281,12 @@ public class MembreRechercheBean implements Serializable { } // Filtre dates d'adhésion - if (filtres.getDateAdhesionDebut() != null + if (filtres.getDateAdhesionDebut() != null && membre.getDateAdhesion() != null && membre.getDateAdhesion().isBefore(filtres.getDateAdhesionDebut())) { return false; } - if (filtres.getDateAdhesionFin() != null && membre.getDateAdhesion().isAfter(filtres.getDateAdhesionFin())) { + if (filtres.getDateAdhesionFin() != null && membre.getDateAdhesion() != null + && membre.getDateAdhesion().isAfter(filtres.getDateAdhesionFin())) { return false; } @@ -500,7 +503,7 @@ public class MembreRechercheBean implements Serializable { private String profession; private List statuts = new ArrayList<>(); private List typesMembre = new ArrayList<>(); - private List entites = new ArrayList<>(); + private List entites = new ArrayList<>(); private List statutsCotisation = new ArrayList<>(); private List genres = new ArrayList<>(); private Integer ageMin; @@ -579,11 +582,11 @@ public class MembreRechercheBean implements Serializable { this.typesMembre = typesMembre; } - public List getEntites() { + public List getEntites() { return entites; } - public void setEntites(List entites) { + public void setEntites(List entites) { this.entites = entites; } @@ -975,11 +978,11 @@ public class MembreRechercheBean implements Serializable { } public String getDernierPaiement() { - return cotisationStatut.equals("À jour") ? "Ce mois" : "En retard"; + return "À jour".equals(cotisationStatut) ? "Ce mois" : "En retard"; } public String getCotisationColor() { - return cotisationStatut.equals("À jour") ? "text-green-500" : "text-red-500"; + return "À jour".equals(cotisationStatut) ? "text-green-500" : "text-red-500"; } } diff --git a/src/main/java/dev/lions/unionflow/client/view/MesCotisationsPaiementBean.java b/src/main/java/dev/lions/unionflow/client/view/MesCotisationsPaiementBean.java index 89684bf..88c49c1 100644 --- a/src/main/java/dev/lions/unionflow/client/view/MesCotisationsPaiementBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/MesCotisationsPaiementBean.java @@ -84,6 +84,7 @@ public class MesCotisationsPaiementBean implements Serializable { // Formulaires dialogs // Dialog Paiement en Ligne private UUID cotisationSelectionneeId; + private CotisationPerso cotisationSelectionnee; private String methodePaiementChoisie = "WAVE"; private String numeroTelephone; @@ -266,9 +267,32 @@ public class MesCotisationsPaiementBean implements Serializable { // ═══════════════════════════════════════════════════════════════════════ /** - * Initie un paiement en ligne (Wave, Orange, Free Money, Carte) + * Sélectionne une cotisation et prépare le dialog de paiement en ligne */ - public void initierPaiementEnLigne() { + public void initierPaiementEnLigne(CotisationPerso cotis) { + this.cotisationSelectionnee = cotis; + this.cotisationSelectionneeId = cotis != null ? cotis.getId() : null; + this.numeroTelephone = null; + this.methodePaiementChoisie = "WAVE_MONEY"; + LOG.infof("Dialog paiement en ligne ouvert pour cotisation: %s", cotisationSelectionneeId); + } + + /** + * Sélectionne une cotisation et prépare le dialog de paiement manuel + */ + public void selectionnerCotisation(CotisationPerso cotis) { + this.cotisationSelectionnee = cotis; + this.cotisationSelectionneeId = cotis != null ? cotis.getId() : null; + this.methodePaiementManuel = "ESPECES"; + this.referencePaiementManuel = null; + this.commentairePaiement = null; + LOG.infof("Dialog paiement manuel ouvert pour cotisation: %s", cotisationSelectionneeId); + } + + /** + * Procède au paiement en ligne (Wave, Orange, Free Money, Carte) + */ + public void procederPaiementEnLigne() { if (cotisationSelectionneeId == null) { errorHandler.showWarning("Attention", "Veuillez sélectionner une cotisation à payer"); return; @@ -342,12 +366,13 @@ public class MesCotisationsPaiementBean implements Serializable { /** * Télécharge le reçu PDF d'un paiement */ - public void telechargerRecu(UUID paiementId) { - if (paiementId == null) { + public void telechargerRecu(PaiementPerso paiement) { + if (paiement == null || paiement.getId() == null) { errorHandler.showWarning("Attention", "Impossible de télécharger le reçu"); return; } + UUID paiementId = paiement.getId(); try { // TODO: Créer endpoint GET /api/paiements/telecharger-recu/{id} byte[] recu = retryService.executeWithRetrySupplier( @@ -365,27 +390,6 @@ public class MesCotisationsPaiementBean implements Serializable { } } - /** - * Ouvre le dialog de paiement en ligne pour une cotisation - */ - public void ouvrirDialogPaiementEnLigne(UUID cotisationId) { - this.cotisationSelectionneeId = cotisationId; - this.numeroTelephone = null; - this.methodePaiementChoisie = "WAVE"; - LOG.infof("Dialog paiement en ligne ouvert pour cotisation: %s", cotisationId); - } - - /** - * Ouvre le dialog de paiement manuel pour une cotisation - */ - public void ouvrirDialogPaiementManuel(UUID cotisationId) { - this.cotisationSelectionneeId = cotisationId; - this.methodePaiementManuel = "ESPECES"; - this.referencePaiementManuel = null; - this.commentairePaiement = null; - LOG.infof("Dialog paiement manuel ouvert pour cotisation: %s", cotisationId); - } - /** * Actualise les données */ @@ -455,6 +459,8 @@ public class MesCotisationsPaiementBean implements Serializable { public UUID getCotisationSelectionneeId() { return cotisationSelectionneeId; } public void setCotisationSelectionneeId(UUID cotisationSelectionneeId) { this.cotisationSelectionneeId = cotisationSelectionneeId; } + public CotisationPerso getCotisationSelectionnee() { return cotisationSelectionnee; } + public String getMethodePaiementChoisie() { return methodePaiementChoisie; } public void setMethodePaiementChoisie(String methodePaiementChoisie) { this.methodePaiementChoisie = methodePaiementChoisie; } @@ -531,6 +537,42 @@ public class MesCotisationsPaiementBean implements Serializable { public String getDateEcheanceFormattee() { return dateEcheance != null ? dateEcheance.format(DATE_FORMATTER) : "-"; } + + public String getTypeLibelle() { + if (type == null) return "Cotisation"; + return switch (type) { + case "MENSUELLE" -> "Mensuelle"; + case "SPECIALE" -> "Spéciale"; + case "ADHESION" -> "Adhésion"; + case "ANNUELLE" -> "Annuelle"; + default -> type; + }; + } + + public String getLibelle() { + return getTypeLibelle() + (periode != null && !periode.isBlank() ? " — " + periode : ""); + } + + public String getPeriodeFormatee() { + return periode != null ? periode : "-"; + } + + public String getRetardColor() { + if (dateEcheance == null) return "text-600"; + long jours = java.time.temporal.ChronoUnit.DAYS.between(LocalDate.now(), dateEcheance); + if (jours < 0) return "text-red-600 font-bold"; + if (jours <= 7) return "text-orange-600"; + return "text-600"; + } + + public String getStatutEcheance() { + if (dateEcheance == null) return "-"; + long jours = java.time.temporal.ChronoUnit.DAYS.between(LocalDate.now(), dateEcheance); + if (jours < 0) return "En retard (" + Math.abs(jours) + " j)"; + if (jours == 0) return "Aujourd'hui"; + if (jours == 1) return "Demain"; + return "Dans " + jours + " jours"; + } } public static class PaiementPerso implements Serializable { @@ -570,5 +612,37 @@ public class MesCotisationsPaiementBean implements Serializable { public String getDatePaiementFormattee() { return datePaiement != null ? datePaiement.format(DATE_FORMATTER) : "-"; } + + public String getTypeLibelle() { + return periode != null && !periode.isBlank() ? periode : "Cotisation"; + } + + public String getMethodeSeverity() { + if (methodePaiement == null) return "secondary"; + return switch (methodePaiement) { + case "WAVE_MONEY" -> "success"; + case "ORANGE_MONEY" -> "warning"; + case "FREE_MONEY" -> "info"; + case "CARTE_BANCAIRE" -> "primary"; + case "ESPECES" -> "secondary"; + case "VIREMENT" -> "info"; + case "CHEQUE" -> "secondary"; + default -> "secondary"; + }; + } + + public String getMethodeIcon() { + if (methodePaiement == null) return "pi-credit-card"; + return switch (methodePaiement) { + case "WAVE_MONEY" -> "pi-mobile"; + case "ORANGE_MONEY" -> "pi-mobile"; + case "FREE_MONEY" -> "pi-mobile"; + case "CARTE_BANCAIRE" -> "pi-credit-card"; + case "ESPECES" -> "pi-money-bill"; + case "VIREMENT" -> "pi-building"; + case "CHEQUE" -> "pi-file"; + default -> "pi-credit-card"; + }; + } } } diff --git a/src/main/java/dev/lions/unionflow/client/view/MessagingBean.java b/src/main/java/dev/lions/unionflow/client/view/MessagingBean.java new file mode 100644 index 0000000..039642e --- /dev/null +++ b/src/main/java/dev/lions/unionflow/client/view/MessagingBean.java @@ -0,0 +1,205 @@ +package dev.lions.unionflow.client.view; + +import dev.lions.unionflow.client.service.ConversationService; +import dev.lions.unionflow.client.service.MessageService; +import dev.lions.unionflow.server.api.dto.communication.request.CreateConversationRequest; +import dev.lions.unionflow.server.api.dto.communication.request.SendMessageRequest; +import dev.lions.unionflow.server.api.dto.communication.response.ConversationResponse; +import dev.lions.unionflow.server.api.dto.communication.response.MessageResponse; +import dev.lions.unionflow.server.api.enums.communication.ConversationType; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.SessionScoped; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.FacesContext; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.logging.Logger; + +@Named("messagingBean") +@SessionScoped +@Getter +@Setter +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class MessagingBean implements Serializable { + + private static final Logger LOGGER = Logger.getLogger(MessagingBean.class.getName()); + + @Inject + @RestClient + private ConversationService conversationService; + + @Inject + @RestClient + private MessageService messageService; + + @Inject + private UserSession userSession; + + private List conversations = new ArrayList<>(); + private List messagesConversationActive = new ArrayList<>(); + private ConversationResponse conversationActive; + private NouvelleConversation nouvelleConversation = new NouvelleConversation(); + private String contenuMessage; + private int pageConversations = 0; + private int taille = 20; + + @PostConstruct + public void init() { + chargerConversations(); + } + + public void chargerConversations() { + try { + conversations = conversationService.listerConversations(pageConversations, taille); + } catch (Exception e) { + LOGGER.severe("Erreur chargement conversations: " + e.getMessage()); + conversations = new ArrayList<>(); + ajouterMessageErreur("Impossible de charger les conversations."); + } + } + + public void ouvrirConversation(ConversationResponse conversation) { + this.conversationActive = conversation; + chargerMessages(conversation.id()); + try { + conversationService.marquerCommeLue(conversation.id()); + } catch (Exception e) { + LOGGER.warning("Impossible de marquer comme lue: " + e.getMessage()); + } + } + + public void chargerMessages(UUID conversationId) { + try { + messagesConversationActive = messageService.listerMessages(conversationId, 50); + } catch (Exception e) { + LOGGER.severe("Erreur chargement messages: " + e.getMessage()); + messagesConversationActive = new ArrayList<>(); + ajouterMessageErreur("Impossible de charger les messages."); + } + } + + public void envoyerMessage() { + if (conversationActive == null || contenuMessage == null || contenuMessage.isBlank()) { + return; + } + try { + SendMessageRequest request = SendMessageRequest.builder() + .conversationId(conversationActive.id()) + .content(contenuMessage.trim()) + .build(); + MessageResponse envoye = messageService.envoyer(request); + messagesConversationActive.add(envoye); + contenuMessage = null; + } catch (Exception e) { + LOGGER.severe("Erreur envoi message: " + e.getMessage()); + ajouterMessageErreur("Impossible d'envoyer le message."); + } + } + + public void creerConversation() { + try { + List participantIds = new ArrayList<>(); + if (nouvelleConversation.participantsIds != null) { + for (String id : nouvelleConversation.participantsIds.split(",")) { + String trimmed = id.trim(); + if (!trimmed.isEmpty()) { + participantIds.add(UUID.fromString(trimmed)); + } + } + } + CreateConversationRequest request = CreateConversationRequest.builder() + .name(nouvelleConversation.nom) + .description(nouvelleConversation.description) + .type(ConversationType.valueOf(nouvelleConversation.type)) + .participantIds(participantIds) + .build(); + ConversationResponse creee = conversationService.creer(request); + conversations.add(0, creee); + nouvelleConversation = new NouvelleConversation(); + ajouterMessageSucces("Conversation créée."); + } catch (Exception e) { + LOGGER.severe("Erreur création conversation: " + e.getMessage()); + ajouterMessageErreur("Impossible de créer la conversation."); + } + } + + public void archiverConversation(ConversationResponse conversation) { + try { + conversationService.archiver(conversation.id(), !conversation.isArchived()); + chargerConversations(); + } catch (Exception e) { + ajouterMessageErreur("Impossible de modifier la conversation."); + } + } + + public void basculerSilence(ConversationResponse conversation) { + try { + conversationService.basculerSilence(conversation.id()); + chargerConversations(); + } catch (Exception e) { + ajouterMessageErreur("Impossible de modifier le silence."); + } + } + + public void basculerEpinglage(ConversationResponse conversation) { + try { + conversationService.basculerEpinglage(conversation.id()); + chargerConversations(); + } catch (Exception e) { + ajouterMessageErreur("Impossible de modifier l'épinglage."); + } + } + + public void supprimerMessage(MessageResponse message) { + try { + messageService.supprimer(message.id()); + messagesConversationActive.remove(message); + ajouterMessageSucces("Message supprimé."); + } catch (Exception e) { + ajouterMessageErreur("Impossible de supprimer le message."); + } + } + + public void actualiser() { + chargerConversations(); + if (conversationActive != null) { + chargerMessages(conversationActive.id()); + } + } + + public String getStatusSeverity(String status) { + if (status == null) return "info"; + return switch (status) { + case "READ" -> "success"; + case "FAILED" -> "danger"; + default -> "info"; + }; + } + + private void ajouterMessageSucces(String detail) { + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", detail)); + } + + private void ajouterMessageErreur(String detail) { + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", detail)); + } + + @Data + public static class NouvelleConversation { + private String nom; + private String description; + private String type = "GROUP"; + private String participantsIds; + } +} diff --git a/src/main/java/dev/lions/unionflow/client/view/NavigationBean.java b/src/main/java/dev/lions/unionflow/client/view/NavigationBean.java index 24f2b02..93ea91d 100644 --- a/src/main/java/dev/lions/unionflow/client/view/NavigationBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/NavigationBean.java @@ -93,10 +93,14 @@ public class NavigationBean implements Serializable { switch (userSession.getTypeCompte()) { case "SUPER_ADMIN": return "/pages/super-admin/dashboard.xhtml"; - case "ADMIN_ENTITE": - return "/pages/admin/dashboard.xhtml"; + case "ADMIN_ORGANISATION": + return "/pages/secure/dashboard.xhtml"; + case "MODERATEUR": + return "/pages/secure/dashboard.xhtml"; + case "MEMBRE_ACTIF": + return "/pages/secure/dashboard-membre.xhtml"; case "MEMBRE": - return "/pages/membre/dashboard.xhtml"; + return "/pages/secure/dashboard-membre.xhtml"; default: LOGGER.warning("Type de compte non reconnu: " + userSession.getTypeCompte()); return "/pages/secure/dashboard.xhtml"; diff --git a/src/main/java/dev/lions/unionflow/client/view/OrganisationDetailBean.java b/src/main/java/dev/lions/unionflow/client/view/OrganisationDetailBean.java index c0d8f8f..3778fe1 100644 --- a/src/main/java/dev/lions/unionflow/client/view/OrganisationDetailBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/OrganisationDetailBean.java @@ -1,8 +1,11 @@ package dev.lions.unionflow.client.view; import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse; +import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationSummaryResponse; +import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse; +import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationSummaryResponse; import dev.lions.unionflow.server.api.dto.common.PagedResponse; -import dev.lions.unionflow.client.service.AssociationService; +import dev.lions.unionflow.client.service.OrganisationService; import dev.lions.unionflow.client.service.ErrorHandlerService; import dev.lions.unionflow.client.service.RestClientExceptionMapper; import dev.lions.unionflow.client.service.RetryService; @@ -39,7 +42,7 @@ public class OrganisationDetailBean implements Serializable { @Inject @RestClient - AssociationService associationService; + OrganisationService organisationService; @Inject ErrorHandlerService errorHandler; @@ -82,7 +85,7 @@ public class OrganisationDetailBean implements Serializable { if (organisationId == null) return; try { organisation = retryService.executeWithRetrySupplier( - () -> associationService.obtenirParId(organisationId), + () -> organisationService.obtenirParId(organisationId), "chargement des détails d'une organisation" ); if (organisation != null) { @@ -123,7 +126,7 @@ public class OrganisationDetailBean implements Serializable { try { sanitiserAvantEnvoi(organisation); OrganisationResponse maj = retryService.executeWithRetrySupplier( - () -> associationService.modifier(organisationId, organisation), + () -> organisationService.modifier(organisationId, organisation), "modification de l'organisation" ); organisation = maj; @@ -294,11 +297,11 @@ public class OrganisationDetailBean implements Serializable { return regions.stream().filter(r -> r.toLowerCase().contains(q)).collect(Collectors.toList()); } - public List rechercherOrganisations(String query) { + public List rechercherOrganisations(String query) { if (query == null || query.trim().isEmpty()) return List.of(); try { - PagedResponse response = - associationService.rechercher(query, null, null, null, null, 0, 100); + PagedResponse response = + organisationService.rechercher(query, null, null, null, null, 0, 100); return (response != null && response.getData() != null) ? response.getData() : List.of(); } catch (Exception e) { LOG.errorf(e, "Erreur recherche organisations pour '%s'", query); diff --git a/src/main/java/dev/lions/unionflow/client/view/OrganisationStatistiquesBean.java b/src/main/java/dev/lions/unionflow/client/view/OrganisationStatistiquesBean.java index 467d447..adecba2 100644 --- a/src/main/java/dev/lions/unionflow/client/view/OrganisationStatistiquesBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/OrganisationStatistiquesBean.java @@ -2,8 +2,9 @@ package dev.lions.unionflow.client.view; import dev.lions.unionflow.client.constants.StatutOrganisationConstants; import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse; +import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationSummaryResponse; import dev.lions.unionflow.server.api.dto.common.PagedResponse; -import dev.lions.unionflow.client.service.AssociationService; +import dev.lions.unionflow.client.service.OrganisationService; import jakarta.faces.view.ViewScoped; import jakarta.inject.Named; import jakarta.inject.Inject; @@ -31,7 +32,7 @@ public class OrganisationStatistiquesBean implements Serializable { @Inject @RestClient - private AssociationService associationService; + private OrganisationService organisationService; // Statistiques private Map statistiques = new HashMap<>(); @@ -44,7 +45,7 @@ public class OrganisationStatistiquesBean implements Serializable { public void chargerStatistiques() { try { // Appeler le service pour obtenir les vraies statistiques - AssociationService.StatistiquesAssociationDTO stats = associationService.obtenirStatistiques(); + OrganisationService.StatistiquesOrganisationDTO stats = organisationService.obtenirStatistiques(); statistiques = new HashMap<>(); if (stats != null && stats.getTotalAssociations() != null && stats.getTotalAssociations() > 0) { @@ -75,13 +76,13 @@ public class OrganisationStatistiquesBean implements Serializable { private void calculerStatistiquesDepuisListe() { try { // Charger toutes les organisations - PagedResponse response = associationService.listerToutes(0, 1000); - List organisations = (response != null && response.getData() != null) ? response.getData() - : new ArrayList<>(); + PagedResponse response = organisationService.listerToutes(0, 1000); + List organisations = (response != null && response.getData() != null) ? response.getData() + : new ArrayList(); long total = organisations.size(); long actives = organisations.stream() - .filter(o -> o.getStatut() != null && StatutOrganisationConstants.ACTIVE.equals(o.getStatut())) + .filter(o -> o.statut() != null && StatutOrganisationConstants.ACTIVE.equals(o.statut())) .count(); long inactives = total - actives; diff --git a/src/main/java/dev/lions/unionflow/client/view/OrganisationsBean.java b/src/main/java/dev/lions/unionflow/client/view/OrganisationsBean.java index 7f75615..ade9826 100644 --- a/src/main/java/dev/lions/unionflow/client/view/OrganisationsBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/OrganisationsBean.java @@ -2,10 +2,11 @@ package dev.lions.unionflow.client.view; import dev.lions.unionflow.client.constants.StatutOrganisationConstants; import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse; +import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationSummaryResponse; import dev.lions.unionflow.server.api.dto.common.PagedResponse; import dev.lions.unionflow.server.api.dto.reference.response.TypeReferenceResponse; import dev.lions.unionflow.server.api.dto.common.PagedResponse; -import dev.lions.unionflow.client.service.AssociationService; +import dev.lions.unionflow.client.service.OrganisationService; import dev.lions.unionflow.client.service.CacheService; import dev.lions.unionflow.client.service.ErrorHandlerService; import dev.lions.unionflow.client.service.RetryService; @@ -51,7 +52,7 @@ public class OrganisationsBean implements Serializable { @Inject @RestClient - AssociationService associationService; + OrganisationService organisationService; @Inject ErrorHandlerService errorHandler; @@ -67,8 +68,8 @@ public class OrganisationsBean implements Serializable { // ========== DONNÉES ========== - private List organisations = new ArrayList<>(); - private List organisationsFiltrees; + private List organisations = new ArrayList<>(); + private List organisationsFiltrees; private OrganisationResponse organisationSelectionnee; private OrganisationResponse nouvelleOrganisation; @@ -116,11 +117,11 @@ public class OrganisationsBean implements Serializable { public void chargerOrganisations() { try { LOG.debug("Chargement des organisations"); - PagedResponse response = retryService.executeWithRetrySupplier( - () -> associationService.listerToutes(0, 1000), + PagedResponse response = retryService.executeWithRetrySupplier( + () -> organisationService.listerToutes(0, 1000), "chargement de toutes les organisations"); - organisations = (response != null && response.getData() != null) ? response.getData() : new ArrayList<>(); - typeCatalogueService.enrichir(organisations); + organisations = (response != null && response.getData() != null) ? response.getData() : new ArrayList(); + // typeCatalogueService.enrichir(organisations); // DISABLED: enrichir expects OrganisationResponse, OrganisationSummaryResponse already has typeOrganisationLibelle organisationsFiltrees = new ArrayList<>(organisations); LOG.infof("Organisations chargées: %d", organisations.size()); @@ -137,17 +138,17 @@ public class OrganisationsBean implements Serializable { */ private void chargerStatistiques() { try { - List toutes = cacheService.getOrLoad( + List toutes = cacheService.getOrLoad( "organisations-toutes", () -> { LOG.debug("Chargement de toutes les organisations pour les statistiques"); try { - PagedResponse response = retryService + PagedResponse response = retryService .executeWithRetrySupplier( - () -> associationService.listerToutes(0, 10000), + () -> organisationService.listerToutes(0, 10000), "chargement des statistiques"); return (response != null && response.getData() != null) ? response.getData() - : new ArrayList<>(); + : new ArrayList(); } catch (Exception e) { LOG.errorf(e, "Erreur lors du retry pour les statistiques"); throw new RuntimeException(e); @@ -158,7 +159,7 @@ public class OrganisationsBean implements Serializable { totalOrganisations = toutes.size(); organisationsActives = toutes.stream() - .filter(o -> o.getStatut() != null && StatutOrganisationConstants.ACTIVE.equals(o.getStatut())) + .filter(o -> o.statut() != null && StatutOrganisationConstants.ACTIVE.equals(o.statut())) .count(); organisationsInactives = totalOrganisations - organisationsActives; @@ -218,7 +219,7 @@ public class OrganisationsBean implements Serializable { try { OrganisationResponse creee = retryService.executeWithRetrySupplier( - () -> associationService.creer(nouvelleOrganisation), + () -> organisationService.creer(nouvelleOrganisation), "création d'une organisation"); LOG.infof("Organisation créée avec succès: id=%s, nom=%s", @@ -314,6 +315,20 @@ public class OrganisationsBean implements Serializable { /** * Modifie une organisation existante. */ + /** + * Prépare la modification depuis la liste : charge la fiche complète depuis l'API. + */ + public void preparerModification(OrganisationSummaryResponse org) { + try { + organisationSelectionnee = retryService.executeWithRetrySupplier( + () -> organisationService.obtenirParId(org.id()), + "chargement de l'organisation pour modification"); + } catch (Exception e) { + errorHandler.handleException(e, "lors du chargement de l'organisation", null); + organisationSelectionnee = null; + } + } + public void modifierOrganisation() { if (organisationSelectionnee == null || organisationSelectionnee.getId() == null) { errorHandler.showWarning("Erreur", "Aucune organisation sélectionnée"); @@ -330,7 +345,7 @@ public class OrganisationsBean implements Serializable { try { OrganisationResponse modifiee = retryService.executeWithRetrySupplier( - () -> associationService.modifier(organisationSelectionnee.getId(), organisationSelectionnee), + () -> organisationService.modifier(organisationSelectionnee.getId(), organisationSelectionnee), "modification d'une organisation"); LOG.infof("Organisation modifiée avec succès: id=%s", modifiee.getId()); @@ -348,6 +363,17 @@ public class OrganisationsBean implements Serializable { } } + /** + * Prépare la suppression depuis la liste (reçoit un résumé OrganisationSummaryResponse). + */ + public void preparerSuppression(OrganisationSummaryResponse org) { + OrganisationResponse full = new OrganisationResponse(); + full.setId(org.id()); + full.setNom(org.nom()); + full.setStatut(org.statut()); + organisationPourSuppression = full; + } + /** * Prépare la suppression (ouvre le dialogue de confirmation). */ @@ -368,6 +394,17 @@ public class OrganisationsBean implements Serializable { supprimerOrganisation(org); } + /** + * Prépare la bascule de statut depuis la liste (reçoit un résumé OrganisationSummaryResponse). + */ + public void preparerBasculerStatut(OrganisationSummaryResponse org) { + OrganisationResponse full = new OrganisationResponse(); + full.setId(org.id()); + full.setNom(org.nom()); + full.setStatut(org.statut()); + organisationPourStatut = full; + } + /** * Prépare la bascule de statut (ouvre le dialogue de confirmation). */ @@ -399,7 +436,7 @@ public class OrganisationsBean implements Serializable { try { retryService.executeWithRetrySupplier( () -> { - associationService.supprimer(organisation.getId()); + organisationService.supprimer(organisation.getId()); return null; }, "suppression d'une organisation"); @@ -428,7 +465,7 @@ public class OrganisationsBean implements Serializable { try { retryService.executeWithRetrySupplier( - () -> associationService.activer(organisation.getId()), + () -> organisationService.activer(organisation.getId()), "activation d'une organisation"); cacheService.invalidate("organisations-toutes"); @@ -454,7 +491,7 @@ public class OrganisationsBean implements Serializable { try { retryService.executeWithRetrySupplier( - () -> associationService.suspendre(organisation.getId()), + () -> organisationService.suspendre(organisation.getId()), "suspension d'une organisation"); cacheService.invalidate("organisations-toutes"); @@ -489,15 +526,15 @@ public class OrganisationsBean implements Serializable { * Recherche les organisations dont le nom contient la requête fournie. * Utilisée par p:autoComplete pour la recherche d'organisation parente. */ - public List rechercherOrganisations(String query) { + public List rechercherOrganisations(String query) { if (query == null || query.trim().isEmpty()) { return List.of(); } try { - PagedResponse response = associationService.rechercher( + PagedResponse response = organisationService.rechercher( query, null, null, null, null, 0, 100); - List resultats = (response != null && response.getData() != null) ? response.getData() - : new ArrayList<>(); + List resultats = (response != null && response.getData() != null) ? response.getData() + : new ArrayList(); LOG.infof("Recherche d'organisations pour '%s': %d résultat(s)", query, resultats.size()); return resultats; } catch (Exception e) { @@ -572,31 +609,33 @@ public class OrganisationsBean implements Serializable { .filter(org -> { if (rechercheGlobale != null && !rechercheGlobale.trim().isEmpty()) { String recherche = rechercheGlobale.toLowerCase(); - return (org.getNom() != null && org.getNom().toLowerCase().contains(recherche)) || - (org.getVille() != null && org.getVille().toLowerCase().contains(recherche)) || - (org.getDescription() != null - && org.getDescription().toLowerCase().contains(recherche)); + return (org.nom() != null && org.nom().toLowerCase().contains(recherche)) || + (org.nomCourt() != null && org.nomCourt().toLowerCase().contains(recherche)) || + (org.typeOrganisationLibelle() != null + && org.typeOrganisationLibelle().toLowerCase().contains(recherche)); } return true; }) .filter(org -> { if (filtreStatut != null && !filtreStatut.trim().isEmpty()) { - return filtreStatut.equals(org.getStatut()); + return filtreStatut.equals(org.statut()); } return true; }) .filter(org -> { if (filtreType != null && !filtreType.trim().isEmpty()) { - return filtreType.equals(org.getTypeOrganisation()); + return filtreType.equals(org.typeOrganisation()); } return true; }) - .filter(org -> { + // DISABLED: OrganisationSummaryResponse doesn't have region field + // TODO: Add region to OrganisationSummaryResponse or load full Response for region filter + /*.filter(org -> { if (filtreRegion != null && !filtreRegion.trim().isEmpty()) { return filtreRegion.equals(org.getRegion()); } return true; - }) + })*/ .collect(java.util.stream.Collectors.toList()); LOG.debugf("Filtres appliqués: %d organisations sur %d", organisationsFiltrees.size(), organisations.size()); @@ -616,22 +655,22 @@ public class OrganisationsBean implements Serializable { // ========== GETTERS/SETTERS ========== - public List getOrganisations() { + public List getOrganisations() { return organisations; } - public void setOrganisations(List organisations) { + public void setOrganisations(List organisations) { this.organisations = organisations; } - public List getOrganisationsFiltrees() { + public List getOrganisationsFiltrees() { if (organisationsFiltrees == null) { organisationsFiltrees = new ArrayList<>(organisations); } return organisationsFiltrees; } - public void setOrganisationsFiltrees(List organisationsFiltrees) { + public void setOrganisationsFiltrees(List organisationsFiltrees) { this.organisationsFiltrees = organisationsFiltrees; } diff --git a/src/main/java/dev/lions/unionflow/client/view/PersonnelBean.java b/src/main/java/dev/lions/unionflow/client/view/PersonnelBean.java index 609d8ee..4a42733 100644 --- a/src/main/java/dev/lions/unionflow/client/view/PersonnelBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/PersonnelBean.java @@ -75,22 +75,11 @@ public class PersonnelBean implements Serializable { */ private void chargerProfil() { try { - if (userSession != null && userSession.getCurrentUser() != null) { - String email = userSession.getCurrentUser().getEmail(); - if (email != null) { - // Rechercher le membre par email - List membres = membreService.listerTous().getData(); - membre = membres.stream() - .filter(m -> email.equals(m.getEmail())) - .findFirst() - .orElse(null); - - if (membre == null) { - LOG.warnf("Aucun membre trouvé pour l'email: %s", email); - } else { - LOG.infof("Profil chargé pour le membre: %s", membre.getNomComplet()); - } - } + membre = membreService.obtenirMembreConnecte(); + if (membre == null) { + LOG.warn("Aucun profil membre retourné par /api/membres/me"); + } else { + LOG.infof("Profil chargé pour le membre: %s", membre.getNomComplet()); } } catch (Exception e) { LOG.errorf(e, "Erreur lors du chargement du profil"); diff --git a/src/main/java/dev/lions/unionflow/client/view/SauvegardeBean.java b/src/main/java/dev/lions/unionflow/client/view/SauvegardeBean.java new file mode 100644 index 0000000..981d8a9 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/client/view/SauvegardeBean.java @@ -0,0 +1,244 @@ +package dev.lions.unionflow.client.view; + +import dev.lions.unionflow.client.service.BackupService; +import dev.lions.unionflow.server.api.dto.backup.request.CreateBackupRequest; +import dev.lions.unionflow.server.api.dto.backup.request.RestoreBackupRequest; +import dev.lions.unionflow.server.api.dto.backup.request.UpdateBackupConfigRequest; +import dev.lions.unionflow.server.api.dto.backup.response.BackupConfigResponse; +import dev.lions.unionflow.server.api.dto.backup.response.BackupResponse; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.SessionScoped; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.FacesContext; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import lombok.Data; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.logging.Logger; + +@Named("sauvegardeBean") +@SessionScoped +@Data +public class SauvegardeBean implements Serializable { + + private static final Logger LOGGER = Logger.getLogger(SauvegardeBean.class.getName()); + + @Inject + @RestClient + private BackupService backupService; + + private List sauvegardes = new ArrayList<>(); + private BackupConfigResponse configuration; + private BackupResponse sauvegardeSelectionnee; + + private NouvelleSauvegarde nouvelleSauvegarde = new NouvelleSauvegarde(); + private OptionsRestauration optionsRestauration = new OptionsRestauration(); + private ConfigSauvegarde configSauvegarde = new ConfigSauvegarde(); + + private int page = 0; + private int taille = 20; + + @PostConstruct + public void init() { + chargerSauvegardes(); + chargerConfiguration(); + } + + public void chargerSauvegardes() { + try { + sauvegardes = backupService.listerSauvegardes(page, taille); + } catch (Exception e) { + LOGGER.severe("Erreur chargement sauvegardes: " + e.getMessage()); + sauvegardes = new ArrayList<>(); + ajouterMessageErreur("Impossible de charger les sauvegardes."); + } + } + + public void chargerConfiguration() { + try { + configuration = backupService.obtenirConfiguration(); + if (configuration != null) { + configSauvegarde.autoActivee = configuration.getAutoBackupEnabled(); + configSauvegarde.frequence = configuration.getFrequency(); + configSauvegarde.retentionJours = configuration.getRetentionDays(); + configSauvegarde.heure = configuration.getBackupTime(); + configSauvegarde.includeDatabase = configuration.getIncludeDatabase(); + configSauvegarde.includeFiles = configuration.getIncludeFiles(); + configSauvegarde.includeConfiguration = configuration.getIncludeConfiguration(); + } + } catch (Exception e) { + LOGGER.warning("Configuration sauvegarde indisponible: " + e.getMessage()); + } + } + + public void creerSauvegarde() { + try { + CreateBackupRequest request = CreateBackupRequest.builder() + .name(nouvelleSauvegarde.nom) + .description(nouvelleSauvegarde.description) + .type(nouvelleSauvegarde.type) + .includeDatabase(nouvelleSauvegarde.includeDatabase) + .includeFiles(nouvelleSauvegarde.includeFiles) + .includeConfiguration(nouvelleSauvegarde.includeConfiguration) + .build(); + BackupResponse creee = backupService.creerSauvegarde(request); + sauvegardes.add(0, creee); + nouvelleSauvegarde = new NouvelleSauvegarde(); + ajouterMessageSucces("Sauvegarde lancée : " + creee.getName()); + } catch (Exception e) { + LOGGER.severe("Erreur création sauvegarde: " + e.getMessage()); + ajouterMessageErreur("Impossible de créer la sauvegarde."); + } + } + + public void creerPointDeRestauration() { + try { + CreateBackupRequest request = CreateBackupRequest.builder() + .name("Point de restauration - " + java.time.LocalDateTime.now()) + .type("RESTORE_POINT") + .includeDatabase(true) + .includeConfiguration(true) + .build(); + BackupResponse point = backupService.creerPointDeRestauration(request); + sauvegardes.add(0, point); + ajouterMessageSucces("Point de restauration créé."); + } catch (Exception e) { + LOGGER.severe("Erreur création point restauration: " + e.getMessage()); + ajouterMessageErreur("Impossible de créer le point de restauration."); + } + } + + public void selectionnerPourRestauration(BackupResponse sauvegarde) { + this.sauvegardeSelectionnee = sauvegarde; + this.optionsRestauration = new OptionsRestauration(); + this.optionsRestauration.backupId = sauvegarde.getId(); + } + + public void restaurer() { + if (sauvegardeSelectionnee == null) return; + try { + RestoreBackupRequest request = RestoreBackupRequest.builder() + .backupId(sauvegardeSelectionnee.getId()) + .restoreDatabase(optionsRestauration.restoreDatabase) + .restoreFiles(optionsRestauration.restoreFiles) + .restoreConfiguration(optionsRestauration.restoreConfiguration) + .createRestorePoint(optionsRestauration.creerPointAvant) + .build(); + backupService.restaurer(request); + sauvegardeSelectionnee = null; + ajouterMessageSucces("Restauration lancée. Le système va redémarrer."); + } catch (Exception e) { + LOGGER.severe("Erreur restauration: " + e.getMessage()); + ajouterMessageErreur("Impossible de lancer la restauration."); + } + } + + public void supprimer(BackupResponse sauvegarde) { + try { + backupService.supprimer(sauvegarde.getId()); + sauvegardes.remove(sauvegarde); + ajouterMessageSucces("Sauvegarde supprimée."); + } catch (Exception e) { + LOGGER.severe("Erreur suppression sauvegarde: " + e.getMessage()); + ajouterMessageErreur("Impossible de supprimer la sauvegarde."); + } + } + + public void sauvegarderConfiguration() { + try { + UpdateBackupConfigRequest request = UpdateBackupConfigRequest.builder() + .autoBackupEnabled(configSauvegarde.autoActivee) + .frequency(configSauvegarde.frequence) + .retentionDays(configSauvegarde.retentionJours) + .backupTime(configSauvegarde.heure) + .includeDatabase(configSauvegarde.includeDatabase) + .includeFiles(configSauvegarde.includeFiles) + .includeConfiguration(configSauvegarde.includeConfiguration) + .build(); + configuration = backupService.mettreAJourConfiguration(request); + ajouterMessageSucces("Configuration sauvegardée."); + } catch (Exception e) { + LOGGER.severe("Erreur mise à jour configuration: " + e.getMessage()); + ajouterMessageErreur("Impossible de mettre à jour la configuration."); + } + } + + public void actualiser() { + chargerSauvegardes(); + chargerConfiguration(); + } + + public String getStatusSeverity(String status) { + if (status == null) return "warning"; + return switch (status) { + case "COMPLETED" -> "success"; + case "IN_PROGRESS" -> "info"; + case "FAILED" -> "danger"; + default -> "warning"; + }; + } + + public String getStatusIcon(String status) { + if (status == null) return "pi-clock"; + return switch (status) { + case "COMPLETED" -> "pi-check-circle"; + case "IN_PROGRESS" -> "pi-spin pi-spinner"; + case "FAILED" -> "pi-times-circle"; + default -> "pi-clock"; + }; + } + + public String getTypeSeverity(String type) { + if (type == null) return "info"; + return switch (type) { + case "AUTO" -> "info"; + case "RESTORE_POINT" -> "warning"; + default -> "success"; + }; + } + + private void ajouterMessageSucces(String detail) { + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", detail)); + } + + private void ajouterMessageErreur(String detail) { + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", detail)); + } + + @Data + public static class NouvelleSauvegarde { + private String nom = "Sauvegarde manuelle"; + private String description; + private String type = "MANUAL"; + private Boolean includeDatabase = true; + private Boolean includeFiles = false; + private Boolean includeConfiguration = true; + } + + @Data + public static class OptionsRestauration { + private UUID backupId; + private Boolean restoreDatabase = true; + private Boolean restoreFiles = false; + private Boolean restoreConfiguration = true; + private Boolean creerPointAvant = true; + } + + @Data + public static class ConfigSauvegarde { + private Boolean autoActivee = true; + private String frequence = "DAILY"; + private Integer retentionJours = 30; + private String heure = "02:00"; + private Boolean includeDatabase = true; + private Boolean includeFiles = false; + private Boolean includeConfiguration = true; + } +} diff --git a/src/main/java/dev/lions/unionflow/client/view/SouscriptionBean.java b/src/main/java/dev/lions/unionflow/client/view/SouscriptionBean.java index 33d85ef..0a41de0 100644 --- a/src/main/java/dev/lions/unionflow/client/view/SouscriptionBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/SouscriptionBean.java @@ -1,6 +1,6 @@ package dev.lions.unionflow.client.view; -import dev.lions.unionflow.server.api.dto.abonnement.response.AbonnementResponse; +import dev.lions.unionflow.server.api.dto.souscription.SouscriptionStatutResponse; import dev.lions.unionflow.client.service.SouscriptionService; import jakarta.enterprise.context.SessionScoped; import jakarta.inject.Inject; @@ -16,36 +16,35 @@ import java.util.logging.Logger; @Named("souscriptionBean") @SessionScoped public class SouscriptionBean implements Serializable { - + private static final long serialVersionUID = 1L; private static final Logger LOGGER = Logger.getLogger(SouscriptionBean.class.getName()); - - // Constantes de navigation outcomes (WOU/DRY - réutilisables) + private static final String OUTCOME_SOUSCRIPTION_UPGRADE = "souscriptionUpgradePage"; private static final String OUTCOME_SOUSCRIPTION_CHANGE_PLAN = "souscriptionChangePlanPage"; private static final String OUTCOME_SOUSCRIPTION_RENEW = "souscriptionRenewPage"; - + @Inject @RestClient private SouscriptionService souscriptionService; - - private UUID organisationId; // À injecter depuis la session - - private List souscriptionsOrganisation; - private AbonnementResponse souscriptionActive; - private AbonnementResponse souscriptionSelectionnee; - + + private UUID organisationId; + + private List souscriptionsOrganisation; + private SouscriptionStatutResponse souscriptionActive; + private SouscriptionStatutResponse souscriptionSelectionnee; + // Statistiques quota private int membresActuels = 0; private int quotaMaximum = 0; private boolean quotaAtteint = false; private int membresRestants = 0; - + // Alertes private boolean alerteExpirationProche = false; private boolean alerteQuotaProche = false; private int joursAvantExpiration = 0; - + @PostConstruct public void init() { if (organisationId != null) { @@ -55,14 +54,14 @@ public class SouscriptionBean implements Serializable { souscriptionsOrganisation = new ArrayList<>(); } } - + private void initializeData() { try { souscriptionsOrganisation = souscriptionService.listerToutes(organisationId, 0, 100); souscriptionActive = souscriptionService.obtenirActive(organisationId); - if (souscriptionActive == null && !souscriptionsOrganisation.isEmpty()) { + if (souscriptionActive == null && souscriptionsOrganisation != null && !souscriptionsOrganisation.isEmpty()) { souscriptionActive = souscriptionsOrganisation.stream() - .filter(s -> dev.lions.unionflow.server.api.enums.abonnement.StatutAbonnement.ACTIF.equals(s.getStatut())) + .filter(s -> "ACTIVE".equals(s.getStatut())) .findFirst() .orElse(null); } @@ -72,91 +71,62 @@ public class SouscriptionBean implements Serializable { souscriptionsOrganisation = new ArrayList<>(); } } - + private void updateStatistiques() { if (souscriptionActive != null) { - membresActuels = souscriptionActive.getNombreMembresActuels(); // Corrigé: getNombreMembresActuels - quotaMaximum = souscriptionActive.getMaxMembres(); // Corrigé: getMaxMembres - membresRestants = souscriptionActive.getMembresRestants(); - quotaAtteint = souscriptionActive.isQuotaAtteint(); - - // Calculer les alertes - joursAvantExpiration = (int) souscriptionActive.getJoursRestants(); - alerteExpirationProche = souscriptionActive.isExpirationProche(); - alerteQuotaProche = souscriptionActive.getPourcentageUtilisation() >= 85; + membresActuels = souscriptionActive.getQuotaUtilise() != null ? souscriptionActive.getQuotaUtilise() : 0; + quotaMaximum = souscriptionActive.getQuotaMax() != null ? souscriptionActive.getQuotaMax() : 0; + membresRestants = souscriptionActive.getQuotaRestant() != null ? souscriptionActive.getQuotaRestant() : 0; + quotaAtteint = souscriptionActive.isQuotaDepasse(); + joursAvantExpiration = (int) souscriptionActive.getJoursAvantExpiration(); + alerteExpirationProche = joursAvantExpiration >= 0 && joursAvantExpiration <= 30; + int pourcentage = quotaMaximum > 0 ? membresActuels * 100 / quotaMaximum : 0; + alerteQuotaProche = pourcentage >= 85; } } - + public boolean peutAccepterNouveauMembre() { - return souscriptionActive != null && - souscriptionActive.isActive() && - !souscriptionActive.isQuotaAtteint(); + return souscriptionActive != null + && "ACTIVE".equals(souscriptionActive.getStatut()) + && !souscriptionActive.isQuotaDepasse(); } - + public String getMessageQuota() { if (souscriptionActive == null) { return "Aucune souscription active"; } - if (quotaAtteint) { return "Quota maximum atteint (" + quotaMaximum + " membres)"; } - if (alerteQuotaProche) { return "Attention: quota bientôt atteint (" + membresActuels + "/" + quotaMaximum + ")"; } - return membresRestants + " membre(s) restant(s) sur " + quotaMaximum; } - + public String getCouleurJaugeQuota() { - int pourcentage = souscriptionActive != null ? souscriptionActive.getPourcentageUtilisation() : 0; - + int pourcentage = quotaMaximum > 0 ? membresActuels * 100 / quotaMaximum : 0; if (pourcentage >= 100) return "danger"; if (pourcentage >= 85) return "warning"; if (pourcentage >= 70) return "info"; return "success"; } - + public String upgraderFormulaire() { - // Logique pour upgrader vers un formulaire supérieur - // Utilisation de navigation outcome au lieu de chemin direct (WOU/DRY) return OUTCOME_SOUSCRIPTION_UPGRADE + "?faces-redirect=true"; } - + public String changerFormulaire() { - // Logique pour changer de formulaire - // Utilisation de navigation outcome au lieu de chemin direct (WOU/DRY) return OUTCOME_SOUSCRIPTION_CHANGE_PLAN + "?faces-redirect=true"; } - + public String renouvelerSouscription() { - // Logique pour renouveler la souscription - // Utilisation de navigation outcome au lieu de chemin direct (WOU/DRY) return OUTCOME_SOUSCRIPTION_RENEW + "?faces-redirect=true"; } - - public void activerNotificationQuota(boolean activer) { - if (souscriptionActive != null) { - // Note: AbonnementResponse n'a pas de setNotificationQuotaAtteint - // Cette fonctionnalité sera ajoutée ultérieurement si nécessaire - souscriptionActive.setAlertesActivees(activer); - // Appel service pour sauvegarder - } - } - public void activerNotificationExpiration(boolean activer) { - if (souscriptionActive != null) { - // Note: AbonnementResponse n'a pas de setNotificationExpiration - // Cette fonctionnalité sera ajoutée ultérieurement si nécessaire - souscriptionActive.setAlertesActivees(activer); - // Appel service pour sauvegarder - } - } - public List getAlertesQuota() { List alertes = new ArrayList<>(); - + if (alerteExpirationProche) { AlerteQuota alerte = new AlerteQuota(); alerte.setType("EXPIRATION"); @@ -168,7 +138,7 @@ public class SouscriptionBean implements Serializable { alerte.setActionUrl("/pages/secure/souscription/renew"); alertes.add(alerte); } - + if (alerteQuotaProche && !quotaAtteint) { AlerteQuota alerte = new AlerteQuota(); alerte.setType("QUOTA_PROCHE"); @@ -180,7 +150,7 @@ public class SouscriptionBean implements Serializable { alerte.setActionUrl("/pages/secure/souscription/upgrade"); alertes.add(alerte); } - + if (quotaAtteint) { AlerteQuota alerte = new AlerteQuota(); alerte.setType("QUOTA_ATTEINT"); @@ -192,56 +162,59 @@ public class SouscriptionBean implements Serializable { alerte.setActionUrl("/pages/secure/souscription/upgrade"); alertes.add(alerte); } - + return alertes; } - + public String getSeveriteQuota() { if (quotaAtteint) return "danger"; if (alerteQuotaProche) return "warning"; return "info"; } - + public String getIconeStatut() { if (souscriptionActive == null) return "pi-times-circle text-red-500"; - if (souscriptionActive.isActive()) return "pi-check-circle text-green-500"; - // Corrigé: utiliser les méthodes utilitaires isExpire() et isSuspendu() au lieu de StatutSouscription - if (souscriptionActive.isExpire()) return "pi-clock text-red-500"; - if (souscriptionActive.isSuspendu()) return "pi-pause text-orange-500"; + String statut = souscriptionActive.getStatut(); + if ("ACTIVE".equals(statut)) return "pi-check-circle text-green-500"; + if ("EXPIREE".equals(statut)) return "pi-clock text-red-500"; + if ("SUSPENDUE".equals(statut)) return "pi-pause text-orange-500"; return "pi-info-circle text-blue-500"; } - + // Getters et Setters - public List getSouscriptionsOrganisation() { return souscriptionsOrganisation; } - public void setSouscriptionsOrganisation(List souscriptionsOrganisation) { this.souscriptionsOrganisation = souscriptionsOrganisation; } - - public AbonnementResponse getSouscriptionActive() { return souscriptionActive; } - public void setSouscriptionActive(AbonnementResponse souscriptionActive) { this.souscriptionActive = souscriptionActive; } - - public AbonnementResponse getSouscriptionSelectionnee() { return souscriptionSelectionnee; } - public void setSouscriptionSelectionnee(AbonnementResponse souscriptionSelectionnee) { this.souscriptionSelectionnee = souscriptionSelectionnee; } - + public List getSouscriptionsOrganisation() { return souscriptionsOrganisation; } + public void setSouscriptionsOrganisation(List souscriptionsOrganisation) { this.souscriptionsOrganisation = souscriptionsOrganisation; } + + public SouscriptionStatutResponse getSouscriptionActive() { return souscriptionActive; } + public void setSouscriptionActive(SouscriptionStatutResponse souscriptionActive) { this.souscriptionActive = souscriptionActive; } + + public SouscriptionStatutResponse getSouscriptionSelectionnee() { return souscriptionSelectionnee; } + public void setSouscriptionSelectionnee(SouscriptionStatutResponse souscriptionSelectionnee) { this.souscriptionSelectionnee = souscriptionSelectionnee; } + + public UUID getOrganisationId() { return organisationId; } + public void setOrganisationId(UUID organisationId) { this.organisationId = organisationId; } + public int getMembresActuels() { return membresActuels; } public void setMembresActuels(int membresActuels) { this.membresActuels = membresActuels; } - + public int getQuotaMaximum() { return quotaMaximum; } public void setQuotaMaximum(int quotaMaximum) { this.quotaMaximum = quotaMaximum; } - + public boolean isQuotaAtteint() { return quotaAtteint; } public void setQuotaAtteint(boolean quotaAtteint) { this.quotaAtteint = quotaAtteint; } - + public int getMembresRestants() { return membresRestants; } public void setMembresRestants(int membresRestants) { this.membresRestants = membresRestants; } - + public boolean isAlerteExpirationProche() { return alerteExpirationProche; } public void setAlerteExpirationProche(boolean alerteExpirationProche) { this.alerteExpirationProche = alerteExpirationProche; } - + public boolean isAlerteQuotaProche() { return alerteQuotaProche; } public void setAlerteQuotaProche(boolean alerteQuotaProche) { this.alerteQuotaProche = alerteQuotaProche; } - + public int getJoursAvantExpiration() { return joursAvantExpiration; } public void setJoursAvantExpiration(int joursAvantExpiration) { this.joursAvantExpiration = joursAvantExpiration; } - + // Classe interne pour les alertes public static class AlerteQuota implements Serializable { private String type; @@ -251,26 +224,25 @@ public class SouscriptionBean implements Serializable { private String message; private String action; private String actionUrl; - - // Getters et Setters + public String getType() { return type; } public void setType(String type) { this.type = type; } - + public String getSeverite() { return severite; } public void setSeverite(String severite) { this.severite = severite; } - + public String getIcone() { return icone; } public void setIcone(String icone) { this.icone = icone; } - + public String getTitre() { return titre; } public void setTitre(String titre) { this.titre = titre; } - + public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } - + public String getAction() { return action; } public void setAction(String action) { this.action = action; } - + public String getActionUrl() { return actionUrl; } public void setActionUrl(String actionUrl) { this.actionUrl = actionUrl; } } diff --git a/src/main/java/dev/lions/unionflow/client/view/SuperAdminBean.java b/src/main/java/dev/lions/unionflow/client/view/SuperAdminBean.java index 9a571a8..22503d2 100644 --- a/src/main/java/dev/lions/unionflow/client/view/SuperAdminBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/SuperAdminBean.java @@ -1,13 +1,14 @@ package dev.lions.unionflow.client.view; import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse; +import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationSummaryResponse; 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.dto.souscription.SouscriptionStatutResponse; import dev.lions.unionflow.client.service.AdminUserService; -import dev.lions.unionflow.client.service.AssociationService; +import dev.lions.unionflow.client.service.OrganisationService; import dev.lions.unionflow.client.service.AuditService; import dev.lions.unionflow.client.service.CotisationService; +import dev.lions.unionflow.client.service.MembreService; import dev.lions.unionflow.client.service.MetricsService; import dev.lions.unionflow.client.service.SouscriptionService; import io.quarkus.security.identity.SecurityIdentity; @@ -23,6 +24,7 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.UUID; import org.jboss.logging.Logger; @@ -41,10 +43,12 @@ public class SuperAdminBean implements Serializable { private static final String OUTCOME_SUPER_ADMIN_CONFIGURATION = "superAdminConfigurationPage"; private static final String OUTCOME_SUPER_ADMIN_ALERTES = "superAdminAlertesPage"; private static final String OUTCOME_SUPER_ADMIN_ACTIVITE = "superAdminActivitePage"; + private static final String OUTCOME_SUPER_ADMIN_AUDIT = "/pages/admin/audit/journal"; + private static final String OUTCOME_SUPER_ADMIN_BACKUP = "/pages/secure/admin/sauvegarde"; @Inject @RestClient - private AssociationService associationService; + private OrganisationService organisationService; @Inject @RestClient @@ -62,6 +66,10 @@ public class SuperAdminBean implements Serializable { @RestClient private SouscriptionService souscriptionService; + @Inject + @RestClient + private MembreService membreService; + @Inject private MetricsService metricsService; @@ -116,6 +124,10 @@ public class SuperAdminBean implements Serializable { private RevenusData revenus; private String periodeEvolution = "12M"; + // Caches transients des stats (non sérialisés — rechargés à chaque nouvelle session) + private transient OrganisationService.StatistiquesOrganisationDTO orgStats; + private transient MembreService.StatistiquesMembreDTO membreStats; + @PostConstruct public void init() { initializeUserInfo(); @@ -142,6 +154,8 @@ public class SuperAdminBean implements Serializable { } private void initializeKPIs() { + loadStatsOrganisations(); + loadStatsMembres(); initializeAssociationKPIs(); initializeAdminCount(); initializeCotisationKPIs(); @@ -151,22 +165,41 @@ public class SuperAdminBean implements Serializable { calculerPourcentagesJauges(); } - private void initializeAssociationKPIs() { + private void loadStatsOrganisations() { try { - PagedResponse response = associationService.listerToutes(0, 1000); - List associations = (response != null && response.getData() != null) ? response.getData() - : new ArrayList<>(); - totalEntites = associations.size(); - totalMembres = associations.stream() - .mapToInt(a -> a.getNombreMembres() != null ? a.getNombreMembres() : 0) - .sum(); - croissanceEntites = "0"; - nouvellesEntites = 0; - croissanceMembres = "0"; + orgStats = organisationService.obtenirStatistiques(); } catch (Exception e) { - LOG.debugf("Impossible de charger les KPIs associations: %s", e.getMessage()); + LOG.debugf("Impossible de charger les stats organisations: %s", e.getMessage()); + orgStats = null; + } + } + + private void loadStatsMembres() { + try { + membreStats = membreService.obtenirStatistiques(); + } catch (Exception e) { + LOG.debugf("Impossible de charger les stats membres: %s", e.getMessage()); + membreStats = null; + } + } + + private void initializeAssociationKPIs() { + if (orgStats != null) { + totalEntites = orgStats.totalAssociations != null ? orgStats.totalAssociations.intValue() : 0; + nouvellesEntites = orgStats.nouvellesAssociations30Jours != null + ? orgStats.nouvellesAssociations30Jours.intValue() : 0; + } else { totalEntites = 0; + nouvellesEntites = 0; + } + croissanceEntites = nouvellesEntites > 0 ? "+" + nouvellesEntites : "0"; + if (membreStats != null) { + totalMembres = membreStats.membresActifs != null ? membreStats.membresActifs.intValue() : 0; + croissanceMembres = membreStats.nouveauxMembres30Jours != null + ? membreStats.nouveauxMembres30Jours.toString() : "0"; + } else { totalMembres = 0; + croissanceMembres = "0"; } } @@ -189,14 +222,20 @@ public class SuperAdminBean implements Serializable { try { Map stats = cotisationService.obtenirStatistiques(); if (stats != null) { - Object montantTotal = stats.get("montantTotal"); + Object montantTotal = stats.get("montantTotalPaye"); if (montantTotal instanceof Number) { revenusGlobaux = String.format("%,.0f FCFA", ((Number) montantTotal).doubleValue()); } else { revenusGlobaux = "0 FCFA"; } - Object croissance = stats.get("croissance"); - croissanceRevenus = croissance != null ? String.valueOf(croissance) : "0"; + // tauxPaiement = % de cotisations payées (proxy de la santé financière) + Object croissance = stats.get("tauxPaiement"); + if (croissance instanceof Number) { + double val = ((Number) croissance).doubleValue(); + croissanceRevenus = val == 0.0 ? "0" : String.format(Locale.US, "%.1f", val); + } else { + croissanceRevenus = "0"; + } } else { revenusGlobaux = "0 FCFA"; croissanceRevenus = "0"; @@ -212,10 +251,12 @@ public class SuperAdminBean implements Serializable { try { Map stats = auditService.getStatistiques(); if (stats != null) { - Object activitesJour = stats.get("activitesAujourdhui"); - activiteJournaliere = activitesJour instanceof Number ? ((Number) activitesJour).intValue() : 0; - Object alertes = stats.get("alertes"); - alertesCount = alertes instanceof Number ? ((Number) alertes).intValue() : 0; + // "total" = nombre total d'actions enregistrées (proxy activité) + Object total = stats.get("total"); + activiteJournaliere = total instanceof Number ? ((Number) total).intValue() : 0; + // "errors" = erreurs critiques enregistrées (proxy alertes système) + Object errors = stats.get("errors"); + alertesCount = errors instanceof Number ? ((Number) errors).intValue() : 0; } else { activiteJournaliere = 0; alertesCount = 0; @@ -230,11 +271,11 @@ public class SuperAdminBean implements Serializable { private void initializeSouscriptionKPIs() { try { - List souscriptions = souscriptionService.listerToutes(null, 0, 1000); + List souscriptions = souscriptionService.listerToutes(null, 0, 1000); if (souscriptions != null) { totalSouscriptions = souscriptions.size(); souscriptionsActives = (int) souscriptions.stream() - .filter(s -> s.getStatut() == dev.lions.unionflow.server.api.enums.abonnement.StatutAbonnement.ACTIF) + .filter(s -> "ACTIVE".equals(s.getStatut())) .count(); LocalDate dans30Jours = LocalDate.now().plusDays(30); souscriptionsExpirantSous30Jours = (int) souscriptions.stream() @@ -293,22 +334,22 @@ public class SuperAdminBean implements Serializable { private void initializeEntites() { topEntites = new ArrayList<>(); try { - PagedResponse response = associationService.listerToutes(0, 1000); - List associations = (response != null && response.getData() != null) ? response.getData() + PagedResponse response = organisationService.listerToutes(0, 50); + List associations = (response != null && response.getData() != null) ? response.getData() : new ArrayList<>(); topEntites = associations.stream() .sorted((a1, a2) -> { - int m1 = a1.getNombreMembres() != null ? a1.getNombreMembres() : 0; - int m2 = a2.getNombreMembres() != null ? a2.getNombreMembres() : 0; + int m1 = a1.nombreMembres() != null ? a1.nombreMembres() : 0; + int m2 = a2.nombreMembres() != null ? a2.nombreMembres() : 0; return Integer.compare(m2, m1); }) .limit(5) .map(a -> { Entite entite = new Entite(); - entite.setId(a.getId()); - entite.setNom(a.getNom()); - entite.setTypeEntite(a.getTypeOrganisation()); - entite.setNombreMembres(a.getNombreMembres() != null ? a.getNombreMembres() : 0); + entite.setId(a.id()); + entite.setNom(a.nom()); + entite.setTypeEntite(a.typeOrganisation()); + entite.setNombreMembres(a.nombreMembres() != null ? a.nombreMembres() : 0); return entite; }) .collect(java.util.stream.Collectors.toList()); @@ -319,52 +360,47 @@ public class SuperAdminBean implements Serializable { private void initializeRepartitionTypes() { repartitionTypes = new ArrayList<>(); - try { - PagedResponse response = associationService.listerToutes(0, 1000); - List associations = (response != null && response.getData() != null) ? response.getData() - : new ArrayList<>(); - if (associations == null || associations.isEmpty()) - return; - java.util.Map parType = associations.stream() - .filter(a -> a.getTypeOrganisation() != null && !a.getTypeOrganisation().isBlank()) - .collect(java.util.stream.Collectors.groupingBy(OrganisationResponse::getTypeOrganisation, - java.util.stream.Collectors.counting())); - int total = associations.size(); - String[] couleurs = { "blue", "green", "orange", "purple", "teal" }; - int idx = 0; - for (java.util.Map.Entry entry : parType.entrySet()) { - TypeEntite typeEntite = new TypeEntite(); - typeEntite.setNom(entry.getKey()); - typeEntite.setDescription(entry.getKey()); - typeEntite.setNombre(entry.getValue().intValue()); - typeEntite.setPourcentage(total > 0 ? (entry.getValue().intValue() * 100) / total : 0); - typeEntite.setIcone("pi-building"); - typeEntite.setCouleurBg(idx < couleurs.length ? couleurs[idx] : "secondary"); - typeEntite.setCouleurTexte("white"); - repartitionTypes.add(typeEntite); - idx++; - } - } catch (Exception e) { - LOG.warnf(e, "Impossible de calculer la répartition des types"); + if (orgStats == null || orgStats.repartitionParType == null || orgStats.repartitionParType.isEmpty()) { + return; + } + int total = orgStats.totalAssociations != null ? orgStats.totalAssociations.intValue() : 0; + String[] couleurs = { "blue", "green", "orange", "purple", "teal" }; + int idx = 0; + for (java.util.Map.Entry entry : orgStats.repartitionParType.entrySet()) { + TypeEntite typeEntite = new TypeEntite(); + typeEntite.setNom(entry.getKey()); + typeEntite.setDescription(entry.getKey()); + typeEntite.setNombre(entry.getValue().intValue()); + typeEntite.setPourcentage(total > 0 ? (entry.getValue().intValue() * 100) / total : 0); + typeEntite.setIcone("pi-building"); + typeEntite.setCouleurBg(idx < couleurs.length ? couleurs[idx] : "secondary"); + typeEntite.setCouleurTexte("white"); + repartitionTypes.add(typeEntite); + idx++; } } private void initializeActivites() { activitesRecentes = new ArrayList<>(); try { - Map auditResult = auditService.listerTous(0, 10, "date", "desc"); - if (auditResult != null && auditResult.containsKey("content")) { + // sortBy=dateHeure (nom réel du champ), sortOrder=desc + Map auditResult = auditService.listerTous(0, 10, "dateHeure", "desc"); + if (auditResult != null && auditResult.containsKey("data")) { @SuppressWarnings("unchecked") - List> entries = (List>) auditResult.get("content"); + List> entries = (List>) auditResult.get("data"); if (entries != null) { for (Map entry : entries) { Activite activite = new Activite(); activite.setId(UUID.randomUUID()); - activite.setDescription(entry.getOrDefault("action", "").toString()); + // typeAction = action métier, description = détail lisible + Object desc = entry.get("description"); + Object typeAction = entry.get("typeAction"); + activite.setDescription(desc != null ? desc.toString() + : (typeAction != null ? typeAction.toString() : "")); activite.setEntite(entry.getOrDefault("module", "").toString()); activite.setUtilisateur(entry.getOrDefault("utilisateur", "").toString()); activite.setDetails(entry.getOrDefault("details", "").toString()); - Object dateObj = entry.get("date"); + Object dateObj = entry.get("dateHeure"); activite.setDate(dateObj != null ? dateObj.toString() : ""); activite.setIcone("pi-history"); activitesRecentes.add(activite); @@ -423,6 +459,14 @@ public class SuperAdminBean implements Serializable { return OUTCOME_SUPER_ADMIN_ACTIVITE + "?faces-redirect=true"; } + public String auditSysteme() { + return OUTCOME_SUPER_ADMIN_AUDIT + "?faces-redirect=true"; + } + + public String backup() { + return OUTCOME_SUPER_ADMIN_BACKUP + "?faces-redirect=true"; + } + public void exporterRapportFinancier() { LOG.info("Export du rapport financier généré"); } @@ -615,15 +659,15 @@ public class SuperAdminBean implements Serializable { } public String getTauxConversionFormat() { - return String.format("%.1f%%", tauxConversion); + return String.format(Locale.US, "%.1f%%", tauxConversion); } public String getDisponibiliteSystemeFormat() { - return String.format("%.1f%%", disponibiliteSysteme); + return String.format(Locale.US, "%.1f%%", disponibiliteSysteme); } public String getSatisfactionClientFormat() { - return String.format("%.1f/5", satisfactionClient); + return String.format(Locale.US, "%.1f/5", satisfactionClient); } public List getAlertesRecentes() { diff --git a/src/main/java/dev/lions/unionflow/client/view/UserSession.java b/src/main/java/dev/lions/unionflow/client/view/UserSession.java index 2d24bd7..761c218 100644 --- a/src/main/java/dev/lions/unionflow/client/view/UserSession.java +++ b/src/main/java/dev/lions/unionflow/client/view/UserSession.java @@ -1,14 +1,20 @@ package dev.lions.unionflow.client.view; +import dev.lions.unionflow.client.service.MembreService; +import dev.lions.unionflow.server.api.dto.membre.response.MembreResponse; import io.quarkus.oidc.IdToken; import io.quarkus.security.identity.SecurityIdentity; import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.SessionScoped; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.FacesContext; import jakarta.inject.Inject; import jakarta.inject.Named; import org.eclipse.microprofile.jwt.JsonWebToken; +import org.eclipse.microprofile.rest.client.inject.RestClient; import java.io.Serializable; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.logging.Logger; @@ -38,6 +44,10 @@ public class UserSession implements Serializable { @Inject SecurityIdentity securityIdentity; + @Inject + @RestClient + private MembreService membreService; + private String username; private boolean authenticated = false; private String typeCompte; @@ -46,6 +56,24 @@ public class UserSession implements Serializable { private CurrentUser currentUser; private EntiteInfo entite; + // Multi-org context + private UUID activeOrganisationId; + private String activeOrganisationNom; + private List mesOrganisations; + private boolean organisationsChargees = false; + + // Guard thread-safety pour le lazy-load de l'entité + private volatile boolean entiteEnChargement = false; + + /** + * Guard contre la réentrance CDI : positionné à true sur le thread courant pendant + * l'exécution de {@code @PostConstruct init()}, afin d'éviter que + * {@link dev.lions.unionflow.client.security.AuthHeaderFactory} ne déclenche + * la création d'une nouvelle instance UserSession en résolvant le proxy CDI + * pendant l'initialisation (StackOverflowError). + */ + public static final ThreadLocal INITIALIZING_ON_THREAD = new ThreadLocal<>(); + public UserSession() { // Session par défaut non authentifiée clearSession(); @@ -57,11 +85,16 @@ public class UserSession implements Serializable { */ @PostConstruct public void init() { - if (securityIdentity != null && !securityIdentity.isAnonymous()) { - LOGGER.info("Initialisation automatique de la session utilisateur (authentifié via OIDC)"); - initializeFromOidcToken(); - } else { - LOGGER.info("Utilisateur non authentifié, session reste vide"); + INITIALIZING_ON_THREAD.set(Boolean.TRUE); + try { + if (securityIdentity != null && !securityIdentity.isAnonymous()) { + LOGGER.info("Initialisation automatique de la session utilisateur (authentifié via OIDC)"); + initializeFromOidcToken(); + } else { + LOGGER.info("Utilisateur non authentifié, session reste vide"); + } + } finally { + INITIALIZING_ON_THREAD.remove(); } } @@ -105,6 +138,40 @@ public class UserSession implements Serializable { this.currentUser.setId(UUID.nameUUIDFromBytes(subject.getBytes())); } } + + // Charger l'organisation depuis le backend via /api/membres/me + chargerEntiteDepuisBackend(); + } + } + + /** + * Appelle GET /api/membres/me pour récupérer l'organisationId du membre connecté + * et peupler l'entite en session. Silencieux en cas d'erreur. + */ + private void chargerEntiteDepuisBackend() { + try { + MembreResponse moi = membreService.obtenirMembreConnecte(); + if (moi != null && moi.getOrganisationId() != null) { + EntiteInfo entiteInfo = new EntiteInfo(); + entiteInfo.setId(moi.getOrganisationId()); + entiteInfo.setNom(moi.getOrganisationNom() != null ? moi.getOrganisationNom() : ""); + this.entite = entiteInfo; + LOGGER.info("Entité chargée depuis /api/membres/me : " + entiteInfo.getId() + + " (" + entiteInfo.getNom() + ")"); + + // Charger aussi la liste des organisations pour le switcher + chargerMesOrganisations(); + + // Si aucune org active encore sélectionnée, utiliser celle de /me + if (this.activeOrganisationId == null) { + this.activeOrganisationId = moi.getOrganisationId(); + this.activeOrganisationNom = moi.getOrganisationNom(); + } + } else { + LOGGER.info("Aucune organisation trouvée pour l'utilisateur connecté (SUPER_ADMIN ou non rattaché)"); + } + } catch (Exception e) { + LOGGER.warning("Impossible de charger l'entité depuis le backend : " + e.getMessage()); } } @@ -316,23 +383,66 @@ public class UserSession implements Serializable { return "SUPER_ADMIN"; } - // Vérifier ADMIN_ENTITE (mais pas si c'est juste "ADMIN" qui pourrait être - // ambigu) - if (rolesToCheck.contains("ADMIN_ENTITE")) { - LOGGER.info("Type de compte détecté: ADMIN_ENTITE"); - return "ADMIN_ENTITE"; + // Vérifier le rôle ADMIN global (équivalent SUPER_ADMIN) + if (rolesToCheck.contains("ADMIN")) { + LOGGER.info("Type de compte détecté: SUPER_ADMIN (via rôle ADMIN)"); + return "SUPER_ADMIN"; } - // Vérifier les autres rôles admin (avec précaution pour éviter les faux - // positifs) - for (String role : rolesToCheck) { - if (role != null && (role.equals("ADMIN") || role.equalsIgnoreCase("admin"))) { - LOGGER.info("Type de compte détecté: ADMIN_ENTITE (via rôle ADMIN)"); - return "ADMIN_ENTITE"; + // Vérifier ADMIN_ORGANISATION et rôles du bureau + if (rolesToCheck.contains("ADMIN_ORGANISATION")) { + LOGGER.info("Type de compte détecté: ADMIN_ORGANISATION"); + return "ADMIN_ORGANISATION"; + } + if (rolesToCheck.contains("PRESIDENT")) { + LOGGER.info("Type de compte détecté: ADMIN_ORGANISATION (via rôle PRESIDENT)"); + return "ADMIN_ORGANISATION"; + } + + // Modérateur plateforme (rôle Keycloak MODERATOR) + if (rolesToCheck.contains("MODERATOR") || rolesToCheck.contains("MODERATEUR")) { + LOGGER.info("Type de compte détecté: MODERATEUR (rôle plateforme)"); + return "MODERATEUR"; + } + + // Consultant (accès lecture seule étendu) + if (rolesToCheck.contains("CONSULTANT")) { + LOGGER.info("Type de compte détecté: CONSULTANT"); + return "CONSULTANT"; + } + + // Gestionnaire RH + if (rolesToCheck.contains("GESTIONNAIRE_RH")) { + LOGGER.info("Type de compte détecté: GESTIONNAIRE_RH"); + return "GESTIONNAIRE_RH"; + } + + // Modérateurs / responsables fonctionnels + for (String moderatorRole : java.util.Arrays.asList( + "TRESORIER", "SECRETAIRE", "RESPONSABLE_MEMBRES", + "RESPONSABLE_TECHNIQUE", "RESPONSABLE_SOCIAL", + "RESPONSABLE_EVENEMENTS", "RESPONSABLE_CREDIT", + "ORGANISATEUR_EVENEMENT", "GESTIONNAIRE_MEMBRE", + "VICE_PRESIDENT")) { + if (rolesToCheck.contains(moderatorRole)) { + LOGGER.info("Type de compte détecté: MODERATEUR (via rôle " + moderatorRole + ")"); + return "MODERATEUR"; } } - LOGGER.warning("Aucun rôle admin trouvé, type de compte par défaut: MEMBRE"); + // Membre actif (participation aux activités) + if (rolesToCheck.contains("ACTIVEMEMBER") || rolesToCheck.contains("MEMBRE_ACTIF")) { + LOGGER.info("Type de compte détecté: MEMBRE_ACTIF"); + return "MEMBRE_ACTIF"; + } + + // Visiteur (lecture seule) + if (rolesToCheck.contains("VISITOR") || rolesToCheck.contains("VISITEUR")) { + LOGGER.info("Type de compte détecté: VISITEUR"); + return "VISITEUR"; + } + + LOGGER.info("Aucun rôle spécialisé trouvé, type de compte par défaut: MEMBRE"); return "MEMBRE"; } @@ -344,10 +454,152 @@ public class UserSession implements Serializable { this.permissions = null; this.currentUser = null; this.entite = null; + this.activeOrganisationId = null; + this.activeOrganisationNom = null; + this.mesOrganisations = null; + this.organisationsChargees = false; LOGGER.info("Session utilisateur effacée"); } + // ========================================================= + // Multi-org context switching + // ========================================================= + + /** + * Retourne la liste des organisations actives du membre connecté. + * Chargement paresseux : un seul appel backend par session. + */ + public List getMesOrganisations() { + if (!organisationsChargees && authenticated && !isSuperAdmin()) { + chargerMesOrganisations(); + } + return mesOrganisations != null ? mesOrganisations : Collections.emptyList(); + } + + /** + * Charge les organisations depuis GET /api/membres/mes-organisations. + */ + private void chargerMesOrganisations() { + try { + List orgs = membreService.mesOrganisations(); + this.mesOrganisations = orgs != null ? orgs : Collections.emptyList(); + this.organisationsChargees = true; + + // Auto-select si une seule org + if (this.activeOrganisationId == null && this.mesOrganisations.size() == 1) { + appliquerOrganisationActive(this.mesOrganisations.get(0)); + } + + LOGGER.info("Organisations chargées : " + this.mesOrganisations.size()); + } catch (Exception e) { + LOGGER.warning("Impossible de charger mes organisations : " + e.getMessage()); + this.mesOrganisations = Collections.emptyList(); + this.organisationsChargees = true; + } + } + + /** + * Appelé depuis le sélecteur dans la topbar — change l'organisation active + * et rafraîchit la page courante. + * + * @param orgIdStr UUID de l'organisation cible (String depuis la vue JSF) + */ + public void changerOrganisationActive(String orgIdStr) { + if (orgIdStr == null || orgIdStr.isBlank()) { + return; + } + try { + UUID orgId = UUID.fromString(orgIdStr.trim()); + // Trouver le DTO correspondant + MembreService.OrganisationSwitcherDTO cible = getMesOrganisations().stream() + .filter(o -> orgId.toString().equals(o.getOrganisationId())) + .findFirst() + .orElse(null); + + if (cible != null) { + appliquerOrganisationActive(cible); + LOGGER.info("Organisation active changée : " + activeOrganisationNom + " (" + activeOrganisationId + ")"); + } else { + LOGGER.warning("Organisation introuvable dans la liste : " + orgIdStr); + FacesContext fc = FacesContext.getCurrentInstance(); + if (fc != null) { + fc.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_WARN, + "Organisation introuvable", "Vous n'êtes pas membre de cette organisation.")); + } + } + } catch (IllegalArgumentException e) { + LOGGER.warning("UUID organisation invalide : " + orgIdStr); + } + } + + private void appliquerOrganisationActive(MembreService.OrganisationSwitcherDTO org) { + this.activeOrganisationId = UUID.fromString(org.getOrganisationId()); + this.activeOrganisationNom = org.getNom(); + // Mettre à jour l'entite pour compatibilité avec le code existant + if (this.entite == null) { + this.entite = new EntiteInfo(); + } + this.entite.setId(this.activeOrganisationId); + this.entite.setNom(org.getNom()); + this.entite.setType(org.getTypeOrganisation()); + } + + public boolean hasMultipleOrganisations() { + return getMesOrganisations().size() > 1; + } + + /** + * Retourne le type de l'organisation active (ex: ASSOCIATION, TONTINE, MUTUELLE...). + * Cherche dans la liste des organisations de l'utilisateur. + */ + public String getActiveOrganisationType() { + if (activeOrganisationId == null) return null; + return getMesOrganisations().stream() + .filter(o -> activeOrganisationId.toString().equals(o.getOrganisationId())) + .map(MembreService.OrganisationSwitcherDTO::getTypeOrganisation) + .findFirst() + .orElse(entite != null ? entite.getType() : null); + } + + /** + * Vérifie si un module est actif pour l'organisation active. + * Les modules actifs sont stockés sous forme de chaîne CSV dans la réponse du backend. + * + * @param module Nom du module ex: "TONTINE", "EPARGNE", "CREDIT" + */ + public boolean hasModuleActif(String module) { + if (module == null || activeOrganisationId == null) return false; + return getMesOrganisations().stream() + .filter(o -> activeOrganisationId.toString().equals(o.getOrganisationId())) + .map(MembreService.OrganisationSwitcherDTO::getModulesActifs) + .filter(m -> m != null && !m.isBlank()) + .anyMatch(modules -> { + for (String m : modules.split(",")) { + if (module.equalsIgnoreCase(m.trim())) return true; + } + return false; + }); + } + + public UUID getActiveOrganisationId() { + return activeOrganisationId; + } + + public void setActiveOrganisationId(UUID activeOrganisationId) { + this.activeOrganisationId = activeOrganisationId; + } + + public String getActiveOrganisationNom() { + if (activeOrganisationNom != null) return activeOrganisationNom; + if (entite != null && entite.getNom() != null) return entite.getNom(); + return "—"; + } + + public void setActiveOrganisationNom(String activeOrganisationNom) { + this.activeOrganisationNom = activeOrganisationNom; + } + // Méthodes de vérification des rôles et permissions public boolean hasRole(String role) { if (roles == null || roles.isEmpty()) { @@ -392,11 +644,45 @@ public class UserSession implements Serializable { } public boolean isAdmin() { - return isSuperAdmin() || "ADMIN_ENTITE".equals(typeCompte) || hasRole("ADMIN_ENTITE"); + return isSuperAdmin() + || "ADMIN_ORGANISATION".equals(typeCompte) + || hasRole("ADMIN_ORGANISATION") + || hasRole("PRESIDENT"); } public boolean isMembre() { - return "MEMBRE".equals(typeCompte) || hasRole("MEMBRE"); + // Tout utilisateur authentifié (admin inclus) peut accéder aux pages membre + return typeCompte != null || (securityIdentity != null && !securityIdentity.isAnonymous()); + } + + public boolean isModerateur() { + return "MODERATEUR".equals(typeCompte) + || hasRole("MODERATOR") || hasRole("MODERATEUR") + || hasRole("TRESORIER") || hasRole("SECRETAIRE") + || hasRole("RESPONSABLE_MEMBRES") || hasRole("RESPONSABLE_TECHNIQUE") + || hasRole("RESPONSABLE_SOCIAL") || hasRole("RESPONSABLE_EVENEMENTS") + || hasRole("RESPONSABLE_CREDIT") || hasRole("ORGANISATEUR_EVENEMENT") + || hasRole("GESTIONNAIRE_MEMBRE") || hasRole("VICE_PRESIDENT"); + } + + public boolean isMembreActif() { + return "MEMBRE_ACTIF".equals(typeCompte) || hasRole("MEMBRE_ACTIF") || hasRole("ACTIVEMEMBER"); + } + + public boolean isConsultant() { + return "CONSULTANT".equals(typeCompte) || hasRole("CONSULTANT"); + } + + public boolean isGestionnaireRh() { + return "GESTIONNAIRE_RH".equals(typeCompte) || hasRole("GESTIONNAIRE_RH"); + } + + public boolean isVisiteur() { + return "VISITEUR".equals(typeCompte) || hasRole("VISITOR") || hasRole("VISITEUR"); + } + + public boolean hasReadOnlyAccess() { + return isVisiteur() || isConsultant(); } // Méthode pour obtenir le rôle principal @@ -475,6 +761,20 @@ public class UserSession implements Serializable { } public EntiteInfo getEntite() { + // Lazy-load thread-safe : double-checked locking pour éviter les appels concurrents + if (entite == null && authenticated && !isSuperAdmin() && !entiteEnChargement) { + synchronized (this) { + if (entite == null && !entiteEnChargement) { + entiteEnChargement = true; + try { + LOGGER.info("getEntite() — entite null, tentative de rechargement depuis /api/membres/me"); + chargerEntiteDepuisBackend(); + } finally { + entiteEnChargement = false; + } + } + } + } return entite; } diff --git a/src/main/java/dev/lions/unionflow/client/view/UtilisateursBean.java b/src/main/java/dev/lions/unionflow/client/view/UtilisateursBean.java index 0af8dde..4d201b9 100644 --- a/src/main/java/dev/lions/unionflow/client/view/UtilisateursBean.java +++ b/src/main/java/dev/lions/unionflow/client/view/UtilisateursBean.java @@ -6,8 +6,9 @@ import dev.lions.unionflow.server.api.dto.user.response.UserResponse; import dev.lions.unionflow.server.api.dto.base.PageResponse; 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.organisation.response.OrganisationSummaryResponse; import dev.lions.unionflow.client.service.AdminUserService; -import dev.lions.unionflow.client.service.AssociationService; +import dev.lions.unionflow.client.service.OrganisationService; import jakarta.enterprise.context.SessionScoped; import jakarta.inject.Inject; import jakarta.inject.Named; @@ -37,7 +38,7 @@ public class UtilisateursBean implements Serializable { @Inject @RestClient - private AssociationService associationService; + private OrganisationService organisationService; @Inject @RestClient @@ -85,13 +86,13 @@ public class UtilisateursBean implements Serializable { private void initializeOrganisations() { organisationsDisponibles = new ArrayList<>(); try { - PagedResponse response = associationService.listerToutes(0, 1000); - List associations = (response != null && response.getData() != null) ? response.getData() + PagedResponse response = organisationService.listerToutes(0, 1000); + List associations = (response != null && response.getData() != null) ? response.getData() : new ArrayList<>(); - for (OrganisationResponse assoc : associations) { + for (OrganisationSummaryResponse assoc : associations) { Organisation org = new Organisation(); - org.setId(assoc.getId()); - org.setNom(assoc.getNom()); + org.setId(assoc.id()); + org.setNom(assoc.nom()); organisationsDisponibles.add(org); } } catch (Exception e) { diff --git a/src/main/resources/META-INF/resources/pages/admin/cotisations/gestion.xhtml b/src/main/resources/META-INF/resources/pages/admin/cotisations/gestion.xhtml index 0e37385..80f3cba 100644 --- a/src/main/resources/META-INF/resources/pages/admin/cotisations/gestion.xhtml +++ b/src/main/resources/META-INF/resources/pages/admin/cotisations/gestion.xhtml @@ -259,10 +259,10 @@ value="#{cotisationsGestionBean.filtres.statut}" styleClass="w-full"> - + - + @@ -465,25 +465,29 @@
- - + + rendered="#{cotisation.statut == 'PAYEE' or cotisation.statut == 'PARTIELLEMENT_PAYEE'}" /> - + action="#{cotisationsGestionBean.voirDetails(cotisation)}" + update="formDetailCotisation" + process="@this" />
@@ -721,5 +725,185 @@ + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+ +
+ + + + + + +
+
+
+ + + + +
+ + + + + + + + + + + + +
+
+ + #{cotisationsGestionBean.cotisationDetail.numeroReference} +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
Progression du paiement
+ +
#{cotisationsGestionBean.cotisationDetail.pourcentagePaiement}%
+
+ +
+
+ + #{cotisationsGestionBean.cotisationDetail.retardTexte} +
+
+ +
+
Observations
+
#{cotisationsGestionBean.cotisationDetail.observations}
+
+
+
+ + + +
\ No newline at end of file diff --git a/src/main/resources/META-INF/resources/pages/membre/dashboard.xhtml b/src/main/resources/META-INF/resources/pages/membre/dashboard.xhtml index d16d816..38f2b35 100644 --- a/src/main/resources/META-INF/resources/pages/membre/dashboard.xhtml +++ b/src/main/resources/META-INF/resources/pages/membre/dashboard.xhtml @@ -33,20 +33,20 @@

- Membre depuis #{membreDashboardBean.membre.dateAdhesion} + Membre depuis #{membreDashboardBean.membre.dateAdhesionFormatee}

- - + + action="/pages/secure/cotisation/historique.xhtml?faces-redirect=true" />
@@ -154,10 +154,10 @@
Cotisations
Consultez votre situation et payez en ligne
- + action="/pages/secure/cotisation/historique.xhtml?faces-redirect=true" /> @@ -168,10 +168,10 @@
Événements
Découvrez et inscrivez-vous aux événements
- + action="/pages/secure/evenement/gestion.xhtml?faces-redirect=true" /> @@ -182,10 +182,10 @@
Demandes
Faites une demande d'aide ou de service
- + action="/pages/secure/aide/demande.xhtml?faces-redirect=true" /> @@ -202,10 +202,10 @@ Mes Prochains Événements - + action="/pages/secure/evenement/gestion.xhtml?faces-redirect=true" /> @@ -252,10 +252,10 @@
Aucun événement à venir
- + action="/pages/secure/evenement/gestion.xhtml?faces-redirect=true" /> @@ -328,10 +328,10 @@ Activité Récente - + action="/pages/secure/cotisation/historique.xhtml?faces-redirect=true" /> @@ -365,18 +365,18 @@ styleClass="ui-button-success" action="#{membreDashboardBean.payerCotisations}" rendered="#{membreDashboardBean.peutPayerCotisations}" /> - - + - + + action="/pages/secure/documents/mes-documents.xhtml?faces-redirect=true" /> diff --git a/src/main/resources/META-INF/resources/pages/secure/adhesion/paiement.xhtml b/src/main/resources/META-INF/resources/pages/secure/adhesion/paiement.xhtml index 867b114..883a0d9 100644 --- a/src/main/resources/META-INF/resources/pages/secure/adhesion/paiement.xhtml +++ b/src/main/resources/META-INF/resources/pages/secure/adhesion/paiement.xhtml @@ -111,7 +111,7 @@ + styleClass="#{adhesion.montantRestant.signum() gt 0 ? 'text-orange-500 font-bold' : 'text-green-500'}" /> diff --git a/src/main/resources/META-INF/resources/pages/secure/admin/sauvegarde.xhtml b/src/main/resources/META-INF/resources/pages/secure/admin/sauvegarde.xhtml index 8dd7442..628f3db 100644 --- a/src/main/resources/META-INF/resources/pages/secure/admin/sauvegarde.xhtml +++ b/src/main/resources/META-INF/resources/pages/secure/admin/sauvegarde.xhtml @@ -6,7 +6,6 @@ xmlns:p="http://primefaces.org/ui" template="/templates/main-template.xhtml"> - Sauvegarde et Restauration - UnionFlow @@ -21,46 +20,91 @@ Sauvegarde et Restauration -

- Gérez les sauvegardes et restaurez la base de données -

+

Gérez les sauvegardes et restaurez le système

- + + +
- -
-
État du Système
-
-
-
-
Dernière sauvegarde
-
#{configurationBean.derniereSauvegarde}
+ +
+
+
+
État des sauvegardes
+
+
+
+
Dernière sauvegarde
+
+ + + + +
+
+
+
+
+
Prochaine
+
+ + + +
+
+
+
+
+
Total sauvegardes
+
#{sauvegardeBean.configuration.totalBackups}
+
+
+
+
+
Espace utilisé
+
#{sauvegardeBean.configuration.totalSizeFormatted}
+
+
-
-
-
Fréquence
-
#{configurationBean.frequenceSauvegarde}
-
-
-
-
-
Rétention
-
#{configurationBean.retentionSauvegardes} jours
-
-
-
-
-
Temps d'activité
-
#{configurationBean.tempsActivite}
+
+
+
+
Configuration auto
+
+
+ Sauvegarde automatique + +
+
+ Fréquence : #{sauvegardeBean.configuration.frequency}
+ Heure : #{sauvegardeBean.configuration.backupTime}
+ Rétention : #{sauvegardeBean.configuration.retentionDays} jours +
+
@@ -68,55 +112,241 @@
-
Sauvegardes Disponibles
- - - - - +
Sauvegardes disponibles
+ + + + +
+
#{sauv.name}
+
#{sauv.description}
+
+
+ + + + + + + #{sauv.sizeFormatted != null ? sauv.sizeFormatted : '-'} + + + + + + + + - - -
#{sauvegarde.taille}
+ + #{sauv.createdBy} + + +
+ + + +
- - - - - - - - - - + +
- - - + +
+ + + + +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+ + +
+
+
+ + + + +
+ +
+
Action irréversible
+
La restauration va écraser les données actuelles.
+
+
+
+ +
+ + +
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+ + + + +
+
+
+ + +
+
+
+
+ + + + + + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
diff --git a/src/main/resources/META-INF/resources/pages/secure/communication/conversations.xhtml b/src/main/resources/META-INF/resources/pages/secure/communication/conversations.xhtml new file mode 100644 index 0000000..c17401e --- /dev/null +++ b/src/main/resources/META-INF/resources/pages/secure/communication/conversations.xhtml @@ -0,0 +1,241 @@ + + + + Messagerie - UnionFlow + + + + + + +
+
+
+

+ + Messagerie +

+

Conversations et messages de l'organisation

+
+
+ + +
+
+
+ + +
+ + +
+
+
Conversations
+ + +
+ +
+
+ #{conv.name} + +
+
+ +
+
+ + + + + + +
+
+
+ +
+ +
+
+
+
+
+ + +
+
+ +
+
+ +

Sélectionnez une conversation

+
+
+
+ + + +
+
+
#{messagingBean.conversationActive.name}
+ #{messagingBean.conversationActive.participantIds.size()} participant(s) +
+
+ + +
+
+ + +
+ +
+
+
+ #{msg.senderName} + + + + + +
+

#{msg.content}

+
+ + +
+
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+
+ + + + +
+
+
+ + +
+
+
+
+ + + + + + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
diff --git a/src/main/resources/META-INF/resources/pages/secure/cotisation/collect.xhtml b/src/main/resources/META-INF/resources/pages/secure/cotisation/collect.xhtml index 3ec913e..4f3f11c 100644 --- a/src/main/resources/META-INF/resources/pages/secure/cotisation/collect.xhtml +++ b/src/main/resources/META-INF/resources/pages/secure/cotisation/collect.xhtml @@ -220,7 +220,7 @@ - + @@ -438,13 +438,13 @@
-

#{cotisationsBean.cotisationSelectionnee.dateEcheanceFormatee}

+

#{cotisationsBean.cotisationSelectionnee.dateEcheanceFormattee}

-

#{cotisationsBean.cotisationSelectionnee.datePaiementFormatee}

+

#{cotisationsBean.cotisationSelectionnee.datePaiementFormattee}

diff --git a/src/main/resources/META-INF/resources/pages/secure/cotisation/historique.xhtml b/src/main/resources/META-INF/resources/pages/secure/cotisation/historique.xhtml index 85bdf26..7a34c8a 100644 --- a/src/main/resources/META-INF/resources/pages/secure/cotisation/historique.xhtml +++ b/src/main/resources/META-INF/resources/pages/secure/cotisation/historique.xhtml @@ -152,7 +152,7 @@ - + @@ -205,7 +205,7 @@
-

#{cotisationsBean.cotisationSelectionnee.dateEcheanceFormatee}

+

#{cotisationsBean.cotisationSelectionnee.dateEcheanceFormattee}

@@ -229,7 +229,7 @@
-

#{cotisationsBean.cotisationSelectionnee.datePaiementFormatee}

+

#{cotisationsBean.cotisationSelectionnee.datePaiementFormattee}

diff --git a/src/main/resources/META-INF/resources/pages/secure/cotisation/paiement.xhtml b/src/main/resources/META-INF/resources/pages/secure/cotisation/paiement.xhtml index 4d7b1ea..c97a1a6 100644 --- a/src/main/resources/META-INF/resources/pages/secure/cotisation/paiement.xhtml +++ b/src/main/resources/META-INF/resources/pages/secure/cotisation/paiement.xhtml @@ -7,14 +7,14 @@ template="/templates/main-template.xhtml"> - Paiement de Cotisations - UnionFlow + #{cotisationsBean.vueAdmin ? 'Paiement de Cotisations' : 'Payer mes Cotisations'} - UnionFlow - - + +
@@ -33,7 +33,7 @@ -
+ @@ -61,10 +61,10 @@ -
+ -
+
Répartition par Méthode de Paiement @@ -107,17 +107,16 @@
-
+
-
Cotisations en Attente de Paiement
+
#{cotisationsBean.vueAdmin ? 'Cotisations en Attente de Paiement' : 'Mes Cotisations en Attente'}
- +
+ rendered="#{cotisationsBean.vueAdmin}">
#{cotisation.nomMembre}
#{cotisation.numeroMembre}
@@ -160,28 +159,33 @@ - + - + - -
- +
+ + + + -
@@ -201,7 +205,7 @@ - + @@ -213,18 +217,18 @@ - + - + - + - +
@@ -260,31 +264,34 @@
-
- + - + + + + - + - +
@@ -298,7 +305,7 @@ - + @@ -306,5 +313,161 @@ + + + + + + + + +
+ + +
+
#{cotisationsBean.cotisationSelectionnee.montantDuFormatte}
+
#{cotisationsBean.cotisationSelectionnee.numeroReference} + — #{cotisationsBean.cotisationSelectionnee.typeCotisationLibelle}
+
+ + + + + + +
+ +
+

Scannez ce QR code avec l'application Wave

+

Le QR code expire dans 30 minutes

+
+ + + +
+
Paiement confirmé !
+

Votre cotisation a été enregistrée.

+
+ + + +
⏱️
+
Session expirée
+

Veuillez fermer et recommencer.

+
+ +
+ + + +
    +
  1. Ouvrez l'application Wave sur votre téléphone
  2. +
  3. Appuyez sur Scanner
  4. +
  5. Scannez le QR code ci-dessus
  6. +
  7. Confirmez le paiement de #{cotisationsBean.cotisationSelectionnee.montantDuFormatte}
  8. +
  9. Cette page se mettra à jour automatiquement
  10. +
+
+ +
+ + +
+ +
+
+
+ + + + // + + + + + diff --git a/src/main/resources/META-INF/resources/pages/secure/cotisation/relances.xhtml b/src/main/resources/META-INF/resources/pages/secure/cotisation/relances.xhtml index 57bc8b0..de12092 100644 --- a/src/main/resources/META-INF/resources/pages/secure/cotisation/relances.xhtml +++ b/src/main/resources/META-INF/resources/pages/secure/cotisation/relances.xhtml @@ -106,7 +106,7 @@ - + @@ -163,7 +163,7 @@
- + diff --git a/src/main/resources/META-INF/resources/pages/secure/dashboard-membre.xhtml b/src/main/resources/META-INF/resources/pages/secure/dashboard-membre.xhtml index f13b9f7..425e980 100644 --- a/src/main/resources/META-INF/resources/pages/secure/dashboard-membre.xhtml +++ b/src/main/resources/META-INF/resources/pages/secure/dashboard-membre.xhtml @@ -89,184 +89,273 @@ - +
Historique de mes cotisations
- - - - - - - - - - - - - #{cotis.montant} FCFA - - - - - - - - + + + + #{cotis.numeroReference} + + + #{cotis.periode} + + + #{cotis.montantFormatte} FCFA + + + + + + + FCFA + + + + + + + + + + +
+ + +
+
+
Cotisations en attente de paiement
+ +
+ + + + #{cotisAtt.numeroReference} + + + #{cotisAtt.periode} + + + #{cotisAtt.montantFormatte} FCFA + + + + + + + + + + +
- -
-
-
Actions rapides
- Ce que je peux faire -
-
- -
- - - - - - - - Cotisation mensuelle -
- -
- - - - - - - - Calendrier -
- -
- - - - - - - - Nouvelle demande -
- -
- - - - - - - - Mes informations -
-
+
+
+
Actions rapides
+ Ce que je peux faire
- - - -
-
Mes notifications
-
- -
- -
-
#{notif.titre}
- #{notif.message} -
- - - - - -
-
- -
- -

Aucune notification pour le moment

-
-
+
+ +
+ + + + + + + + Cotisation mensuelle +
+ +
+ + + + + + + + Calendrier +
+ +
+ + + + + + + + Nouvelle demande +
+ +
+ + + + + + + + Mes informations +
-
- -
+
-
-
Événements à venir
- - - - - - - - - -
- -
- -
-
-
- - #{evt.daysUntilEvent} -
-
#{evt.title}
-

#{evt.description}

-
- - - - -
-
- - #{evt.location} -
-
- #{evt.participationSummary} - - - - - - - - - -
-
+
Statut de mes cotisations
+
+
+
+ + Total historique
- -
- - -
- -

Aucun événement prévu pour le moment

+
-
+
+
+ + En attente +
+ +
+
+
+ + Taux de paiement +
+ + + + + +
+
+ + + + + + + + + + + + +
+

Récapitulatif de la cotisation à régler :

+
+ + + + + + + + + + + + + + +

+ + +
+ + + + + + + +
+ + +
+
+ + + + + + + + + + + +
+
+ + +
+ + + + + + + +
+ + +
+ + + + + + +
+
+ + + + + +
+ diff --git a/src/main/resources/META-INF/resources/pages/secure/dashboard.xhtml b/src/main/resources/META-INF/resources/pages/secure/dashboard.xhtml index d595cb9..f274bfa 100644 --- a/src/main/resources/META-INF/resources/pages/secure/dashboard.xhtml +++ b/src/main/resources/META-INF/resources/pages/secure/dashboard.xhtml @@ -380,8 +380,10 @@
- +
+ +
#{activity.userNom}
#{activity.userRole} @@ -413,7 +415,7 @@ - + @@ -425,7 +427,7 @@ - + @@ -437,7 +439,7 @@ - + @@ -475,7 +477,7 @@
- +
@@ -491,7 +493,7 @@
- +
@@ -507,7 +509,7 @@
- +
@@ -523,7 +525,7 @@
- +
diff --git a/src/main/resources/META-INF/resources/pages/secure/epargne/comptes.xhtml b/src/main/resources/META-INF/resources/pages/secure/epargne/comptes.xhtml new file mode 100644 index 0000000..fab457d --- /dev/null +++ b/src/main/resources/META-INF/resources/pages/secure/epargne/comptes.xhtml @@ -0,0 +1,373 @@ + + + + Épargne - UnionFlow + + + + + + +
+
+
+

+ + Comptes d'Épargne +

+

Gestion des comptes d'épargne (conforme LCB-FT)

+
+
+ + +
+
+
+ + +
+
+ +
+
Conformité LCB-FT
+
+ Les dépôts ≥ 500 000 FCFA nécessitent une déclaration d'origine des fonds. + Toute transaction suspecte est automatiquement signalée. +
+
+
+
+ + + + + + +
+ +
+
+ +
+
+
#{compte.numeroCompte}
+
#{compte.typeCompte}
+
+ +
+
+
Solde disponible
+
+ + + +
+ +
+ + Bloqué : + + + +
+
+
+
+ Ouvert le + + + +
+
+
+
+
+ + +
+
+ +

Aucun compte d'épargne. Ouvrez votre premier compte !

+
+
+
+
+
+ + + + + + + + + #{compte.numeroCompte} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+
+
+
+ + + + +
+ + +
+
+
+
Solde disponible
+
+ + + +
+
+
+
+
+
Type
+
#{epargneBean.compteSelectionne.typeCompte}
+
+
+
+
+
Statut
+ +
+
+
+ + + +
+
+ + + + + +
+
+ +
+
+ +
+
+ +
+ +
+
+ + Montant ≥ 500 000 FCFA : déclaration d'origine des fonds obligatoire (LCB-FT) +
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + #{tx.motif} + + + + + + + +
+ +
+
+
+
+
+ + + + +
+
+
+ + +
+
+
+
+ + + + + + + + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
diff --git a/src/main/resources/META-INF/resources/pages/secure/evenement/creation.xhtml b/src/main/resources/META-INF/resources/pages/secure/evenement/creation.xhtml index 07740e7..3332ede 100644 --- a/src/main/resources/META-INF/resources/pages/secure/evenement/creation.xhtml +++ b/src/main/resources/META-INF/resources/pages/secure/evenement/creation.xhtml @@ -33,7 +33,7 @@
- Informations Générales +
@@ -95,7 +95,7 @@ - Dates et Horaires +
@@ -139,7 +139,7 @@ - Localisation +
@@ -181,7 +181,7 @@ - Organisation et Participants +
@@ -221,7 +221,7 @@ - Budget +
diff --git a/src/main/resources/META-INF/resources/pages/secure/finance/approbations.xhtml b/src/main/resources/META-INF/resources/pages/secure/finance/approbations.xhtml new file mode 100644 index 0000000..7c31217 --- /dev/null +++ b/src/main/resources/META-INF/resources/pages/secure/finance/approbations.xhtml @@ -0,0 +1,351 @@ + + + + Workflow Financier - UnionFlow + + + + + + +
+
+
+

+ + Workflow Financier +

+

Approbations de transactions et gestion des budgets

+
+
+ + +
+
+
+ + +
+
+
+
+ #{approbationsFinanceBean.nombreEnAttente} +
+
En attente d'approbation
+
+
+
+ + + + + + + + + + #{appro.transactionId.toString().substring(0,8)}... + + + + + + + + + + + + + + + + #{appro.requesterName} + + + + + + + + + + + + + + + + + +
+ + +
+
+
+
+ + + + + + + #{budget.name} + + + + #{budget.period} #{budget.year} + + + + + + + + + + + + + + + + + + #{budget.realizationRate != null ? budget.realizationRate.intValue() : 0}% + + + + + + + + + + + + + + + +
+
+ + + + + + + +
+
+ +
+
+ +
+
+ +
+
+ + + + + + + + + + + #{h.requesterName} + + + + + + + + + + + + + + +
+
+
+ + + + +
+ + +
+
+ + +
+
+
+ + + + +
+ + +
+
+ + +
+
+
+ + + + +
+
+
+ + +
+
+
+
+ + + + + + + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
diff --git a/src/main/resources/META-INF/resources/pages/secure/membre/liste.xhtml b/src/main/resources/META-INF/resources/pages/secure/membre/liste.xhtml index 3381167..cf7a8ef 100644 --- a/src/main/resources/META-INF/resources/pages/secure/membre/liste.xhtml +++ b/src/main/resources/META-INF/resources/pages/secure/membre/liste.xhtml @@ -28,43 +28,49 @@ - -
- - - - - - -
-
- - - - - - -
-
- - - - - - -
-
- - - - - - -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
-
+ + @@ -116,14 +123,16 @@ itemLabel="#{org.nom}" itemValue="#{org.id}" /> -
+ - - +
+ + +
@@ -178,9 +187,8 @@ - - - + + @@ -188,10 +196,10 @@ - - - - + + + + @@ -205,10 +213,9 @@ - - - - + + + @@ -249,14 +256,14 @@ actionListener="#{membreListeBean.preparerSuspendre(membre)}" update=":formMembres:dlgConfirmSuspendre" oncomplete="PF('dlgConfirmSuspendre').show();" - rendered="#{menuBean.gestionMembresMenuVisible and membre.statut == 'ACTIF'}" + rendered="#{menuBean.gestionMembresMenuVisible and membre.statutCompte == 'ACTIF'}" styleClass="ui-button-danger ui-button-sm ui-button-rounded mr-1" /> diff --git a/src/main/resources/META-INF/resources/pages/secure/membre/profil.xhtml b/src/main/resources/META-INF/resources/pages/secure/membre/profil.xhtml index 744e8f6..6bec3c8 100644 --- a/src/main/resources/META-INF/resources/pages/secure/membre/profil.xhtml +++ b/src/main/resources/META-INF/resources/pages/secure/membre/profil.xhtml @@ -609,7 +609,66 @@ - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/META-INF/resources/pages/secure/organisation/liste.xhtml b/src/main/resources/META-INF/resources/pages/secure/organisation/liste.xhtml index 8988a53..d36a995 100644 --- a/src/main/resources/META-INF/resources/pages/secure/organisation/liste.xhtml +++ b/src/main/resources/META-INF/resources/pages/secure/organisation/liste.xhtml @@ -196,14 +196,9 @@ -
+
- - #{empty org.ville ? '' : org.ville}#{(not empty org.ville and not empty org.region) ? ', ' : ''}#{empty org.region ? '' : org.region} - -
-
- #{org.pays} +
@@ -239,10 +234,9 @@ - - + oncomplete="PF('dlgModifier').show();" /> + severity="#{role == 'SUPER_ADMIN' ? 'danger' : (role == 'ADMIN_ORGANISATION' ? 'warning' : 'info')}" />
diff --git a/src/main/resources/META-INF/resources/pages/secure/rapport/tableaux-bord.xhtml b/src/main/resources/META-INF/resources/pages/secure/rapport/tableaux-bord.xhtml index f063caf..c0a5d50 100644 --- a/src/main/resources/META-INF/resources/pages/secure/rapport/tableaux-bord.xhtml +++ b/src/main/resources/META-INF/resources/pages/secure/rapport/tableaux-bord.xhtml @@ -9,6 +9,7 @@ Tableaux de Bord - UnionFlow +
@@ -136,6 +137,7 @@
+ diff --git a/src/main/resources/META-INF/resources/pages/super-admin/dashboard.xhtml b/src/main/resources/META-INF/resources/pages/super-admin/dashboard.xhtml index 2cc2888..193f028 100644 --- a/src/main/resources/META-INF/resources/pages/super-admin/dashboard.xhtml +++ b/src/main/resources/META-INF/resources/pages/super-admin/dashboard.xhtml @@ -9,462 +9,351 @@ Dashboard Super-Administrateur - UnionFlow -
- -
-
-
-

- - Tableau de bord Super-Administrateur -

-

- Vue globale de la plateforme UnionFlow • - #{superAdminBean.totalEntites} organisations • - #{superAdminBean.totalMembres} membres actifs -

+
+ + +
+
+
+
+

+ + Tableau de bord Super-Administrateur +

+

+ #{superAdminBean.nomComplet} • + #{superAdminBean.totalEntites} organisations • + #{superAdminBean.totalMembres} membres actifs +

+

Dernière connexion : #{superAdminBean.derniereConnexion}

+
+
+ + + + + + + + + + + + + + + +
-
-
#{superAdminBean.nomComplet}
-
Dernière connexion: #{superAdminBean.derniereConnexion}
- - - - +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Actions Rapides +
+ +
+
+ + + + + + + +
+
+ + + + + + + +
+
+ + + + + + + +
+
+ + + + + + + +
+
+ + + + + + + +
+
+ + + + + + + +
+
+
+
+
+ + +
+
+
+
+ + Alertes Système +
+ +
+ + +
+ +
+
#{alerte.titre}
+ #{alerte.entite} • #{alerte.date} +
+ + + +
+
+ +
+ +
Aucune alerte active
+
+ +
+ + + + + + + +
+
+
+ + +
+
+
+ + Top 5 Entités +
+ +
+
+ #{status.index + 1} +
+
+
#{entite.nom}
+ #{entite.typeEntite} +
+
+
#{entite.nombreMembres}
+ membres +
+
+
+
+ +
Aucune entité enregistrée
+
+
+
+ + +
+
+
+ + Répartition par Type +
+ +
+
+ +
+
#{type.nom}
+
+
+
+
#{type.nombre}
+ +
+
+
+
+ +
Aucune donnée disponible
+
+
+
+ + +
+
+
+
+ + Activité Récente +
+ + + + + + + +
+ + +
+ +
+
#{activite.description}
+
#{activite.entite}
+
+ #{activite.date} + + Par #{activite.utilisateur} + +
+
+
+
+ +
+ +
Aucune activité récente
+
+
+
+ + +
+
+
+
+ + Performance Financière Globale +
+ + + + + + + +
-
-
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
- - Actions Rapides -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
+
+
+
+
#{superAdminBean.revenus.mensuel}
+
Revenus ce mois
+
-
-
- - -
-
-
-
-
- - Alertes Système -
- +
+
+
#{superAdminBean.revenus.annuel}
+
Revenus annuels
- -
-
- - #{superAdminBean.alertesCount} alertes nécessitent votre attention -
+
+
+
+
#{superAdminBean.revenus.croissance}%
+
Croissance annuelle
- - -
-
-
- -
-
#{alerte.titre}
-
#{alerte.entite} • #{alerte.date}
-
-
- - - -
-
-
- -
- - - +
+
+
+
#{superAdminBean.revenus.moyenne}
+
Revenu moyen / entité
- -
- -
-
-
-
-
- - Évolution des Entités -
- - - - - -
- -
-
- -
-
#{mois.valeur}
-
-
#{mois.periode}
-
-
-
-
-
- - +#{superAdminBean.croissanceEntites}% - ce mois -
-
- Total: #{superAdminBean.totalEntites} entités -
-
-
-
-
-
- - -
-
-
-
- - Top 5 Entités -
- - -
-
-
-
- #{status.index + 1} -
-
-
#{entite.nom}
-
#{entite.typeEntite}
-
-
-
-
#{entite.nombreMembres}
-
membres
-
-
-
-
-
-
-
-
- - -
- -
-
-
-
- - Répartition par Type -
- - -
-
-
-
- -
-
-
#{type.nom}
-
#{type.description}
-
-
-
-
#{type.nombre}
- -
-
-
-
- - -
-
- Répartition globale - #{superAdminBean.totalEntites} entités -
-
- -
-
-
-
-
-
-
- - -
-
-
-
- - Activité Récente -
- -
- -
- -
-
- -
-
-
- - -
-
-
- #{activite.description} - #{activite.date} -
-
#{activite.entite}
-
- #{activite.details} -
-
-
- Par #{activite.utilisateur} -
-
-
-
-
-
- -
- - - -
-
-
-
-
- - -
-
-
- - Performance Financière Globale -
- -
-
-
-
Revenus Ce Mois
-
#{superAdminBean.revenus.mensuel}
-
- - +#{superAdminBean.revenus.croissanceMensuelle}% -
-
-
- -
-
-
Revenus Annuels
-
#{superAdminBean.revenus.annuel}
-
- Objectif: #{superAdminBean.revenus.objectifAnnuel} -
-
-
- -
-
-
Croissance Annuelle
-
#{superAdminBean.revenus.croissance}%
-
- - Tendance positive -
-
-
- -
-
-
Revenu Moyen/Entité
-
#{superAdminBean.revenus.moyenne}
-
- Sur #{superAdminBean.totalEntites} entités -
-
-
-
- - -
-
- Évolution des revenus (6 derniers mois) - - - -
-
- -
-
#{mois.valeur}
-
-
#{mois.nom}
-
-
-
-
- - Dernière mise à jour: #{superAdminBean.revenus.derniereMAJ} -
-
-
-
- \ No newline at end of file + + diff --git a/src/main/resources/META-INF/resources/resources/css/topbar-elite.css b/src/main/resources/META-INF/resources/resources/css/topbar-elite.css index 9606ee4..afab7ac 100644 --- a/src/main/resources/META-INF/resources/resources/css/topbar-elite.css +++ b/src/main/resources/META-INF/resources/resources/css/topbar-elite.css @@ -791,4 +791,125 @@ .user-info { display: none; } .elite-dropdown { min-width: 300px; } .search-dropdown { min-width: 280px; } + .org-switcher-label { display: none; } +} + +/* ═══════════════════════════════════════════════════════════ */ +/* ORG SWITCHER */ +/* ═══════════════════════════════════════════════════════════ */ + +.org-switcher-item { + position: relative; +} + +.org-switcher-trigger { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.4rem 0.75rem; + border-radius: 8px; + background: rgba(var(--primary-color-rgb, 99,102,241), 0.08); + border: 1px solid rgba(var(--primary-color-rgb, 99,102,241), 0.2); + color: var(--text-color, #374151); + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + text-decoration: none; + transition: background 0.2s, border-color 0.2s; + max-width: 220px; +} + +.org-switcher-trigger:hover { + background: rgba(var(--primary-color-rgb, 99,102,241), 0.15); + border-color: var(--primary-color, #6366f1); + text-decoration: none; +} + +.org-switcher-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 160px; +} + +.org-switcher-dropdown { + display: none; + position: absolute; + top: calc(100% + 8px); + left: 0; + min-width: 280px; + background: var(--surface-card, #fff); + border: 1px solid var(--surface-border, #e5e7eb); + border-radius: 12px; + box-shadow: 0 10px 40px rgba(0,0,0,0.12); + z-index: 1000; + padding: 0.5rem 0; + list-style: none; + margin: 0; +} + +.org-switcher-item:hover .org-switcher-dropdown, +.org-switcher-item:focus-within .org-switcher-dropdown { + display: block; +} + +.org-item { + padding: 0; +} + +.org-item-link { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.6rem 1rem; + cursor: pointer; + text-decoration: none; + color: var(--text-color, #374151); + transition: background 0.15s; +} + +.org-item-link:hover { + background: var(--surface-hover, #f9fafb); +} + +.org-item-active .org-item-link { + background: rgba(var(--primary-color-rgb, 99,102,241), 0.06); +} + +.org-item-content { + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.org-item-name { + font-weight: 600; + font-size: 0.875rem; + color: var(--text-color, #111827); +} + +.org-item-meta { + display: flex; + gap: 0.4rem; +} + +.org-type-badge { + font-size: 0.7rem; + background: var(--surface-ground, #f3f4f6); + color: var(--text-color-secondary, #6b7280); + padding: 0.1rem 0.4rem; + border-radius: 4px; +} + +.org-role-badge { + font-size: 0.7rem; + background: #ecfdf5; + color: #065f46; + padding: 0.1rem 0.4rem; + border-radius: 4px; +} + +.org-check-icon { + color: var(--primary-color, #6366f1); + font-size: 0.875rem; } diff --git a/src/main/resources/META-INF/resources/templates/components/buttons/button-icon.xhtml b/src/main/resources/META-INF/resources/templates/components/buttons/button-icon.xhtml index 3fc43f0..73cfc0d 100644 --- a/src/main/resources/META-INF/resources/templates/components/buttons/button-icon.xhtml +++ b/src/main/resources/META-INF/resources/templates/components/buttons/button-icon.xhtml @@ -3,30 +3,49 @@ xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:p="http://primefaces.org/ui" - xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"> - - - + + + - + + + + + - diff --git a/src/main/resources/META-INF/resources/templates/components/buttons/button-info.xhtml b/src/main/resources/META-INF/resources/templates/components/buttons/button-info.xhtml index f97d8a5..a3e1dea 100644 --- a/src/main/resources/META-INF/resources/templates/components/buttons/button-info.xhtml +++ b/src/main/resources/META-INF/resources/templates/components/buttons/button-info.xhtml @@ -2,37 +2,39 @@ xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" - xmlns:p="http://primefaces.org/ui"> - - - + + + - - - - diff --git a/src/main/resources/META-INF/resources/templates/components/buttons/button-primary.xhtml b/src/main/resources/META-INF/resources/templates/components/buttons/button-primary.xhtml index f9b9e8c..90fad05 100644 --- a/src/main/resources/META-INF/resources/templates/components/buttons/button-primary.xhtml +++ b/src/main/resources/META-INF/resources/templates/components/buttons/button-primary.xhtml @@ -2,9 +2,10 @@ xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" - xmlns:p="http://primefaces.org/ui"> - - - + + + - - - - diff --git a/src/main/resources/META-INF/resources/templates/components/buttons/button-secondary.xhtml b/src/main/resources/META-INF/resources/templates/components/buttons/button-secondary.xhtml index 8822b27..6a36cd4 100644 --- a/src/main/resources/META-INF/resources/templates/components/buttons/button-secondary.xhtml +++ b/src/main/resources/META-INF/resources/templates/components/buttons/button-secondary.xhtml @@ -2,9 +2,10 @@ xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" - xmlns:p="http://primefaces.org/ui"> - - - + + + - - - - - - + + + - - - - - - + + + - - - - diff --git a/src/main/resources/META-INF/resources/templates/components/cards/kpi-card.xhtml b/src/main/resources/META-INF/resources/templates/components/cards/kpi-card.xhtml index 2da3975..5da02a9 100644 --- a/src/main/resources/META-INF/resources/templates/components/cards/kpi-card.xhtml +++ b/src/main/resources/META-INF/resources/templates/components/cards/kpi-card.xhtml @@ -112,23 +112,23 @@ -
+
+#{growthValue} #{growthLabel}
-
+
#{noDataLabel}
-
+
+#{growthValue}% #{growthLabel}
-
+
#{noDataLabel}
diff --git a/src/main/resources/META-INF/resources/templates/components/dialogs/form-dialog.xhtml b/src/main/resources/META-INF/resources/templates/components/dialogs/form-dialog.xhtml index 6d0ed03..423fae9 100644 --- a/src/main/resources/META-INF/resources/templates/components/dialogs/form-dialog.xhtml +++ b/src/main/resources/META-INF/resources/templates/components/dialogs/form-dialog.xhtml @@ -57,7 +57,7 @@ fitViewport="#{empty fitViewport ? true : fitViewport}" showEffect="#{empty showEffect ? 'fade' : showEffect}" hideEffect="#{empty hideEffect ? 'fade' : hideEffect}" - dynamic="true" + dynamic="#{empty dynamic ? true : dynamic}" styleClass="#{styleClass}">
diff --git a/src/main/resources/META-INF/resources/templates/components/forms/form-field-number.xhtml b/src/main/resources/META-INF/resources/templates/components/forms/form-field-number.xhtml index c689a5c..15e29a1 100644 --- a/src/main/resources/META-INF/resources/templates/components/forms/form-field-number.xhtml +++ b/src/main/resources/META-INF/resources/templates/components/forms/form-field-number.xhtml @@ -19,15 +19,15 @@
- - +
diff --git a/src/main/resources/META-INF/resources/templates/components/forms/form-section.xhtml b/src/main/resources/META-INF/resources/templates/components/forms/form-section.xhtml index 8ceb512..93e4018 100644 --- a/src/main/resources/META-INF/resources/templates/components/forms/form-section.xhtml +++ b/src/main/resources/META-INF/resources/templates/components/forms/form-section.xhtml @@ -1,31 +1,26 @@ - - + - + -
#{title}
- - - +
+
- - diff --git a/src/main/resources/META-INF/resources/templates/components/layout/menu.xhtml b/src/main/resources/META-INF/resources/templates/components/layout/menu.xhtml index 225cd01..ce0a7b4 100644 --- a/src/main/resources/META-INF/resources/templates/components/layout/menu.xhtml +++ b/src/main/resources/META-INF/resources/templates/components/layout/menu.xhtml @@ -40,10 +40,10 @@ - - - - + + + + @@ -53,7 +53,7 @@ - + @@ -89,20 +89,97 @@ + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -126,7 +203,7 @@ - + @@ -137,6 +214,7 @@ + @@ -152,7 +230,7 @@ - + @@ -173,7 +251,8 @@ - + + diff --git a/src/main/resources/META-INF/resources/templates/components/layout/topbar.xhtml b/src/main/resources/META-INF/resources/templates/components/layout/topbar.xhtml index a24bd34..a892102 100644 --- a/src/main/resources/META-INF/resources/templates/components/layout/topbar.xhtml +++ b/src/main/resources/META-INF/resources/templates/components/layout/topbar.xhtml @@ -1,6 +1,7 @@ @@ -36,6 +37,42 @@
diff --git a/src/main/resources/META-INF/resources/templates/main-template.xhtml b/src/main/resources/META-INF/resources/templates/main-template.xhtml index 2ae2fa7..66f1353 100644 --- a/src/main/resources/META-INF/resources/templates/main-template.xhtml +++ b/src/main/resources/META-INF/resources/templates/main-template.xhtml @@ -18,6 +18,8 @@ <ui:insert name="title">UnionFlow</ui:insert> + + diff --git a/src/main/resources/META-INF/resources/ui/includes/membre-form.xhtml b/src/main/resources/META-INF/resources/ui/includes/membre-form.xhtml index c9c08fe..1aaaffe 100644 --- a/src/main/resources/META-INF/resources/ui/includes/membre-form.xhtml +++ b/src/main/resources/META-INF/resources/ui/includes/membre-form.xhtml @@ -133,7 +133,7 @@
- + diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index e012162..8236e94 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -3,9 +3,6 @@ # Configuration logging pour développement quarkus.log.category."dev.lions.unionflow".level=DEBUG -quarkus.log.category."jakarta.faces".level=INFO -quarkus.log.category."org.apache.myfaces".level=INFO -quarkus.log.category."org.primefaces".level=INFO # Hot reload quarkus.live-reload.instrumentation=true diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 968cd69..400a61d 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -30,7 +30,7 @@ jakarta.faces.PROJECT_STAGE - Development + Production @@ -159,15 +159,10 @@ /* - - - Character Encoding Filter - org.primefaces.freya.filter.CharacterEncodingFilter - - - Character Encoding Filter - Faces Servlet - +