feat(sprint-14 web 2026-04-25): picker p:autoComplete pour Compliance Officer (UX vs UUID textuel)
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.
This commit is contained in:
@@ -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).
|
||||||
|
*
|
||||||
|
* <p>Réutilise {@link MembreService} existant — DRY strict, aucun nouveau REST client.
|
||||||
|
*
|
||||||
|
* <p>Usage XHTML :
|
||||||
|
* <pre>
|
||||||
|
* <p:autoComplete value="#{model.complianceOfficerId}"
|
||||||
|
* completeMethod="#{complianceOfficerPickerBean.suggest}"
|
||||||
|
* var="m" itemLabel="#{complianceOfficerPickerBean.label(m)}"
|
||||||
|
* itemValue="#{m.id}" forceSelection="true" />
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @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<MembreSummaryResponse> suggest(String query) {
|
||||||
|
if (query == null || query.isBlank()) return Collections.emptyList();
|
||||||
|
try {
|
||||||
|
// Recherche larges : nom OU prénom contenant la query
|
||||||
|
List<MembreResponse> byNom = membreService.rechercher(
|
||||||
|
query, null, null, null, null, null, 0, 10);
|
||||||
|
List<MembreResponse> byPrenom = membreService.rechercher(
|
||||||
|
null, query, null, null, null, null, 0, 10);
|
||||||
|
|
||||||
|
java.util.LinkedHashMap<UUID, MembreSummaryResponse> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -674,15 +674,22 @@
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Compliance Officer -->
|
<!-- Compliance Officer (picker autoComplete) -->
|
||||||
<div class="field col-12 md:col-6">
|
<div class="field col-12 md:col-6">
|
||||||
<p:outputLabel for="complianceOfficer" value="Compliance Officer (UUID membre)"
|
<p:outputLabel for="complianceOfficer" value="Compliance Officer"
|
||||||
styleClass="font-semibold" />
|
styleClass="font-semibold" />
|
||||||
<p:inputText id="complianceOfficer"
|
<p:autoComplete id="complianceOfficer"
|
||||||
value="#{model.complianceOfficerId}"
|
value="#{model.complianceOfficerId}"
|
||||||
placeholder="00000000-0000-0000-0000-000000000000" />
|
completeMethod="#{complianceOfficerPickerBean.suggest}"
|
||||||
|
var="m"
|
||||||
|
itemLabel="#{complianceOfficerPickerBean.label(m)}"
|
||||||
|
itemValue="#{m.id}"
|
||||||
|
forceSelection="true"
|
||||||
|
minQueryLength="2"
|
||||||
|
queryDelay="300"
|
||||||
|
placeholder="Tapez 2+ lettres du nom ou prénom..." />
|
||||||
<p:tooltip for="complianceOfficer" position="top"
|
<p:tooltip for="complianceOfficer" position="top"
|
||||||
value="Désignation obligatoire selon Instr. BCEAO 001-03-2025 (LBC/FT). Doit être un membre rattaché à la direction, distinct du trésorier (séparation des pouvoirs)." />
|
value="Désignation obligatoire selon Instr. BCEAO 001-03-2025 (LBC/FT). Membre rattaché à la direction, distinct du trésorier (séparation des pouvoirs)." />
|
||||||
<small class="text-500">
|
<small class="text-500">
|
||||||
<i class="pi pi-shield mr-1"/>
|
<i class="pi pi-shield mr-1"/>
|
||||||
Instr. BCEAO 001-03-2025 — LBC/FT
|
Instr. BCEAO 001-03-2025 — LBC/FT
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user