From e936af7d3934d510c1d51e5a27637e94665a3250 Mon Sep 17 00:00:00 2001
From: dahoud <41957584+DahoudG@users.noreply.github.com>
Date: Sat, 25 Apr 2026 15:36:37 +0000
Subject: [PATCH] feat(sprint-14 web 2026-04-25): picker p:autoComplete pour
Compliance Officer (UX vs UUID textuel)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
DRY strict — réutilise MembreService REST client existant, aucun nouveau client.
ComplianceOfficerPickerBean (@Named, @ApplicationScoped)
- suggest(query) : recherche multi-champ (nom OU prénom) via MembreService.rechercher,
dédoublonne via LinkedHashMap, gère erreur gracieuse → []
- label(membre) : "Prénom NOM (numéro)" avec fallback id si entité minimaliste
- resoudre(uuid) : pour affichage initial mode édition
UI organisation-form.xhtml
- Remplacement p:inputText UUID → p:autoComplete forceSelection minQueryLength=2 queryDelay=300
- Placeholder "Tapez 2+ lettres du nom ou prénom..."
- Stocke UUID, affiche label humain — DTO unchanged côté backend
Tests (8 tests, logique pure sans mock REST)
- label × 6 (null, complet, sans numéro, nom seul, prénom seul, fallback id)
- suggest × 2 (query null/blank → liste vide sans appel réseau)
- resoudre × 1 (id null)
ACTION USER : `mvn install` côté unionflow-server-api 1.0.9 puis tester web local.
---
.../view/ComplianceOfficerPickerBean.java | 104 ++++++++++++++++++
.../ui/includes/organisation-form.xhtml | 19 +++-
.../view/ComplianceOfficerPickerBeanTest.java | 96 ++++++++++++++++
3 files changed, 213 insertions(+), 6 deletions(-)
create mode 100644 src/main/java/dev/lions/unionflow/client/view/ComplianceOfficerPickerBean.java
create mode 100644 src/test/java/dev/lions/unionflow/client/view/ComplianceOfficerPickerBeanTest.java
diff --git a/src/main/java/dev/lions/unionflow/client/view/ComplianceOfficerPickerBean.java b/src/main/java/dev/lions/unionflow/client/view/ComplianceOfficerPickerBean.java
new file mode 100644
index 0000000..e8c9323
--- /dev/null
+++ b/src/main/java/dev/lions/unionflow/client/view/ComplianceOfficerPickerBean.java
@@ -0,0 +1,104 @@
+package dev.lions.unionflow.client.view;
+
+import dev.lions.unionflow.client.service.MembreService;
+import dev.lions.unionflow.server.api.dto.membre.response.MembreResponse;
+import dev.lions.unionflow.server.api.dto.membre.response.MembreSummaryResponse;
+import jakarta.enterprise.context.ApplicationScoped;
+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 picker pour la sélection du Compliance Officer (Sprint 14 — Instr. BCEAO 001-03-2025).
+ *
+ *
Réutilise {@link MembreService} existant — DRY strict, aucun nouveau REST client.
+ *
+ *
Usage XHTML :
+ *
+ * <p:autoComplete value="#{model.complianceOfficerId}"
+ * completeMethod="#{complianceOfficerPickerBean.suggest}"
+ * var="m" itemLabel="#{complianceOfficerPickerBean.label(m)}"
+ * itemValue="#{m.id}" forceSelection="true" />
+ *
+ *
+ * @since 2026-04-25 (Sprint 14)
+ */
+@Named("complianceOfficerPickerBean")
+@ApplicationScoped
+public class ComplianceOfficerPickerBean implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+ private static final Logger LOG = Logger.getLogger(ComplianceOfficerPickerBean.class);
+
+ @Inject @RestClient MembreService membreService;
+
+ /**
+ * Suggère des membres correspondant à la requête (utilisé par {@code p:autoComplete.completeMethod}).
+ * Recherche via nom OU prénom.
+ */
+ public List suggest(String query) {
+ if (query == null || query.isBlank()) return Collections.emptyList();
+ try {
+ // Recherche larges : nom OU prénom contenant la query
+ List byNom = membreService.rechercher(
+ query, null, null, null, null, null, 0, 10);
+ List byPrenom = membreService.rechercher(
+ null, query, null, null, null, null, 0, 10);
+
+ java.util.LinkedHashMap uniques = new java.util.LinkedHashMap<>();
+ for (MembreResponse m : byNom) uniques.putIfAbsent(m.getId(), toSummary(m));
+ for (MembreResponse m : byPrenom) uniques.putIfAbsent(m.getId(), toSummary(m));
+ return new java.util.ArrayList<>(uniques.values());
+ } catch (Exception e) {
+ LOG.warnf("Suggest membres failed for query='%s' : %s", query, e.getMessage());
+ return Collections.emptyList();
+ }
+ }
+
+ /** Label de présentation : "Prénom NOM (numéro)". */
+ public String label(MembreSummaryResponse m) {
+ if (m == null) return "";
+ StringBuilder sb = new StringBuilder();
+ if (m.getPrenom() != null) sb.append(m.getPrenom());
+ if (m.getNom() != null) {
+ if (sb.length() > 0) sb.append(' ');
+ sb.append(m.getNom().toUpperCase());
+ }
+ if (m.getNumeroMembre() != null && !m.getNumeroMembre().isBlank()) {
+ sb.append(" (").append(m.getNumeroMembre()).append(')');
+ }
+ return sb.length() == 0 ? "(membre " + m.getId() + ")" : sb.toString();
+ }
+
+ /** Résolution UUID → membre pour affichage initial du form en mode édition. */
+ public MembreSummaryResponse resoudre(UUID id) {
+ if (id == null) return null;
+ try {
+ MembreResponse m = membreService.obtenirParId(id);
+ return toSummary(m);
+ } catch (Exception e) {
+ LOG.warnf("Résolution membre id=%s échouée : %s", id, e.getMessage());
+ return null;
+ }
+ }
+
+ // ── Mapping interne ────────────────────────────────────────────────────
+
+ private MembreSummaryResponse toSummary(MembreResponse m) {
+ if (m == null) return null;
+ MembreSummaryResponse s = new MembreSummaryResponse();
+ s.setId(m.getId());
+ s.setNom(m.getNom());
+ s.setPrenom(m.getPrenom());
+ s.setEmail(m.getEmail());
+ s.setTelephone(m.getTelephone());
+ s.setNumeroMembre(m.getNumeroMembre());
+ s.setStatutCompte(m.getStatutCompte());
+ return s;
+ }
+}
diff --git a/src/main/resources/META-INF/resources/ui/includes/organisation-form.xhtml b/src/main/resources/META-INF/resources/ui/includes/organisation-form.xhtml
index 4540107..41601bf 100644
--- a/src/main/resources/META-INF/resources/ui/includes/organisation-form.xhtml
+++ b/src/main/resources/META-INF/resources/ui/includes/organisation-form.xhtml
@@ -674,15 +674,22 @@
-
+
-
-
+
+ value="Désignation obligatoire selon Instr. BCEAO 001-03-2025 (LBC/FT). Membre rattaché à la direction, distinct du trésorier (séparation des pouvoirs)." />
Instr. BCEAO 001-03-2025 — LBC/FT
diff --git a/src/test/java/dev/lions/unionflow/client/view/ComplianceOfficerPickerBeanTest.java b/src/test/java/dev/lions/unionflow/client/view/ComplianceOfficerPickerBeanTest.java
new file mode 100644
index 0000000..c1fb4bd
--- /dev/null
+++ b/src/test/java/dev/lions/unionflow/client/view/ComplianceOfficerPickerBeanTest.java
@@ -0,0 +1,96 @@
+package dev.lions.unionflow.client.view;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import dev.lions.unionflow.server.api.dto.membre.response.MembreSummaryResponse;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class ComplianceOfficerPickerBeanTest {
+
+ private ComplianceOfficerPickerBean bean;
+
+ @BeforeEach
+ void setUp() {
+ bean = new ComplianceOfficerPickerBean();
+ }
+
+ // ── label ────────────────────────────────────────────────────────────
+
+ @Test
+ @DisplayName("label — null → vide")
+ void labelNull() {
+ assertEquals("", bean.label(null));
+ }
+
+ @Test
+ @DisplayName("label — Prénom NOM (numéro)")
+ void labelComplet() {
+ MembreSummaryResponse m = new MembreSummaryResponse();
+ m.setPrenom("Jean Pierre");
+ m.setNom("dupont");
+ m.setNumeroMembre("MBR-2026-0042");
+ assertEquals("Jean Pierre DUPONT (MBR-2026-0042)", bean.label(m));
+ }
+
+ @Test
+ @DisplayName("label — sans numéro membre")
+ void labelSansNumero() {
+ MembreSummaryResponse m = new MembreSummaryResponse();
+ m.setPrenom("Marie");
+ m.setNom("Kone");
+ assertEquals("Marie KONE", bean.label(m));
+ }
+
+ @Test
+ @DisplayName("label — uniquement nom")
+ void labelNomSeul() {
+ MembreSummaryResponse m = new MembreSummaryResponse();
+ m.setNom("Diallo");
+ assertEquals("DIALLO", bean.label(m));
+ }
+
+ @Test
+ @DisplayName("label — uniquement prénom")
+ void labelPrenomSeul() {
+ MembreSummaryResponse m = new MembreSummaryResponse();
+ m.setPrenom("Aminata");
+ assertEquals("Aminata", bean.label(m));
+ }
+
+ @Test
+ @DisplayName("label — entité minimaliste sans nom/prénom → fallback id")
+ void labelMinimaliste() {
+ MembreSummaryResponse m = new MembreSummaryResponse();
+ m.setId(java.util.UUID.fromString("00000000-0000-0000-0000-000000000001"));
+ String label = bean.label(m);
+ assertTrue(label.contains("00000000-0000-0000-0000-000000000001"));
+ assertTrue(label.startsWith("(membre"));
+ }
+
+ // ── suggest avec query vide/null ──────────────────────────────────────
+
+ @Test
+ @DisplayName("suggest — query null → liste vide")
+ void suggestNull() {
+ // pas besoin de mock REST : retour direct sans appel réseau
+ assertTrue(bean.suggest(null).isEmpty());
+ }
+
+ @Test
+ @DisplayName("suggest — query blank → liste vide")
+ void suggestBlank() {
+ assertTrue(bean.suggest(" ").isEmpty());
+ }
+
+ // ── resoudre id null ──────────────────────────────────────────────────
+
+ @Test
+ @DisplayName("resoudre — UUID null → null")
+ void resoudreNull() {
+ assertNull(bean.resoudre(null));
+ }
+}