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)); + } +}