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:
dahoud
2026-04-25 15:36:37 +00:00
parent 11a1299bc7
commit e936af7d39
3 changed files with 213 additions and 6 deletions

View File

@@ -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>
* &lt;p:autoComplete value="#{model.complianceOfficerId}"
* completeMethod="#{complianceOfficerPickerBean.suggest}"
* var="m" itemLabel="#{complianceOfficerPickerBean.label(m)}"
* itemValue="#{m.id}" forceSelection="true" /&gt;
* </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;
}
}