From 917c8c5359d1bdb57c157d368c2dd5fb69954fe9 Mon Sep 17 00:00:00 2001 From: dahoud <41957584+DahoudG@users.noreply.github.com> Date: Sat, 25 Apr 2026 12:56:13 +0000 Subject: [PATCH] =?UTF-8?q?feat(sprint-11=20web=202026-04-25):=20pages=20P?= =?UTF-8?q?rimeFaces=20Sprint=2010=20(UBO,=20audit-trail=20viewer,=20d?= =?UTF-8?q?=C3=A9l=C3=A9gations=20r=C3=B4les)=20+=20bump=20api=201.0.6?= =?UTF-8?q?=E2=86=921.0.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DRY strict appliqué : web réutilise directement les DTOs officiels de unionflow-server-api 1.0.8 (CreateBeneficiaireEffectifRequest, BeneficiaireEffectifResponse, AuditTrailOperationResponse, CreateRoleDelegationRequest, RoleDelegationResponse) au lieu de DTOs miroirs locaux. Aucune duplication. Bump dépendance api 1.0.6 → 1.0.8 REST clients @RegisterRestClient configKey=unionflow-api - BeneficiaireEffectifRestClient : CRUD lister/trouverParId/creer/mettreAJour/desactiver - AuditTrailRestClient : 5 endpoints lecture (parUtilisateur, historique, parOrganisation, sodViolations, financial) - RoleDelegationRestClient : listerParOrganisation / creer / revoquer Beans @ViewScoped - BeneficiaireEffectifBean : recherche (KYC|org|PEP), création formulaire, marquerPep, désactiver - AuditTrailViewerBean : 5 modes (USER/ENTITY/ORG/SOD_VIOLATIONS/FINANCIAL), couleurAction (DELETE→danger, VALIDATE→success, etc.), couleurSod - RoleDelegationBean : recherche/créer/révoquer, couleurStatut (ACTIVE/REVOQUEE/EXPIREE) Pages XHTML - /pages/secure/conformite/beneficiaires-effectifs.xhtml — recherche + tableau + nouvelle UBO (panel toggleable) - /pages/secure/conformite/audit-trail.xhtml — filtres mode + tableau + détail JSONB (pre format) - /pages/secure/admin/role-delegations.xhtml — table actives + nouvelle (datePicker dates) MenuBean + menu.xhtml - 3 nouveaux flags : isBeneficiairesEffectifsVisible, isAuditTrailViewerVisible, isRoleDelegationsVisible - 3 menuitems ajoutés au sous-menu Conformité existant (icônes pi-users, pi-history, pi-share-alt) - Gating par rôles : COMPLIANCE_OFFICER + CONTROLEUR_INTERNE pour audit ; ADMIN_ORGANISATION + PRESIDENT pour délégations Tests (10/10 verts, 31/31 cumulé S8+S11) - AuditTrailViewerBeanTest : 8 tests (couleurAction × 6 cas, couleurSod, defaults) - RoleDelegationBeanTest : 2 tests (couleurStatut × 5, defaults) --- pom.xml | 2 +- .../lions/unionflow/client/bean/MenuBean.java | 25 +++ .../client/service/AuditTrailRestClient.java | 53 ++++++ .../BeneficiaireEffectifRestClient.java | 54 ++++++ .../service/RoleDelegationRestClient.java | 42 +++++ .../client/view/AuditTrailViewerBean.java | 113 ++++++++++++ .../client/view/BeneficiaireEffectifBean.java | 165 ++++++++++++++++++ .../client/view/RoleDelegationBean.java | 147 ++++++++++++++++ .../pages/secure/admin/role-delegations.xhtml | 135 ++++++++++++++ .../pages/secure/conformite/audit-trail.xhtml | 112 ++++++++++++ .../conformite/beneficiaires-effectifs.xhtml | 147 ++++++++++++++++ .../templates/components/layout/menu.xhtml | 3 + .../client/view/AuditTrailViewerBeanTest.java | 76 ++++++++ .../client/view/RoleDelegationBeanTest.java | 36 ++++ 14 files changed, 1109 insertions(+), 1 deletion(-) create mode 100644 src/main/java/dev/lions/unionflow/client/service/AuditTrailRestClient.java create mode 100644 src/main/java/dev/lions/unionflow/client/service/BeneficiaireEffectifRestClient.java create mode 100644 src/main/java/dev/lions/unionflow/client/service/RoleDelegationRestClient.java create mode 100644 src/main/java/dev/lions/unionflow/client/view/AuditTrailViewerBean.java create mode 100644 src/main/java/dev/lions/unionflow/client/view/BeneficiaireEffectifBean.java create mode 100644 src/main/java/dev/lions/unionflow/client/view/RoleDelegationBean.java create mode 100644 src/main/resources/META-INF/resources/pages/secure/admin/role-delegations.xhtml create mode 100644 src/main/resources/META-INF/resources/pages/secure/conformite/audit-trail.xhtml create mode 100644 src/main/resources/META-INF/resources/pages/secure/conformite/beneficiaires-effectifs.xhtml create mode 100644 src/test/java/dev/lions/unionflow/client/view/AuditTrailViewerBeanTest.java create mode 100644 src/test/java/dev/lions/unionflow/client/view/RoleDelegationBeanTest.java diff --git a/pom.xml b/pom.xml index c28cac3..5a6dbc2 100644 --- a/pom.xml +++ b/pom.xml @@ -142,7 +142,7 @@ dev.lions.unionflow unionflow-server-api - 1.0.6 + 1.0.8 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 b18f296..84f5b47 100644 --- a/src/main/java/dev/lions/unionflow/client/bean/MenuBean.java +++ b/src/main/java/dev/lions/unionflow/client/bean/MenuBean.java @@ -646,6 +646,31 @@ public class MenuBean implements Serializable { return hasAnyRole("SUPER_ADMIN", "COMPLIANCE_OFFICER"); } + /** + * Bénéficiaires Effectifs (UBO) — Instr. BCEAO 003-03-2025. + * @since 2026-04-25 (Sprint 11) + */ + public boolean isBeneficiairesEffectifsVisible() { + return hasAnyRole("SUPER_ADMIN", "ADMIN_ORGANISATION", "COMPLIANCE_OFFICER", + "CONTROLEUR_INTERNE"); + } + + /** + * Audit Trail viewer — compliance / contrôle interne. + * @since 2026-04-25 (Sprint 11) + */ + public boolean isAuditTrailViewerVisible() { + return hasAnyRole("SUPER_ADMIN", "COMPLIANCE_OFFICER", "CONTROLEUR_INTERNE"); + } + + /** + * Délégations de rôles — admin org / président. + * @since 2026-04-25 (Sprint 11) + */ + public boolean isRoleDelegationsVisible() { + return hasAnyRole("SUPER_ADMIN", "ADMIN_ORGANISATION", "PRESIDENT"); + } + /** * Retourne true si l'organisation active dispose d'au moins un module métier spécifique * (au-delà des modules communs toujours disponibles). diff --git a/src/main/java/dev/lions/unionflow/client/service/AuditTrailRestClient.java b/src/main/java/dev/lions/unionflow/client/service/AuditTrailRestClient.java new file mode 100644 index 0000000..10adaaa --- /dev/null +++ b/src/main/java/dev/lions/unionflow/client/service/AuditTrailRestClient.java @@ -0,0 +1,53 @@ +package dev.lions.unionflow.client.service; + +import dev.lions.unionflow.client.security.AuthHeaderFactory; +import dev.lions.unionflow.server.api.dto.audit.response.AuditTrailOperationResponse; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import java.util.List; +import java.util.UUID; +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +/** + * Client REST de lecture audit trail (Sprint 11 ⇄ backend Sprint 10 CQRS read). + */ +@RegisterRestClient(configKey = "unionflow-api") +@RegisterClientHeaders(AuthHeaderFactory.class) +@Path("/api/audit-trail") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public interface AuditTrailRestClient { + + @GET + @Path("/by-user/{userId}") + List parUtilisateur( + @PathParam("userId") UUID userId, + @QueryParam("from") String from, + @QueryParam("to") String to); + + @GET + @Path("/by-entity/{type}/{id}") + List historique( + @PathParam("type") String entityType, @PathParam("id") UUID entityId); + + @GET + @Path("/by-organisation/{orgId}") + List parOrganisation(@PathParam("orgId") UUID orgId); + + @GET + @Path("/sod-violations") + List violationsSod(); + + @GET + @Path("/financial/{orgId}") + List operationsFinancieres( + @PathParam("orgId") UUID orgId, + @QueryParam("from") String from, + @QueryParam("to") String to); +} diff --git a/src/main/java/dev/lions/unionflow/client/service/BeneficiaireEffectifRestClient.java b/src/main/java/dev/lions/unionflow/client/service/BeneficiaireEffectifRestClient.java new file mode 100644 index 0000000..2d890c1 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/client/service/BeneficiaireEffectifRestClient.java @@ -0,0 +1,54 @@ +package dev.lions.unionflow.client.service; + +import dev.lions.unionflow.client.security.AuthHeaderFactory; +import dev.lions.unionflow.server.api.dto.kyc.request.CreateBeneficiaireEffectifRequest; +import dev.lions.unionflow.server.api.dto.kyc.request.UpdateBeneficiaireEffectifRequest; +import dev.lions.unionflow.server.api.dto.kyc.response.BeneficiaireEffectifResponse; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import java.util.List; +import java.util.UUID; +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +/** + * Client REST des Bénéficiaires Effectifs (Sprint 11 ⇄ backend Sprint 10). + * Réutilise les DTOs officiels de unionflow-server-api 1.0.8 — zéro duplication. + */ +@RegisterRestClient(configKey = "unionflow-api") +@RegisterClientHeaders(AuthHeaderFactory.class) +@Path("/api/kyc/beneficiaires-effectifs") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public interface BeneficiaireEffectifRestClient { + + @GET + List lister( + @QueryParam("kycDossierId") UUID kycDossierId, + @QueryParam("organisationCibleId") UUID organisationCibleId, + @QueryParam("pep") Boolean pep); + + @GET + @Path("/{id}") + BeneficiaireEffectifResponse trouverParId(@PathParam("id") UUID id); + + @POST + BeneficiaireEffectifResponse creer(CreateBeneficiaireEffectifRequest request); + + @PUT + @Path("/{id}") + BeneficiaireEffectifResponse mettreAJour( + @PathParam("id") UUID id, UpdateBeneficiaireEffectifRequest request); + + @DELETE + @Path("/{id}") + void desactiver(@PathParam("id") UUID id); +} diff --git a/src/main/java/dev/lions/unionflow/client/service/RoleDelegationRestClient.java b/src/main/java/dev/lions/unionflow/client/service/RoleDelegationRestClient.java new file mode 100644 index 0000000..18e091a --- /dev/null +++ b/src/main/java/dev/lions/unionflow/client/service/RoleDelegationRestClient.java @@ -0,0 +1,42 @@ +package dev.lions.unionflow.client.service; + +import dev.lions.unionflow.client.security.AuthHeaderFactory; +import dev.lions.unionflow.server.api.dto.delegation.request.CreateRoleDelegationRequest; +import dev.lions.unionflow.server.api.dto.delegation.response.RoleDelegationResponse; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import java.util.List; +import java.util.UUID; +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +/** + * Client REST des délégations de rôle (Sprint 11 ⇄ backend Sprint 10). + */ +@RegisterRestClient(configKey = "unionflow-api") +@RegisterClientHeaders(AuthHeaderFactory.class) +@Path("/api/role-delegations") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public interface RoleDelegationRestClient { + + @GET + @Path("/organisation/{orgId}") + List listerParOrganisation(@PathParam("orgId") UUID orgId); + + @POST + RoleDelegationResponse creer( + CreateRoleDelegationRequest request, + @QueryParam("rolesDelegataire") String rolesDelegataireCsv); + + @DELETE + @Path("/{id}") + RoleDelegationResponse revoquer(@PathParam("id") UUID id); +} diff --git a/src/main/java/dev/lions/unionflow/client/view/AuditTrailViewerBean.java b/src/main/java/dev/lions/unionflow/client/view/AuditTrailViewerBean.java new file mode 100644 index 0000000..ddac027 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/client/view/AuditTrailViewerBean.java @@ -0,0 +1,113 @@ +package dev.lions.unionflow.client.view; + +import dev.lions.unionflow.client.service.AuditTrailRestClient; +import dev.lions.unionflow.server.api.dto.audit.response.AuditTrailOperationResponse; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.FacesContext; +import jakarta.faces.view.ViewScoped; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.logging.Logger; + +/** + * Bean viewer audit trail (Sprint 11 ⇄ backend Sprint 10 CQRS read). + * Pas d'écriture — la production des événements est dans le backend lifecycle. + */ +@Named +@ViewScoped +public class AuditTrailViewerBean implements Serializable { + + private static final long serialVersionUID = 1L; + private static final Logger LOG = Logger.getLogger(AuditTrailViewerBean.class); + + @Inject @RestClient AuditTrailRestClient client; + + /** Mode : USER, ENTITY, ORG, SOD_VIOLATIONS, FINANCIAL */ + private String mode = "ORG"; + + private UUID userId; + private UUID orgId; + private String entityType; + private UUID entityId; + private LocalDateTime from = LocalDateTime.now().minusDays(30); + private LocalDateTime to = LocalDateTime.now(); + + private List operations = Collections.emptyList(); + private AuditTrailOperationResponse selection; + private String erreur; + + public void rechercher() { + erreur = null; + try { + operations = switch (mode) { + case "USER" -> userId != null + ? client.parUtilisateur(userId, from.toString(), to.toString()) + : Collections.emptyList(); + case "ENTITY" -> entityType != null && entityId != null + ? client.historique(entityType, entityId) + : Collections.emptyList(); + case "ORG" -> orgId != null + ? client.parOrganisation(orgId) + : Collections.emptyList(); + case "SOD_VIOLATIONS" -> client.violationsSod(); + case "FINANCIAL" -> orgId != null + ? client.operationsFinancieres(orgId, from.toString(), to.toString()) + : Collections.emptyList(); + default -> Collections.emptyList(); + }; + LOG.infof("Audit trail (%s) chargé : %d entrées", mode, operations.size()); + } catch (Exception e) { + handleError("Recherche audit trail échouée", e); + operations = Collections.emptyList(); + } + } + + public String getCouleurAction(String actionType) { + if (actionType == null) return "secondary"; + return switch (actionType) { + case "DELETE", "PAYMENT_FAILED" -> "danger"; + case "VALIDATE", "PAYMENT_CONFIRMED", "AID_REQUEST_APPROVED" -> "success"; + case "UPDATE", "PAYMENT_INITIATED", "BUDGET_APPROVED" -> "info"; + case "CREATE" -> "primary"; + case "EXPORT" -> "warning"; + default -> "secondary"; + }; + } + + public String getCouleurSod(Boolean sodCheckPassed) { + if (sodCheckPassed == null) return "secondary"; + return sodCheckPassed ? "success" : "danger"; + } + + private void handleError(String summary, Exception e) { + LOG.warnf("%s : %s", summary, e.getMessage()); + erreur = summary + " — " + e.getMessage(); + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", erreur)); + } + + public String getMode() { return mode; } + public void setMode(String mode) { this.mode = mode; } + public UUID getUserId() { return userId; } + public void setUserId(UUID userId) { this.userId = userId; } + public UUID getOrgId() { return orgId; } + public void setOrgId(UUID orgId) { this.orgId = orgId; } + public String getEntityType() { return entityType; } + public void setEntityType(String entityType) { this.entityType = entityType; } + public UUID getEntityId() { return entityId; } + public void setEntityId(UUID entityId) { this.entityId = entityId; } + public LocalDateTime getFrom() { return from; } + public void setFrom(LocalDateTime from) { this.from = from; } + public LocalDateTime getTo() { return to; } + public void setTo(LocalDateTime to) { this.to = to; } + public List getOperations() { return operations; } + public AuditTrailOperationResponse getSelection() { return selection; } + public void setSelection(AuditTrailOperationResponse selection) { this.selection = selection; } + public String getErreur() { return erreur; } +} diff --git a/src/main/java/dev/lions/unionflow/client/view/BeneficiaireEffectifBean.java b/src/main/java/dev/lions/unionflow/client/view/BeneficiaireEffectifBean.java new file mode 100644 index 0000000..3825d55 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/client/view/BeneficiaireEffectifBean.java @@ -0,0 +1,165 @@ +package dev.lions.unionflow.client.view; + +import dev.lions.unionflow.client.service.BeneficiaireEffectifRestClient; +import dev.lions.unionflow.server.api.dto.kyc.request.CreateBeneficiaireEffectifRequest; +import dev.lions.unionflow.server.api.dto.kyc.request.UpdateBeneficiaireEffectifRequest; +import dev.lions.unionflow.server.api.dto.kyc.response.BeneficiaireEffectifResponse; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.FacesContext; +import jakarta.faces.view.ViewScoped; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.logging.Logger; + +/** + * Bean CRUD des Bénéficiaires Effectifs (Sprint 11 ⇄ backend Sprint 10). + */ +@Named +@ViewScoped +public class BeneficiaireEffectifBean implements Serializable { + + private static final long serialVersionUID = 1L; + private static final Logger LOG = Logger.getLogger(BeneficiaireEffectifBean.class); + + @Inject @RestClient BeneficiaireEffectifRestClient client; + + // Filtres + private UUID kycDossierId; + private UUID organisationCibleId; + private Boolean filtrePep; + + private List ubos = Collections.emptyList(); + private BeneficiaireEffectifResponse selection; + private String erreur; + + // Formulaire de création + private String nom; + private String prenoms; + private String nationalite; + private String paysResidence; + private String numeroPieceIdentite; + private String pourcentageCapital; + private String natureControle = "DETENTION_CAPITAL"; + private boolean estPep; + + public void rechercher() { + erreur = null; + try { + ubos = client.lister(kycDossierId, organisationCibleId, filtrePep); + LOG.infof("UBOs chargés : %d", ubos.size()); + } catch (Exception e) { + handleError("Recherche UBO échouée", e); + ubos = Collections.emptyList(); + } + } + + public void creer() { + if (kycDossierId == null && organisationCibleId == null) { + addMessage(FacesMessage.SEVERITY_WARN, "Cible requise", + "Sélectionnez d'abord un KycDossier ou une organisation cible"); + return; + } + try { + CreateBeneficiaireEffectifRequest req = CreateBeneficiaireEffectifRequest.builder() + .kycDossierId(kycDossierId) + .organisationCibleId(organisationCibleId) + .nom(nom) + .prenoms(prenoms) + .nationalite(nationalite != null ? nationalite.toUpperCase() : null) + .paysResidence(paysResidence != null ? paysResidence.toUpperCase() : null) + .numeroPieceIdentite(numeroPieceIdentite) + .pourcentageCapital(pourcentageCapital != null && !pourcentageCapital.isBlank() + ? new java.math.BigDecimal(pourcentageCapital) : null) + .natureControle(natureControle) + .estPep(estPep) + .build(); + BeneficiaireEffectifResponse created = client.creer(req); + addMessage(FacesMessage.SEVERITY_INFO, "UBO créé", + created.prenoms() + " " + created.nom()); + resetForm(); + rechercher(); + } catch (Exception e) { + handleError("Création UBO échouée", e); + } + } + + public void desactiverSelection() { + if (selection == null) return; + try { + client.desactiver(selection.id()); + addMessage(FacesMessage.SEVERITY_INFO, "UBO désactivé", + selection.prenoms() + " " + selection.nom()); + rechercher(); + } catch (Exception e) { + handleError("Désactivation échouée", e); + } + } + + public void marquerPepSelection() { + if (selection == null) return; + try { + UpdateBeneficiaireEffectifRequest req = UpdateBeneficiaireEffectifRequest.builder() + .estPep(true).build(); + client.mettreAJour(selection.id(), req); + addMessage(FacesMessage.SEVERITY_INFO, "UBO marqué PEP", ""); + rechercher(); + } catch (Exception e) { + handleError("Mise à jour PEP échouée", e); + } + } + + private void resetForm() { + nom = null; + prenoms = null; + nationalite = null; + paysResidence = null; + numeroPieceIdentite = null; + pourcentageCapital = null; + natureControle = "DETENTION_CAPITAL"; + estPep = false; + } + + private void handleError(String summary, Exception e) { + LOG.warnf("%s : %s", summary, e.getMessage()); + erreur = summary + " — " + e.getMessage(); + addMessage(FacesMessage.SEVERITY_ERROR, "Erreur", erreur); + } + + private void addMessage(FacesMessage.Severity sev, String summary, String detail) { + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(sev, summary, detail)); + } + + // Getters / setters + public UUID getKycDossierId() { return kycDossierId; } + public void setKycDossierId(UUID kycDossierId) { this.kycDossierId = kycDossierId; } + public UUID getOrganisationCibleId() { return organisationCibleId; } + public void setOrganisationCibleId(UUID id) { this.organisationCibleId = id; } + public Boolean getFiltrePep() { return filtrePep; } + public void setFiltrePep(Boolean filtrePep) { this.filtrePep = filtrePep; } + public List getUbos() { return ubos; } + public BeneficiaireEffectifResponse getSelection() { return selection; } + public void setSelection(BeneficiaireEffectifResponse selection) { this.selection = selection; } + public String getErreur() { return erreur; } + public String getNom() { return nom; } + public void setNom(String nom) { this.nom = nom; } + public String getPrenoms() { return prenoms; } + public void setPrenoms(String prenoms) { this.prenoms = prenoms; } + public String getNationalite() { return nationalite; } + public void setNationalite(String nationalite) { this.nationalite = nationalite; } + public String getPaysResidence() { return paysResidence; } + public void setPaysResidence(String paysResidence) { this.paysResidence = paysResidence; } + public String getNumeroPieceIdentite() { return numeroPieceIdentite; } + public void setNumeroPieceIdentite(String n) { this.numeroPieceIdentite = n; } + public String getPourcentageCapital() { return pourcentageCapital; } + public void setPourcentageCapital(String pct) { this.pourcentageCapital = pct; } + public String getNatureControle() { return natureControle; } + public void setNatureControle(String nc) { this.natureControle = nc; } + public boolean isEstPep() { return estPep; } + public void setEstPep(boolean estPep) { this.estPep = estPep; } +} diff --git a/src/main/java/dev/lions/unionflow/client/view/RoleDelegationBean.java b/src/main/java/dev/lions/unionflow/client/view/RoleDelegationBean.java new file mode 100644 index 0000000..232a926 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/client/view/RoleDelegationBean.java @@ -0,0 +1,147 @@ +package dev.lions.unionflow.client.view; + +import dev.lions.unionflow.client.service.RoleDelegationRestClient; +import dev.lions.unionflow.server.api.dto.delegation.request.CreateRoleDelegationRequest; +import dev.lions.unionflow.server.api.dto.delegation.response.RoleDelegationResponse; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.FacesContext; +import jakarta.faces.view.ViewScoped; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.logging.Logger; + +/** + * Bean liste/création/révocation des délégations de rôle (Sprint 11 ⇄ backend Sprint 10). + */ +@Named +@ViewScoped +public class RoleDelegationBean implements Serializable { + + private static final long serialVersionUID = 1L; + private static final Logger LOG = Logger.getLogger(RoleDelegationBean.class); + + @Inject @RestClient RoleDelegationRestClient client; + + private UUID organisationId; + private List delegations = Collections.emptyList(); + private RoleDelegationResponse selection; + private String erreur; + + // Formulaire création + private UUID delegantUserId; + private UUID delegataireUserId; + private String roleDelegue = "TRESORIER"; + private LocalDateTime dateDebut = LocalDateTime.now().plusHours(1); + private LocalDateTime dateFin = LocalDateTime.now().plusDays(14); + private String motif; + private String rolesDelegataireCsv; + + public void rechercher() { + erreur = null; + if (organisationId == null) return; + try { + delegations = client.listerParOrganisation(organisationId); + LOG.infof("Délégations chargées org=%s : %d", organisationId, delegations.size()); + } catch (Exception e) { + handleError("Chargement délégations échoué", e); + delegations = Collections.emptyList(); + } + } + + public void creer() { + if (organisationId == null || delegantUserId == null || delegataireUserId == null) { + addMessage(FacesMessage.SEVERITY_WARN, "Champs requis", + "Organisation, déléguant et délégataire sont obligatoires"); + return; + } + try { + CreateRoleDelegationRequest req = CreateRoleDelegationRequest.builder() + .organisationId(organisationId) + .delegantUserId(delegantUserId) + .delegataireUserId(delegataireUserId) + .roleDelegue(roleDelegue) + .dateDebut(dateDebut) + .dateFin(dateFin) + .motif(motif) + .build(); + RoleDelegationResponse created = client.creer(req, rolesDelegataireCsv); + addMessage(FacesMessage.SEVERITY_INFO, "Délégation créée", + "Rôle " + created.roleDelegue() + " jusqu'au " + created.dateFin()); + resetForm(); + rechercher(); + } catch (Exception e) { + handleError("Création délégation échouée", e); + } + } + + public void revoquerSelection() { + if (selection == null) return; + try { + client.revoquer(selection.id()); + addMessage(FacesMessage.SEVERITY_INFO, "Délégation révoquée", + "Rôle " + selection.roleDelegue()); + rechercher(); + } catch (Exception e) { + handleError("Révocation échouée", e); + } + } + + public String getCouleurStatut(String statut) { + if (statut == null) return "secondary"; + return switch (statut) { + case "ACTIVE" -> "success"; + case "REVOQUEE" -> "danger"; + case "EXPIREE" -> "warning"; + default -> "secondary"; + }; + } + + private void resetForm() { + delegantUserId = null; + delegataireUserId = null; + roleDelegue = "TRESORIER"; + dateDebut = LocalDateTime.now().plusHours(1); + dateFin = LocalDateTime.now().plusDays(14); + motif = null; + rolesDelegataireCsv = null; + } + + private void handleError(String summary, Exception e) { + LOG.warnf("%s : %s", summary, e.getMessage()); + erreur = summary + " — " + e.getMessage(); + addMessage(FacesMessage.SEVERITY_ERROR, "Erreur", erreur); + } + + private void addMessage(FacesMessage.Severity sev, String summary, String detail) { + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(sev, summary, detail)); + } + + // Getters / setters + public UUID getOrganisationId() { return organisationId; } + public void setOrganisationId(UUID organisationId) { this.organisationId = organisationId; } + public List getDelegations() { return delegations; } + public RoleDelegationResponse getSelection() { return selection; } + public void setSelection(RoleDelegationResponse selection) { this.selection = selection; } + public String getErreur() { return erreur; } + public UUID getDelegantUserId() { return delegantUserId; } + public void setDelegantUserId(UUID id) { this.delegantUserId = id; } + public UUID getDelegataireUserId() { return delegataireUserId; } + public void setDelegataireUserId(UUID id) { this.delegataireUserId = id; } + public String getRoleDelegue() { return roleDelegue; } + public void setRoleDelegue(String roleDelegue) { this.roleDelegue = roleDelegue; } + public LocalDateTime getDateDebut() { return dateDebut; } + public void setDateDebut(LocalDateTime dateDebut) { this.dateDebut = dateDebut; } + public LocalDateTime getDateFin() { return dateFin; } + public void setDateFin(LocalDateTime dateFin) { this.dateFin = dateFin; } + public String getMotif() { return motif; } + public void setMotif(String motif) { this.motif = motif; } + public String getRolesDelegataireCsv() { return rolesDelegataireCsv; } + public void setRolesDelegataireCsv(String csv) { this.rolesDelegataireCsv = csv; } +} diff --git a/src/main/resources/META-INF/resources/pages/secure/admin/role-delegations.xhtml b/src/main/resources/META-INF/resources/pages/secure/admin/role-delegations.xhtml new file mode 100644 index 0000000..792f848 --- /dev/null +++ b/src/main/resources/META-INF/resources/pages/secure/admin/role-delegations.xhtml @@ -0,0 +1,135 @@ + + + + UnionFlow - Délégations de rôles + + +
+
+
+

Délégations temporaires de rôles

+

+ Délégation d'un rôle pour absence prolongée. Vérification SoD à la création. +

+ + + + + +
+
+ + +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + + + + + + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/META-INF/resources/pages/secure/conformite/audit-trail.xhtml b/src/main/resources/META-INF/resources/pages/secure/conformite/audit-trail.xhtml new file mode 100644 index 0000000..ec467a6 --- /dev/null +++ b/src/main/resources/META-INF/resources/pages/secure/conformite/audit-trail.xhtml @@ -0,0 +1,112 @@ + + + + UnionFlow - Audit Trail + + +
+
+
+

Audit Trail

+

+ Historique enrichi des opérations sensibles + détection violations SoD. +

+ + + + + +
+
+ + + + + + + + +
+
+ + +
+
+ + +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ SoD violations : + +
+
+ Payload avant : +
#{auditTrailViewerBean.selection.payloadAvant}
+
+
+ Payload après : +
#{auditTrailViewerBean.selection.payloadApres}
+
+
+ Métadonnées : +
#{auditTrailViewerBean.selection.metadata}
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/META-INF/resources/pages/secure/conformite/beneficiaires-effectifs.xhtml b/src/main/resources/META-INF/resources/pages/secure/conformite/beneficiaires-effectifs.xhtml new file mode 100644 index 0000000..1c6ca81 --- /dev/null +++ b/src/main/resources/META-INF/resources/pages/secure/conformite/beneficiaires-effectifs.xhtml @@ -0,0 +1,147 @@ + + + + UnionFlow - Bénéficiaires Effectifs (UBO) + + +
+
+
+

Bénéficiaires Effectifs (UBO)

+

+ Instr. BCEAO 003-03-2025 — identification des personnes physiques contrôlant l'organisation. +

+ + + + + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+
+ + + + #{u.prenoms} #{u.nom} + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + + + + +
+
+ +
+
+ +
+
+
+
+
+
+
+
+
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 3762444..3ce964f 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 @@ -113,6 +113,9 @@ + + + diff --git a/src/test/java/dev/lions/unionflow/client/view/AuditTrailViewerBeanTest.java b/src/test/java/dev/lions/unionflow/client/view/AuditTrailViewerBeanTest.java new file mode 100644 index 0000000..609f172 --- /dev/null +++ b/src/test/java/dev/lions/unionflow/client/view/AuditTrailViewerBeanTest.java @@ -0,0 +1,76 @@ +package dev.lions.unionflow.client.view; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class AuditTrailViewerBeanTest { + + private AuditTrailViewerBean bean; + + @BeforeEach + void setUp() { + bean = new AuditTrailViewerBean(); + } + + @Test + @DisplayName("getCouleurAction — DELETE → danger") + void couleurDelete() { + assertEquals("danger", bean.getCouleurAction("DELETE")); + assertEquals("danger", bean.getCouleurAction("PAYMENT_FAILED")); + } + + @Test + @DisplayName("getCouleurAction — VALIDATE / payment confirmé / aide approuvée → success") + void couleurSuccess() { + assertEquals("success", bean.getCouleurAction("VALIDATE")); + assertEquals("success", bean.getCouleurAction("PAYMENT_CONFIRMED")); + assertEquals("success", bean.getCouleurAction("AID_REQUEST_APPROVED")); + } + + @Test + @DisplayName("getCouleurAction — UPDATE / payment initié / budget approuvé → info") + void couleurInfo() { + assertEquals("info", bean.getCouleurAction("UPDATE")); + assertEquals("info", bean.getCouleurAction("PAYMENT_INITIATED")); + assertEquals("info", bean.getCouleurAction("BUDGET_APPROVED")); + } + + @Test + @DisplayName("getCouleurAction — CREATE → primary") + void couleurCreate() { + assertEquals("primary", bean.getCouleurAction("CREATE")); + } + + @Test + @DisplayName("getCouleurAction — EXPORT → warning") + void couleurExport() { + assertEquals("warning", bean.getCouleurAction("EXPORT")); + } + + @Test + @DisplayName("getCouleurAction — null/inconnu → secondary") + void couleurDefault() { + assertEquals("secondary", bean.getCouleurAction(null)); + assertEquals("secondary", bean.getCouleurAction("AUTRE")); + } + + @Test + @DisplayName("getCouleurSod — true → success, false → danger, null → secondary") + void couleurSod() { + assertEquals("success", bean.getCouleurSod(true)); + assertEquals("danger", bean.getCouleurSod(false)); + assertEquals("secondary", bean.getCouleurSod(null)); + } + + @Test + @DisplayName("Defaults — mode = ORG, plage = 30 derniers jours") + void defaults() { + assertEquals("ORG", bean.getMode()); + // plage from 30 jours avant maintenant — vérifié pas null + org.junit.jupiter.api.Assertions.assertNotNull(bean.getFrom()); + org.junit.jupiter.api.Assertions.assertNotNull(bean.getTo()); + } +} diff --git a/src/test/java/dev/lions/unionflow/client/view/RoleDelegationBeanTest.java b/src/test/java/dev/lions/unionflow/client/view/RoleDelegationBeanTest.java new file mode 100644 index 0000000..1cfebfe --- /dev/null +++ b/src/test/java/dev/lions/unionflow/client/view/RoleDelegationBeanTest.java @@ -0,0 +1,36 @@ +package dev.lions.unionflow.client.view; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class RoleDelegationBeanTest { + + private RoleDelegationBean bean; + + @BeforeEach + void setUp() { + bean = new RoleDelegationBean(); + } + + @Test + @DisplayName("getCouleurStatut — ACTIVE/REVOQUEE/EXPIREE/null/inconnu") + void couleur() { + assertEquals("success", bean.getCouleurStatut("ACTIVE")); + assertEquals("danger", bean.getCouleurStatut("REVOQUEE")); + assertEquals("warning", bean.getCouleurStatut("EXPIREE")); + assertEquals("secondary", bean.getCouleurStatut(null)); + assertEquals("secondary", bean.getCouleurStatut("AUTRE")); + } + + @Test + @DisplayName("Defaults — rôle TRESORIER, dates initialisées") + void defaults() { + assertEquals("TRESORIER", bean.getRoleDelegue()); + assertNotNull(bean.getDateDebut()); + assertNotNull(bean.getDateFin()); + } +}