Update client submodule to latest commit (removed lions-user-manager dependency)

This commit is contained in:
dahoud
2025-12-10 00:21:36 +00:00
parent 105b1cec7f
commit 6d0dd660f2
8 changed files with 1334 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
package dev.lions.unionflow.client.service;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.jboss.resteasy.reactive.PartType;
import java.util.UUID;
public class MembreImportMultipartForm {
@FormParam("file")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
public byte[] file;
@FormParam("fileName")
public String fileName;
@FormParam("organisationId")
public UUID organisationId;
@FormParam("typeMembreDefaut")
public String typeMembreDefaut;
@FormParam("mettreAJourExistants")
public boolean mettreAJourExistants;
@FormParam("ignorerErreurs")
public boolean ignorerErreurs;
}

View File

@@ -0,0 +1,322 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.client.service.MembreService;
import dev.lions.unionflow.client.service.AssociationService;
import dev.lions.unionflow.server.api.dto.organisation.OrganisationDTO;
import dev.lions.unionflow.client.dto.AssociationDTO;
import lombok.Getter;
import lombok.Setter;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import jakarta.inject.Inject;
import jakarta.annotation.PostConstruct;
import jakarta.faces.context.FacesContext;
import jakarta.faces.application.FacesMessage;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import java.io.IOException;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.logging.Logger;
@Named("membreExportBean")
@ViewScoped
@Getter
@Setter
public class MembreExportBean implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger LOGGER = Logger.getLogger(MembreExportBean.class.getName());
@Inject
@RestClient
MembreService membreService;
@Inject
@RestClient
AssociationService associationService;
// Configuration de l'export
private String formatExport = "EXCEL";
private String scopeExport = "TOUS";
private List<String> colonnesExport = new ArrayList<>();
// Filtres
private String statutFilter = "";
private String typeFilter = "";
private UUID organisationId;
private LocalDate dateAdhesionDebut;
private LocalDate dateAdhesionFin;
// Options d'export
private boolean inclureHeaders = true;
private boolean formaterDates = true;
private boolean inclureStatistiques = false;
private boolean chiffrerDonnees = false;
private String motDePasseExport = "";
// Organisations disponibles
private List<OrganisationDTO> organisationsDisponibles = new ArrayList<>();
// Statistiques
private int totalMembres = 0;
private int membresActifs = 0;
private int membresInactifs = 0;
private int nombreMembresAExporter = 0;
// Historique des exports
private List<ExportHistorique> historiqueExports = new ArrayList<>();
@PostConstruct
public void init() {
chargerOrganisations();
chargerStatistiques();
initialiserColonnesExport();
}
private void chargerOrganisations() {
organisationsDisponibles = new ArrayList<>();
try {
List<AssociationDTO> associations = associationService.listerToutes(0, 1000);
for (AssociationDTO assoc : associations) {
OrganisationDTO org = new OrganisationDTO();
org.setId(assoc.getId());
org.setNom(assoc.getNom());
org.setVille(assoc.getVille());
organisationsDisponibles.add(org);
}
LOGGER.info("Chargement de " + organisationsDisponibles.size() + " organisations disponibles");
} catch (Exception e) {
LOGGER.severe("Erreur lors du chargement des organisations: " + e.getMessage());
}
}
private void chargerStatistiques() {
try {
MembreService.StatistiquesMembreDTO stats = membreService.obtenirStatistiques();
if (stats != null) {
totalMembres = stats.getTotalMembres() != null ? stats.getTotalMembres().intValue() : 0;
membresActifs = stats.getMembresActifs() != null ? stats.getMembresActifs().intValue() : 0;
membresInactifs = stats.getMembresInactifs() != null ? stats.getMembresInactifs().intValue() : 0;
}
actualiserCompteur();
} catch (Exception e) {
LOGGER.severe("Erreur lors du chargement des statistiques: " + e.getMessage());
}
}
private void initialiserColonnesExport() {
colonnesExport = new ArrayList<>();
colonnesExport.add("PERSO");
colonnesExport.add("CONTACT");
colonnesExport.add("ADHESION");
}
public void actualiserCompteur() {
actualiserCompteur(null);
}
public void actualiserCompteur(jakarta.faces.event.AjaxBehaviorEvent event) {
try {
// Appel au backend pour obtenir le comptage exact selon les filtres
String statut = null;
if ("ACTIFS".equals(scopeExport)) {
statut = "ACTIF";
} else if ("INACTIFS".equals(scopeExport)) {
statut = "INACTIF";
} else if (statutFilter != null && !statutFilter.isEmpty()) {
statut = statutFilter;
}
String dateAdhesionDebutStr = dateAdhesionDebut != null ? dateAdhesionDebut.toString() : null;
String dateAdhesionFinStr = dateAdhesionFin != null ? dateAdhesionFin.toString() : null;
Long count = membreService.compterMembresPourExport(
organisationId,
statut,
typeFilter != null && !typeFilter.isEmpty() ? typeFilter : null,
dateAdhesionDebutStr,
dateAdhesionFinStr
);
nombreMembresAExporter = count != null ? count.intValue() : 0;
LOGGER.info("Comptage des membres pour export: " + nombreMembresAExporter);
} catch (Exception e) {
LOGGER.severe("Erreur lors de l'actualisation du compteur: " + e.getMessage());
// Fallback sur estimation basée sur les statistiques
if ("TOUS".equals(scopeExport)) {
nombreMembresAExporter = totalMembres;
} else if ("ACTIFS".equals(scopeExport)) {
nombreMembresAExporter = membresActifs;
} else if ("INACTIFS".equals(scopeExport)) {
nombreMembresAExporter = membresInactifs;
} else {
nombreMembresAExporter = 0;
}
}
}
public void exporterMembres() {
if (colonnesExport == null || colonnesExport.isEmpty()) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_WARN, "Attention",
"Veuillez sélectionner au moins une catégorie de colonnes à exporter"));
return;
}
if (nombreMembresAExporter == 0) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_WARN, "Attention",
"Aucun membre ne correspond aux critères sélectionnés"));
return;
}
try {
LOGGER.info("Export des membres: format=" + formatExport + ", nombre=" + nombreMembresAExporter);
String dateAdhesionDebutStr = dateAdhesionDebut != null ? dateAdhesionDebut.toString() : null;
String dateAdhesionFinStr = dateAdhesionFin != null ? dateAdhesionFin.toString() : null;
// Générer un mot de passe aléatoire si le chiffrement est demandé
String motDePasse = null;
if (chiffrerDonnees) {
if (motDePasseExport != null && !motDePasseExport.trim().isEmpty()) {
motDePasse = motDePasseExport;
} else {
// Générer un mot de passe aléatoire de 12 caractères
motDePasse = genererMotDePasseAleatoire();
}
}
byte[] exportData = membreService.exporterExcel(
formatExport,
organisationId,
statutFilter,
typeFilter,
dateAdhesionDebutStr,
dateAdhesionFinStr,
colonnesExport,
inclureHeaders,
formaterDates,
inclureStatistiques && "EXCEL".equals(formatExport), // Statistiques uniquement pour Excel
motDePasse
);
FacesContext facesContext = FacesContext.getCurrentInstance();
HttpServletResponse response = (HttpServletResponse) facesContext.getExternalContext().getResponse();
response.reset();
String contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
String extension = "xlsx";
if ("CSV".equals(formatExport)) {
contentType = "text/csv";
extension = "csv";
}
response.setContentType(contentType);
response.setHeader("Content-Disposition", "attachment; filename=\"membres_export_" +
LocalDate.now() + "." + extension + "\"");
response.setContentLength(exportData.length);
response.getOutputStream().write(exportData);
response.getOutputStream().flush();
facesContext.responseComplete();
// Ajouter à l'historique
ExportHistorique historique = new ExportHistorique();
historique.setDate(LocalDateTime.now());
historique.setFormat(formatExport);
historique.setNombreMembres(nombreMembresAExporter);
historique.setTaille(formatTaille(exportData.length));
historiqueExports.add(0, historique); // Ajouter au début
// Afficher le mot de passe si le chiffrement était demandé
if (chiffrerDonnees && motDePasse != null) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Fichier protégé",
"Le fichier a été protégé par un mot de passe. " +
"Mot de passe: " + motDePasse +
" (Note: Le fichier est protégé contre la modification, mais peut toujours être ouvert)"));
}
LOGGER.info("Export généré et téléchargé: " + exportData.length + " bytes");
} catch (IOException e) {
LOGGER.severe("Erreur lors du téléchargement de l'export: " + e.getMessage());
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur",
"Impossible de télécharger l'export: " + e.getMessage()));
} catch (Exception e) {
LOGGER.severe("Erreur lors de l'export: " + e.getMessage());
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur",
"Impossible d'exporter les membres: " + e.getMessage()));
}
}
private String formatTaille(long bytes) {
if (bytes < 1024) {
return bytes + " B";
} else if (bytes < 1024 * 1024) {
return String.format("%.2f KB", bytes / 1024.0);
} else {
return String.format("%.2f MB", bytes / (1024.0 * 1024.0));
}
}
public void telechargerExport(ExportHistorique export) {
// L'historique est stocké localement dans la session, pas de téléchargement depuis le serveur
LOGGER.info("Export historique consulté: " + export.getDate() + " - " + export.getFormat());
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Information",
"L'export du " + export.getDate().format(java.time.format.DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm")) +
" n'est plus disponible. Veuillez générer un nouvel export."));
}
private String genererMotDePasseAleatoire() {
String caracteres = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%&*";
java.util.Random random = new java.util.Random();
StringBuilder motDePasse = new StringBuilder(12);
for (int i = 0; i < 12; i++) {
motDePasse.append(caracteres.charAt(random.nextInt(caracteres.length())));
}
return motDePasse.toString();
}
public void reinitialiser() {
formatExport = "EXCEL";
scopeExport = "TOUS";
colonnesExport = new ArrayList<>();
colonnesExport.add("PERSO");
colonnesExport.add("CONTACT");
colonnesExport.add("ADHESION");
statutFilter = "";
typeFilter = "";
organisationId = null;
dateAdhesionDebut = null;
dateAdhesionFin = null;
inclureHeaders = true;
formaterDates = true;
inclureStatistiques = false;
chiffrerDonnees = false;
motDePasseExport = "";
actualiserCompteur();
}
// Classe interne pour l'historique des exports
@Getter
@Setter
public static class ExportHistorique implements Serializable {
private LocalDateTime date;
private String format;
private int nombreMembres;
private String taille;
}
}

View File

@@ -0,0 +1,213 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.client.service.MembreService;
import dev.lions.unionflow.client.service.MembreImportMultipartForm;
import dev.lions.unionflow.client.service.AssociationService;
import dev.lions.unionflow.server.api.dto.organisation.OrganisationDTO;
import dev.lions.unionflow.client.dto.AssociationDTO;
import lombok.Getter;
import lombok.Setter;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import jakarta.inject.Inject;
import jakarta.annotation.PostConstruct;
import jakarta.faces.context.FacesContext;
import jakarta.faces.application.FacesMessage;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.primefaces.event.FileUploadEvent;
import org.primefaces.model.file.UploadedFile;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.logging.Logger;
@Named("membreImportBean")
@ViewScoped
@Getter
@Setter
public class MembreImportBean implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger LOGGER = Logger.getLogger(MembreImportBean.class.getName());
@Inject
@RestClient
MembreService membreService;
@Inject
@RestClient
AssociationService associationService;
// Fichier à importer
private UploadedFile fichierImport;
// Options d'import
private boolean mettreAJourExistants = false;
private boolean ignorerErreurs = false;
private UUID organisationId;
private String typeMembreDefaut = "";
// Organisations disponibles
private List<OrganisationDTO> organisationsDisponibles = new ArrayList<>();
// Résultat de l'import
private ResultatImport resultatImport;
@PostConstruct
public void init() {
chargerOrganisations();
}
private void chargerOrganisations() {
organisationsDisponibles = new ArrayList<>();
try {
List<AssociationDTO> associations = associationService.listerToutes(0, 1000);
for (AssociationDTO assoc : associations) {
OrganisationDTO org = new OrganisationDTO();
org.setId(assoc.getId());
org.setNom(assoc.getNom());
org.setVille(assoc.getVille());
organisationsDisponibles.add(org);
}
LOGGER.info("Chargement de " + organisationsDisponibles.size() + " organisations disponibles");
} catch (Exception e) {
LOGGER.severe("Erreur lors du chargement des organisations: " + e.getMessage());
}
}
/**
* Gère l'upload du fichier (appelé par PrimeFaces FileUpload)
*/
public void handleFileUpload(FileUploadEvent event) {
fichierImport = event.getFile();
LOGGER.info("Fichier sélectionné: " + (fichierImport != null ? fichierImport.getFileName() : "null"));
}
/**
* Lance l'import des membres
*/
public void importerMembres() {
if (fichierImport == null || fichierImport.getFileName() == null || fichierImport.getFileName().isEmpty()) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_WARN, "Attention",
"Veuillez sélectionner un fichier à importer"));
return;
}
try {
LOGGER.info("Import du fichier: " + fichierImport.getFileName());
byte[] fileContent = fichierImport.getContent();
String fileName = fichierImport.getFileName();
// Créer le formulaire multipart
MembreImportMultipartForm form = new MembreImportMultipartForm();
form.file = fileContent;
form.fileName = fileName;
form.organisationId = organisationId;
form.typeMembreDefaut = typeMembreDefaut != null && !typeMembreDefaut.isEmpty() ? typeMembreDefaut : "ACTIF";
form.mettreAJourExistants = mettreAJourExistants;
form.ignorerErreurs = ignorerErreurs;
// Appeler le service REST
MembreService.ResultatImportDTO result = membreService.importerDonnees(form);
// Convertir le résultat
resultatImport = new ResultatImport();
resultatImport.setTotalTraite(result.getTotalLignes() != null ? result.getTotalLignes() : 0);
resultatImport.setReussis(result.getLignesTraitees() != null ? result.getLignesTraitees() : 0);
resultatImport.setEchecs(result.getLignesErreur() != null ? result.getLignesErreur() : 0);
resultatImport.setIgnores(0);
// Convertir les erreurs
List<ErreurImport> erreursList = new ArrayList<>();
if (result.getErreurs() != null) {
for (int i = 0; i < result.getErreurs().size(); i++) {
ErreurImport erreur = new ErreurImport();
erreur.setLigne(i + 1);
erreur.setMessage(result.getErreurs().get(i));
erreursList.add(erreur);
}
}
resultatImport.setErreurs(erreursList);
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès",
"Import terminé: " + resultatImport.getReussis() + " membres importés avec succès" +
(resultatImport.getEchecs() > 0 ? ", " + resultatImport.getEchecs() + " erreurs" : "")));
// Réinitialiser le fichier
fichierImport = null;
} catch (Exception e) {
LOGGER.severe("Erreur lors de l'import: " + e.getMessage());
LOGGER.log(java.util.logging.Level.SEVERE, "Détails de l'erreur d'import", e);
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur",
"Impossible d'importer le fichier: " + e.getMessage()));
}
}
public void telechargerModele() {
try {
LOGGER.info("Téléchargement du modèle d'import");
byte[] modele = membreService.telechargerModeleImport();
FacesContext facesContext = FacesContext.getCurrentInstance();
HttpServletResponse response = (HttpServletResponse) facesContext.getExternalContext().getResponse();
response.reset();
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=\"modele_import_membres.xlsx\"");
response.setContentLength(modele.length);
response.getOutputStream().write(modele);
response.getOutputStream().flush();
facesContext.responseComplete();
LOGGER.info("Modèle d'import téléchargé");
} catch (IOException e) {
LOGGER.severe("Erreur lors du téléchargement du modèle: " + e.getMessage());
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur",
"Impossible de télécharger le modèle: " + e.getMessage()));
} catch (Exception e) {
LOGGER.severe("Erreur lors du téléchargement du modèle: " + e.getMessage());
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur",
"Impossible de télécharger le modèle: " + e.getMessage()));
}
}
public void reinitialiser() {
fichierImport = null;
mettreAJourExistants = false;
ignorerErreurs = false;
organisationId = null;
typeMembreDefaut = "";
resultatImport = null;
}
// Classe interne pour le résultat de l'import
@Getter
@Setter
public static class ResultatImport implements Serializable {
private int totalTraite;
private int reussis;
private int echecs;
private int ignores;
private List<ErreurImport> erreurs = new ArrayList<>();
}
@Getter
@Setter
public static class ErreurImport implements Serializable {
private int ligne;
private String message;
}
}

View File

@@ -0,0 +1,310 @@
<!DOCTYPE html>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/templates/main-template.xhtml">
<ui:param name="page" value="#{membreExportBean}"/>
<ui:define name="title">Export des Membres - UnionFlow</ui:define>
<ui:define name="content">
<!-- En-tête -->
<ui:include src="/templates/components/layout/page-header.xhtml">
<ui:param name="icon" value="pi pi-download text-blue-500" />
<ui:param name="title" value="Export des Membres" />
<ui:param name="description" value="Exportez les données des membres dans différents formats" />
<ui:define name="actions">
<h:form id="formActionsEntete">
<div class="flex gap-2">
<ui:include src="/templates/components/buttons/button-secondary.xhtml">
<ui:param name="value" value="Retour" />
<ui:param name="icon" value="pi pi-arrow-left" />
<ui:param name="outcome" value="/pages/secure/membre/liste" />
<ui:param name="outlined" value="true" />
</ui:include>
</div>
</h:form>
</ui:define>
</ui:include>
<!-- Statistiques -->
<div class="grid mb-3">
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Total Membres" />
<ui:param name="value" value="#{membreExportBean.totalMembres}" />
<ui:param name="icon" value="pi-users" />
<ui:param name="iconColor" value="blue-600" />
<ui:param name="colSize" value="col-12 md:col-4" />
</ui:include>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Membres Actifs" />
<ui:param name="value" value="#{membreExportBean.membresActifs}" />
<ui:param name="icon" value="pi-check-circle" />
<ui:param name="iconColor" value="green-600" />
<ui:param name="colSize" value="col-12 md:col-4" />
</ui:include>
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Membres Inactifs" />
<ui:param name="value" value="#{membreExportBean.membresInactifs}" />
<ui:param name="icon" value="pi-times-circle" />
<ui:param name="iconColor" value="orange-600" />
<ui:param name="colSize" value="col-12 md:col-4" />
</ui:include>
</div>
<!-- Formulaire d'export -->
<div class="card">
<h:form id="formExport">
<h5 class="mb-4">Configuration de l'export</h5>
<div class="grid">
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="formatExport" value="Format d'export *" />
<p:selectOneMenu id="formatExport" value="#{membreExportBean.formatExport}" required="true" styleClass="w-full">
<f:selectItem itemLabel="Excel (.xlsx)" itemValue="EXCEL" />
<f:selectItem itemLabel="CSV (.csv)" itemValue="CSV" />
</p:selectOneMenu>
</div>
</div>
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="scopeExport" value="Portée de l'export *" />
<p:selectOneMenu id="scopeExport" value="#{membreExportBean.scopeExport}" required="true" styleClass="w-full">
<f:selectItem itemLabel="Tous les membres" itemValue="TOUS" />
<f:selectItem itemLabel="Membres actifs uniquement" itemValue="ACTIFS" />
<f:selectItem itemLabel="Membres inactifs uniquement" itemValue="INACTIFS" />
<f:selectItem itemLabel="Membres sélectionnés" itemValue="SELECTION" />
<p:ajax event="change" listener="#{membreExportBean.actualiserCompteur}" update=":formExport" />
</p:selectOneMenu>
</div>
</div>
</div>
<ui:decorate template="/templates/components/forms/form-section.xhtml">
<ui:param name="title" value="Colonnes à exporter" />
<ui:define name="content">
<div class="field">
<p:outputLabel for="colonnesExport" value="Sélectionnez les colonnes à inclure *" />
<p:selectCheckboxMenu id="colonnesExport"
value="#{membreExportBean.colonnesExport}"
multiple="true"
styleClass="w-full">
<f:selectItem itemLabel="Informations personnelles (Nom, Prénom, Date naissance, Genre)" itemValue="PERSO" />
<f:selectItem itemLabel="Coordonnées (Email, Téléphone, Adresse)" itemValue="CONTACT" />
<f:selectItem itemLabel="Informations adhésion (Date adhésion, Type membre, Statut)" itemValue="ADHESION" />
<f:selectItem itemLabel="Cotisations (Statut cotisations, Dernier paiement)" itemValue="COTISATIONS" />
<f:selectItem itemLabel="Participation événements (Taux participation, Événements)" itemValue="EVENEMENTS" />
<f:selectItem itemLabel="Organisation (Entité, Ville)" itemValue="ORGANISATION" />
<f:selectItem itemLabel="Famille (Membres de famille déclarés)" itemValue="FAMILLE" />
</p:selectCheckboxMenu>
<small class="text-600">Sélectionnez au moins une catégorie de colonnes</small>
</div>
</ui:define>
</ui:decorate>
<ui:decorate template="/templates/components/forms/form-section.xhtml">
<ui:param name="title" value="Filtres optionnels" />
<ui:define name="content">
<div class="grid">
<div class="col-12 md:col-4">
<div class="field">
<p:outputLabel for="statutFilter" value="Statut" />
<p:selectOneMenu id="statutFilter" value="#{membreExportBean.statutFilter}" styleClass="w-full">
<f:selectItem itemLabel="Tous les statuts" itemValue="" />
<f:selectItem itemLabel="Actif" itemValue="ACTIF" />
<f:selectItem itemLabel="Inactif" itemValue="INACTIF" />
<f:selectItem itemLabel="Suspendu" itemValue="SUSPENDU" />
<f:selectItem itemLabel="Radié" itemValue="RADIE" />
<p:ajax event="change" listener="#{membreExportBean.actualiserCompteur}" update=":formExport" />
</p:selectOneMenu>
</div>
</div>
<div class="col-12 md:col-4">
<div class="field">
<p:outputLabel for="typeFilter" value="Type de membre" />
<p:selectOneMenu id="typeFilter" value="#{membreExportBean.typeFilter}" styleClass="w-full">
<f:selectItem itemLabel="Tous les types" itemValue="" />
<f:selectItem itemLabel="Actif" itemValue="ACTIF" />
<f:selectItem itemLabel="Associé" itemValue="ASSOCIE" />
<f:selectItem itemLabel="Bienfaiteur" itemValue="BIENFAITEUR" />
<f:selectItem itemLabel="Honoraire" itemValue="HONORAIRE" />
<p:ajax event="change" listener="#{membreExportBean.actualiserCompteur}" update=":formExport" />
</p:selectOneMenu>
</div>
</div>
<div class="col-12 md:col-4">
<div class="field">
<p:outputLabel for="organisationFilter" value="Organisation" />
<p:selectOneMenu id="organisationFilter" value="#{membreExportBean.organisationId}" styleClass="w-full">
<f:selectItem itemLabel="Toutes les organisations" itemValue="" />
<f:selectItems value="#{membreExportBean.organisationsDisponibles}"
var="org"
itemLabel="#{org.nom} (#{org.ville})"
itemValue="#{org.id}" />
<p:ajax event="change" listener="#{membreExportBean.actualiserCompteur}" update=":formExport" />
</p:selectOneMenu>
</div>
</div>
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="dateAdhesionDebut" value="Adhésion après le" />
<p:calendar id="dateAdhesionDebut"
value="#{membreExportBean.dateAdhesionDebut}"
showIcon="true"
navigator="true"
locale="fr"
pattern="dd/MM/yyyy"
styleClass="w-full">
<p:ajax event="dateSelect" listener="#{membreExportBean.actualiserCompteur}" update=":formExport" />
</p:calendar>
</div>
</div>
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="dateAdhesionFin" value="Adhésion avant le" />
<p:calendar id="dateAdhesionFin"
value="#{membreExportBean.dateAdhesionFin}"
showIcon="true"
navigator="true"
locale="fr"
pattern="dd/MM/yyyy"
styleClass="w-full">
<p:ajax event="dateSelect" listener="#{membreExportBean.actualiserCompteur}" update=":formExport" />
</p:calendar>
</div>
</div>
</div>
</ui:define>
</ui:decorate>
<!-- Options d'export -->
<ui:decorate template="/templates/components/forms/form-section.xhtml">
<ui:param name="title" value="Options d'export" />
<ui:define name="content">
<div class="grid">
<div class="col-12 md:col-6">
<div class="field">
<p:selectBooleanCheckbox id="inclureHeaders" value="#{membreExportBean.inclureHeaders}" />
<p:outputLabel for="inclureHeaders" value="Inclure les en-têtes de colonnes" />
</div>
</div>
<div class="col-12 md:col-6">
<div class="field">
<p:selectBooleanCheckbox id="formaterDates" value="#{membreExportBean.formaterDates}" />
<p:outputLabel for="formaterDates" value="Formater les dates (DD/MM/YYYY)" />
</div>
</div>
<div class="col-12 md:col-6">
<div class="field">
<p:selectBooleanCheckbox id="inclureStatistiques" value="#{membreExportBean.inclureStatistiques}" />
<p:outputLabel for="inclureStatistiques" value="Inclure un onglet statistiques (Excel uniquement)" />
</div>
</div>
<div class="col-12 md:col-6">
<div class="field">
<p:selectBooleanCheckbox id="chiffrerDonnees" value="#{membreExportBean.chiffrerDonnees}" />
<p:outputLabel for="chiffrerDonnees" value="Chiffrer le fichier exporté" />
<small class="text-600 block mt-1">Le fichier sera protégé par un mot de passe (généré automatiquement ou personnalisé ci-dessous)</small>
</div>
</div>
<div class="col-12" rendered="#{membreExportBean.chiffrerDonnees}">
<div class="field">
<p:outputLabel for="motDePasseExport" value="Mot de passe personnalisé (optionnel)" />
<p:password id="motDePasseExport" value="#{membreExportBean.motDePasseExport}"
placeholder="Laisser vide pour générer automatiquement"
styleClass="w-full" />
<small class="text-600 block mt-1">Si vide, un mot de passe aléatoire sera généré et affiché après l'export</small>
</div>
</div>
</div>
</ui:define>
</ui:decorate>
<!-- Aperçu du nombre de membres à exporter -->
<div class="surface-50 p-3 border-round mb-4">
<div class="flex align-items-center justify-content-between">
<div>
<div class="font-medium mb-1">Nombre de membres à exporter :</div>
<div class="text-600">#{membreExportBean.nombreMembresAExporter} membre(s) correspond(ent) aux critères sélectionnés</div>
</div>
<ui:include src="/templates/components/buttons/button-info.xhtml">
<ui:param name="value" value="Actualiser le compteur" />
<ui:param name="icon" value="pi pi-refresh" />
<ui:param name="action" value="#{membreExportBean.actualiserCompteur}" />
<ui:param name="update" value=":formExport" />
<ui:param name="outlined" value="true" />
<ui:param name="styleClass" value="ui-button-sm" />
</ui:include>
</div>
</div>
<!-- Actions -->
<div class="flex gap-2">
<ui:include src="/templates/components/buttons/button-success.xhtml">
<ui:param name="value" value="Générer l'export" />
<ui:param name="icon" value="pi pi-download" />
<ui:param name="action" value="#{membreExportBean.exporterMembres}" />
<ui:param name="update" value="none" />
<ui:param name="disabled" value="#{membreExportBean.colonnesExport == null or membreExportBean.colonnesExport.isEmpty() or membreExportBean.nombreMembresAExporter == 0}" />
</ui:include>
<ui:include src="/templates/components/buttons/button-secondary.xhtml">
<ui:param name="value" value="Réinitialiser" />
<ui:param name="icon" value="pi pi-refresh" />
<ui:param name="action" value="#{membreExportBean.reinitialiser}" />
<ui:param name="update" value=":formExport" />
<ui:param name="outlined" value="true" />
</ui:include>
</div>
</h:form>
</div>
<!-- Historique des exports -->
<div class="card mt-3">
<h5 class="mb-3">Historique des exports</h5>
<p:dataTable value="#{membreExportBean.historiqueExports}" var="export"
styleClass="p-datatable-sm"
emptyMessage="Aucun export effectué">
<p:column headerText="Date">
<h:outputText value="#{export.date}">
<f:convertDateTime pattern="dd/MM/yyyy HH:mm" type="localDateTime" />
</h:outputText>
</p:column>
<p:column headerText="Format">
<p:tag value="#{export.format}" severity="info" />
</p:column>
<p:column headerText="Nombre de membres">
<h:outputText value="#{export.nombreMembres}" />
</p:column>
<p:column headerText="Taille">
<h:outputText value="#{export.taille}" />
</p:column>
<p:column headerText="Actions">
<ui:include src="/templates/components/buttons/button-info.xhtml">
<ui:param name="value" value="" />
<ui:param name="icon" value="pi pi-download" />
<ui:param name="action" value="#{membreExportBean.telechargerExport(export)}" />
<ui:param name="update" value="none" />
<ui:param name="title" value="Télécharger" />
<ui:param name="styleClass" value="ui-button-sm" />
</ui:include>
</p:column>
</p:dataTable>
</div>
</ui:define>
</ui:composition>

View File

@@ -0,0 +1,243 @@
<!DOCTYPE html>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/templates/main-template.xhtml">
<ui:param name="page" value="#{membreImportBean}"/>
<ui:define name="title">Import en Masse des Membres - UnionFlow</ui:define>
<ui:define name="content">
<!-- En-tête -->
<ui:include src="/templates/components/layout/page-header.xhtml">
<ui:param name="icon" value="pi pi-upload text-green-500" />
<ui:param name="title" value="Import en Masse des Membres" />
<ui:param name="description" value="Importez plusieurs membres à la fois depuis un fichier Excel" />
<ui:define name="actions">
<h:form id="formActionsEntete">
<div class="flex gap-2">
<ui:include src="/templates/components/buttons/button-secondary.xhtml">
<ui:param name="value" value="Retour" />
<ui:param name="icon" value="pi pi-arrow-left" />
<ui:param name="outcome" value="/pages/secure/membre/liste" />
<ui:param name="outlined" value="true" />
</ui:include>
</div>
</h:form>
</ui:define>
</ui:include>
<!-- Instructions -->
<div class="card mb-3">
<div class="flex align-items-start">
<i class="pi pi-info-circle text-blue-500 text-2xl mr-3"></i>
<div class="flex-1">
<h5 class="mt-0 mb-2">Instructions d'import</h5>
<p class="text-600 mb-3">
Téléchargez le modèle Excel, remplissez-le avec les données des membres, puis importez-le ici.
</p>
<div class="grid">
<div class="col-12 md:col-6">
<h6 class="mb-2">Format du fichier :</h6>
<ul class="text-600 text-sm m-0 pl-3">
<li>Format Excel (.xlsx) ou CSV (.csv)</li>
<li>Maximum 1000 lignes par import</li>
<li>Taille maximale : 10 MB</li>
</ul>
</div>
<div class="col-12 md:col-6">
<h6 class="mb-2">Colonnes requises :</h6>
<ul class="text-600 text-sm m-0 pl-3">
<li>Nom, Prénom (obligatoires)</li>
<li>Email, Téléphone (obligatoires)</li>
<li>Date de naissance, Adresse</li>
<li>Profession, Type membre</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Formulaire d'import -->
<div class="card">
<h:form id="formImport" enctype="multipart/form-data">
<h5 class="mb-4">Fichier à importer</h5>
<div class="grid">
<div class="col-12 md:col-8">
<div class="field">
<p:outputLabel for="fichierImport" value="Fichier Excel/CSV *" />
<p:fileUpload id="fichierImport"
mode="advanced"
dragDropSupport="true"
skinSimple="false"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel,text/csv"
fileLimit="1"
sizeLimit="10485760"
uploadLabel="Importer"
cancelLabel="Annuler"
chooseLabel="Sélectionner le fichier"
invalidFileMessage="Type de fichier non supporté"
fileLimitMessage="Un seul fichier autorisé"
invalidSizeMessage="Taille de fichier trop importante (max 10MB)"
fileUploadListener="#{membreImportBean.handleFileUpload}"
update=":formImport"
styleClass="w-full" />
<small class="text-600">Formats acceptés : .xlsx, .xls, .csv - Maximum 10 MB</small>
</div>
</div>
<div class="col-12 md:col-4">
<div class="field">
<p:outputLabel />
<div class="flex flex-column gap-2">
<ui:include src="/templates/components/buttons/button-info.xhtml">
<ui:param name="value" value="Télécharger modèle" />
<ui:param name="icon" value="pi pi-download" />
<ui:param name="action" value="#{membreImportBean.telechargerModele}" />
<ui:param name="update" value="none" />
<ui:param name="outlined" value="true" />
<ui:param name="styleClass" value="w-full" />
</ui:include>
</div>
</div>
</div>
</div>
<ui:decorate template="/templates/components/forms/form-section.xhtml">
<ui:param name="title" value="Options d'import" />
<ui:define name="content">
<div class="grid">
<div class="col-12 md:col-6">
<div class="field">
<p:selectBooleanCheckbox id="mettreAJourExistants" value="#{membreImportBean.mettreAJourExistants}" />
<p:outputLabel for="mettreAJourExistants" value="Mettre à jour les membres existants" />
<small class="text-600 block mt-1">Si coché, les membres existants (même email) seront mis à jour</small>
</div>
</div>
<div class="col-12 md:col-6">
<div class="field">
<p:selectBooleanCheckbox id="ignorerErreurs" value="#{membreImportBean.ignorerErreurs}" />
<p:outputLabel for="ignorerErreurs" value="Ignorer les lignes en erreur" />
<small class="text-600 block mt-1">Continuer l'import même si certaines lignes contiennent des erreurs</small>
</div>
</div>
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="organisationImport" value="Organisation par défaut" />
<p:selectOneMenu id="organisationImport" value="#{membreImportBean.organisationId}" styleClass="w-full">
<f:selectItem itemLabel="Sélectionner une organisation..." itemValue="" />
<f:selectItems value="#{membreImportBean.organisationsDisponibles}"
var="org"
itemLabel="#{org.nom} (#{org.ville})"
itemValue="#{org.id}" />
</p:selectOneMenu>
</div>
</div>
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="typeMembreImport" value="Type de membre par défaut" />
<p:selectOneMenu id="typeMembreImport" value="#{membreImportBean.typeMembreDefaut}" styleClass="w-full">
<f:selectItem itemLabel="Sélectionner..." itemValue="" />
<f:selectItem itemLabel="Actif" itemValue="ACTIF" />
<f:selectItem itemLabel="Associé" itemValue="ASSOCIE" />
<f:selectItem itemLabel="Bienfaiteur" itemValue="BIENFAITEUR" />
<f:selectItem itemLabel="Honoraire" itemValue="HONORAIRE" />
</p:selectOneMenu>
</div>
</div>
</div>
</ui:define>
</ui:decorate>
<!-- Résultats de l'import -->
<div class="mt-4" rendered="#{membreImportBean.resultatImport != null}">
<ui:decorate template="/templates/components/forms/form-section.xhtml">
<ui:param name="title" value="Résultat de l'import" />
<ui:define name="content">
<div class="grid">
<div class="col-12 md:col-3">
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Total traité" />
<ui:param name="value" value="#{membreImportBean.resultatImport.totalTraite}" />
<ui:param name="icon" value="pi-file" />
<ui:param name="iconColor" value="blue-600" />
<ui:param name="colSize" value="col-12" />
</ui:include>
</div>
<div class="col-12 md:col-3">
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Réussis" />
<ui:param name="value" value="#{membreImportBean.resultatImport.reussis}" />
<ui:param name="icon" value="pi-check" />
<ui:param name="iconColor" value="green-600" />
<ui:param name="colSize" value="col-12" />
</ui:include>
</div>
<div class="col-12 md:col-3">
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Échecs" />
<ui:param name="value" value="#{membreImportBean.resultatImport.echecs}" />
<ui:param name="icon" value="pi-times" />
<ui:param name="iconColor" value="red-600" />
<ui:param name="colSize" value="col-12" />
</ui:include>
</div>
<div class="col-12 md:col-3">
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Ignorés" />
<ui:param name="value" value="#{membreImportBean.resultatImport.ignores}" />
<ui:param name="icon" value="pi-eye-slash" />
<ui:param name="iconColor" value="orange-600" />
<ui:param name="colSize" value="col-12" />
</ui:include>
</div>
</div>
<div class="mt-3" rendered="#{not empty membreImportBean.resultatImport.erreurs}">
<h6 class="mb-2">Détails des erreurs :</h6>
<div class="surface-50 p-3 border-round">
<ui:repeat value="#{membreImportBean.resultatImport.erreurs}" var="erreur">
<div class="mb-2">
<span class="font-medium">Ligne #{erreur.ligne}:</span>
<span class="text-red-500 ml-2">#{erreur.message}</span>
</div>
</ui:repeat>
</div>
</div>
</ui:define>
</ui:decorate>
</div>
<!-- Actions -->
<div class="flex gap-2 mt-4">
<ui:include src="/templates/components/buttons/button-success.xhtml">
<ui:param name="value" value="Lancer l'import" />
<ui:param name="icon" value="pi pi-upload" />
<ui:param name="action" value="#{membreImportBean.importerMembres}" />
<ui:param name="update" value=":formImport" />
<ui:param name="disabled" value="#{membreImportBean.fichierImport == null}" />
</ui:include>
<ui:include src="/templates/components/buttons/button-secondary.xhtml">
<ui:param name="value" value="Réinitialiser" />
<ui:param name="icon" value="pi pi-refresh" />
<ui:param name="action" value="#{membreImportBean.reinitialiser}" />
<ui:param name="update" value=":formImport" />
<ui:param name="outlined" value="true" />
</ui:include>
</div>
</h:form>
</div>
</ui:define>
</ui:composition>

View File

@@ -0,0 +1,150 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<!--
Composant réutilisable: Carte KPI (Indicateur de Performance)
Paramètres:
- title: Titre du KPI (requis)
- value: Valeur principale à afficher (requis)
- icon: Classe d'icône PrimeIcons (requis, ex: pi-users)
- iconColor: Couleur de l'icône (requis, ex: blue-600, green-600, purple-600, orange-600)
- growthValue: Valeur de croissance (optionnel, ex: "12.5", "5", "+10")
- growthLabel: Libellé de la croissance (optionnel, ex: "ce mois", "vs mois dernier")
- growthType: Type de croissance - "percentage" (défaut) ou "number" ou "status"
- showGrowth: Afficher la section croissance (défaut: true si growthValue fourni)
- noDataLabel: Libellé si pas de données (optionnel, défaut: "Données non disponibles")
- progressValue: Valeur pour la barre de progression 0-100 (optionnel)
- showProgress: Afficher la barre de progression (défaut: true si progressValue fourni)
- statusIcon: Icône de statut au lieu de croissance (optionnel, ex: "pi-check-circle")
- statusLabel: Libellé de statut (optionnel, ex: "En ligne")
- statusValue: Valeur de statut (optionnel, ex: "5 actifs")
- colSize: Taille de colonne (optionnel, défaut: "col-12 md:col-6 lg:col-3")
Exemples d'utilisation:
1. KPI simple avec croissance en pourcentage:
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Membres Actifs" />
<ui:param name="value" value="#{bean.totalMembres}" />
<ui:param name="icon" value="pi-users" />
<ui:param name="iconColor" value="blue-600" />
<ui:param name="growthValue" value="#{bean.croissanceMembres}" />
<ui:param name="growthLabel" value="ce mois" />
<ui:param name="progressValue" value="#{bean.pourcentageMembres}" />
</ui:include>
2. KPI avec croissance en nombre:
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Organisations" />
<ui:param name="value" value="#{bean.totalEntites}" />
<ui:param name="icon" value="pi-sitemap" />
<ui:param name="iconColor" value="green-600" />
<ui:param name="growthValue" value="#{bean.nouvellesEntites}" />
<ui:param name="growthLabel" value="nouvelles" />
<ui:param name="growthType" value="number" />
<ui:param name="progressValue" value="#{bean.pourcentageOrganisations}" />
</ui:include>
3. KPI avec statut au lieu de croissance:
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Activité du Jour" />
<ui:param name="value" value="#{bean.activiteJournaliere}" />
<ui:param name="icon" value="pi-chart-line" />
<ui:param name="iconColor" value="orange-600" />
<ui:param name="statusIcon" value="pi-check-circle" />
<ui:param name="statusLabel" value="En ligne" />
<ui:param name="statusValue" value="#{bean.utilisateursActifs} actifs" />
<ui:param name="progressValue" value="#{bean.pourcentageActivite}" />
</ui:include>
4. KPI sans croissance ni progression:
<ui:include src="/templates/components/cards/kpi-card.xhtml">
<ui:param name="title" value="Total" />
<ui:param name="value" value="#{bean.total}" />
<ui:param name="icon" value="pi-calendar" />
<ui:param name="iconColor" value="purple-600" />
<ui:param name="showGrowth" value="false" />
<ui:param name="showProgress" value="false" />
</ui:include>
-->
<ui:param name="colSize" value="col-12 md:col-6 lg:col-3" />
<ui:param name="showGrowth" value="#{not empty growthValue}" />
<ui:param name="showProgress" value="#{not empty progressValue}" />
<ui:param name="growthType" value="percentage" />
<ui:param name="noDataLabel" value="Données non disponibles" />
<div class="field #{colSize}">
<div class="card surface-0 hover:surface-100 border-round-lg transition-all transition-duration-200">
<div class="p-4" style="min-height: 9rem;">
<!-- Header: Titre et Icône -->
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">#{title}</span>
<div class="flex align-items-center justify-content-center surface-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi #{icon} text-#{iconColor} text-lg"></i>
</div>
</div>
<!-- Valeur principale -->
<div class="text-900 font-bold text-2xl mb-3">#{value}</div>
<!-- Section Croissance ou Statut -->
<c:choose>
<!-- Mode Statut (statusIcon fourni) -->
<c:when test="#{not empty statusIcon}">
<div class="flex align-items-center mb-2" rendered="#{not empty statusValue and statusValue != '0'}">
<div class="bg-green-500 border-circle mr-2" style="width: 8px; height: 8px;"></div>
<span class="text-green-600 font-semibold text-sm mr-2">#{statusLabel}</span>
<span class="text-500 text-xs">#{statusValue}</span>
</div>
<div class="text-500 text-xs" rendered="#{empty statusValue or statusValue == '0'}">
Aucun utilisateur actif
</div>
</c:when>
<!-- Mode Croissance -->
<c:otherwise>
<c:choose>
<!-- Croissance en nombre -->
<c:when test="#{growthType == 'number'}">
<div class="flex align-items-center mb-2" rendered="#{showGrowth and not empty growthValue and growthValue != '0' and growthValue != '0.0'}">
<i class="pi pi-arrow-up text-green-500 text-sm mr-2"></i>
<span class="text-green-600 font-semibold text-sm mr-2">+#{growthValue}</span>
<span class="text-500 text-xs">#{growthLabel}</span>
</div>
<div class="text-500 text-xs" rendered="#{not showGrowth or empty growthValue or growthValue == '0' or growthValue == '0.0'}">
#{noDataLabel}
</div>
</c:when>
<!-- Croissance en pourcentage (défaut) -->
<c:otherwise>
<div class="flex align-items-center mb-2" rendered="#{showGrowth and not empty growthValue and growthValue != '0' and growthValue != '0.0'}">
<i class="pi pi-arrow-up text-green-500 text-sm mr-2"></i>
<span class="text-green-600 font-semibold text-sm mr-2">+#{growthValue}%</span>
<span class="text-500 text-xs">#{growthLabel}</span>
</div>
<div class="text-500 text-xs" rendered="#{not showGrowth or empty growthValue or growthValue == '0' or growthValue == '0.0'}">
#{noDataLabel}
</div>
</c:otherwise>
</c:choose>
</c:otherwise>
</c:choose>
<!-- Barre de progression -->
<p:progressBar value="#{progressValue}"
showValue="false"
styleClass="surface-200"
style="height: 0.5rem; width: 100%;"
rendered="#{showProgress}" />
</div>
</div>
</div>
</ui:composition>

View File

@@ -0,0 +1,26 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui">
<!--
Composant réutilisable: Champ de détail en ligne (label à gauche, valeur à droite)
Usage:
<ui:include src="/templates/components/forms/detail-field-row.xhtml">
<ui:param name="label" value="Nom complet" />
<ui:param name="value" value="#{bean.property}" />
<ui:param name="valueClass" value="font-bold text-primary" />
<ui:param name="suffix" value="(optionnel)" />
</ui:include>
-->
<div class="flex justify-content-between align-items-center mb-2">
<span class="font-medium text-900">#{label}:</span>
<div class="text-right">
<span class="#{valueClass}">#{value}</span>
<small class="text-600 ml-1" rendered="#{not empty suffix}">#{suffix}</small>
</div>
</div>
</ui:composition>

View File

@@ -0,0 +1,41 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui">
<!--
Composant réutilisable: Photo de profil avec upload
Paramètres:
- photoId: ID du panelGroup pour la photo (requis)
- photoUrl: URL de la photo (optionnel)
- initiales: Initiales à afficher si pas de photo (requis)
- formId: ID du formulaire pour l'upload (requis)
- listener: Méthode bean pour gérer l'upload (requis)
- update: Composants à mettre à jour après upload (requis)
- size: Taille de la photo en px (optionnel, défaut: 120)
-->
<ui:param name="size" value="120" />
<h:panelGroup id="#{photoId}">
<div class="border-circle overflow-hidden" style="width: #{size}px; height: #{size}px;">
<h:graphicImage value="#{photoUrl}"
style="width: 100%; height: 100%; object-fit: cover;"
rendered="#{photoUrl != null}" />
<div class="bg-primary text-white flex align-items-center justify-content-center w-full h-full"
rendered="#{photoUrl == null}">
<span style="font-size: #{size / 2}px;">#{initiales}</span>
</div>
</div>
</h:panelGroup>
<h:form id="#{formId}" enctype="multipart/form-data">
<p:fileUpload mode="simple" skinSimple="true"
label="Changer photo" chooseLabel="Modifier"
accept="image/*" maxFileSize="2000000"
listener="#{listener}"
update="#{update}"
styleClass="mt-2 w-full" />
</h:form>
</ui:composition>