Sync: code local unifié

Synchronisation du code source local (fait foi).

Signed-off-by: lions dev Team
This commit is contained in:
dahoud
2026-03-15 16:25:45 +00:00
parent 6b28cf751e
commit 0dc050f422
179 changed files with 3355 additions and 992 deletions

View File

@@ -0,0 +1,35 @@
package dev.lions.unionflow.client.api.dto;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* DTO received from the backend for the member dashboard synthesis.
*/
public record MembreDashboardResponse(
String prenom,
String nom,
LocalDate dateInscription,
// Cotisations
BigDecimal mesCotisationsPaiement,
String statutCotisations,
Integer tauxCotisationsPerso,
// Epargne
BigDecimal monSoldeEpargne,
BigDecimal evolutionEpargneNombre,
String evolutionEpargne,
Integer objectifEpargne,
// Evenements
Integer mesEvenementsInscrits,
Integer evenementsAVenir,
Integer tauxParticipationPerso,
// Aides
Integer mesDemandesAide,
Integer aidesEnCours,
Integer tauxAidesApprouvees) implements Serializable {
}

View File

@@ -130,12 +130,22 @@ public class MenuBean implements Serializable {
/**
* Annuaire des Membres - Consultation de la liste (pas de modification)
* Visible à partir de MEMBRE_ACTIF (pour créer du lien social)
* Visible pour les responsables et bureau SEULEMENT (PAS pour MEMBRE_ACTIF)
*
* Raison métier: Un membre simple n'a généralement pas besoin de voir la liste complète
* des autres membres. Cela peut poser des problèmes de:
* - RGPD: Exposition non justifiée de données personnelles
* - Sécurité: Risque de spam/phishing entre membres
* - UX: Surcharge du menu pour un usage limité
*
* Si l'organisation souhaite activer l'annuaire pour MEMBRE_ACTIF, cela doit être
* fait via configuration explicite (future Phase 3).
*/
public boolean isAnnuaireMembresVisible() {
return hasAnyRole("SUPER_ADMIN", "ADMIN_ORGANISATION", "SECRETAIRE", "TRESORIER",
"RESPONSABLE_SOCIAL", "RESPONSABLE_EVENEMENTS", "RESPONSABLE_CREDIT",
"MEMBRE_BUREAU", "MEMBRE_ACTIF");
"MEMBRE_BUREAU");
// MEMBRE_ACTIF retiré intentionnellement pour raisons UX et RGPD
}
/**

View File

@@ -0,0 +1,179 @@
package dev.lions.unionflow.client.bean;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.jboss.logging.Logger;
import java.io.IOException;
/**
* Bean centralisé pour la sécurisation des pages basée sur les rôles.
* Fournit des méthodes réutilisables pour vérifier l'accès et rediriger si nécessaire.
*
* <p>Principe DRY/WOU : Une seule implémentation de la logique de sécurité,
* réutilisée par toutes les pages via un composant Facelet.
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-02
*/
@Named("pageSecurityBean")
@ApplicationScoped
public class PageSecurityBean {
private static final Logger LOG = Logger.getLogger(PageSecurityBean.class);
private static final String ACCESS_DENIED_PAGE = "/pages/secure/access-denied.xhtml";
@Inject
SecurityIdentity securityIdentity;
@Inject
MenuBean menuBean;
/**
* Vérifie si l'utilisateur a le droit d'accéder à une page donnée.
* Si non autorisé, redirige vers la page access-denied.
*
* @param allowedRoles Rôles autorisés séparés par des virgules (ex: "ADMIN,TRESORIER")
* @return true si autorisé, false sinon (après redirection)
*/
public boolean checkAccessOrRedirect(String allowedRoles) {
if (allowedRoles == null || allowedRoles.trim().isEmpty()) {
// Aucune restriction = accès autorisé pour tous les utilisateurs authentifiés
return !securityIdentity.isAnonymous();
}
String[] roles = allowedRoles.split(",");
boolean hasAccess = false;
for (String role : roles) {
String trimmedRole = role.trim();
if (hasRole(trimmedRole)) {
hasAccess = true;
break;
}
}
if (!hasAccess) {
LOG.warnf("Accès refusé pour l'utilisateur %s à une page nécessitant les rôles: %s",
securityIdentity.getPrincipal().getName(), allowedRoles);
redirectToAccessDenied();
return false;
}
return true;
}
/**
* Vérifie si l'utilisateur possède un rôle spécifique.
*
* @param role Le rôle à vérifier
* @return true si l'utilisateur a ce rôle
*/
private boolean hasRole(String role) {
return switch (role) {
case "SUPER_ADMIN" -> menuBean.isSuperAdmin();
case "ADMIN_ORGANISATION", "ADMIN" -> menuBean.isAdminOrganisation() || menuBean.isSuperAdmin();
case "TRESORIER" -> menuBean.isTresorier() || menuBean.isAdminOrganisation() || menuBean.isSuperAdmin();
case "SECRETAIRE" -> menuBean.isSecretaire() || menuBean.isAdminOrganisation() || menuBean.isSuperAdmin();
case "RESPONSABLE_SOCIAL" -> menuBean.isResponsableSocial() || menuBean.isAdminOrganisation() || menuBean.isSuperAdmin();
case "RESPONSABLE_EVENEMENTS" -> menuBean.isResponsableEvenements() || menuBean.isAdminOrganisation() || menuBean.isSuperAdmin();
case "RESPONSABLE_CREDIT" -> menuBean.isResponsableCredit() || menuBean.isAdminOrganisation() || menuBean.isSuperAdmin();
case "MEMBRE_BUREAU" -> menuBean.isMembreBureau() || menuBean.isAdminOrganisation() || menuBean.isSuperAdmin();
case "MEMBRE_ACTIF" -> menuBean.isMembreActif() || menuBean.isMembreBureau() || menuBean.isAdminOrganisation() || menuBean.isSuperAdmin();
case "MEMBRE_SIMPLE" -> menuBean.isMembreSimple() || menuBean.isMembreActif() || menuBean.isMembreBureau() || menuBean.isAdminOrganisation() || menuBean.isSuperAdmin();
case "ALL" -> !securityIdentity.isAnonymous(); // Tous les utilisateurs authentifiés
default -> {
LOG.warnf("Rôle inconnu: %s", role);
yield false;
}
};
}
/**
* Redirige vers la page d'accès refusé.
*/
private void redirectToAccessDenied() {
try {
FacesContext ctx = FacesContext.getCurrentInstance();
if (ctx != null && !ctx.getResponseComplete()) {
String contextPath = ctx.getExternalContext().getRequestContextPath();
ctx.getExternalContext().redirect(contextPath + ACCESS_DENIED_PAGE);
ctx.responseComplete();
}
} catch (IOException e) {
LOG.error("Erreur lors de la redirection vers access-denied", e);
}
}
// ═══════════════════════════════════════════════════════════════════════
// Méthodes helper pour vérifications rapides (utilisées dans les pages)
// ═══════════════════════════════════════════════════════════════════════
/**
* Vérifie si l'utilisateur peut gérer les membres.
* @return true si SECRETAIRE, ADMIN, ou SUPER_ADMIN
*/
public boolean canManageMembers() {
return hasRole("SECRETAIRE");
}
/**
* Vérifie si l'utilisateur peut gérer les finances.
* @return true si TRESORIER, ADMIN, ou SUPER_ADMIN
*/
public boolean canManageFinances() {
return hasRole("TRESORIER");
}
/**
* Vérifie si l'utilisateur peut gérer les événements.
* @return true si RESPONSABLE_EVENEMENTS, SECRETAIRE, ADMIN, ou SUPER_ADMIN
*/
public boolean canManageEvents() {
return hasRole("RESPONSABLE_EVENEMENTS");
}
/**
* Vérifie si l'utilisateur peut gérer les aides sociales.
* @return true si RESPONSABLE_SOCIAL, ADMIN, ou SUPER_ADMIN
*/
public boolean canManageSocialAid() {
return hasRole("RESPONSABLE_SOCIAL");
}
/**
* Vérifie si l'utilisateur peut voir les rapports financiers.
* @return true si TRESORIER, SECRETAIRE, ADMIN, ou SUPER_ADMIN
*/
public boolean canViewFinancialReports() {
return hasRole("TRESORIER") || hasRole("SECRETAIRE");
}
/**
* Vérifie si l'utilisateur peut exporter des données.
* @return true si TRESORIER, SECRETAIRE, ADMIN, ou SUPER_ADMIN
*/
public boolean canExportData() {
return hasRole("TRESORIER") || hasRole("SECRETAIRE");
}
/**
* Vérifie si l'utilisateur est un simple membre (MEMBRE_ACTIF uniquement).
* @return true si MEMBRE_ACTIF mais pas d'autre rôle administratif
*/
public boolean isSimpleMember() {
return menuBean.isMembreActif() &&
!menuBean.isSecretaire() &&
!menuBean.isTresorier() &&
!menuBean.isResponsableSocial() &&
!menuBean.isResponsableEvenements() &&
!menuBean.isResponsableCredit() &&
!menuBean.isMembreBureau() &&
!menuBean.isAdminOrganisation() &&
!menuBean.isSuperAdmin();
}
}

View File

@@ -0,0 +1,37 @@
package dev.lions.unionflow.client.converter;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.convert.Converter;
import jakarta.faces.convert.ConverterException;
import jakarta.faces.convert.FacesConverter;
import java.util.UUID;
/**
* Convertisseur JSF pour les paramètres de vue et champs liés à {@link UUID}.
* Permet la conversion String ↔ UUID dans les f:viewParam et composants d'entrée.
*/
@FacesConverter(value = "uuidConverter", managed = true)
public class UuidConverter implements Converter<UUID> {
@Override
public UUID getAsObject(FacesContext context, UIComponent component, String value) {
if (value == null || value.isBlank()) {
return null;
}
try {
return UUID.fromString(value.trim());
} catch (IllegalArgumentException e) {
throw new ConverterException("Identifiant invalide : " + value, e);
}
}
@Override
public String getAsString(FacesContext context, UIComponent component, UUID value) {
if (value == null) {
return "";
}
return value.toString();
}
}

View File

@@ -1,5 +1,6 @@
package dev.lions.unionflow.client.exception;
import jakarta.el.PropertyNotFoundException;
import jakarta.faces.FacesException;
import jakarta.faces.application.ViewExpiredException;
import jakarta.faces.context.ExceptionHandler;
@@ -12,28 +13,57 @@ import java.util.logging.Level;
import java.util.logging.Logger;
public class ViewExpiredExceptionHandler extends ExceptionHandlerWrapper {
private static final Logger LOG = Logger.getLogger(ViewExpiredExceptionHandler.class.getName());
private ExceptionHandler wrapped;
public ViewExpiredExceptionHandler(ExceptionHandler wrapped) {
this.wrapped = wrapped;
}
@Override
public ExceptionHandler getWrapped() {
return wrapped;
}
private static boolean isPropertyNotFound(Throwable t) {
for (Throwable x = t; x != null; x = x.getCause()) {
if (x instanceof PropertyNotFoundException) return true;
}
return false;
}
@Override
public void handle() throws FacesException {
Iterator<ExceptionQueuedEvent> iterator = getUnhandledExceptionQueuedEvents().iterator();
while (iterator.hasNext()) {
ExceptionQueuedEvent event = iterator.next();
ExceptionQueuedEventContext context = (ExceptionQueuedEventContext) event.getSource();
Throwable throwable = context.getException();
if (isPropertyNotFound(throwable)) {
LOG.log(Level.WARNING, "PropertyNotFoundException EL (évite page d''erreur TreeMap): {0}",
throwable.getMessage());
try {
FacesContext fc = FacesContext.getCurrentInstance();
if (fc != null && fc.getExternalContext() != null && !fc.getResponseComplete()) {
fc.getExternalContext().redirect(
fc.getExternalContext().getRequestContextPath() + "/pages/secure/organisation/liste.xhtml");
fc.responseComplete();
}
} catch (Exception e) {
String msg = e != null ? e.getMessage() : "";
if (msg != null && (msg.contains("already commited") || msg.contains("already committed"))) {
LOG.log(Level.WARNING, "Redirection impossible (réponse déjà envoyée): {0}", msg);
} else {
LOG.log(Level.SEVERE, "Redirection après PropertyNotFoundException: {0}", msg);
}
}
iterator.remove();
continue;
}
if (throwable instanceof ViewExpiredException) {
ViewExpiredException vee = (ViewExpiredException) throwable;
FacesContext facesContext = FacesContext.getCurrentInstance();

View File

@@ -1,5 +1,6 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@@ -16,41 +17,17 @@ import java.util.UUID;
public interface AssociationService {
@GET
PagedResponseDTO<OrganisationResponse> listerToutes(
PagedResponse<OrganisationResponse> listerToutes(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("1000") int size);
class PagedResponseDTO<T> {
public List<T> data;
public Long total;
public Integer page;
public Integer size;
public Integer totalPages;
public List<T> getData() {
return data;
}
public void setData(List<T> data) {
this.data = data;
}
public Long getTotal() {
return total;
}
public void setTotal(Long total) {
this.total = total;
}
}
@GET
@Path("/{id}")
OrganisationResponse obtenirParId(@PathParam("id") UUID id);
@GET
@Path("/recherche")
PagedResponseDTO<OrganisationResponse> rechercher(
PagedResponse<OrganisationResponse> rechercher(
@QueryParam("nom") String nom,
@QueryParam("type") String type,
@QueryParam("statut") String statut,

View File

@@ -1,5 +1,6 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.evenement.response.EvenementResponse;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@@ -27,7 +28,7 @@ public interface EvenementService {
* Liste tous les événements actifs avec pagination
*/
@GET
Map<String, Object> listerTous(
PagedResponse<EvenementResponse> listerTous(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size,
@QueryParam("sort") @DefaultValue("dateDebut") String sortField,
@@ -66,17 +67,17 @@ public interface EvenementService {
*/
@GET
@Path("/a-venir")
Map<String, Object> listerAVenir(
PagedResponse<EvenementResponse> listerAVenir(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("10") int size
);
/**
* Recherche d'événements avec filtres
*/
@GET
@Path("/search")
Map<String, Object> rechercher(
PagedResponse<EvenementResponse> rechercher(
@QueryParam("titre") String titre,
@QueryParam("type") String type,
@QueryParam("statut") String statut,
@@ -85,24 +86,24 @@ public interface EvenementService {
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size
);
/**
* Liste les événements par statut
*/
@GET
@Path("/statut/{statut}")
Map<String, Object> listerParStatut(
PagedResponse<EvenementResponse> listerParStatut(
@PathParam("statut") String statut,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size
);
/**
* Liste les événements par association
*/
@GET
@Path("/association/{associationId}")
Map<String, Object> listerParAssociation(
PagedResponse<EvenementResponse> listerParAssociation(
@PathParam("associationId") UUID associationId,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size

View File

@@ -0,0 +1,23 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.client.api.dto.MembreDashboardResponse;
import dev.lions.unionflow.client.security.AuthHeaderFactory;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(AuthHeaderFactory.class)
@Path("/api/dashboard/membre")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface MembreDashboardRestClient {
@GET
@Path("/me")
MembreDashboardResponse getMonDashboard();
}

View File

@@ -1,5 +1,6 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.membre.response.MembreResponse;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@@ -16,7 +17,7 @@ import java.util.UUID;
public interface MembreService {
@GET
List<MembreResponse> listerTous();
PagedResponse<MembreResponse> listerTous();
@GET
@Path("/{id}")
@@ -26,6 +27,10 @@ public interface MembreService {
@Path("/numero/{numeroMembre}")
MembreResponse obtenirParNumero(@PathParam("numeroMembre") String numeroMembre);
@GET
@Path("/me")
MembreResponse obtenirMembreConnecte();
@GET
@Path("/search")
List<MembreResponse> rechercher(

View File

@@ -13,9 +13,13 @@ import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
import dev.lions.unionflow.server.api.dto.membre.response.MembreResponse;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.finance.request.*;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.finance.response.*;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.client.service.AdhesionService;
import dev.lions.unionflow.client.service.AssociationService;
import dev.lions.unionflow.client.service.ErrorHandlerService;
@@ -109,7 +113,7 @@ public class AdhesionsBean implements Serializable {
}
try {
AssociationService.PagedResponseDTO<OrganisationResponse> response = retryService.executeWithRetrySupplier(
PagedResponse<OrganisationResponse> response = retryService.executeWithRetrySupplier(
() -> associationService.listerToutes(0, 1000),
"chargement des associations");
listeAssociations = (response != null && response.getData() != null) ? response.getData()

View File

@@ -1,10 +1,15 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.server.api.dto.cotisation.request.CreateCotisationRequest;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationResponse;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.membre.response.MembreResponse;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.paiement.WaveCheckoutSessionDTO;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.client.service.CotisationService;
import dev.lions.unionflow.client.service.AssociationService;
import dev.lions.unionflow.client.service.MembreService;
@@ -304,7 +309,7 @@ public class CotisationsGestionBean implements Serializable {
this.filtres = new FiltresCotisations();
this.listeOrganisations = new ArrayList<>();
try {
AssociationService.PagedResponseDTO<OrganisationResponse> response = associationService.listerToutes(0, 1000);
PagedResponse<OrganisationResponse> response = associationService.listerToutes(0, 1000);
if (response != null && response.getData() != null) {
for (OrganisationResponse assoc : response.getData()) {
Organisation org = new Organisation();
@@ -340,7 +345,7 @@ public class CotisationsGestionBean implements Serializable {
private void chargerTopOrganisations() {
this.topOrganisations = new ArrayList<>();
try {
AssociationService.PagedResponseDTO<OrganisationResponse> response = associationService.listerToutes(0, 1000);
PagedResponse<OrganisationResponse> response = associationService.listerToutes(0, 1000);
List<OrganisationResponse> associations = response != null && response.getData() != null ? response.getData()
: new ArrayList<>();
List<CotisationResponse> cotisationsDTO = cotisationService.listerToutes(0, 1000);

View File

@@ -8,10 +8,12 @@ import dev.lions.unionflow.server.api.dto.dashboard.DashboardStatsResponse;
import dev.lions.unionflow.server.api.dto.dashboard.RecentActivityResponse;
import dev.lions.unionflow.server.api.dto.dashboard.UpcomingEventResponse;
import jakarta.annotation.PostConstruct;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import java.io.IOException;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
@@ -51,10 +53,13 @@ public class DashboardBean implements Serializable {
@Inject
private UserSession userSession;
@Inject
private dev.lions.unionflow.client.bean.MenuBean menuBean;
@Inject
ErrorHandlerService errorHandler;
@Inject
RetryService retryService;
@@ -134,8 +139,30 @@ public class DashboardBean implements Serializable {
@PostConstruct
public void init() {
// Charger les données pour les rôles administratifs
chargerDonneesBackend();
}
/**
* Méthode appelée par f:viewAction pour rediriger les MEMBRE_ACTIF.
* S'exécute AVANT le rendu de la page (phase INVOKE_APPLICATION).
*/
public void checkAccessAndRedirect() {
if (menuBean != null && menuBean.isMembreActif() &&
!menuBean.isSecretaire() && !menuBean.isTresorier() &&
!menuBean.isResponsableSocial() && !menuBean.isResponsableEvenements() &&
!menuBean.isAdminOrganisation() && !menuBean.isSuperAdmin()) {
try {
FacesContext ctx = FacesContext.getCurrentInstance();
String redirectUrl = ctx.getExternalContext().getRequestContextPath() +
"/pages/secure/dashboard-membre.xhtml?faces-redirect=true";
ctx.getExternalContext().redirect(redirectUrl);
ctx.responseComplete();
} catch (IOException e) {
LOG.error("Erreur lors de la redirection vers dashboard-membre", e);
}
}
}
/**
* Charge toutes les données depuis le service Dashboard (DRY/WOU - un seul appel)

View File

@@ -0,0 +1,386 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.server.api.dto.dashboard.UpcomingEventResponse;
import dev.lions.unionflow.server.api.dto.evenement.response.EvenementResponse;
import dev.lions.unionflow.client.api.dto.MembreDashboardResponse;
import dev.lions.unionflow.client.service.MembreDashboardRestClient;
import dev.lions.unionflow.client.service.ErrorHandlerService;
import dev.lions.unionflow.client.service.RetryService;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.jboss.logging.Logger;
import java.io.IOException;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* Bean de gestion du dashboard personnel pour les membres actifs
* (MEMBRE_ACTIF).
* Affiche uniquement les données personnelles du membre connecté, pas les
* statistiques globales.
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-02
*/
@Named("dashboardMembreBean")
@ViewScoped
public class DashboardMembreBean implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger LOG = Logger.getLogger(DashboardMembreBean.class);
@Inject
SecurityIdentity securityIdentity;
@Inject
ErrorHandlerService errorHandler;
@Inject
RetryService retryService;
@Inject
@RestClient
MembreDashboardRestClient dashboardClient;
// Informations personnelles du membre
private String prenomMembre;
private String nomMembre;
private LocalDate dateInscription;
// KPI personnels - TOUTES LES VALEURS DOIVENT ÊTRE CALCULÉES DEPUIS LES DONNÉES
// RÉELLES
// IMPORTANT: Ces valeurs par défaut (0, "", null) sont TEMPORAIRES en attendant
// l'implémentation des endpoints REST
// Une fois les endpoints implémentés, ces valeurs seront REMPLACÉES par les
// données calculées depuis PostgreSQL
// Cotisations
private String mesCotisationsPaiement = "0"; // TEMPORAIRE - Sera remplacé par le montant réel depuis API
private String statutCotisations = "Non disponible"; // TEMPORAIRE - Sera remplacé par "À jour"/"En retard" depuis
// API
private Integer tauxCotisationsPerso = null; // null = pas de jauge affichée en attendant les données réelles
// Épargne
private String monSoldeEpargne = "0"; // TEMPORAIRE - Sera remplacé par le solde réel depuis API
private String evolutionEpargne = "+0%"; // TEMPORAIRE - Sera remplacé par l'évolution réelle depuis API
private String evolutionEpargneNombre = "0"; // TEMPORAIRE - Sera remplacé par l'évolution en FCFA depuis API
private Integer objectifEpargne = null; // null = pas de jauge affichée en attendant les données réelles
// Événements
private Integer mesEvenementsInscrits = 0; // TEMPORAIRE - Sera remplacé par le nombre réel depuis API
private Integer evenementsAVenir = 0; // TEMPORAIRE - Sera remplacé par le nombre réel depuis API
private Integer tauxParticipationPerso = null; // null = pas de jauge affichée en attendant les données réelles
// Aides
private Integer mesDemandesAide = 0; // TEMPORAIRE - Sera remplacé par le nombre réel depuis API
private Integer aidesEnCours = 0; // TEMPORAIRE - Sera remplacé par le nombre réel depuis API
private Integer tauxAidesApprouvees = null; // null = pas de jauge affichée en attendant les données réelles
// Collections
private List<CotisationPerso> historiqueCotisations = new ArrayList<>();
private List<NotificationPerso> mesNotifications = new ArrayList<>();
private List<UpcomingEventResponse> evenementsPublics = new ArrayList<>();
@PostConstruct
public void init() {
LOG.info("Initialisation du dashboard personnel membre");
chargerDonneesPersonnelles();
}
/**
* Charge les données personnelles du membre connecté depuis les endpoints REST.
* Les données de synthèse sont récupérées via l'API membre/me.
*/
private void chargerDonneesPersonnelles() {
try {
LOG.info("Chargement des données du dashboard depuis l'API...");
MembreDashboardResponse data = dashboardClient.getMonDashboard();
if (data != null) {
this.prenomMembre = data.prenom();
this.nomMembre = data.nom();
this.dateInscription = data.dateInscription();
this.mesCotisationsPaiement = formatMontant(data.mesCotisationsPaiement());
this.statutCotisations = data.statutCotisations() != null ? data.statutCotisations() : "Non disponible";
this.tauxCotisationsPerso = data.tauxCotisationsPerso();
this.monSoldeEpargne = formatMontant(data.monSoldeEpargne());
this.evolutionEpargneNombre = formatMontant(data.evolutionEpargneNombre());
this.evolutionEpargne = data.evolutionEpargne() != null ? data.evolutionEpargne() : "+0%";
this.objectifEpargne = data.objectifEpargne();
this.mesEvenementsInscrits = data.mesEvenementsInscrits() != null ? data.mesEvenementsInscrits() : 0;
this.evenementsAVenir = data.evenementsAVenir() != null ? data.evenementsAVenir() : 0;
this.tauxParticipationPerso = data.tauxParticipationPerso();
this.mesDemandesAide = data.mesDemandesAide() != null ? data.mesDemandesAide() : 0;
this.aidesEnCours = data.aidesEnCours() != null ? data.aidesEnCours() : 0;
this.tauxAidesApprouvees = data.tauxAidesApprouvees();
}
// Pour l'historique et événements, on mock en attendant les endpoints détaillés
// si nécessaires
// ou on laissera vide vu que le dashboard principal est fonctionnel avec les
// KPI
historiqueCotisations = new ArrayList<>();
mesNotifications = new ArrayList<>();
evenementsPublics = new ArrayList<>();
} catch (Exception e) {
LOG.error("Erreur lors du chargement des données de synthèse du dashboard", e);
errorHandler.handleException(e, "lors du chargement de votre dashboard", null);
}
}
private String formatMontant(BigDecimal montant) {
if (montant == null)
return "0";
// Format simple, on pourrait rajouter des espaces pour les milliers
return String.format("%,d", montant.longValue()).replace(',', ' ');
}
// ═══════════════════════════════════════════════════════════════════════
// Actions
// ═══════════════════════════════════════════════════════════════════════
public void payerCotisation() {
try {
// TODO: Rediriger vers la page de paiement des cotisations
LOG.info("Redirection vers paiement cotisation");
} catch (Exception e) {
errorHandler.handleException(e, "lors de la redirection", null);
}
}
public void inscrireEvenement() {
try {
// TODO: Rediriger vers /pages/secure/evenement/calendrier.xhtml
// Liste des événements publics où le membre peut s'inscrire
LOG.info("Redirection vers calendrier des événements disponibles");
} catch (Exception e) {
errorHandler.handleException(e, "lors de la redirection", null);
}
}
public void demanderAide() {
try {
// TODO: Rediriger vers le formulaire de demande d'aide
LOG.info("Redirection vers demande d'aide");
} catch (Exception e) {
errorHandler.handleException(e, "lors de la redirection", null);
}
}
public void allerAMonProfil() {
try {
// TODO: Rediriger vers le profil personnel
LOG.info("Redirection vers mon profil");
} catch (Exception e) {
errorHandler.handleException(e, "lors de la redirection", null);
}
}
public void allerAuxEvenements() {
try {
// TODO: Rediriger vers la liste complète des événements
LOG.info("Redirection vers liste événements");
} catch (Exception e) {
errorHandler.handleException(e, "lors de la redirection", null);
}
}
public void inscrireAEvenement(String evenementId) {
try {
// TODO: Appeler API pour inscrire le membre à l'événement
LOG.infof("Inscription à l'événement %s", evenementId);
errorHandler.showSuccess("Inscription confirmée", "Vous êtes inscrit à cet événement");
} catch (Exception e) {
errorHandler.handleException(e, "lors de l'inscription à l'événement", null);
}
}
// ═══════════════════════════════════════════════════════════════════════
// Helpers
// ═══════════════════════════════════════════════════════════════════════
private String extractPrenomFromUsername(String username) {
// Extraction basique depuis le username en attendant l'API
if (username != null && username.contains("@")) {
return username.split("@")[0];
}
return username != null ? username : "Membre";
}
private String extractNomFromUsername(String username) {
// TODO: Appeler GET /api/membres/mon-profil pour récupérer le nom complet
return "";
}
// ═══════════════════════════════════════════════════════════════════════
// DTOs internes
// ═══════════════════════════════════════════════════════════════════════
public static class CotisationPerso implements Serializable {
private static final long serialVersionUID = 1L;
private LocalDate datePaiement;
private LocalDate periode;
private String montant;
private String modePaiement;
private String statut;
public CotisationPerso(LocalDate datePaiement, LocalDate periode, String montant,
String modePaiement, String statut) {
this.datePaiement = datePaiement;
this.periode = periode;
this.montant = montant;
this.modePaiement = modePaiement;
this.statut = statut;
}
// Getters
public LocalDate getDatePaiement() {
return datePaiement;
}
public LocalDate getPeriode() {
return periode;
}
public String getMontant() {
return montant;
}
public String getModePaiement() {
return modePaiement;
}
public String getStatut() {
return statut;
}
}
public static class NotificationPerso implements Serializable {
private static final long serialVersionUID = 1L;
private String icon;
private String titre;
private String message;
private LocalDateTime date;
public NotificationPerso(String icon, String titre, String message, LocalDateTime date) {
this.icon = icon;
this.titre = titre;
this.message = message;
this.date = date;
}
// Getters
public String getIcon() {
return icon;
}
public String getTitre() {
return titre;
}
public String getMessage() {
return message;
}
public LocalDateTime getDate() {
return date;
}
}
// ═══════════════════════════════════════════════════════════════════════
// Getters pour JSF
// ═══════════════════════════════════════════════════════════════════════
public String getPrenomMembre() {
return prenomMembre;
}
public String getNomMembre() {
return nomMembre;
}
public LocalDate getDateInscription() {
return dateInscription;
}
public String getMesCotisationsPaiement() {
return mesCotisationsPaiement;
}
public String getStatutCotisations() {
return statutCotisations;
}
public Integer getTauxCotisationsPerso() {
return tauxCotisationsPerso;
}
public String getMonSoldeEpargne() {
return monSoldeEpargne;
}
public String getEvolutionEpargne() {
return evolutionEpargne;
}
public String getEvolutionEpargneNombre() {
return evolutionEpargneNombre;
}
public Integer getObjectifEpargne() {
return objectifEpargne;
}
public Integer getMesEvenementsInscrits() {
return mesEvenementsInscrits;
}
public Integer getEvenementsAVenir() {
return evenementsAVenir;
}
public Integer getTauxParticipationPerso() {
return tauxParticipationPerso;
}
public Integer getMesDemandesAide() {
return mesDemandesAide;
}
public Integer getAidesEnCours() {
return aidesEnCours;
}
public Integer getTauxAidesApprouvees() {
return tauxAidesApprouvees;
}
public List<CotisationPerso> getHistoriqueCotisations() {
return historiqueCotisations;
}
public List<NotificationPerso> getMesNotifications() {
return mesNotifications;
}
public List<UpcomingEventResponse> getEvenementsPublics() {
return evenementsPublics;
}
}

View File

@@ -109,7 +109,7 @@ public class DemandesAideBean implements Serializable {
try {
// Charger toutes les demandes depuis le backend pour calculer les étapes
List<DemandeAideResponse> demandesDTO = demandeAideService.listerToutes(0, 10000);
List<DemandeAideResponse> demandesDTO = demandeAideService.listerToutes(0, 1000);
// Calculer le nombre de demandes par statut depuis les données réelles
long enAttenteCount = demandesDTO.stream().filter(d -> StatutAide.EN_ATTENTE.equals(d.getStatut())).count();

View File

@@ -65,7 +65,7 @@ public class DemandesBean implements Serializable {
private void initializeDemandes() {
demandes = new ArrayList<>();
try {
List<DemandeAideResponse> dtos = demandeAideService.listerToutes(0, 10000);
List<DemandeAideResponse> dtos = demandeAideService.listerToutes(0, 1000);
if (dtos != null) {
for (DemandeAideResponse dto : dtos) {
demandes.add(mapToDemande(dto));

View File

@@ -1,7 +1,9 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.abonnement.response.AbonnementResponse;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.enums.abonnement.StatutAbonnement;
import dev.lions.unionflow.client.service.AssociationService;
import dev.lions.unionflow.client.service.CotisationService;
@@ -105,7 +107,7 @@ public class EntitesGestionBean implements Serializable {
private void initializeStatistiques() {
statistiques = new Statistiques();
try {
AssociationService.PagedResponseDTO<OrganisationResponse> response = associationService.listerToutes(0, 1000);
PagedResponse<OrganisationResponse> response = associationService.listerToutes(0, 1000);
List<OrganisationResponse> associations = new ArrayList<>();
if (response != null && response.getData() != null) {
associations = response.getData();
@@ -138,7 +140,7 @@ public class EntitesGestionBean implements Serializable {
private void initializeEntites() {
toutesLesEntites = new ArrayList<>();
try {
AssociationService.PagedResponseDTO<OrganisationResponse> response = associationService.listerToutes(0, 1000);
PagedResponse<OrganisationResponse> response = associationService.listerToutes(0, 1000);
if (response != null && response.getData() != null) {
for (OrganisationResponse dto : response.getData()) {
Entite entite = convertToEntite(dto);

View File

@@ -114,44 +114,16 @@ public class EvenementsBean implements Serializable {
public void chargerEvenements() {
try {
LOG.info("Chargement des événements depuis le backend");
Map<String, Object> response = retryService.executeWithRetrySupplier(
var response = retryService.executeWithRetrySupplier(
() -> evenementService.listerTous(0, 1000, "dateDebut", "asc"),
"chargement de tous les événements"
);
tousLesEvenements = new ArrayList<>();
// Le backend peut retourner soit une liste de DTOs, soit une Map avec "data"
if (response.containsKey("data")) {
@SuppressWarnings("unchecked")
List<Object> data = (List<Object>) response.get("data");
if (data != null) {
for (Object item : data) {
if (item instanceof EvenementResponse) {
tousLesEvenements.add((EvenementResponse) item);
} else if (item instanceof Map) {
@SuppressWarnings("unchecked")
EvenementResponse dto = convertMapToDTO((Map<String, Object>) item);
tousLesEvenements.add(dto);
}
}
}
} else {
// Si la réponse est directement une liste
@SuppressWarnings("unchecked")
List<Object> data = (List<Object>) response.get("evenements");
if (data != null) {
for (Object item : data) {
if (item instanceof EvenementResponse) {
tousLesEvenements.add((EvenementResponse) item);
} else if (item instanceof Map) {
@SuppressWarnings("unchecked")
EvenementResponse dto = convertMapToDTO((Map<String, Object>) item);
tousLesEvenements.add(dto);
}
}
}
// Récupérer les données depuis PagedResponse
if (response != null && response.getData() != null) {
tousLesEvenements.addAll(response.getData());
}
appliquerFiltres();
@@ -170,22 +142,17 @@ public class EvenementsBean implements Serializable {
public void chargerEvenementsProchains() {
try {
LOG.info("Chargement des événements à venir");
Map<String, Object> response = retryService.executeWithRetrySupplier(
var response = retryService.executeWithRetrySupplier(
() -> evenementService.listerAVenir(0, 6),
"chargement des événements à venir"
);
@SuppressWarnings("unchecked")
List<Map<String, Object>> data = (List<Map<String, Object>>) response.get("data");
if (data != null) {
evenementsProchains = data.stream()
.map(this::convertMapToDTO)
.collect(Collectors.toList());
if (response != null && response.getData() != null) {
evenementsProchains = new ArrayList<>(response.getData());
} else {
evenementsProchains = new ArrayList<>();
}
} catch (Exception e) {
LOG.errorf(e, "Erreur lors du chargement des événements à venir");
evenementsProchains = new ArrayList<>();

View File

@@ -45,20 +45,23 @@ public class MembreCotisationBean implements Serializable {
@Inject
@RestClient
private MembreService membreService;
@Inject
@RestClient
private CotisationService cotisationService;
@Inject
@RestClient
private ExportClientService exportService;
@Inject
ErrorHandlerService errorHandler;
@Inject
RetryService retryService;
@Inject
io.quarkus.security.identity.SecurityIdentity securityIdentity;
// ID du membre (depuis viewParam)
private UUID membreId;
@@ -135,14 +138,43 @@ public class MembreCotisationBean implements Serializable {
}
}
}
// Si toujours null, auto-détecter le membre connecté (Pattern DRY depuis DashboardMembreBean)
if (membreId == null) {
try {
String username = securityIdentity.getPrincipal().getName();
LOG.infof("Auto-détection du membre connecté: %s", username);
// Récupérer directement le membre connecté via l'endpoint /me
MembreResponse membreConnecte = retryService.executeWithRetrySupplier(
() -> membreService.obtenirMembreConnecte(),
"récupération du membre connecté"
);
if (membreConnecte != null) {
membreId = membreConnecte.getId();
numeroMembre = membreConnecte.getNumeroMembre();
LOG.infof("Membre connecté détecté: %s (%s)", numeroMembre, membreId);
} else {
LOG.warnf("Aucun membre trouvé pour l'utilisateur: %s", username);
errorHandler.showWarning("Attention",
"Impossible de charger vos cotisations. Veuillez contacter l'administrateur.");
initialiserDonneesVides();
return;
}
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'auto-détection du membre connecté");
errorHandler.handleException(e, "lors du chargement de vos cotisations", null);
initialiserDonneesVides();
return;
}
}
if (membreId != null) {
chargerMembre();
chargerCotisations();
calculerStatistiques();
} else {
LOG.warn("Aucun membreId fourni, impossible de charger les cotisations");
errorHandler.showWarning("Attention", "Aucun membre sélectionné");
initialiserDonneesVides();
}
}

View File

@@ -5,8 +5,11 @@ import dev.lions.unionflow.client.service.AssociationService;
import dev.lions.unionflow.client.service.ErrorHandlerService;
import dev.lions.unionflow.client.service.RetryService;
import dev.lions.unionflow.server.api.dto.organisation.request.*;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.organisation.response.*;
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 lombok.Getter;
import lombok.Setter;
import jakarta.faces.view.ViewScoped;
@@ -88,7 +91,7 @@ public class MembreExportBean implements Serializable {
private void chargerOrganisations() {
organisationsDisponibles = new ArrayList<>();
try {
AssociationService.PagedResponseDTO<OrganisationResponse> response = retryService.executeWithRetrySupplier(
PagedResponse<OrganisationResponse> response = retryService.executeWithRetrySupplier(
() -> associationService.listerToutes(0, 1000),
"chargement des organisations pour export");
List<OrganisationResponse> associations = (response != null && response.getData() != null) ? response.getData()

View File

@@ -6,8 +6,11 @@ import dev.lions.unionflow.client.service.AssociationService;
import dev.lions.unionflow.client.service.ErrorHandlerService;
import dev.lions.unionflow.client.service.RetryService;
import dev.lions.unionflow.server.api.dto.organisation.request.*;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.organisation.response.*;
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 lombok.Getter;
import lombok.Setter;
import jakarta.faces.view.ViewScoped;
@@ -71,7 +74,7 @@ public class MembreImportBean implements Serializable {
private void chargerOrganisations() {
organisationsDisponibles = new ArrayList<>();
try {
AssociationService.PagedResponseDTO<OrganisationResponse> response = retryService.executeWithRetrySupplier(
PagedResponse<OrganisationResponse> response = retryService.executeWithRetrySupplier(
() -> associationService.listerToutes(0, 1000),
"chargement des organisations pour import");
List<OrganisationResponse> associations = (response != null && response.getData() != null) ? response.getData()

View File

@@ -1,7 +1,9 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.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.ErrorHandlerService;
@@ -120,7 +122,7 @@ public class MembreInscriptionBean implements Serializable {
// Charger les organisations actives (DRY/WOU - utilise AssociationService)
try {
AssociationService.PagedResponseDTO<OrganisationResponse> response = retryService.executeWithRetrySupplier(
PagedResponse<OrganisationResponse> response = retryService.executeWithRetrySupplier(
() -> associationService.listerToutes(0, 1000),
"chargement des associations");
organisationsDisponibles = (response != null && response.getData() != null) ? response.getData()

View File

@@ -53,7 +53,7 @@ public class MembreLazyDataModel extends LazyDataModelBase<MembreSummaryResponse
// Sans critère, l'endpoint /search/advanced refuse la requête (HTTP 400)
if (!searchCriteria.hasAnyCriteria()) {
List<MembreResponse> all = membreService.listerTous();
List<MembreResponse> all = membreService.listerTous().getData();
if (all == null || all.isEmpty()) return List.of();
int toIndex = Math.min(first + pageSize, all.size());
// Conversion MembreResponse → MembreSummaryResponse
@@ -71,7 +71,9 @@ public class MembreLazyDataModel extends LazyDataModelBase<MembreSummaryResponse
m.getStatutCompteLibelle(),
m.getStatutCompteSeverity(),
"ACTIF".equals(m.getStatutCompte()), // actif
m.getRoles()))
m.getRoles(),
m.getOrganisationId(),
m.getAssociationNom()))
.collect(java.util.stream.Collectors.toList());
}
@@ -106,7 +108,7 @@ public class MembreLazyDataModel extends LazyDataModelBase<MembreSummaryResponse
// Sans critère, compter via listerTous()
if (!searchCriteria.hasAnyCriteria()) {
List<MembreResponse> all = membreService.listerTous();
List<MembreResponse> all = membreService.listerTous().getData();
return all != null ? all.size() : 0;
}

View File

@@ -1,6 +1,7 @@
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.NotificationService;
@@ -8,7 +9,9 @@ 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 lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
@@ -108,6 +111,8 @@ public class MembreListeBean implements Serializable {
// === Contact membre ===
private MembreResponse membreAContacter;
/** Membre en attente de confirmation de suspension (flux explicite). */
private MembreResponse membrePourSuspension;
private String messageContact;
private String sujetContact;
private boolean dialogContactVisible = false;
@@ -198,7 +203,7 @@ public class MembreListeBean implements Serializable {
private void chargerOrganisations() {
organisationsDisponibles = new ArrayList<>();
try {
AssociationService.PagedResponseDTO<OrganisationResponse> response = retryService.executeWithRetrySupplier(
PagedResponse<OrganisationResponse> response = retryService.executeWithRetrySupplier(
() -> associationService.listerToutes(0, 1000),
"chargement des organisations");
if (response != null && response.getData() != null) {
@@ -396,6 +401,25 @@ public class MembreListeBean implements Serializable {
// ACTIONS SUR UN MEMBRE
// ========================================================================
/**
* Prépare la suspension (ouvre le dialogue de confirmation).
*/
public void preparerSuspendre(MembreResponse membre) {
this.membrePourSuspension = membre;
}
/**
* Confirme et exécute la suspension après clic sur « Oui » dans le dialogue.
*/
public void confirmerSuspendre() {
if (membrePourSuspension == null) {
return;
}
MembreResponse m = membrePourSuspension;
membrePourSuspension = null;
suspendreMembre(m);
}
public void suspendreMembre(MembreResponse membre) {
try {
retryService.executeWithRetrySupplier(

View File

@@ -1,6 +1,7 @@
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 jakarta.enterprise.context.SessionScoped;
@@ -67,7 +68,7 @@ public class MembreRechercheBean implements Serializable {
private void initializeStatistiques() {
statistiques = new Statistiques();
try {
List<MembreResponse> membres = membreService.listerTous();
List<MembreResponse> membres = membreService.listerTous().getData();
statistiques.setTotalMembres(membres.size());
} catch (Exception e) {
LOG.errorf(e, "Erreur lors du calcul des statistiques");
@@ -83,7 +84,7 @@ public class MembreRechercheBean implements Serializable {
selectedMembres = new ArrayList<>();
try {
List<MembreResponse> membresDTO = membreService.listerTous();
List<MembreResponse> membresDTO = membreService.listerTous().getData();
for (MembreResponse dto : membresDTO) {
Membre membre = convertToMembre(dto);
tousLesMembres.add(membre);
@@ -118,7 +119,7 @@ public class MembreRechercheBean implements Serializable {
private void initializeEntites() {
entitesDisponibles = new ArrayList<>();
try {
AssociationService.PagedResponseDTO<dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse> response = associationService
PagedResponse<dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse> response = associationService
.listerToutes(0, 1000);
if (response != null && response.getData() != null) {
for (dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse assoc : response.getData()) {

View File

@@ -0,0 +1,574 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationResponse;
import dev.lions.unionflow.server.api.dto.membre.response.MembreResponse;
import dev.lions.unionflow.server.api.dto.paiement.response.PaiementResponse;
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.RetryService;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.annotation.PostConstruct;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
import java.io.OutputStream;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Bean pour le paiement des cotisations du membre connecté (MEMBRE_ACTIF).
* Affiche uniquement les cotisations personnelles, pas les données admin.
*
* Pattern DRY: Réutilise la logique de MembreCotisationBean et DashboardMembreBean
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-02
*/
@Named("mesCotisationsPaiementBean")
@ViewScoped
public class MesCotisationsPaiementBean implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger LOG = Logger.getLogger(MesCotisationsPaiementBean.class);
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy");
@Inject
SecurityIdentity securityIdentity;
@Inject
@RestClient
private MembreService membreService;
@Inject
@RestClient
private CotisationService cotisationService;
@Inject
@RestClient
private ExportClientService exportService;
@Inject
ErrorHandlerService errorHandler;
@Inject
RetryService retryService;
// Informations du membre connecté
private UUID membreId;
private String numeroMembre;
private MembreResponse membre;
// KPI personnels - Cotisations à payer
private Integer cotisationsEnAttente = 0;
private String montantDu = "0 FCFA";
private String prochaineEcheance = "-";
private String totalPaye = "0 FCFA";
private Integer anneeEnCours = LocalDate.now().getYear();
// Listes
private List<CotisationPerso> mesCotisationsEnAttente = new ArrayList<>();
private List<PaiementPerso> derniersPaiements = new ArrayList<>();
// Formulaires dialogs
// Dialog Paiement en Ligne
private UUID cotisationSelectionneeId;
private String methodePaiementChoisie = "WAVE";
private String numeroTelephone;
// Dialog Paiement Manuel
private String methodePaiementManuel = "ESPECES";
private String referencePaiementManuel;
private String commentairePaiement;
// Configuration organisation (TODO: charger depuis API)
private boolean paiementManuelActive = true;
@PostConstruct
public void init() {
LOG.info("Initialisation du bean de paiement des cotisations personnelles");
detecterMembreConnecte();
if (membreId != null) {
chargerDonnees();
}
}
/**
* Auto-détection du membre connecté (Pattern DRY depuis DashboardMembreBean)
*/
private void detecterMembreConnecte() {
try {
String username = securityIdentity.getPrincipal().getName();
LOG.infof("Auto-détection du membre connecté: %s", username);
// Récupérer directement le membre connecté via l'endpoint /me
membre = retryService.executeWithRetrySupplier(
() -> membreService.obtenirMembreConnecte(),
"récupération du membre connecté"
);
if (membre != null) {
membreId = membre.getId();
numeroMembre = membre.getNumeroMembre();
LOG.infof("Membre connecté détecté: %s (%s)", numeroMembre, membreId);
} else {
LOG.warnf("Aucun membre trouvé pour l'utilisateur: %s", username);
errorHandler.showWarning("Attention",
"Impossible de charger vos cotisations. Veuillez contacter l'administrateur.");
}
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'auto-détection du membre connecté");
errorHandler.handleException(e, "lors du chargement de vos cotisations", null);
}
}
/**
* Charge toutes les données personnelles de cotisations et paiements
*/
private void chargerDonnees() {
chargerCotisationsEnAttente();
chargerDerniersPaiements();
calculerKPI();
}
/**
* Charge les cotisations en attente du membre connecté
* Pattern DRY: Réutilise la logique de MembreCotisationBean.chargerCotisations()
*/
private void chargerCotisationsEnAttente() {
try {
// TODO: Créer endpoint GET /api/cotisations/mes-cotisations/en-attente
// Pour l'instant, utiliser l'endpoint existant avec filtre statut
List<CotisationResponse> cotisationsDTO = retryService.executeWithRetrySupplier(
() -> cotisationService.rechercher(
membreId,
"EN_ATTENTE", // Statut
null, // Type
anneeEnCours,
null, // Mois
0,
100
),
"chargement des cotisations en attente"
);
mesCotisationsEnAttente = new ArrayList<>();
for (CotisationResponse dto : cotisationsDTO) {
CotisationPerso cotisation = new CotisationPerso();
cotisation.setId(dto.getId());
cotisation.setReference(dto.getNumeroReference() != null ? dto.getNumeroReference() : "");
cotisation.setType(dto.getTypeCotisation() != null ? dto.getTypeCotisation() : "MENSUELLE");
cotisation.setPeriode(formaterPeriode(dto.getDateEcheance()));
cotisation.setMontantDu(dto.getMontantDu() != null ? dto.getMontantDu() : BigDecimal.ZERO);
cotisation.setDateEcheance(dto.getDateEcheance());
mesCotisationsEnAttente.add(cotisation);
}
LOG.infof("Cotisations en attente chargées: %d", mesCotisationsEnAttente.size());
} catch (Exception e) {
LOG.errorf(e, "Erreur lors du chargement des cotisations en attente");
errorHandler.handleException(e, "lors du chargement de vos cotisations en attente", null);
mesCotisationsEnAttente = new ArrayList<>();
}
}
/**
* Charge les 5 derniers paiements du membre connecté
*/
private void chargerDerniersPaiements() {
try {
// TODO: Créer endpoint GET /api/paiements/mes-paiements/historique?limit=5
// Pour l'instant, charger toutes les cotisations payées et prendre les 5 dernières
List<CotisationResponse> cotisationsPayees = retryService.executeWithRetrySupplier(
() -> cotisationService.rechercher(
membreId,
"PAYEE", // Statut
null, // Type
null, // Année
null, // Mois
0,
5
),
"chargement de l'historique des paiements"
);
derniersPaiements = new ArrayList<>();
for (CotisationResponse dto : cotisationsPayees) {
PaiementPerso paiement = new PaiementPerso();
paiement.setId(dto.getId());
paiement.setReference(dto.getNumeroReference() != null ? dto.getNumeroReference() : "");
paiement.setPeriode(formaterPeriode(dto.getDateEcheance()));
paiement.setMontant(dto.getMontantDu() != null ? dto.getMontantDu() : BigDecimal.ZERO);
if (dto.getDatePaiement() != null) {
paiement.setDatePaiement(dto.getDatePaiement().toLocalDate());
}
// Note: methodePaiement non disponible dans CotisationResponse
paiement.setMethodePaiement("Non renseignée");
derniersPaiements.add(paiement);
}
LOG.infof("Derniers paiements chargés: %d", derniersPaiements.size());
} catch (Exception e) {
LOG.errorf(e, "Erreur lors du chargement des derniers paiements");
errorHandler.handleException(e, "lors du chargement de votre historique", null);
derniersPaiements = new ArrayList<>();
}
}
/**
* Calcule les KPI personnels depuis les données chargées
*/
private void calculerKPI() {
// Cotisations en attente
cotisationsEnAttente = mesCotisationsEnAttente.size();
// Montant dû total
BigDecimal montantTotal = mesCotisationsEnAttente.stream()
.map(CotisationPerso::getMontantDu)
.reduce(BigDecimal.ZERO, BigDecimal::add);
montantDu = formaterMontant(montantTotal);
// Prochaine échéance
if (!mesCotisationsEnAttente.isEmpty()) {
LocalDate prochaine = mesCotisationsEnAttente.stream()
.map(CotisationPerso::getDateEcheance)
.filter(d -> d != null)
.min(LocalDate::compareTo)
.orElse(null);
prochaineEcheance = prochaine != null ? prochaine.format(DATE_FORMATTER) : "-";
} else {
prochaineEcheance = "Aucune";
}
// Total payé cette année
BigDecimal totalPayeAnnee = derniersPaiements.stream()
.filter(p -> p.getDatePaiement() != null && p.getDatePaiement().getYear() == anneeEnCours)
.map(PaiementPerso::getMontant)
.reduce(BigDecimal.ZERO, BigDecimal::add);
totalPaye = formaterMontant(totalPayeAnnee);
LOG.infof("KPI calculés: %d cotisations en attente, %s à payer", cotisationsEnAttente, montantDu);
}
// ═══════════════════════════════════════════════════════════════════════
// Actions
// ═══════════════════════════════════════════════════════════════════════
/**
* Initie un paiement en ligne (Wave, Orange, Free Money, Carte)
*/
public void initierPaiementEnLigne() {
if (cotisationSelectionneeId == null) {
errorHandler.showWarning("Attention", "Veuillez sélectionner une cotisation à payer");
return;
}
if (numeroTelephone == null || numeroTelephone.trim().isEmpty()) {
errorHandler.showWarning("Attention", "Veuillez saisir votre numéro de téléphone");
return;
}
try {
// TODO: Créer endpoint POST /api/paiements/initier-paiement-en-ligne
// Body: { cotisationId, methodePaiement, numeroTelephone }
// Retour: { redirectUrl, transactionId }
LOG.infof("Paiement en ligne initié: cotisation=%s, méthode=%s, téléphone=%s",
cotisationSelectionneeId, methodePaiementChoisie, numeroTelephone);
errorHandler.showInfo("Paiement en ligne",
"Redirection vers le gateway de paiement " + methodePaiementChoisie + "...");
// TODO: Rediriger vers l'URL du gateway de paiement
// ExternalContext ec = FacesContext.getCurrentInstance().getExternalContext();
// ec.redirect(paymentGatewayUrl);
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'initiation du paiement en ligne");
errorHandler.handleException(e, "lors de l'initiation du paiement", null);
}
}
/**
* Déclare un paiement manuel (espèces, virement, chèque)
* Statut: EN_ATTENTE_VALIDATION (le trésorier doit valider)
*/
public void declarerPaiementManuel() {
if (cotisationSelectionneeId == null) {
errorHandler.showWarning("Attention", "Veuillez sélectionner une cotisation");
return;
}
if (methodePaiementManuel == null || methodePaiementManuel.trim().isEmpty()) {
errorHandler.showWarning("Attention", "Veuillez sélectionner une méthode de paiement");
return;
}
try {
// TODO: Créer endpoint POST /api/paiements/declarer-paiement-manuel
// Body: { cotisationId, methodePaiement, reference, commentaire }
// Retour: 201 Created
LOG.infof("Paiement manuel déclaré: cotisation=%s, méthode=%s, ref=%s",
cotisationSelectionneeId, methodePaiementManuel, referencePaiementManuel);
errorHandler.showSuccess("Paiement déclaré",
"Votre paiement a été enregistré. Il sera validé par le trésorier.");
// Recharger les données
chargerDonnees();
// Réinitialiser le formulaire
cotisationSelectionneeId = null;
methodePaiementManuel = "ESPECES";
referencePaiementManuel = null;
commentairePaiement = null;
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la déclaration du paiement manuel");
errorHandler.handleException(e, "lors de la déclaration du paiement", null);
}
}
/**
* Télécharge le reçu PDF d'un paiement
*/
public void telechargerRecu(UUID paiementId) {
if (paiementId == null) {
errorHandler.showWarning("Attention", "Impossible de télécharger le reçu");
return;
}
try {
// TODO: Créer endpoint GET /api/paiements/telecharger-recu/{id}
byte[] recu = retryService.executeWithRetrySupplier(
() -> exportService.genererRecu(paiementId),
"génération d'un reçu"
);
String nomFichier = "recu-" + paiementId + ".pdf";
telechargerFichier(recu, nomFichier, "application/pdf");
errorHandler.showSuccess("Reçu téléchargé", "Le reçu a été téléchargé avec succès");
} catch (Exception e) {
LOG.errorf(e, "Erreur lors du téléchargement du reçu");
errorHandler.handleException(e, "lors du téléchargement du reçu", null);
}
}
/**
* 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
*/
public void actualiser() {
LOG.info("Actualisation des données de paiement");
chargerDonnees();
errorHandler.showSuccess("Actualisation", "Les données ont été actualisées");
}
// ═══════════════════════════════════════════════════════════════════════
// Helpers
// ═══════════════════════════════════════════════════════════════════════
private String formaterPeriode(LocalDate dateEcheance) {
if (dateEcheance == null) {
return "";
}
String[] moisNoms = {"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"};
int mois = dateEcheance.getMonthValue();
int annee = dateEcheance.getYear();
return moisNoms[mois - 1] + " " + annee;
}
private String formaterMontant(BigDecimal montant) {
if (montant == null) {
return "0 FCFA";
}
return String.format("%,.0f FCFA", montant);
}
private void telechargerFichier(byte[] data, String nomFichier, String contentType) {
try {
jakarta.faces.context.FacesContext fc = jakarta.faces.context.FacesContext.getCurrentInstance();
ExternalContext ec = fc.getExternalContext();
ec.responseReset();
ec.setResponseContentType(contentType + "; charset=UTF-8");
ec.setResponseContentLength(data.length);
ec.setResponseHeader("Content-Disposition", "attachment; filename=\"" + nomFichier + "\"");
OutputStream output = ec.getResponseOutputStream();
output.write(data);
output.flush();
fc.responseComplete();
} catch (Exception e) {
LOG.errorf(e, "Erreur téléchargement fichier");
throw new RuntimeException("Erreur lors du téléchargement", e);
}
}
// ═══════════════════════════════════════════════════════════════════════
// Getters / Setters
// ═══════════════════════════════════════════════════════════════════════
public UUID getMembreId() { return membreId; }
public String getNumeroMembre() { return numeroMembre; }
public MembreResponse getMembre() { return membre; }
public Integer getCotisationsEnAttente() { return cotisationsEnAttente; }
public String getMontantDu() { return montantDu; }
public String getProchaineEcheance() { return prochaineEcheance; }
public String getTotalPaye() { return totalPaye; }
public Integer getAnneeEnCours() { return anneeEnCours; }
public List<CotisationPerso> getMesCotisationsEnAttente() { return mesCotisationsEnAttente; }
public List<PaiementPerso> getDerniersPaiements() { return derniersPaiements; }
public UUID getCotisationSelectionneeId() { return cotisationSelectionneeId; }
public void setCotisationSelectionneeId(UUID cotisationSelectionneeId) { this.cotisationSelectionneeId = cotisationSelectionneeId; }
public String getMethodePaiementChoisie() { return methodePaiementChoisie; }
public void setMethodePaiementChoisie(String methodePaiementChoisie) { this.methodePaiementChoisie = methodePaiementChoisie; }
public String getNumeroTelephone() { return numeroTelephone; }
public void setNumeroTelephone(String numeroTelephone) { this.numeroTelephone = numeroTelephone; }
public String getMethodePaiementManuel() { return methodePaiementManuel; }
public void setMethodePaiementManuel(String methodePaiementManuel) { this.methodePaiementManuel = methodePaiementManuel; }
public String getReferencePaiementManuel() { return referencePaiementManuel; }
public void setReferencePaiementManuel(String referencePaiementManuel) { this.referencePaiementManuel = referencePaiementManuel; }
public String getCommentairePaiement() { return commentairePaiement; }
public void setCommentairePaiement(String commentairePaiement) { this.commentairePaiement = commentairePaiement; }
public boolean isPaiementManuelActive() { return paiementManuelActive; }
// ═══════════════════════════════════════════════════════════════════════
// DTOs internes
// ═══════════════════════════════════════════════════════════════════════
public static class CotisationPerso implements Serializable {
private static final long serialVersionUID = 1L;
private UUID id;
private String reference;
private String type;
private String periode;
private BigDecimal montantDu;
private LocalDate dateEcheance;
// Getters / Setters
public UUID getId() { return id; }
public void setId(UUID id) { this.id = id; }
public String getReference() { return reference; }
public void setReference(String reference) { this.reference = reference; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getPeriode() { return periode; }
public void setPeriode(String periode) { this.periode = periode; }
public BigDecimal getMontantDu() { return montantDu; }
public void setMontantDu(BigDecimal montantDu) { this.montantDu = montantDu; }
public LocalDate getDateEcheance() { return dateEcheance; }
public void setDateEcheance(LocalDate dateEcheance) { this.dateEcheance = dateEcheance; }
// Méthodes dérivées pour l'affichage
public String getTypeSeverity() {
return switch (type) {
case "MENSUELLE" -> "info";
case "SPECIALE" -> "warning";
case "ADHESION" -> "success";
default -> "secondary";
};
}
public String getTypeIcon() {
return switch (type) {
case "MENSUELLE" -> "pi-calendar";
case "SPECIALE" -> "pi-star";
case "ADHESION" -> "pi-user-plus";
default -> "pi-circle";
};
}
public String getMontantDuFormatte() {
return montantDu != null ? String.format("%,.0f FCFA", montantDu) : "0 FCFA";
}
public String getDateEcheanceFormattee() {
return dateEcheance != null ? dateEcheance.format(DATE_FORMATTER) : "-";
}
}
public static class PaiementPerso implements Serializable {
private static final long serialVersionUID = 1L;
private UUID id;
private String reference;
private String periode;
private BigDecimal montant;
private LocalDate datePaiement;
private String methodePaiement;
// Getters / Setters
public UUID getId() { return id; }
public void setId(UUID id) { this.id = id; }
public String getReference() { return reference; }
public void setReference(String reference) { this.reference = reference; }
public String getPeriode() { return periode; }
public void setPeriode(String periode) { this.periode = periode; }
public BigDecimal getMontant() { return montant; }
public void setMontant(BigDecimal montant) { this.montant = montant; }
public LocalDate getDatePaiement() { return datePaiement; }
public void setDatePaiement(LocalDate datePaiement) { this.datePaiement = datePaiement; }
public String getMethodePaiement() { return methodePaiement; }
public void setMethodePaiement(String methodePaiement) { this.methodePaiement = methodePaiement; }
// Méthodes dérivées pour l'affichage
public String getMontantFormatte() {
return montant != null ? String.format("%,.0f FCFA", montant) : "0 FCFA";
}
public String getDatePaiementFormattee() {
return datePaiement != null ? datePaiement.format(DATE_FORMATTER) : "-";
}
}
}

View File

@@ -1,6 +1,7 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.client.service.AssociationService;
import dev.lions.unionflow.client.service.ErrorHandlerService;
import dev.lions.unionflow.client.service.RestClientExceptionMapper;
@@ -296,7 +297,7 @@ public class OrganisationDetailBean implements Serializable {
public List<OrganisationResponse> rechercherOrganisations(String query) {
if (query == null || query.trim().isEmpty()) return List.of();
try {
AssociationService.PagedResponseDTO<OrganisationResponse> response =
PagedResponse<OrganisationResponse> response =
associationService.rechercher(query, null, null, null, null, 0, 100);
return (response != null && response.getData() != null) ? response.getData() : List.of();
} catch (Exception e) {
@@ -322,6 +323,17 @@ public class OrganisationDetailBean implements Serializable {
return organisation.getTypeOrganisationLibelle();
}
/**
* Alias pour la vue (detail.xhtml) : libellé du type d'organisation.
* Délègue à typeLibelle du DTO si présent, sinon typeOrganisationLibelle.
*/
public String getTypeLibelle() {
if (organisation == null) return "";
String libelle = organisation.getTypeLibelle();
if (libelle != null && !libelle.isBlank()) return libelle;
return organisation.getTypeOrganisationLibelle() != null ? organisation.getTypeOrganisationLibelle() : "";
}
public String getStatutLibelle() {
if (organisation == null || organisation.getStatut() == null) return "Non renseigné";
return switch (organisation.getStatut()) {

View File

@@ -2,6 +2,7 @@ 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.common.PagedResponse;
import dev.lions.unionflow.client.service.AssociationService;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
@@ -74,7 +75,7 @@ public class OrganisationStatistiquesBean implements Serializable {
private void calculerStatistiquesDepuisListe() {
try {
// Charger toutes les organisations
AssociationService.PagedResponseDTO<OrganisationResponse> response = associationService.listerToutes(0, 1000);
PagedResponse<OrganisationResponse> response = associationService.listerToutes(0, 1000);
List<OrganisationResponse> organisations = (response != null && response.getData() != null) ? response.getData()
: new ArrayList<>();

View File

@@ -2,7 +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.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.CacheService;
import dev.lions.unionflow.client.service.ErrorHandlerService;
@@ -71,6 +73,10 @@ public class OrganisationsBean implements Serializable {
private OrganisationResponse organisationSelectionnee;
private OrganisationResponse nouvelleOrganisation;
private OrganisationResponse backupOrganisation; // Pour rollback
/** Organisation en attente de confirmation de suppression (flux explicite). */
private OrganisationResponse organisationPourSuppression;
/** Organisation en attente de confirmation de bascule de statut (flux explicite). */
private OrganisationResponse organisationPourStatut;
private long totalOrganisations;
private long organisationsActives;
@@ -110,7 +116,7 @@ public class OrganisationsBean implements Serializable {
public void chargerOrganisations() {
try {
LOG.debug("Chargement des organisations");
AssociationService.PagedResponseDTO<OrganisationResponse> response = retryService.executeWithRetrySupplier(
PagedResponse<OrganisationResponse> response = retryService.executeWithRetrySupplier(
() -> associationService.listerToutes(0, 1000),
"chargement de toutes les organisations");
organisations = (response != null && response.getData() != null) ? response.getData() : new ArrayList<>();
@@ -136,7 +142,7 @@ public class OrganisationsBean implements Serializable {
() -> {
LOG.debug("Chargement de toutes les organisations pour les statistiques");
try {
AssociationService.PagedResponseDTO<OrganisationResponse> response = retryService
PagedResponse<OrganisationResponse> response = retryService
.executeWithRetrySupplier(
() -> associationService.listerToutes(0, 10000),
"chargement des statistiques");
@@ -342,6 +348,45 @@ public class OrganisationsBean implements Serializable {
}
}
/**
* Prépare la suppression (ouvre le dialogue de confirmation).
*/
public void preparerSuppression(OrganisationResponse organisation) {
organisationPourSuppression = organisation;
}
/**
* Confirme et exécute la suppression après clic sur « Oui » dans le dialogue.
*/
public void confirmerSuppression() {
if (organisationPourSuppression == null) {
errorHandler.showWarning("Erreur", "Aucune organisation à supprimer");
return;
}
OrganisationResponse org = organisationPourSuppression;
organisationPourSuppression = null;
supprimerOrganisation(org);
}
/**
* Prépare la bascule de statut (ouvre le dialogue de confirmation).
*/
public void preparerBasculerStatut(OrganisationResponse organisation) {
organisationPourStatut = organisation;
}
/**
* Confirme et exécute la bascule de statut après clic sur « Oui » dans le dialogue.
*/
public void confirmerBasculerStatut() {
if (organisationPourStatut == null) {
return;
}
OrganisationResponse org = organisationPourStatut;
organisationPourStatut = null;
basculerStatutOrganisation(org);
}
/**
* Supprime une organisation.
*/
@@ -449,7 +494,7 @@ public class OrganisationsBean implements Serializable {
return List.of();
}
try {
AssociationService.PagedResponseDTO<OrganisationResponse> response = associationService.rechercher(
PagedResponse<OrganisationResponse> response = associationService.rechercher(
query, null, null, null, null, 0, 100);
List<OrganisationResponse> resultats = (response != null && response.getData() != null) ? response.getData()
: new ArrayList<>();

View File

@@ -79,7 +79,7 @@ public class PersonnelBean implements Serializable {
String email = userSession.getCurrentUser().getEmail();
if (email != null) {
// Rechercher le membre par email
List<MembreResponse> membres = membreService.listerTous();
List<MembreResponse> membres = membreService.listerTous().getData();
membre = membres.stream()
.filter(m -> email.equals(m.getEmail()))
.findFirst()
@@ -132,14 +132,10 @@ public class PersonnelBean implements Serializable {
try {
if (membre != null) {
List<CotisationResponse> cotisations = cotisationService.obtenirParMembre(membre.getId(), 0, 100);
Map<String, Object> evenementsMap = evenementService.listerTous(0, 100, "dateDebut", "desc");
var evenementsResponse = evenementService.listerTous(0, 100, "dateDebut", "desc");
int nbCotisations = cotisations != null ? cotisations.size() : 0;
int nbEvenements = 0;
if (evenementsMap != null && evenementsMap.containsKey("content")) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> content = (List<Map<String, Object>>) evenementsMap.get("content");
nbEvenements = content != null ? content.size() : 0;
}
int nbEvenements = evenementsResponse != null && evenementsResponse.getData() != null
? evenementsResponse.getData().size() : 0;
return nbCotisations + nbEvenements;
}
} catch (Exception e) {
@@ -152,14 +148,10 @@ public class PersonnelBean implements Serializable {
try {
if (membre != null) {
// Récupérer tous les événements et filtrer ceux où le membre a participé
Map<String, Object> evenementsMap = evenementService.listerTous(0, 100, "dateDebut", "desc");
if (evenementsMap != null && evenementsMap.containsKey("content")) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> content = (List<Map<String, Object>>) evenementsMap.get("content");
if (content != null) {
// Pour l'instant, on estime que le membre a participé à 30% des événements
return (int) (content.size() * 0.3);
}
var evenementsResponse = evenementService.listerTous(0, 100, "dateDebut", "desc");
if (evenementsResponse != null && evenementsResponse.getData() != null) {
// Pour l'instant, on estime que le membre a participé à 30% des événements
return (int) (evenementsResponse.getData().size() * 0.3);
}
}
} catch (Exception e) {
@@ -243,24 +235,20 @@ public class PersonnelBean implements Serializable {
}
// Charger les événements récents
Map<String, Object> evenementsMap = evenementService.listerAVenir(0, 5);
if (evenementsMap != null && evenementsMap.containsKey("content")) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> content = (List<Map<String, Object>>) evenementsMap.get("content");
if (content != null) {
for (Map<String, Object> evtMap : content) {
ActiviteRecente act = new ActiviteRecente();
act.setTitre("Événement: " + (evtMap.get("titre") != null ? evtMap.get("titre").toString() : ""));
act.setDescription("Événement à venir");
if (evtMap.get("dateDebut") != null) {
act.setDateHeure(formatDateRelative(evtMap.get("dateDebut").toString()));
} else {
act.setDateHeure("Bientôt");
}
act.setIcon("pi-calendar");
act.setCouleur("blue-500");
activitesRecentes.add(act);
var evenementsResponse = evenementService.listerAVenir(0, 5);
if (evenementsResponse != null && evenementsResponse.getData() != null) {
for (EvenementResponse evt : evenementsResponse.getData()) {
ActiviteRecente act = new ActiviteRecente();
act.setTitre("Événement: " + (evt.getTitre() != null ? evt.getTitre() : ""));
act.setDescription("Événement à venir");
if (evt.getDateDebut() != null) {
act.setDateHeure(formatDateRelative(evt.getDateDebut().toString()));
} else {
act.setDateHeure("Bientôt");
}
act.setIcon("pi-calendar");
act.setCouleur("blue-500");
activitesRecentes.add(act);
}
}
@@ -384,29 +372,25 @@ public class PersonnelBean implements Serializable {
try {
if (membre != null) {
// Créer des notifications basées sur les événements à venir
Map<String, Object> evenementsMap = evenementService.listerAVenir(0, 5);
if (evenementsMap != null && evenementsMap.containsKey("content")) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> content = (List<Map<String, Object>>) evenementsMap.get("content");
if (content != null) {
for (Map<String, Object> evtMap : content) {
NotificationPersonnelle notif = new NotificationPersonnelle();
notif.setId(UUID.randomUUID());
notif.setTitre("Nouvel événement");
notif.setMessage("Un nouvel événement a été programmé: " +
(evtMap.get("titre") != null ? evtMap.get("titre").toString() : ""));
if (evtMap.get("dateCreation") != null) {
try {
notif.setDateCreation(LocalDate.parse(evtMap.get("dateCreation").toString().substring(0, 10)));
} catch (Exception e) {
notif.setDateCreation(LocalDate.now().minusDays(1));
}
} else {
var evenementsResponse = evenementService.listerAVenir(0, 5);
if (evenementsResponse != null && evenementsResponse.getData() != null) {
for (EvenementResponse evt : evenementsResponse.getData()) {
NotificationPersonnelle notif = new NotificationPersonnelle();
notif.setId(UUID.randomUUID());
notif.setTitre("Nouvel événement");
notif.setMessage("Un nouvel événement a été programmé: " +
(evt.getTitre() != null ? evt.getTitre() : ""));
if (evt.getDateCreation() != null) {
try {
notif.setDateCreation(evt.getDateCreation().toLocalDate());
} catch (Exception e) {
notif.setDateCreation(LocalDate.now().minusDays(1));
}
notif.setLue(false);
notifications.add(notif);
} else {
notif.setDateCreation(LocalDate.now().minusDays(1));
}
notif.setLue(false);
notifications.add(notif);
}
}

View File

@@ -153,8 +153,8 @@ public class RapportsBean implements Serializable {
private void calculerIndicateurs() {
indicateurs = new IndicateursGlobaux();
try {
int totalMembres = membreService.listerTous().size();
int totalEvenements = evenementService.listerTous(0, 1000, "dateCreation", "desc").size();
int totalMembres = membreService.listerTous().getData().size();
int totalEvenements = evenementService.listerTous(0, 1000, "dateCreation", "desc").getData().size();
BigDecimal totalRevenus = cotisationService.listerToutes(0, 1000).stream()
.filter(c -> "PAYEE".equals(c.getStatut()) || "PARTIELLEMENT_PAYEE".equals(c.getStatut()))
@@ -197,7 +197,7 @@ public class RapportsBean implements Serializable {
repartitionMembres = new ArrayList<>();
try {
// Corrigé: MembreResponse est dans server.api, pas client.dto; et getStatut() → getStatutCompte()
List<dev.lions.unionflow.server.api.dto.membre.response.MembreResponse> membres = membreService.listerTous();
List<dev.lions.unionflow.server.api.dto.membre.response.MembreResponse> membres = membreService.listerTous().getData();
long actifs = membres.stream().filter(m -> "ACTIF".equals(m.getStatutCompte())).count();
long inactifs = membres.stream().filter(m -> "INACTIF".equals(m.getStatutCompte())).count();
long total = membres.size();
@@ -248,8 +248,8 @@ public class RapportsBean implements Serializable {
private void calculerObjectifs() {
objectifs = new ArrayList<>();
try {
int totalMembres = membreService.listerTous().size();
int totalEvenements = evenementService.listerTous(0, 1000, "dateCreation", "desc").size();
int totalMembres = membreService.listerTous().getData().size();
int totalEvenements = evenementService.listerTous(0, 1000, "dateCreation", "desc").getData().size();
BigDecimal totalRevenus = cotisationService.listerToutes(0, 1000).stream()
.filter(c -> "PAYEE".equals(c.getStatut()) || "PARTIELLEMENT_PAYEE".equals(c.getStatut()))

View File

@@ -31,6 +31,8 @@ public class RolesBean implements Serializable {
private List<Role> roles;
private Role roleSelectionne;
private Role nouveauRole = new Role();
/** Rôle en attente de confirmation de suppression (flux explicite). */
private Role rolePourSuppression;
@jakarta.annotation.PostConstruct
public void init() {
@@ -120,6 +122,32 @@ public class RolesBean implements Serializable {
public void gererUtilisateurs(Role role) {
this.roleSelectionne = role;
}
/**
* Prépare la suppression (ouvre le dialogue de confirmation).
*/
public void preparerSuppression(Role role) {
this.rolePourSuppression = role;
}
/**
* Confirme et exécute la suppression après clic sur « Oui » dans le dialogue.
*/
public void confirmerSuppression() {
if (rolePourSuppression == null) return;
Role r = rolePourSuppression;
rolePourSuppression = null;
supprimerRole(r);
}
/**
* Supprime un rôle de la liste (côté client ; pas d'API delete dans AdminUserService).
*/
public void supprimerRole(Role role) {
if (role != null && roles != null) {
roles.remove(role);
}
}
public void creerRole() {
// Validation

View File

@@ -1,7 +1,9 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.abonnement.response.AbonnementResponse;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.client.service.AdminUserService;
import dev.lions.unionflow.client.service.AssociationService;
import dev.lions.unionflow.client.service.AuditService;
@@ -151,7 +153,7 @@ public class SuperAdminBean implements Serializable {
private void initializeAssociationKPIs() {
try {
AssociationService.PagedResponseDTO<OrganisationResponse> response = associationService.listerToutes(0, 1000);
PagedResponse<OrganisationResponse> response = associationService.listerToutes(0, 1000);
List<OrganisationResponse> associations = (response != null && response.getData() != null) ? response.getData()
: new ArrayList<>();
totalEntites = associations.size();
@@ -291,7 +293,7 @@ public class SuperAdminBean implements Serializable {
private void initializeEntites() {
topEntites = new ArrayList<>();
try {
AssociationService.PagedResponseDTO<OrganisationResponse> response = associationService.listerToutes(0, 1000);
PagedResponse<OrganisationResponse> response = associationService.listerToutes(0, 1000);
List<OrganisationResponse> associations = (response != null && response.getData() != null) ? response.getData()
: new ArrayList<>();
topEntites = associations.stream()
@@ -318,7 +320,7 @@ public class SuperAdminBean implements Serializable {
private void initializeRepartitionTypes() {
repartitionTypes = new ArrayList<>();
try {
AssociationService.PagedResponseDTO<OrganisationResponse> response = associationService.listerToutes(0, 1000);
PagedResponse<OrganisationResponse> response = associationService.listerToutes(0, 1000);
List<OrganisationResponse> associations = (response != null && response.getData() != null) ? response.getData()
: new ArrayList<>();
if (associations == null || associations.isEmpty())

View File

@@ -56,6 +56,8 @@ public class TypeOrganisationsAdminBean implements Serializable {
/** Type actuellement édité dans le dialogue (nouveau ou existant). */
private TypeReferenceResponse typeCourant;
private TypeReferenceResponse typeSelectionne;
/** ID du type à supprimer (pour dialogue de confirmation explicite). */
private UUID typeASupprimerId;
@PostConstruct
public void init() {
@@ -95,6 +97,11 @@ public class TypeOrganisationsAdminBean implements Serializable {
.domaine("TYPE_ORGANISATION")
.code(typeCourant.getCode())
.libelle(typeCourant.getLibelle())
.description(typeCourant.getDescription())
.ordreAffichage(typeCourant.getOrdreAffichage() != null ? typeCourant.getOrdreAffichage() : 0)
.estDefaut(false)
.estSysteme(false)
.organisationId(null)
.build();
TypeReferenceResponse cree = retryService.executeWithRetrySupplier(
@@ -152,11 +159,11 @@ public class TypeOrganisationsAdminBean implements Serializable {
UpdateTypeReferenceRequest request = new UpdateTypeReferenceRequest(
typeCourant.getCode(),
typeCourant.getLibelle(),
null, // description
typeCourant.getDescription(),
null, // icone
null, // couleur
null, // severity
null, // ordreAffichage
typeCourant.getOrdreAffichage(),
null, // estDefaut
typeCourant.getActif()
);
@@ -179,30 +186,49 @@ public class TypeOrganisationsAdminBean implements Serializable {
}
}
public void desactiverType(UUID id) {
/**
* Ouvre le dialogue de confirmation de suppression (stocke l'id).
*/
public void preparerSuppression(UUID id) {
typeASupprimerId = id;
}
/**
* Confirme et exécute la suppression du type dont l'id a été stocké par preparerSuppression.
*/
public void confirmerSuppression() {
if (typeASupprimerId == null) {
errorHandler.showWarning("Erreur", "Aucun type à supprimer");
return;
}
UUID id = typeASupprimerId;
typeASupprimerId = null;
supprimerType(id);
}
/**
* Supprime définitivement un type d'organisation (appel DELETE côté API).
*/
public void supprimerType(UUID id) {
if (id == null) {
errorHandler.showWarning("Erreur", "Aucun type sélectionné");
return;
}
try {
retryService.executeWithRetrySupplier(
() -> {
typeOrganisationClientService.disable(id);
return null;
},
"désactivation d'un type d'organisation"
"suppression d'un type d'organisation"
);
LOG.infof("Type d'organisation désactivé avec succès: id=%s", id);
LOG.infof("Type d'organisation supprimé avec succès: id=%s", id);
typeCatalogueService.recharger();
chargerTypes();
errorHandler.showSuccess("Succès", "Type d'organisation désactivé");
errorHandler.showSuccess("Succès", "Type d'organisation supprimé");
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la désactivation du type d'organisation");
errorHandler.handleException(e, "lors de la désactivation d'un type d'organisation", null);
LOG.errorf(e, "Erreur lors de la suppression du type d'organisation");
errorHandler.handleException(e, "lors de la suppression d'un type d'organisation", null);
}
}

View File

@@ -2,6 +2,7 @@ package dev.lions.unionflow.client.view;
import io.quarkus.oidc.IdToken;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.SessionScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
@@ -14,12 +15,12 @@ import java.util.logging.Logger;
/**
* Gestion de la session utilisateur avec Keycloak OIDC
*
*
* <p>
* Utilise {@code @IdToken JsonWebToken} pour accéder aux claims du token
* et {@link SecurityIdentity} pour les vérifications d'authentification,
* car l'application est en mode OIDC {@code web-app} (authorization code flow).
*
*
* @author UnionFlow Team
* @version 2.1
*/
@@ -50,6 +51,20 @@ public class UserSession implements Serializable {
clearSession();
}
/**
* Initialise automatiquement la session après injection des dépendances
* Appelé automatiquement par CDI si l'utilisateur est authentifié
*/
@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");
}
}
/**
* Initialise la session depuis le token OIDC Keycloak
* Appelé automatiquement après l'authentification

View File

@@ -4,6 +4,7 @@ import dev.lions.unionflow.server.api.dto.user.request.CreateUserRequest;
import dev.lions.unionflow.server.api.dto.user.request.UpdateUserRequest;
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.client.service.AdminUserService;
import dev.lions.unionflow.client.service.AssociationService;
@@ -84,7 +85,7 @@ public class UtilisateursBean implements Serializable {
private void initializeOrganisations() {
organisationsDisponibles = new ArrayList<>();
try {
AssociationService.PagedResponseDTO<OrganisationResponse> response = associationService.listerToutes(0, 1000);
PagedResponse<OrganisationResponse> response = associationService.listerToutes(0, 1000);
List<OrganisationResponse> associations = (response != null && response.getData() != null) ? response.getData()
: new ArrayList<>();
for (OrganisationResponse assoc : associations) {