feat: Optimisations UX/UI et amélioration import/export CSV

Optimisations majeures de l'interface utilisateur et amélioration du système d'import/export CSV avec rapport d'erreurs détaillé.

## Optimisations UX/UI
- Suppression des blocs Actions Rapides redondants dans les pages list/view
- Consolidation des actions dans les en-têtes de page
- Conversion des filtres en panneau collapsible avec badge Filtres actifs
- Suppression du sous-menu Attribution Rôles (redondant avec /users/edit)
- Amélioration de la navigation et de l'ergonomie générale
- Correction des attributs iconLeft non supportés par fr:fieldInput

## Import/Export CSV
- Ajout de ImportResultDTO avec rapport détaillé des erreurs
- Création de CsvValidationHelper pour validation robuste des données
- Amélioration des messages d'erreur avec numéros de ligne
- Support de colonnes flexibles (username,prenom,nom,email)
- Validation stricte des formats email

## Corrections techniques
- Fix DashboardBeanTest: getRecentActions() → getActionsLast24h()
- Fix UserServiceImplTest: retour ImportResultDTO au lieu de int
- Amélioration de la gestion d'erreurs dans AuditServiceImpl
- Migration Flyway V1.0.0 pour la table audit_logs

## Infrastructure
- Mise à jour .gitignore professionnel (exclusion docs de session)
- Configuration production sécurisée (variables d'environnement)
- Pas de secrets hardcodés dans les fichiers de configuration

Testé et validé en environnement de développement.
This commit is contained in:
lionsdev
2026-01-03 13:53:35 +00:00
parent 564d29a9d2
commit c5cbe61002
22 changed files with 1697 additions and 1536 deletions

View File

@@ -150,5 +150,25 @@ public interface UserServiceClient {
@PathParam("userId") String userId,
@QueryParam("realm") String realmName
);
/**
* Exporter les utilisateurs en CSV
*/
@GET
@Path("/export/csv")
@Produces(MediaType.TEXT_PLAIN)
String exportUsersToCSV(@QueryParam("realm") String realmName);
/**
* Importer des utilisateurs depuis CSV avec rapport détaillé
*/
@POST
@Path("/import/csv")
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.APPLICATION_JSON)
dev.lions.user.manager.dto.importexport.ImportResultDTO importUsersFromCSV(
@QueryParam("realm") String realmName,
String csvContent
);
}

View File

@@ -6,6 +6,7 @@ import dev.lions.user.manager.dto.audit.AuditLogDTO;
import dev.lions.user.manager.enums.audit.TypeActionAudit;
import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
@@ -170,10 +171,29 @@ public class AuditConsultationBean implements Serializable {
String dateFinStr = dateFin != null ? dateFin.format(DATE_FORMATTER) : null;
String csv = auditServiceClient.exportLogsToCSV(dateDebutStr, dateFinStr);
// TODO: Implémenter le téléchargement du fichier CSV
addSuccessMessage("Export CSV généré avec succès");
// Télécharger le fichier CSV
FacesContext facesContext = FacesContext.getCurrentInstance();
ExternalContext externalContext = facesContext.getExternalContext();
String filename = "audit-logs-" +
LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HHmmss")) +
".csv";
externalContext.setResponseContentType("text/csv; charset=UTF-8");
externalContext.setResponseHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
java.io.OutputStream output = externalContext.getResponseOutputStream();
output.write(csv.getBytes(java.nio.charset.StandardCharsets.UTF_8));
output.flush();
facesContext.responseComplete();
LOGGER.info("Export CSV généré avec succès: " + filename);
} catch (java.io.IOException e) {
LOGGER.severe("Erreur I/O lors de l'export CSV: " + e.getMessage());
addErrorMessage("Erreur lors du téléchargement: " + e.getMessage());
} catch (Exception e) {
LOGGER.severe("Erreur lors de l'export: " + e.getMessage());
LOGGER.severe("Erreur lors de l'export CSV: " + e.getMessage());
addErrorMessage("Erreur lors de l'export: " + e.getMessage());
}
}

View File

@@ -1,6 +1,7 @@
package dev.lions.user.manager.client.view;
import dev.lions.user.manager.client.service.AuditServiceClient;
import dev.lions.user.manager.client.service.RealmServiceClient;
import dev.lions.user.manager.client.service.RoleServiceClient;
import dev.lions.user.manager.client.service.UserServiceClient;
import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO;
@@ -18,6 +19,8 @@ import jakarta.faces.context.FacesContext;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
@@ -47,104 +50,278 @@ public class DashboardBean implements Serializable {
@RestClient
private AuditServiceClient auditServiceClient;
// Statistiques
@Inject
@RestClient
private RealmServiceClient realmServiceClient;
@Inject
private UserSessionBean userSessionBean;
// ==================== STATISTIQUES MÉTIER ====================
// Utilisateurs
private Long totalUsers = 0L;
private Long activeUsers = 0L; // enabled=true
private Long inactiveUsers = 0L; // enabled=false
private Long usersCreatedToday = 0L;
private Long usersCreatedThisWeek = 0L;
private Long usersCreatedThisMonth = 0L;
// Rôles
private Long totalRoles = 0L;
private Long recentActions = 0L;
private Long activeSessions = 0L;
private Long onlineUsers = 0L;
// Audit & Activité
private Long actionsLast24h = 0L;
private Long actionsLast7d = 0L;
private Long actionsLast30d = 0L;
private Long successfulActions24h = 0L;
private Long failedActions24h = 0L;
private Double successRate24h = 0.0;
// Sécurité & Alertes
private Long criticalActions24h = 0L;
private Long failedLogins24h = 0L;
private Long usersAtRisk = 0L; // Utilisateurs avec multiples tentatives échouées
// Indicateur de chargement
private boolean loading = false;
// Méthodes pour obtenir les valeurs formatées pour l'affichage
// ==================== MÉTHODES D'AFFICHAGE ====================
public String getTotalUsersDisplay() {
if (loading) return "...";
return totalUsers != null ? String.valueOf(totalUsers) : "0";
return loading ? "..." : String.valueOf(totalUsers);
}
public String getActiveUsersDisplay() {
return loading ? "..." : String.valueOf(activeUsers);
}
public String getInactiveUsersDisplay() {
return loading ? "..." : String.valueOf(inactiveUsers);
}
public String getUsersCreatedTodayDisplay() {
return loading ? "..." : String.valueOf(usersCreatedToday);
}
public String getUsersCreatedThisWeekDisplay() {
return loading ? "..." : String.valueOf(usersCreatedThisWeek);
}
public String getTotalRolesDisplay() {
if (loading) return "...";
return totalRoles != null ? String.valueOf(totalRoles) : "0";
return loading ? "..." : String.valueOf(totalRoles);
}
public String getRecentActionsDisplay() {
if (loading) return "...";
return recentActions != null ? String.valueOf(recentActions) : "0";
public String getActionsLast24hDisplay() {
return loading ? "..." : String.valueOf(actionsLast24h);
}
public String getActionsLast7dDisplay() {
return loading ? "..." : String.valueOf(actionsLast7d);
}
public String getSuccessRate24hDisplay() {
return loading ? "..." : String.format("%.1f%%", successRate24h);
}
public String getCriticalActions24hDisplay() {
return loading ? "..." : String.valueOf(criticalActions24h);
}
public String getFailedLogins24hDisplay() {
return loading ? "..." : String.valueOf(failedLogins24h);
}
public String getUsersAtRiskDisplay() {
return loading ? "..." : String.valueOf(usersAtRisk);
}
public boolean isLoading() {
return loading;
}
// Realm par défaut
private String realmName = "master";
public boolean hasAlerts() {
return criticalActions24h > 0 || failedLogins24h > 5 || usersAtRisk > 0;
}
// Realm - sera défini dynamiquement en fonction de l'utilisateur connecté
private String realmName = "lions-user-manager"; // Valeur par défaut si aucun realm autorisé
private List<String> availableRealms = new ArrayList<>();
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
@PostConstruct
public void init() {
LOGGER.info("=== Initialisation du DashboardBean ===");
LOGGER.info("Realm par défaut: " + realmName);
LOGGER.info("UserServiceClient injecté: " + (userServiceClient != null ? "OUI" : "NON"));
LOGGER.info("RoleServiceClient injecté: " + (roleServiceClient != null ? "OUI" : "NON"));
LOGGER.info("AuditServiceClient injecté: " + (auditServiceClient != null ? "OUI" : "NON"));
LOGGER.info("RealmServiceClient injecté: " + (realmServiceClient != null ? "OUI" : "NON"));
LOGGER.info("UserSessionBean injecté: " + (userSessionBean != null ? "OUI" : "NON"));
// Charger les realms autorisés pour l'utilisateur connecté (multi-tenant)
loadRealms();
LOGGER.info("Realm sélectionné pour le dashboard: " + realmName);
// Charger les statistiques pour le realm de l'utilisateur
loadStatistics();
}
/**
* Charger toutes les statistiques
* Charger toutes les statistiques métier
*/
public void loadStatistics() {
loading = true;
try {
loadTotalUsers();
// Statistiques utilisateurs
loadUserStatistics();
// Statistiques rôles
loadTotalRoles();
loadRecentActions();
// Les sessions actives nécessitent une API spécifique qui n'existe pas encore
// activeSessions = 0L;
// onlineUsers = 0L;
// Statistiques activité & audit
loadActivityStatistics();
// Statistiques sécurité
loadSecurityStatistics();
} catch (Exception e) {
LOGGER.severe("Erreur lors du chargement des statistiques: " + e.getMessage());
e.printStackTrace();
} finally {
loading = false;
}
}
/**
* Charger le nombre total d'utilisateurs
* Charger les statistiques utilisateurs
*/
private void loadTotalUsers() {
private void loadUserStatistics() {
try {
LOGGER.info("Début chargement total utilisateurs pour realm: " + realmName);
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
// Total utilisateurs
UserSearchCriteriaDTO criteriaAll = UserSearchCriteriaDTO.builder()
.realmName(realmName)
.page(0)
.pageSize(1) // On n'a besoin que du count
.pageSize(1)
.build();
LOGGER.info("Appel userServiceClient.searchUsers()...");
UserSearchResultDTO result = userServiceClient.searchUsers(criteria);
LOGGER.info("Résultat reçu: " + (result != null ? "NON NULL" : "NULL"));
if (result != null && result.getTotalCount() != null) {
totalUsers = result.getTotalCount();
LOGGER.info("✅ Total utilisateurs chargé avec succès: " + totalUsers);
} else {
LOGGER.warning("⚠️ Résultat de recherche utilisateurs null ou totalCount null");
if (result == null) {
LOGGER.warning(" - result est null");
} else {
LOGGER.warning(" - result.getTotalCount() est null");
}
totalUsers = 0L;
}
UserSearchResultDTO resultAll = userServiceClient.searchUsers(criteriaAll);
totalUsers = resultAll != null && resultAll.getTotalCount() != null ? resultAll.getTotalCount() : 0L;
// Utilisateurs actifs (enabled=true)
UserSearchCriteriaDTO criteriaActive = UserSearchCriteriaDTO.builder()
.realmName(realmName)
.statut(dev.lions.user.manager.enums.user.StatutUser.ACTIF)
.page(0)
.pageSize(1)
.build();
UserSearchResultDTO resultActive = userServiceClient.searchUsers(criteriaActive);
activeUsers = resultActive != null && resultActive.getTotalCount() != null ? resultActive.getTotalCount() : 0L;
// Utilisateurs inactifs
inactiveUsers = totalUsers - activeUsers;
LOGGER.info("✅ Statistiques utilisateurs: Total=" + totalUsers + ", Actifs=" + activeUsers + ", Inactifs=" + inactiveUsers);
} catch (Exception e) {
LOGGER.severe("❌ ERREUR lors du chargement du nombre d'utilisateurs: " + e.getMessage());
LOGGER.severe(" Type d'erreur: " + e.getClass().getName());
e.printStackTrace();
LOGGER.severe("❌ Erreur chargement statistiques utilisateurs: " + e.getMessage());
totalUsers = 0L;
addErrorMessage("Impossible de charger le nombre d'utilisateurs: " + e.getMessage());
activeUsers = 0L;
inactiveUsers = 0L;
}
}
/**
* Charger les statistiques d'activité (audit)
*/
private void loadActivityStatistics() {
try {
LocalDateTime now = LocalDateTime.now();
// Actions dernières 24h
String date24hAgo = now.minusDays(1).format(DATE_FORMATTER);
String dateNow = now.format(DATE_FORMATTER);
try {
AuditServiceClient.CountResponse success24h = auditServiceClient.getSuccessCount(date24hAgo, dateNow);
AuditServiceClient.CountResponse failure24h = auditServiceClient.getFailureCount(date24hAgo, dateNow);
successfulActions24h = success24h != null ? success24h.count : 0L;
failedActions24h = failure24h != null ? failure24h.count : 0L;
actionsLast24h = successfulActions24h + failedActions24h;
// Taux de réussite
if (actionsLast24h > 0) {
successRate24h = (successfulActions24h * 100.0) / actionsLast24h;
} else {
successRate24h = 100.0;
}
LOGGER.info("✅ Actions 24h: Total=" + actionsLast24h + ", Succès=" + successfulActions24h +
", Échecs=" + failedActions24h + ", Taux=" + String.format("%.1f%%", successRate24h));
} catch (Exception e) {
LOGGER.warning("⚠️ Impossible d'obtenir les stats d'audit 24h: " + e.getMessage());
actionsLast24h = 0L;
successfulActions24h = 0L;
failedActions24h = 0L;
successRate24h = 100.0;
}
// Actions derniers 7 jours
try {
String date7dAgo = now.minusDays(7).format(DATE_FORMATTER);
AuditServiceClient.CountResponse success7d = auditServiceClient.getSuccessCount(date7dAgo, dateNow);
AuditServiceClient.CountResponse failure7d = auditServiceClient.getFailureCount(date7dAgo, dateNow);
actionsLast7d = (success7d != null ? success7d.count : 0L) + (failure7d != null ? failure7d.count : 0L);
} catch (Exception e) {
actionsLast7d = 0L;
}
// Actions derniers 30 jours
try {
String date30dAgo = now.minusDays(30).format(DATE_FORMATTER);
AuditServiceClient.CountResponse success30d = auditServiceClient.getSuccessCount(date30dAgo, dateNow);
AuditServiceClient.CountResponse failure30d = auditServiceClient.getFailureCount(date30dAgo, dateNow);
actionsLast30d = (success30d != null ? success30d.count : 0L) + (failure30d != null ? failure30d.count : 0L);
} catch (Exception e) {
actionsLast30d = 0L;
}
} catch (Exception e) {
LOGGER.severe("❌ Erreur chargement statistiques d'activité: " + e.getMessage());
}
}
/**
* Charger les statistiques de sécurité
*/
private void loadSecurityStatistics() {
try {
LocalDateTime now = LocalDateTime.now();
String date24hAgo = now.minusDays(1).format(DATE_FORMATTER);
String dateNow = now.format(DATE_FORMATTER);
// Actions critiques (suppressions, désactivations, modifications de rôles)
// TODO: Implémenter avec un filtre sur les types d'actions critiques
criticalActions24h = 0L;
// Tentatives de connexion échouées
// TODO: Implémenter avec un filtre sur CONNEXION_ECHOUEE
failedLogins24h = failedActions24h; // Approximation pour l'instant
// Utilisateurs à risque (plus de 3 tentatives échouées)
// TODO: Implémenter avec un groupe by userId sur les échecs
usersAtRisk = failedLogins24h > 10 ? 1L : 0L; // Approximation
LOGGER.info("✅ Stats sécurité: Critiques=" + criticalActions24h +
", Échecs login=" + failedLogins24h + ", Utilisateurs à risque=" + usersAtRisk);
} catch (Exception e) {
LOGGER.severe("❌ Erreur chargement statistiques sécurité: " + e.getMessage());
criticalActions24h = 0L;
failedLogins24h = 0L;
usersAtRisk = 0L;
}
}
@@ -157,7 +334,7 @@ public class DashboardBean implements Serializable {
LOGGER.info("Appel roleServiceClient.getAllRealmRoles()...");
List<?> roles = roleServiceClient.getAllRealmRoles(realmName);
LOGGER.info("Résultat reçu: " + (roles != null ? "NON NULL, taille: " + roles.size() : "NULL"));
if (roles != null) {
totalRoles = (long) roles.size();
LOGGER.info("✅ Total rôles chargé avec succès: " + totalRoles);
@@ -174,61 +351,6 @@ public class DashboardBean implements Serializable {
}
}
/**
* Charger le nombre d'actions récentes (dernières 24h)
*/
private void loadRecentActions() {
try {
LocalDateTime dateDebut = LocalDateTime.now().minusDays(1);
String dateDebutStr = dateDebut.format(DATE_FORMATTER);
String dateFinStr = LocalDateTime.now().format(DATE_FORMATTER);
LOGGER.info("Début chargement actions récentes (24h)");
LOGGER.info(" Date début: " + dateDebutStr);
LOGGER.info(" Date fin: " + dateFinStr);
// Essayer d'abord avec getSuccessCount + getFailureCount (plus efficace)
try {
LOGGER.info("Tentative avec getSuccessCount() et getFailureCount()...");
AuditServiceClient.CountResponse successResponse = auditServiceClient.getSuccessCount(dateDebutStr, dateFinStr);
Long successCount = successResponse != null ? successResponse.count : 0L;
AuditServiceClient.CountResponse failureResponse = auditServiceClient.getFailureCount(dateDebutStr, dateFinStr);
Long failureCount = failureResponse != null ? failureResponse.count : 0L;
LOGGER.info(" SuccessCount: " + successCount);
LOGGER.info(" FailureCount: " + failureCount);
recentActions = (successCount != null ? successCount : 0L) + (failureCount != null ? failureCount : 0L);
LOGGER.info("✅ Actions récentes chargées avec succès: " + recentActions);
} catch (Exception e2) {
LOGGER.warning("⚠️ Impossible d'obtenir les statistiques d'audit, tentative avec searchLogs: " + e2.getMessage());
// Fallback: utiliser searchLogs
List<?> logs = auditServiceClient.searchLogs(
null, // acteur
dateDebutStr, // dateDebut
dateFinStr, // dateFin
null, // typeAction
null, // ressourceType
null, // succes
0, // page
100 // pageSize - récupérer plus de logs pour avoir un meilleur count
);
if (logs != null) {
recentActions = (long) logs.size();
LOGGER.info("✅ Actions récentes chargées via searchLogs: " + recentActions);
} else {
LOGGER.warning("⚠️ searchLogs a retourné null");
recentActions = 0L;
}
}
} catch (Exception e) {
LOGGER.severe("❌ ERREUR lors du chargement des actions récentes: " + e.getMessage());
LOGGER.severe(" Type d'erreur: " + e.getClass().getName());
e.printStackTrace();
recentActions = 0L;
addErrorMessage("Impossible de charger les actions récentes: " + e.getMessage());
}
}
/**
* Rafraîchir les statistiques
*/
@@ -237,7 +359,53 @@ public class DashboardBean implements Serializable {
loadStatistics();
addSuccessMessage("Statistiques rafraîchies avec succès");
}
/**
* Charger les realms disponibles depuis Keycloak en fonction des permissions de l'utilisateur
* Architecture multi-tenant: le realm est déterminé dynamiquement selon l'utilisateur connecté
*/
private void loadRealms() {
try {
// Récupérer tous les realms depuis Keycloak
List<String> allRealms = realmServiceClient.getAllRealms();
if (allRealms == null || allRealms.isEmpty()) {
LOGGER.warning("Aucun realm trouvé dans Keycloak");
availableRealms = Collections.emptyList();
return;
}
List<String> authorizedRealms = userSessionBean.getAuthorizedRealms();
// Si liste vide, l'utilisateur est super admin (peut gérer tous les realms)
if (authorizedRealms.isEmpty()) {
// Super admin - utiliser tous les realms disponibles depuis Keycloak
availableRealms = new ArrayList<>(allRealms);
LOGGER.info("Super admin détecté - " + availableRealms.size() + " realms disponibles depuis Keycloak");
} else {
// Realm admin - filtrer pour ne garder que les realms autorisés qui existent dans Keycloak
availableRealms = new ArrayList<>();
for (String authorizedRealm : authorizedRealms) {
if (allRealms.contains(authorizedRealm)) {
availableRealms.add(authorizedRealm);
}
}
LOGGER.info("Realms autorisés pour l'utilisateur: " + availableRealms.size());
// Définir le premier realm autorisé comme realm par défaut
if (!availableRealms.isEmpty() && !availableRealms.contains(realmName)) {
realmName = availableRealms.get(0);
LOGGER.info("Realm par défaut changé vers: " + realmName);
}
}
} catch (Exception e) {
LOGGER.severe("Erreur lors du chargement des realms depuis Keycloak: " + e.getMessage());
LOGGER.log(java.util.logging.Level.SEVERE, "Exception complète", e);
// Fallback: garder le realm par défaut "lions-user-manager"
availableRealms = Collections.emptyList();
}
}
// Méthodes utilitaires pour les messages
private void addSuccessMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,

View File

@@ -28,6 +28,7 @@ import java.util.Map;
@Named
@ViewScoped
@Slf4j
@SuppressWarnings("deprecation") // ChartData API dépréciée - migration vers JSON prévue
public class DashboardView implements Serializable {
@Inject
@@ -87,6 +88,7 @@ public class DashboardView implements Serializable {
}
}
@SuppressWarnings("deprecation") // ChartData sera remplacé par une approche JSON moderne dans une version future
public void createBarModel() {
barModel = new BarChartModel();
ChartData data = new ChartData();

View File

@@ -120,6 +120,32 @@ public class UserCreationBean implements Serializable {
return "userListPage";
}
/**
* Valider la correspondance des mots de passe en temps réel
*/
public void validatePasswordMatch() {
FacesContext context = FacesContext.getCurrentInstance();
// Vérifier que les deux champs sont remplis
if (password != null && !password.isEmpty() &&
passwordConfirm != null && !passwordConfirm.isEmpty()) {
// Vérifier la correspondance
if (!password.equals(passwordConfirm)) {
context.addMessage("formUserCreation:passwordConfirm",
new FacesMessage(FacesMessage.SEVERITY_ERROR,
"Erreur",
"Les mots de passe ne correspondent pas"));
} else {
// Succès - afficher message de confirmation
context.addMessage("formUserCreation:passwordConfirm",
new FacesMessage(FacesMessage.SEVERITY_INFO,
"Validé",
"Les mots de passe correspondent"));
}
}
}
/**
* Charger les realms disponibles depuis Keycloak
*/

View File

@@ -8,6 +8,7 @@ import dev.lions.user.manager.dto.user.UserSearchResultDTO;
import dev.lions.user.manager.enums.user.StatutUser;
import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;
import jakarta.faces.event.ActionEvent;
import jakarta.faces.view.ViewScoped;
@@ -18,6 +19,7 @@ import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.primefaces.event.data.PageEvent;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -75,6 +77,9 @@ public class UserListBean implements Serializable {
private List<StatutUser> statutOptions = List.of(StatutUser.values());
private List<String> availableRealms = new ArrayList<>();
// Résultats de l'import CSV
private dev.lions.user.manager.dto.importexport.ImportResultDTO lastImportResult;
@PostConstruct
public void init() {
LOGGER.info("Initialisation de UserListBean");
@@ -308,17 +313,134 @@ public class UserListBean implements Serializable {
}
/**
* Exporter vers CSV (placeholder)
* Exporter les utilisateurs en CSV
*/
public void exportToCSV() {
addSuccessMessage("Fonctionnalité d'export en cours de développement");
try {
if (realmName == null || realmName.isEmpty()) {
addErrorMessage("Veuillez sélectionner un realm");
return;
}
String csv = userServiceClient.exportUsersToCSV(realmName);
// Télécharger le fichier CSV
FacesContext facesContext = FacesContext.getCurrentInstance();
ExternalContext externalContext = facesContext.getExternalContext();
String filename = "users_export_" +
LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HHmmss")) +
".csv";
externalContext.setResponseContentType("text/csv; charset=UTF-8");
externalContext.setResponseHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
java.io.OutputStream output = externalContext.getResponseOutputStream();
output.write(csv.getBytes(java.nio.charset.StandardCharsets.UTF_8));
output.flush();
facesContext.responseComplete();
LOGGER.info("Export CSV généré avec succès: " + filename);
} catch (java.io.IOException e) {
LOGGER.severe("Erreur I/O lors de l'export CSV: " + e.getMessage());
addErrorMessage("Erreur lors du téléchargement: " + e.getMessage());
} catch (Exception e) {
LOGGER.severe("Erreur lors de l'export CSV: " + e.getMessage());
addErrorMessage("Erreur lors de l'export: " + e.getMessage());
}
}
/**
* Importer des utilisateurs (placeholder)
* Télécharger un template CSV pour l'import d'utilisateurs
*/
public void downloadCSVTemplate() {
try {
// Créer un template CSV avec des exemples
StringBuilder csvTemplate = new StringBuilder();
csvTemplate.append("username,prenom,nom,email\n");
csvTemplate.append("jdupont,Jean,Dupont,jean.dupont@example.com\n");
csvTemplate.append("mmartin,Marie,Martin,marie.martin@example.com\n");
csvTemplate.append("pbernard,Pierre,Bernard,pierre.bernard@example.com\n");
FacesContext facesContext = FacesContext.getCurrentInstance();
ExternalContext externalContext = facesContext.getExternalContext();
String filename = "template_import_users.csv";
externalContext.setResponseContentType("text/csv; charset=UTF-8");
externalContext.setResponseHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
java.io.OutputStream output = externalContext.getResponseOutputStream();
output.write(csvTemplate.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8));
output.flush();
facesContext.responseComplete();
LOGGER.info("Template CSV téléchargé avec succès: " + filename);
} catch (java.io.IOException e) {
LOGGER.severe("Erreur I/O lors du téléchargement du template CSV: " + e.getMessage());
addErrorMessage("Erreur lors du téléchargement du template: " + e.getMessage());
} catch (Exception e) {
LOGGER.severe("Erreur lors du téléchargement du template CSV: " + e.getMessage());
addErrorMessage("Erreur lors du téléchargement: " + e.getMessage());
}
}
/**
* Importer des utilisateurs depuis un fichier CSV
* Cette méthode sera appelée par le gestionnaire d'upload de fichier
*/
public void importUsers() {
addSuccessMessage("Fonctionnalité d'import en cours de développement");
addInfoMessage("Veuillez utiliser le bouton 'Parcourir' pour sélectionner un fichier CSV");
}
/**
* Gérer l'upload de fichier CSV pour import
*/
public void handleFileUpload(org.primefaces.event.FileUploadEvent event) {
try {
if (realmName == null || realmName.isEmpty()) {
addErrorMessage("Veuillez sélectionner un realm avant d'importer");
return;
}
if (event.getFile() == null) {
addErrorMessage("Aucun fichier sélectionné");
return;
}
// Lire le contenu du fichier
String csvContent = new String(event.getFile().getContent(), java.nio.charset.StandardCharsets.UTF_8);
if (csvContent.trim().isEmpty()) {
addErrorMessage("Le fichier CSV est vide");
return;
}
// Appeler l'API d'import
dev.lions.user.manager.dto.importexport.ImportResultDTO result =
userServiceClient.importUsersFromCSV(realmName, csvContent);
// Stocker le résultat pour l'affichage dans le dialog
this.lastImportResult = result;
// Afficher le résultat
LOGGER.info("Import terminé: " + result.getMessage());
if (result.getErrorCount() == 0) {
addSuccessMessage(result.getMessage());
} else {
addWarningMessage(result.getMessage());
}
// Ouvrir le dialog de résultats détaillés
org.primefaces.PrimeFaces.current().executeScript("PF('importResultDialog').show();");
loadUsers(); // Recharger la liste
} catch (Exception e) {
LOGGER.severe("Erreur lors de l'import: " + e.getMessage());
addErrorMessage("Erreur lors de l'import: " + e.getMessage());
}
}
/**
@@ -396,5 +518,15 @@ public class UserListBean implements Serializable {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
}
private void addInfoMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Information", message));
}
private void addWarningMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_WARN, "Avertissement", message));
}
}

View File

@@ -190,6 +190,23 @@
<redirect />
</navigation-case>
<!-- ================================================================
FREYA EXTENSION SHOWCASE
================================================================ -->
<navigation-case>
<description>Page de démonstration complète Freya Extension</description>
<from-outcome>freyaShowcasePage</from-outcome>
<to-view-id>/pages/user-manager/freya-showcase.xhtml</to-view-id>
<redirect />
</navigation-case>
<navigation-case>
<description>Navigation directe vers Freya Showcase</description>
<from-outcome>/pages/user-manager/freya-showcase</from-outcome>
<to-view-id>/pages/user-manager/freya-showcase.xhtml</to-view-id>
<redirect />
</navigation-case>
</navigation-rule>
</faces-config>

View File

@@ -4,6 +4,7 @@
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:fr="http://primefaces.org/freya"
template="/templates/main-template.xhtml">
<ui:define name="title">Affectation des Realms - Lions User Manager</ui:define>
@@ -24,11 +25,11 @@
<p class="text-600 m-0">Gérer les permissions d'administration par realm (contrôle multi-tenant)</p>
</div>
</div>
<p:commandButton value="Nouvelle Affectation"
icon="pi pi-plus"
styleClass="p-button-success"
onclick="PF('assignRealmDialog').show();"
type="button" />
<fr:commandButton value="Nouvelle Affectation"
icon="pi pi-plus"
severity="success"
onclick="PF('assignRealmDialog').show();"
type="button" />
</div>
</div>
</div>
@@ -91,30 +92,32 @@
<div class="card">
<div class="flex align-items-center justify-content-between mb-4">
<h5 class="m-0">Affectations Actuelles</h5>
<p:commandButton value="Rafraîchir"
icon="pi pi-refresh"
styleClass="p-button-outlined p-button-sm"
action="#{realmAssignmentBean.loadAssignments}"
update=":formRealmAssignments" />
<fr:commandButton value="Rafraîchir"
icon="pi pi-refresh"
outlined="true"
size="small"
action="#{realmAssignmentBean.loadAssignments}"
update=":formRealmAssignments" />
</div>
<p:messages id="messages" showDetail="true" closable="true">
<fr:message id="messages" showDetail="true" closable="true">
<p:autoUpdate />
</p:messages>
</fr:message>
<p:dataTable id="assignmentsTable"
value="#{realmAssignmentBean.assignments}"
var="assignment"
paginator="true"
rows="10"
rows="25"
paginatorPosition="bottom"
paginatorTemplate="{CurrentPageReport} {FirstPageLink} {PreviousPageLink} {PageLinks} {NextPageLink} {LastPageLink} {RowsPerPageDropdown}"
rowsPerPageTemplate="10,20,50"
rowsPerPageTemplate="10,25,50,100"
emptyMessage="Aucune affectation configurée"
responsiveLayout="scroll"
styleClass="p-datatable-sm">
<!-- Colonne Utilisateur -->
<p:column headerText="Utilisateur" sortBy="#{assignment.username}" filterBy="#{assignment.username}" filterMatchMode="contains">
<p:column headerText="Utilisateur" sortBy="#{assignment.username}" filterBy="#{assignment.username}" filterMatchMode="contains" priority="1">
<div class="flex align-items-center gap-2">
<div style="width: 32px; height: 32px; border-radius: 50%; background: linear-gradient(135deg, var(--primary-color), var(--primary-600)); display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: bold; color: white;">
<h:outputText value="#{assignment.username != null and assignment.username.length() >= 2 ? assignment.username.substring(0,2).toUpperCase() : 'U'}" />
@@ -127,92 +130,101 @@
</p:column>
<!-- Colonne Realm -->
<p:column headerText="Realm" sortBy="#{assignment.realmName}" filterBy="#{assignment.realmName}" filterMatchMode="contains">
<p:tag value="#{assignment.realmName}"
severity="info"
icon="pi pi-globe" />
<p:column headerText="Realm" sortBy="#{assignment.realmName}" filterBy="#{assignment.realmName}" filterMatchMode="contains" priority="2">
<fr:tag value="#{assignment.realmName}"
severity="info"
icon="pi pi-globe" />
</p:column>
<!-- Colonne Type -->
<p:column headerText="Type" style="width: 150px">
<p:tag value="Super Admin"
severity="danger"
icon="pi pi-star"
rendered="#{assignment.isSuperAdmin()}" />
<p:tag value="Realm Admin"
severity="success"
icon="pi pi-shield"
rendered="#{!assignment.isSuperAdmin()}" />
<p:column headerText="Type" style="width: 150px" priority="3">
<fr:tag value="Super Admin"
severity="danger"
icon="pi pi-star"
rendered="#{assignment.isSuperAdmin()}" />
<fr:tag value="Realm Admin"
severity="success"
icon="pi pi-shield"
rendered="#{!assignment.isSuperAdmin()}" />
</p:column>
<!-- Colonne Statut -->
<p:column headerText="Statut" style="width: 120px">
<p:tag value="Actif"
severity="success"
icon="pi pi-check-circle"
rendered="#{assignment.active and !assignment.isExpired()}" />
<p:tag value="Inactif"
severity="warning"
icon="pi pi-times-circle"
rendered="#{!assignment.active}" />
<p:tag value="Expiré"
severity="danger"
icon="pi pi-exclamation-circle"
rendered="#{assignment.isExpired()}" />
<p:column headerText="Statut" style="width: 120px" priority="4">
<fr:tag value="Actif"
severity="success"
icon="pi pi-check-circle"
rendered="#{assignment.active and !assignment.isExpired()}" />
<fr:tag value="Inactif"
severity="warning"
icon="pi pi-times-circle"
rendered="#{!assignment.active}" />
<fr:tag value="Expiré"
severity="danger"
icon="pi pi-exclamation-circle"
rendered="#{assignment.isExpired()}" />
</p:column>
<!-- Colonne Assigné le -->
<p:column headerText="Assigné le" sortBy="#{assignment.assignedAt}" style="width: 180px">
<p:column headerText="Assigné le" sortBy="#{assignment.assignedAt}" style="width: 180px" priority="5">
<h:outputText value="#{assignment.assignedAt}">
<f:convertDateTime pattern="dd/MM/yyyy HH:mm" />
</h:outputText>
</p:column>
<!-- Colonne Par -->
<p:column headerText="Par" sortBy="#{assignment.assignedBy}" style="width: 150px">
<p:column headerText="Par" sortBy="#{assignment.assignedBy}" style="width: 150px" priority="6">
<h:outputText value="#{assignment.assignedBy}" />
</p:column>
<!-- Colonne Actions -->
<p:column headerText="Actions" style="width: 120px; text-align: center">
<p:column headerText="Actions" style="width: 120px; text-align: center" priority="1">
<div class="flex gap-1 justify-content-center flex-wrap">
<!-- Bouton Désactiver -->
<p:commandButton icon="pi pi-ban"
styleClass="p-button-rounded p-button-text p-button-sm p-button-warning"
title="Désactiver"
action="#{realmAssignmentBean.deactivateAssignment(assignment)}"
update=":formRealmAssignments"
process="@this"
rendered="#{assignment.active}">
<fr:commandButton icon="pi pi-ban"
rounded="true"
text="true"
size="small"
severity="warning"
title="Désactiver"
action="#{realmAssignmentBean.deactivateAssignment(assignment)}"
update=":formRealmAssignments"
process="@this"
rendered="#{assignment.active}">
<p:confirm header="Confirmation"
message="Désactiver cette affectation ?"
icon="pi pi-exclamation-triangle" />
</p:commandButton>
</fr:commandButton>
<!-- Bouton Activer -->
<p:commandButton icon="pi pi-check"
styleClass="p-button-rounded p-button-text p-button-sm p-button-success"
title="Activer"
action="#{realmAssignmentBean.activateAssignment(assignment)}"
update=":formRealmAssignments"
process="@this"
rendered="#{!assignment.active}">
<fr:commandButton icon="pi pi-check"
rounded="true"
text="true"
size="small"
severity="success"
title="Activer"
action="#{realmAssignmentBean.activateAssignment(assignment)}"
update=":formRealmAssignments"
process="@this"
rendered="#{!assignment.active}">
<p:confirm header="Confirmation"
message="Activer cette affectation ?"
icon="pi pi-question-circle" />
</p:commandButton>
</fr:commandButton>
<!-- Bouton Supprimer -->
<p:commandButton icon="pi pi-trash"
styleClass="p-button-rounded p-button-text p-button-sm p-button-danger"
title="Supprimer"
action="#{realmAssignmentBean.revokeAssignment(assignment)}"
update=":formRealmAssignments"
process="@this">
<fr:commandButton icon="pi pi-trash"
rounded="true"
text="true"
size="small"
severity="danger"
title="Supprimer"
action="#{realmAssignmentBean.revokeAssignment(assignment)}"
update=":formRealmAssignments"
process="@this">
<p:confirm header="Confirmation"
message="Révoquer l'accès de #{assignment.username} au realm #{assignment.realmName} ?"
icon="pi pi-exclamation-triangle" />
</p:commandButton>
</fr:commandButton>
</div>
</p:column>
</p:dataTable>
@@ -234,72 +246,59 @@
<h:form id="formAssignRealm">
<div class="grid">
<div class="col-12">
<label class="block text-900 font-semibold mb-2">
<i class="pi pi-user text-primary mr-1"></i>
Utilisateur *
</label>
<p:selectOneMenu value="#{realmAssignmentBean.selectedUserId}"
styleClass="w-full"
<fr:fieldSelect id="userId"
label="Utilisateur *"
value="#{realmAssignmentBean.selectedUserId}"
filter="true"
filterMatchMode="contains">
filterMatchMode="contains"
iconLeft="pi pi-user">
<f:selectItem itemLabel="Sélectionner un utilisateur" itemValue="" noSelectionOption="true" />
<f:selectItems value="#{realmAssignmentBean.availableUsers}"
var="user"
itemValue="#{user.id}"
itemLabel="#{user.username} (#{user.email})" />
</p:selectOneMenu>
</fr:fieldSelect>
</div>
<div class="col-12">
<label class="block text-900 font-semibold mb-2">
<i class="pi pi-globe text-primary mr-1"></i>
Realm *
</label>
<p:selectOneMenu value="#{realmAssignmentBean.selectedRealmName}"
styleClass="w-full">
<fr:fieldSelect id="realmName"
label="Realm *"
value="#{realmAssignmentBean.selectedRealmName}"
iconLeft="pi pi-globe">
<f:selectItem itemLabel="Sélectionner un realm" itemValue="" noSelectionOption="true" />
<f:selectItems value="#{realmAssignmentBean.availableRealms}" />
</p:selectOneMenu>
</fr:fieldSelect>
</div>
<div class="col-12">
<label class="block text-900 font-semibold mb-2">
<i class="pi pi-comment text-primary mr-1"></i>
Raison
</label>
<p:inputText value="#{realmAssignmentBean.newAssignment.raison}"
styleClass="w-full"
placeholder="Ex: Nouveau gestionnaire du realm client" />
<fr:fieldInput id="raison"
label="Raison"
value="#{realmAssignmentBean.newAssignment.raison}"
placeholder="Ex: Nouveau gestionnaire du realm client"
iconLeft="pi pi-comment" />
</div>
<div class="col-12">
<label class="block text-900 font-semibold mb-2">
<i class="pi pi-file-edit text-primary mr-1"></i>
Commentaires
</label>
<p:inputTextarea value="#{realmAssignmentBean.newAssignment.commentaires}"
rows="3"
styleClass="w-full"
placeholder="Commentaires administratifs (optionnel)" />
<fr:fieldTextarea id="commentaires"
label="Commentaires"
value="#{realmAssignmentBean.newAssignment.commentaires}"
rows="3"
placeholder="Commentaires administratifs (optionnel)"
iconLeft="pi pi-file-edit" />
</div>
<div class="col-12">
<div class="flex align-items-center">
<p:selectBooleanCheckbox value="#{realmAssignmentBean.newAssignment.temporaire}"
itemLabel="Affectation temporaire"
styleClass="mr-2" />
</div>
<fr:fieldCheckbox id="temporaire"
label="Affectation temporaire"
value="#{realmAssignmentBean.newAssignment.temporaire}" />
</div>
<div class="col-12" rendered="#{realmAssignmentBean.newAssignment.temporaire}">
<label class="block text-900 font-semibold mb-2">
<i class="pi pi-calendar text-primary mr-1"></i>
Date d'expiration
</label>
<p:calendar value="#{realmAssignmentBean.newAssignment.dateExpiration}"
pattern="dd/MM/yyyy HH:mm"
showTime="true"
styleClass="w-full" />
<fr:fieldCalendar id="dateExpiration"
label="Date d'expiration"
value="#{realmAssignmentBean.newAssignment.dateExpiration}"
pattern="dd/MM/yyyy HH:mm"
showTime="true" />
</div>
<div class="col-12">
@@ -319,18 +318,20 @@
<div class="col-12">
<div class="flex gap-2">
<p:commandButton value="Annuler"
icon="pi pi-times"
styleClass="p-button-text flex-1"
onclick="PF('assignRealmDialog').hide();"
type="button"
action="#{realmAssignmentBean.resetForm}" />
<p:commandButton value="Assigner"
icon="pi pi-check"
styleClass="p-button-success flex-1"
action="#{realmAssignmentBean.assignRealm}"
update=":formRealmAssignments :formAssignRealm"
oncomplete="if (args.validationFailed == false) PF('assignRealmDialog').hide();" />
<fr:commandButton value="Annuler"
icon="pi pi-times"
text="true"
styleClass="flex-1"
onclick="PF('assignRealmDialog').hide();"
type="button"
action="#{realmAssignmentBean.resetForm}" />
<fr:commandButton value="Assigner"
icon="pi pi-check"
severity="success"
styleClass="flex-1"
action="#{realmAssignmentBean.assignRealm}"
update=":formRealmAssignments :formAssignRealm"
oncomplete="if (args.validationFailed == false) PF('assignRealmDialog').hide();" />
</div>
</div>
</div>
@@ -338,11 +339,12 @@
</p:dialog>
<!-- ================================================================
DIALOG DE CONFIRMATION
DIALOG DE CONFIRMATION (Freya Extension)
================================================================ -->
<!-- Le confirmDialog est géré par p:confirm dans les boutons d'action -->
<p:confirmDialog global="true" showEffect="fade" hideEffect="fade" responsive="true" width="400">
<p:commandButton value="Non" type="button" styleClass="p-button-text" icon="pi pi-times" />
<p:commandButton value="Oui" type="button" styleClass="p-button-danger" icon="pi pi-check" />
<fr:commandButton value="Non" type="button" text="true" icon="pi pi-times" />
<fr:commandButton value="Oui" type="button" severity="danger" icon="pi pi-check" />
</p:confirmDialog>
</ui:define>

View File

@@ -4,6 +4,7 @@
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:fr="http://primefaces.org/freya"
template="/templates/main-template.xhtml">
<ui:param name="page" value="#{auditConsultationBean}"/>
@@ -25,12 +26,11 @@
</div>
</div>
<h:form id="formHeaderActions">
<p:commandButton
value="Exporter CSV"
icon="pi pi-download"
styleClass="p-button-success"
action="#{auditConsultationBean.exportToCSV}"
ajax="false" />
<fr:commandButton value="Exporter CSV"
icon="pi pi-download"
severity="success"
action="#{auditConsultationBean.exportToCSV}"
ajax="false" />
</h:form>
</div>
</div>
@@ -144,74 +144,70 @@
<h:form id="formFilters">
<div class="grid">
<div class="col-12 md:col-6 lg:col-4">
<label for="acteurFilter" class="block text-900 font-medium mb-2">Acteur</label>
<p:inputText id="acteurFilter"
value="#{auditConsultationBean.acteurUsername}"
placeholder="Nom d'utilisateur..."
styleClass="w-full" />
<fr:fieldInput id="acteurFilter"
label="Acteur"
value="#{auditConsultationBean.acteurUsername}"
placeholder="Nom d'utilisateur..."
iconLeft="pi pi-user" />
</div>
<div class="col-12 md:col-6 lg:col-4">
<label for="typeActionFilter" class="block text-900 font-medium mb-2">Type d'action</label>
<p:selectOneMenu id="typeActionFilter"
<fr:fieldSelect id="typeActionFilter"
label="Type d'action"
value="#{auditConsultationBean.selectedTypeAction}"
styleClass="w-full">
iconLeft="pi pi-bolt">
<f:selectItem itemLabel="Tous les types" itemValue="" />
<f:selectItems value="#{auditConsultationBean.typeActionOptions}" />
</p:selectOneMenu>
</fr:fieldSelect>
</div>
<div class="col-12 md:col-6 lg:col-4">
<label for="succesFilter" class="block text-900 font-medium mb-2">Résultat</label>
<p:selectOneMenu id="succesFilter"
<fr:fieldSelect id="succesFilter"
label="Résultat"
value="#{auditConsultationBean.succes}"
styleClass="w-full">
iconLeft="pi pi-check-circle">
<f:selectItem itemLabel="Tous" itemValue="" />
<f:selectItem itemLabel="Succès" itemValue="true" />
<f:selectItem itemLabel="Échec" itemValue="false" />
</p:selectOneMenu>
</fr:fieldSelect>
</div>
<div class="col-12 md:col-6 lg:col-4">
<label for="dateDebutFilter" class="block text-900 font-medium mb-2">Date début</label>
<p:calendar id="dateDebutFilter"
value="#{auditConsultationBean.dateDebut}"
pattern="dd/MM/yyyy"
showIcon="true"
styleClass="w-full" />
<fr:fieldCalendar id="dateDebutFilter"
label="Date début"
value="#{auditConsultationBean.dateDebut}"
pattern="dd/MM/yyyy"
showIcon="true" />
</div>
<div class="col-12 md:col-6 lg:col-4">
<label for="dateFinFilter" class="block text-900 font-medium mb-2">Date fin</label>
<p:calendar id="dateFinFilter"
value="#{auditConsultationBean.dateFin}"
pattern="dd/MM/yyyy"
showIcon="true"
styleClass="w-full" />
<fr:fieldCalendar id="dateFinFilter"
label="Date fin"
value="#{auditConsultationBean.dateFin}"
pattern="dd/MM/yyyy"
showIcon="true" />
</div>
<div class="col-12 md:col-6 lg:col-4">
<label for="ressourceFilter" class="block text-900 font-medium mb-2">Type ressource</label>
<p:inputText id="ressourceFilter"
value="#{auditConsultationBean.ressourceType}"
placeholder="USER, ROLE, CLIENT..."
styleClass="w-full" />
<fr:fieldInput id="ressourceFilter"
label="Type ressource"
value="#{auditConsultationBean.ressourceType}"
placeholder="USER, ROLE, CLIENT..."
iconLeft="pi pi-database" />
</div>
</div>
<div class="flex gap-2 justify-content-end mt-4">
<p:commandButton
value="Rechercher"
icon="pi pi-search"
styleClass="p-button-primary"
action="#{auditConsultationBean.searchLogs}"
update=":formAuditLogs:auditLogsTable" />
<p:commandButton
value="Réinitialiser"
icon="pi pi-refresh"
styleClass="p-button-secondary"
action="#{auditConsultationBean.resetFilters}"
update=":formAuditLogs:auditLogsTable @form" />
<fr:commandButton value="Rechercher"
icon="pi pi-search"
severity="primary"
action="#{auditConsultationBean.searchLogs}"
update=":formAuditLogs:auditLogsTable" />
<fr:commandButton value="Réinitialiser"
icon="pi pi-refresh"
severity="secondary"
action="#{auditConsultationBean.resetFilters}"
update=":formAuditLogs:auditLogsTable @form" />
</div>
</h:form>
</div>
@@ -227,9 +223,9 @@
<i class="pi pi-list text-blue-500" style="font-size: 1.5rem"></i>
<h5 class="m-0">Logs d'Audit</h5>
</div>
<p:tag value="#{auditConsultationBean.totalRecords} log(s)"
severity="info"
icon="pi pi-history" />
<fr:tag value="#{auditConsultationBean.totalRecords} log(s)"
severity="info"
icon="pi pi-history" />
</div>
<h:form id="formAuditLogs">
@@ -245,16 +241,16 @@
currentPageReportTemplate="Affichage {startRecord}-{endRecord} sur {totalRecords}"
styleClass="w-full"
emptyMessage="Aucun log d'audit trouvé"
reflow="true">
responsiveLayout="scroll">
<!-- Colonne Statut -->
<p:column headerText="Statut" style="width: 100px; text-align: center">
<p:tag value="#{log.succes ? 'Succès' : 'Échec'}"
severity="#{log.succes ? 'success' : 'danger'}" />
<p:column headerText="Statut" style="width: 100px; text-align: center" priority="2">
<fr:tag value="#{log.succes ? 'Succès' : 'Échec'}"
severity="#{log.succes ? 'success' : 'danger'}" />
</p:column>
<!-- Colonne Type d'action -->
<p:column headerText="Type d'action" sortBy="#{log.typeAction}" style="width: 180px">
<p:column headerText="Type d'action" sortBy="#{log.typeAction}" style="width: 180px" priority="1">
<div class="flex align-items-center gap-2">
<i class="pi pi-bolt text-orange-500"></i>
<span class="font-semibold text-900">#{log.typeAction}</span>
@@ -262,7 +258,7 @@
</p:column>
<!-- Colonne Acteur -->
<p:column headerText="Acteur" sortBy="#{log.acteurUsername}" style="width: 200px">
<p:column headerText="Acteur" sortBy="#{log.acteurUsername}" style="width: 200px" priority="3">
<div class="flex align-items-center gap-2">
<div class="border-circle bg-primary text-white flex align-items-center justify-content-center"
style="width: 32px; height: 32px; flex-shrink: 0; font-size: 0.75rem;">
@@ -275,7 +271,7 @@
</p:column>
<!-- Colonne Ressource -->
<p:column headerText="Ressource" style="width: 150px">
<p:column headerText="Ressource" style="width: 150px" priority="5">
<div class="flex align-items-center gap-2">
<i class="pi pi-database text-blue-500"></i>
<span class="text-900">#{log.ressourceType}</span>
@@ -283,7 +279,7 @@
</p:column>
<!-- Colonne Date -->
<p:column headerText="Date" sortBy="#{log.dateAction}" style="width: 180px">
<p:column headerText="Date" sortBy="#{log.dateAction}" style="width: 180px" priority="4">
<div class="flex align-items-center gap-2">
<i class="pi pi-calendar text-purple-500"></i>
<span class="text-900">#{log.dateAction}</span>
@@ -291,28 +287,30 @@
</p:column>
<!-- Colonne Détails -->
<p:column headerText="Détails" style="width: 250px">
<p:column headerText="Détails" style="width: 250px" priority="6">
<span class="text-600 text-sm" style="display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
#{not empty log.details ? log.details : '-'}
</span>
</p:column>
<!-- Colonne IP -->
<p:column headerText="Adresse IP" style="width: 130px">
<p:column headerText="Adresse IP" style="width: 130px" priority="6">
<span class="text-600 text-sm font-mono">
#{not empty log.adresseIp ? log.adresseIp : '-'}
</span>
</p:column>
<!-- Colonne Actions -->
<p:column headerText="Actions" style="width: 80px; text-align: center">
<p:commandButton
icon="pi pi-eye"
styleClass="p-button-rounded p-button-text p-button-sm p-button-info"
title="Voir les détails"
onclick="PF('auditLogDetailsDialog').show()">
<p:column headerText="Actions" style="width: 80px; text-align: center" priority="1">
<fr:commandButton icon="pi pi-eye"
rounded="true"
text="true"
size="small"
severity="info"
title="Voir les détails"
onclick="PF('auditLogDetailsDialog').show()">
<f:setPropertyActionListener target="#{auditConsultationBean.selectedLog}" value="#{log}" />
</p:commandButton>
</fr:commandButton>
</p:column>
</p:dataTable>
</h:form>
@@ -336,8 +334,8 @@
<div class="surface-50 border-round p-3">
<div class="flex align-items-center justify-content-between">
<span class="text-600 font-medium">Statut</span>
<p:tag value="#{auditConsultationBean.selectedLog.succes ? 'Succès' : 'Échec'}"
severity="#{auditConsultationBean.selectedLog.succes ? 'success' : 'danger'}" />
<fr:tag value="#{auditConsultationBean.selectedLog.succes ? 'Succès' : 'Échec'}"
severity="#{auditConsultationBean.selectedLog.succes ? 'success' : 'danger'}" />
</div>
</div>
@@ -409,12 +407,11 @@
</div>
<div class="flex justify-content-end mt-4">
<p:commandButton
value="Fermer"
icon="pi pi-times"
styleClass="p-button-secondary"
onclick="PF('auditLogDetailsDialog').hide()"
type="button" />
<fr:commandButton value="Fermer"
icon="pi pi-times"
severity="secondary"
onclick="PF('auditLogDetailsDialog').hide()"
type="button" />
</div>
</h:form>
</p:dialog>

View File

@@ -4,6 +4,7 @@
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:fr="http://primefaces.org/freya"
template="/templates/main-template.xhtml">
<ui:define name="title">Tableau de Bord - Lions User Manager</ui:define>
@@ -21,114 +22,174 @@
<i class="pi pi-home text-blue-500" style="font-size: 2rem"></i>
<div>
<h3 class="m-0 mb-1">Tableau de Bord</h3>
<p class="text-600 m-0">Vue d'ensemble de la gestion des utilisateurs Keycloak</p>
<p class="text-600 m-0">Vue d'ensemble de la gestion des utilisateurs - Realm: #{dashboardBean.realmName}</p>
</div>
</div>
<p:commandButton
value="Rafraîchir"
icon="pi pi-refresh"
styleClass="p-button-secondary"
action="#{dashboardBean.refreshStatistics}"
update=":formDashboard" />
<fr:commandButton value="Rafraîchir"
icon="pi pi-refresh"
severity="secondary"
action="#{dashboardBean.refreshStatistics}"
update=":formDashboard" />
</div>
</div>
</div>
<!-- ================================================================
STATISTIQUES PRINCIPALES (4 KPI CARDS)
STATISTIQUES PRINCIPALES - KPIs MÉTIER
================================================================ -->
<div class="col-12">
<h5 class="mb-3">Statistiques Principales</h5>
<h5 class="mb-3">Indicateurs Clés de Performance</h5>
</div>
<!-- KPI 1: Utilisateurs Actifs -->
<!-- KPI 1: Total Utilisateurs -->
<div class="col-12 md:col-6 lg:col-3">
<div class="card surface-0 border-round-lg hover:surface-100 cursor-pointer transition-colors transition-duration-150">
<p:commandButton
styleClass="p-0 w-full text-left border-none bg-transparent hover:bg-transparent active:bg-transparent"
outcome="/pages/user-manager/users/list">
<div class="flex align-items-start justify-content-between mb-3">
<fr:commandButton styleClass="p-0 w-full text-left border-none bg-transparent hover:bg-transparent active:bg-transparent"
outcome="/pages/user-manager/users/list">
<div class="flex align-items-start justify-content-between">
<div>
<div class="text-500 font-medium mb-1">Utilisateurs Actifs</div>
<div class="text-900 font-bold text-2xl">#{dashboardBean.totalUsersDisplay}</div>
<div class="text-500 font-medium mb-2 text-sm uppercase">Total Utilisateurs</div>
<div class="text-900 font-bold text-4xl">#{dashboardBean.totalUsersDisplay}</div>
<div class="text-600 text-sm mt-2">
<i class="pi pi-users mr-2"></i>
Dans le système
</div>
</div>
<div class="flex align-items-center justify-content-center bg-blue-100 border-circle"
style="width: 2.5rem; height: 2.5rem">
<i class="pi pi-users text-blue-600 text-xl"></i>
style="width: 3.5rem; height: 3.5rem">
<i class="pi pi-users text-blue-600" style="font-size: 1.75rem"></i>
</div>
</div>
<div class="text-500 text-sm">
<i class="pi pi-arrow-right text-600"></i>
<span class="ml-2">Total utilisateurs</span>
</div>
</p:commandButton>
</fr:commandButton>
</div>
</div>
<!-- KPI 2: Rôles Realm -->
<!-- KPI 2: Utilisateurs Actifs -->
<div class="col-12 md:col-6 lg:col-3">
<div class="card surface-0 border-round-lg hover:surface-100 cursor-pointer transition-colors transition-duration-150">
<p:commandButton
styleClass="p-0 w-full text-left border-none bg-transparent hover:bg-transparent active:bg-transparent"
outcome="/pages/user-manager/roles/list">
<div class="flex align-items-start justify-content-between mb-3">
<fr:commandButton styleClass="p-0 w-full text-left border-none bg-transparent hover:bg-transparent active:bg-transparent"
outcome="/pages/user-manager/users/list">
<div class="flex align-items-start justify-content-between">
<div>
<div class="text-500 font-medium mb-1">Rôles Realm</div>
<div class="text-900 font-bold text-2xl">#{dashboardBean.totalRolesDisplay}</div>
<div class="text-500 font-medium mb-2 text-sm uppercase">Utilisateurs Actifs</div>
<div class="text-900 font-bold text-4xl">#{dashboardBean.activeUsersDisplay}</div>
<div class="text-600 text-sm mt-2">
<i class="pi pi-check-circle mr-2"></i>
Comptes activés
</div>
</div>
<div class="flex align-items-center justify-content-center bg-green-100 border-circle"
style="width: 2.5rem; height: 2.5rem">
<i class="pi pi-shield text-green-600 text-xl"></i>
style="width: 3.5rem; height: 3.5rem">
<i class="pi pi-check-circle text-green-600" style="font-size: 1.75rem"></i>
</div>
</div>
<div class="text-500 text-sm">
<i class="pi pi-arrow-right text-600"></i>
<span class="ml-2">Rôles configurés</span>
</div>
</p:commandButton>
</fr:commandButton>
</div>
</div>
<!-- KPI 3: Actions Récentes -->
<!-- KPI 3: Utilisateurs Inactifs -->
<div class="col-12 md:col-6 lg:col-3">
<div class="card surface-0 border-round-lg hover:surface-100 cursor-pointer transition-colors transition-duration-150">
<p:commandButton
styleClass="p-0 w-full text-left border-none bg-transparent hover:bg-transparent active:bg-transparent"
outcome="/pages/user-manager/audit/logs">
<div class="flex align-items-start justify-content-between mb-3">
<fr:commandButton styleClass="p-0 w-full text-left border-none bg-transparent hover:bg-transparent active:bg-transparent"
outcome="/pages/user-manager/users/list">
<div class="flex align-items-start justify-content-between">
<div>
<div class="text-500 font-medium mb-1">Actions Récentes</div>
<div class="text-900 font-bold text-2xl">#{dashboardBean.recentActionsDisplay}</div>
<div class="text-500 font-medium mb-2 text-sm uppercase">Utilisateurs Inactifs</div>
<div class="text-900 font-bold text-4xl">#{dashboardBean.inactiveUsersDisplay}</div>
<div class="text-600 text-sm mt-2">
<i class="pi pi-ban mr-2"></i>
Comptes désactivés
</div>
</div>
<div class="flex align-items-center justify-content-center bg-orange-100 border-circle"
style="width: 2.5rem; height: 2.5rem">
<i class="pi pi-history text-orange-600 text-xl"></i>
style="width: 3.5rem; height: 3.5rem">
<i class="pi pi-ban text-orange-600" style="font-size: 1.75rem"></i>
</div>
</div>
<div class="text-500 text-sm">
<i class="pi pi-arrow-right text-600"></i>
<span class="ml-2">Dernières 24h</span>
</div>
</p:commandButton>
</fr:commandButton>
</div>
</div>
<!-- KPI 4: Taux d'Activation -->
<!-- KPI 4: Taux de Succès 24h -->
<div class="col-12 md:col-6 lg:col-3">
<div class="card surface-0 border-round-lg">
<div class="flex align-items-start justify-content-between mb-3">
<div>
<div class="text-500 font-medium mb-1">Realm Actif</div>
<div class="text-900 font-bold text-xl" style="word-break: break-word;">lions-user-manager</div>
</div>
<div class="flex align-items-center justify-content-center bg-purple-100 border-circle"
style="width: 2.5rem; height: 2.5rem">
<i class="pi pi-globe text-purple-600 text-xl"></i>
<div class="card surface-0 border-round-lg hover:surface-100 cursor-pointer transition-colors transition-duration-150">
<fr:commandButton styleClass="p-0 w-full text-left border-none bg-transparent hover:bg-transparent active:bg-transparent"
outcome="/pages/user-manager/audit/logs">
<div class="flex align-items-start justify-content-between">
<div>
<div class="text-500 font-medium mb-2 text-sm uppercase">Taux de Succès</div>
<div class="text-900 font-bold text-4xl">#{dashboardBean.successRate24hDisplay}</div>
<div class="text-600 text-sm mt-2">
<i class="pi pi-chart-line mr-2"></i>
Dernières 24h
</div>
</div>
<div class="flex align-items-center justify-content-center bg-cyan-100 border-circle"
style="width: 3.5rem; height: 3.5rem">
<i class="pi pi-chart-line text-cyan-600" style="font-size: 1.75rem"></i>
</div>
</div>
</fr:commandButton>
</div>
</div>
<!-- ================================================================
ACTIVITÉ & PERFORMANCE
================================================================ -->
<div class="col-12 lg:col-6">
<div class="card h-full">
<div class="flex align-items-center gap-2 mb-4">
<i class="pi pi-history text-purple-500" style="font-size: 1.5rem"></i>
<h5 class="m-0">Activité &amp; Performance</h5>
</div>
<div class="flex align-items-center gap-2">
<p:tag value="Opérationnel" severity="success" styleClass="text-xs" />
<span class="text-500 text-sm">Realm Keycloak</span>
<div class="grid">
<!-- Actions 24h -->
<div class="col-12 md:col-4">
<div class="surface-50 border-round p-3 text-center">
<div class="text-500 text-xs uppercase mb-2">Actions 24h</div>
<div class="text-900 font-bold text-3xl mb-2">#{dashboardBean.actionsLast24hDisplay}</div>
<fr:tag value="Dernières 24h" severity="info" styleClass="text-xs" />
</div>
</div>
<!-- Actions 7j -->
<div class="col-12 md:col-4">
<div class="surface-50 border-round p-3 text-center">
<div class="text-500 text-xs uppercase mb-2">Actions 7j</div>
<div class="text-900 font-bold text-3xl mb-2">#{dashboardBean.actionsLast7dDisplay}</div>
<fr:tag value="Derniers 7 jours" severity="info" styleClass="text-xs" />
</div>
</div>
<!-- Taux de réussite -->
<div class="col-12 md:col-4">
<div class="surface-50 border-round p-3 text-center">
<div class="text-500 text-xs uppercase mb-2">Performance</div>
<div class="text-900 font-bold text-3xl mb-2">#{dashboardBean.successRate24hDisplay}</div>
<fr:tag value="Taux de succès" severity="success" styleClass="text-xs" />
</div>
</div>
<!-- Détails succès/échecs -->
<div class="col-12 mt-3">
<div class="surface-100 border-round p-3">
<div class="flex align-items-center justify-content-between mb-2">
<div class="flex align-items-center gap-2">
<i class="pi pi-check-circle text-green-500"></i>
<span class="text-700 font-medium">Actions réussies</span>
</div>
<span class="font-bold text-900">#{dashboardBean.successfulActions24h}</span>
</div>
<div class="flex align-items-center justify-content-between">
<div class="flex align-items-center gap-2">
<i class="pi pi-times-circle text-red-500"></i>
<span class="text-700 font-medium">Actions échouées</span>
</div>
<span class="font-bold text-900">#{dashboardBean.failedActions24h}</span>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -145,32 +206,32 @@
<div class="grid">
<div class="col-12 md:col-6">
<p:commandButton
value="Nouvel Utilisateur"
icon="pi pi-user-plus"
styleClass="w-full p-button-success mb-2"
outcome="/pages/user-manager/users/create" />
<fr:commandButton value="Nouvel Utilisateur"
icon="pi pi-user-plus"
severity="success"
styleClass="w-full mb-2"
outcome="/pages/user-manager/users/create" />
</div>
<div class="col-12 md:col-6">
<p:commandButton
value="Liste des Utilisateurs"
icon="pi pi-users"
styleClass="w-full p-button-primary mb-2"
outcome="/pages/user-manager/users/list" />
<fr:commandButton value="Liste des Utilisateurs"
icon="pi pi-users"
severity="primary"
styleClass="w-full mb-2"
outcome="/pages/user-manager/users/list" />
</div>
<div class="col-12 md:col-6">
<p:commandButton
value="Gestion des Rôles"
icon="pi pi-shield"
styleClass="w-full p-button-info mb-2"
outcome="/pages/user-manager/roles/list" />
<fr:commandButton value="Gestion des Rôles"
icon="pi pi-shield"
severity="info"
styleClass="w-full mb-2"
outcome="/pages/user-manager/roles/list" />
</div>
<div class="col-12 md:col-6">
<p:commandButton
value="Journal d'Audit"
icon="pi pi-history"
styleClass="w-full p-button-help mb-2"
outcome="/pages/user-manager/audit/logs" />
<fr:commandButton value="Journal d'Audit"
icon="pi pi-history"
severity="help"
styleClass="w-full mb-2"
outcome="/pages/user-manager/audit/logs" />
</div>
</div>
@@ -179,7 +240,113 @@
<i class="pi pi-lightbulb text-orange-500"></i>
<div>
<div class="text-700 font-semibold text-sm">Conseil</div>
<small class="text-600">Utilisez les raccourcis ci-dessus pour accéder rapidement aux fonctionnalités principales</small>
<small class="text-600">Utilisez ces raccourcis pour accéder rapidement aux fonctionnalités principales</small>
</div>
</div>
</div>
</div>
</div>
<!-- ================================================================
ALERTES DE SÉCURITÉ (Conditionnel)
================================================================ -->
<h:panelGroup layout="block" styleClass="col-12" rendered="#{dashboardBean.hasAlerts()}">
<div class="card border-left-3 border-red-500">
<div class="flex align-items-center gap-2 mb-4">
<i class="pi pi-exclamation-triangle text-red-500" style="font-size: 1.5rem"></i>
<h5 class="m-0 text-red-600">Alertes de Sécurité</h5>
</div>
<div class="grid">
<!-- Actions critiques -->
<div class="col-12 md:col-4">
<div class="surface-50 border-round p-3 text-center">
<i class="pi pi-shield text-red-500 mb-2" style="font-size: 2rem"></i>
<div class="text-900 font-bold text-2xl mb-1">#{dashboardBean.criticalActions24hDisplay}</div>
<div class="text-600 text-sm">Actions critiques</div>
<small class="text-500">Dernières 24h</small>
</div>
</div>
<!-- Tentatives échouées -->
<div class="col-12 md:col-4">
<div class="surface-50 border-round p-3 text-center">
<i class="pi pi-lock text-orange-500 mb-2" style="font-size: 2rem"></i>
<div class="text-900 font-bold text-2xl mb-1">#{dashboardBean.failedLogins24hDisplay}</div>
<div class="text-600 text-sm">Connexions échouées</div>
<small class="text-500">Dernières 24h</small>
</div>
</div>
<!-- Utilisateurs à risque -->
<div class="col-12 md:col-4">
<div class="surface-50 border-round p-3 text-center">
<i class="pi pi-user-minus text-red-500 mb-2" style="font-size: 2rem"></i>
<div class="text-900 font-bold text-2xl mb-1">#{dashboardBean.usersAtRiskDisplay}</div>
<div class="text-600 text-sm">Utilisateurs à risque</div>
<small class="text-500">Nécessitent attention</small>
</div>
</div>
</div>
<div class="mt-3 surface-100 border-round p-3 border-left-3 border-orange-500">
<div class="flex align-items-center gap-2">
<i class="pi pi-info-circle text-orange-500"></i>
<div>
<div class="text-700 font-semibold text-sm">Recommandation</div>
<small class="text-600">Consultez le journal d'audit pour analyser les événements suspects</small>
</div>
</div>
</div>
</div>
</h:panelGroup>
<!-- ================================================================
RESSOURCES MÉTIER
================================================================ -->
<div class="col-12 lg:col-6">
<div class="card h-full">
<div class="flex align-items-center gap-2 mb-4">
<i class="pi pi-database text-blue-500" style="font-size: 1.5rem"></i>
<h5 class="m-0">Ressources</h5>
</div>
<div class="grid">
<!-- Total Rôles -->
<div class="col-12">
<div class="surface-50 border-round p-3">
<div class="flex align-items-center justify-content-between">
<div class="flex align-items-center gap-3">
<div class="flex align-items-center justify-content-center bg-green-100 border-circle"
style="width: 3rem; height: 3rem">
<i class="pi pi-shield text-green-600 text-xl"></i>
</div>
<div>
<div class="text-500 text-xs uppercase mb-1">Rôles Realm</div>
<div class="text-900 font-bold text-2xl">#{dashboardBean.totalRolesDisplay}</div>
</div>
</div>
<fr:commandButton icon="pi pi-arrow-right"
text="true"
severity="secondary"
outcome="/pages/user-manager/roles/list" />
</div>
</div>
</div>
<!-- Realm actif -->
<div class="col-12">
<div class="surface-50 border-round p-3">
<div class="flex align-items-center justify-content-between">
<div class="flex align-items-center gap-2">
<i class="pi pi-globe text-500"></i>
<span class="text-600 font-medium">Realm Keycloak</span>
</div>
<div class="flex align-items-center gap-2">
<span class="font-semibold text-900">#{dashboardBean.realmName}</span>
<fr:tag value="Actif" severity="success" styleClass="text-xs" />
</div>
</div>
</div>
</div>
</div>
@@ -192,7 +359,7 @@
<div class="col-12 lg:col-6">
<div class="card h-full">
<div class="flex align-items-center gap-2 mb-4">
<i class="pi pi-info-circle text-blue-500" style="font-size: 1.5rem"></i>
<i class="pi pi-info-circle text-cyan-500" style="font-size: 1.5rem"></i>
<h5 class="m-0">Informations Système</h5>
</div>
@@ -202,34 +369,12 @@
<div class="flex align-items-center justify-content-between">
<div class="flex align-items-center gap-2">
<i class="pi pi-tag text-500"></i>
<span class="text-600 font-medium">Version</span>
<span class="text-600 font-medium">Version Application</span>
</div>
<span class="font-semibold text-900">1.0.0</span>
</div>
</div>
<!-- Realm Keycloak -->
<div class="surface-50 border-round p-3">
<div class="flex align-items-center justify-content-between">
<div class="flex align-items-center gap-2">
<i class="pi pi-globe text-500"></i>
<span class="text-600 font-medium">Realm Keycloak</span>
</div>
<span class="font-semibold text-900">lions-user-manager</span>
</div>
</div>
<!-- Statut -->
<div class="surface-50 border-round p-3">
<div class="flex align-items-center justify-content-between">
<div class="flex align-items-center gap-2">
<i class="pi pi-check-circle text-500"></i>
<span class="text-600 font-medium">Statut</span>
</div>
<p:tag value="Opérationnel" severity="success" />
</div>
</div>
<!-- Framework -->
<div class="surface-50 border-round p-3">
<div class="flex align-items-center justify-content-between">
@@ -237,90 +382,18 @@
<i class="pi pi-code text-500"></i>
<span class="text-600 font-medium">Framework</span>
</div>
<span class="font-semibold text-900 text-right">Quarkus 3.15.1</span>
<span class="font-semibold text-900 text-right">Quarkus + PrimeFaces Freya</span>
</div>
</div>
<!-- Interface -->
<!-- Statut -->
<div class="surface-50 border-round p-3">
<div class="flex align-items-center justify-content-between">
<div class="flex align-items-center gap-2">
<i class="pi pi-palette text-500"></i>
<span class="text-600 font-medium">Interface</span>
<i class="pi pi-check-circle text-500"></i>
<span class="text-600 font-medium">Statut Système</span>
</div>
<span class="font-semibold text-900">PrimeFaces Freya</span>
</div>
</div>
<!-- Environnement -->
<div class="surface-50 border-round p-3">
<div class="flex align-items-center justify-content-between">
<div class="flex align-items-center gap-2">
<i class="pi pi-server text-500"></i>
<span class="text-600 font-medium">Environnement</span>
</div>
<p:tag value="Développement" severity="warning" styleClass="text-xs" />
</div>
</div>
</div>
</div>
</div>
<!-- ================================================================
ACTIVITÉS RÉCENTES
================================================================ -->
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-4">
<div class="flex align-items-center gap-2">
<i class="pi pi-clock text-purple-500" style="font-size: 1.5rem"></i>
<h5 class="m-0">Activités Récentes</h5>
</div>
<p:commandButton
value="Voir tout"
icon="pi pi-arrow-right"
styleClass="p-button-text p-button-sm"
outcome="/pages/user-manager/audit/logs" />
</div>
<div class="grid">
<!-- Statistique 1: Utilisateurs créés aujourd'hui -->
<div class="col-12 md:col-6 lg:col-3">
<div class="surface-50 border-round p-3 text-center">
<i class="pi pi-user-plus text-blue-500 mb-2" style="font-size: 2rem"></i>
<div class="text-900 font-bold text-xl mb-1">0</div>
<div class="text-600 text-sm">Utilisateurs créés</div>
<small class="text-500">Aujourd'hui</small>
</div>
</div>
<!-- Statistique 2: Rôles modifiés -->
<div class="col-12 md:col-6 lg:col-3">
<div class="surface-50 border-round p-3 text-center">
<i class="pi pi-shield text-green-500 mb-2" style="font-size: 2rem"></i>
<div class="text-900 font-bold text-xl mb-1">0</div>
<div class="text-600 text-sm">Rôles modifiés</div>
<small class="text-500">Cette semaine</small>
</div>
</div>
<!-- Statistique 3: Sessions actives -->
<div class="col-12 md:col-6 lg:col-3">
<div class="surface-50 border-round p-3 text-center">
<i class="pi pi-circle-fill text-orange-500 mb-2" style="font-size: 2rem; animation: pulse 2s ease-in-out infinite;"></i>
<div class="text-900 font-bold text-xl mb-1">-</div>
<div class="text-600 text-sm">Sessions actives</div>
<small class="text-500">En temps réel</small>
</div>
</div>
<!-- Statistique 4: Actions critiques -->
<div class="col-12 md:col-6 lg:col-3">
<div class="surface-50 border-round p-3 text-center">
<i class="pi pi-exclamation-triangle text-red-500 mb-2" style="font-size: 2rem"></i>
<div class="text-900 font-bold text-xl mb-1">0</div>
<div class="text-600 text-sm">Actions critiques</div>
<small class="text-500">24 dernières heures</small>
<fr:tag value="Opérationnel" severity="success" />
</div>
</div>
</div>

View File

@@ -1,304 +0,0 @@
<!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"
xmlns:fr="http://primefaces.org/freya"
template="/templates/main-template.xhtml">
<f:metadata>
<f:viewParam name="userId" value="#{userProfilBean.userId}" />
<f:viewParam name="realm" value="#{userProfilBean.realmName}" />
</f:metadata>
<ui:param name="page" value="#{userProfilBean}"/>
<ui:define name="title">Attribution de Rôles - Lions User Manager</ui:define>
<ui:define name="content">
<div class="grid">
<!-- ================================================================
EN-TÊTE DE LA PAGE
================================================================ -->
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between">
<div class="flex align-items-center gap-2">
<i class="pi pi-key text-purple-500" style="font-size: 2rem"></i>
<div>
<h3 class="m-0 mb-1">Attribution de Rôles</h3>
<p class="text-600 m-0">Gérer les rôles de l'utilisateur</p>
</div>
</div>
<h:link outcome="/pages/user-manager/users/list" styleClass="p-button p-button-text">
<i class="pi pi-arrow-left mr-2"></i>
Retour à la liste
</h:link>
</div>
</div>
</div>
<!-- ================================================================
INFORMATIONS UTILISATEUR
================================================================ -->
<div class="col-12">
<div class="card">
<h3 class="text-900 font-semibold text-lg mb-4 flex align-items-center gap-2">
<i class="pi pi-user text-blue-500"></i>
Informations de l'Utilisateur
</h3>
<h:panelGroup rendered="#{userProfilBean.user != null}">
<div class="grid">
<div class="col-12 md:col-4">
<div class="surface-50 border-round p-3 text-center">
<!-- Avatar -->
<div style="width: 80px; height: 80px; border-radius: 50%; background: linear-gradient(135deg, var(--primary-color), var(--primary-600, #387FE9)); display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem auto; font-size: 2rem; font-weight: bold; color: white; box-shadow: 0 4px 12px rgba(0,0,0,0.12);">
<h:outputText value="#{userProfilBean.user.username.substring(0,2).toUpperCase()}" />
</div>
<h4 class="text-900 font-semibold m-0 mb-1">#{userProfilBean.user.username}</h4>
<p class="text-600 m-0 text-sm">#{userProfilBean.user.email}</p>
</div>
</div>
<div class="col-12 md:col-8">
<div class="grid">
<div class="col-12 md:col-6">
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Prénom</label>
<p class="text-900 m-0">#{userProfilBean.user.prenom}</p>
</div>
</div>
<div class="col-12 md:col-6">
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Nom</label>
<p class="text-900 m-0">#{userProfilBean.user.nom}</p>
</div>
</div>
<div class="col-12 md:col-6">
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Email</label>
<p class="text-900 m-0">#{userProfilBean.user.email}</p>
</div>
</div>
<div class="col-12 md:col-6">
<div class="mb-0">
<label class="block text-600 font-medium mb-2 text-sm">Statut</label>
<div class="flex align-items-center">
<span class="inline-flex align-items-center px-2 py-1 border-round text-xs font-semibold"
style="background-color: #{userProfilBean.user.enabled ? '#C8E6C9' : '#FFCDD2'}; color: #{userProfilBean.user.enabled ? '#2E7D32' : '#C62828'};">
<i class="pi #{userProfilBean.user.enabled ? 'pi-check-circle' : 'pi-times-circle'} mr-1"></i>
<h:outputText value="Actif" rendered="#{userProfilBean.user.enabled}" />
<h:outputText value="Inactif" rendered="#{!userProfilBean.user.enabled}" />
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</h:panelGroup>
<h:panelGroup rendered="#{userProfilBean.user == null}">
<div class="text-center p-5">
<i class="pi pi-exclamation-triangle text-orange-500" style="font-size: 4rem"></i>
<h4 class="text-900 mt-4 mb-2">Utilisateur non trouvé</h4>
<p class="text-600 mb-3">
<h:outputText value="Aucun ID d'utilisateur fourni" rendered="#{userProfilBean.userId == null or userProfilBean.userId == ''}" />
<h:outputText value="L'utilisateur avec l'ID '#{userProfilBean.userId}' n'existe pas dans le realm '#{userProfilBean.realmName}'" rendered="#{userProfilBean.userId != null and userProfilBean.userId != ''}" />
</p>
<small class="text-500 block mb-4">Pour assigner des rôles, accédez à cette page depuis la liste des utilisateurs</small>
<h:link outcome="/pages/user-manager/users/list" styleClass="p-button p-button-primary">
<i class="pi pi-list mr-2"></i>
Aller à la liste des utilisateurs
</h:link>
</div>
</h:panelGroup>
</div>
</div>
<!-- ================================================================
GESTION DES RÔLES
================================================================ -->
<h:panelGroup rendered="#{userProfilBean.user != null}">
<div class="col-12 lg:col-6">
<div class="card h-full">
<h3 class="text-900 font-semibold text-lg mb-4 flex align-items-center gap-2">
<i class="pi pi-shield text-green-500"></i>
Rôles Actuels
</h3>
<h:form id="formCurrentRoles">
<!-- Liste des rôles actuels -->
<div class="flex flex-column gap-2">
<ui:repeat value="#{userProfilBean.user.realmRoles}" var="role">
<div class="surface-50 border-round p-3">
<div class="flex align-items-center justify-content-between">
<div class="flex align-items-center gap-2 flex-grow-1">
<i class="pi pi-tag text-purple-500"></i>
<div>
<div class="text-900 font-semibold">#{role}</div>
<small class="text-500">Rôle Realm</small>
</div>
</div>
<p:commandButton icon="pi pi-times"
styleClass="p-button-rounded p-button-text p-button-sm p-button-danger"
title="Retirer ce rôle"
action="#{roleGestionBean.revokeRoleFromUser(userProfilBean.userId, role)}"
update=":formCurrentRoles :formAvailableRoles"
oncomplete="PF('formCurrentRoles').refresh();">
<p:confirm header="Confirmation"
message="Voulez-vous vraiment retirer le rôle '#{role}' ?"
icon="pi pi-exclamation-triangle" />
</p:commandButton>
</div>
</div>
</ui:repeat>
<!-- Message si aucun rôle -->
<div class="text-center p-4" rendered="#{userProfilBean.user.realmRoles == null or userProfilBean.user.realmRoles.size() == 0}">
<i class="pi pi-inbox text-400" style="font-size: 2.5rem"></i>
<p class="text-600 mt-3 mb-0">Aucun rôle assigné</p>
<small class="text-500">Assignez des rôles depuis la liste disponible</small>
</div>
</div>
<div class="mt-4 flex align-items-center justify-content-between surface-100 border-round p-3">
<div class="flex align-items-center gap-2">
<i class="pi pi-info-circle text-blue-500"></i>
<span class="text-700 font-semibold">Total: #{userProfilBean.user.realmRoles != null ? userProfilBean.user.realmRoles.size() : 0} rôle(s)</span>
</div>
<fr:commandButton value="Rafraîchir"
icon="pi pi-refresh"
outlined="true"
size="small"
action="#{userProfilBean.loadUser}"
update=":formCurrentRoles :formAvailableRoles" />
</div>
</h:form>
</div>
</div>
<div class="col-12 lg:col-6">
<div class="card h-full">
<h3 class="text-900 font-semibold text-lg mb-4 flex align-items-center gap-2">
<i class="pi pi-plus-circle text-blue-500"></i>
Rôles Disponibles
</h3>
<h:form id="formAvailableRoles">
<p:messages id="messages" showDetail="true" closable="true">
<p:autoUpdate />
</p:messages>
<!-- Liste des rôles disponibles -->
<div class="flex flex-column gap-2">
<ui:repeat value="#{roleGestionBean.realmRoles}" var="role">
<!-- N'afficher que si le rôle n'est pas déjà assigné -->
<h:panelGroup rendered="#{!userProfilBean.user.realmRoles.contains(role.name)}">
<div class="surface-50 border-round p-3">
<div class="flex align-items-center justify-content-between">
<div class="flex-grow-1">
<div class="text-900 font-semibold flex align-items-center gap-2 mb-1">
<i class="pi pi-tag text-blue-500"></i>
<span>#{role.name}</span>
</div>
<p class="text-600 text-sm m-0">
<h:outputText value="#{role.description}" rendered="#{role.description != null and role.description != ''}" />
<h:outputText value="Aucune description" styleClass="text-500 italic" rendered="#{role.description == null or role.description == ''}" />
</p>
</div>
<p:commandButton icon="pi pi-plus"
styleClass="p-button-rounded p-button-success p-button-sm"
title="Assigner ce rôle"
action="#{roleGestionBean.assignRoleToUser(userProfilBean.userId, role.name)}"
update=":formCurrentRoles :formAvailableRoles"
oncomplete="PF('formAvailableRoles').refresh();" />
</div>
</div>
</h:panelGroup>
</ui:repeat>
<!-- Message si aucun rôle disponible -->
<div class="text-center p-4" rendered="#{roleGestionBean.realmRoles == null or roleGestionBean.realmRoles.size() == 0}">
<i class="pi pi-inbox text-400" style="font-size: 2.5rem"></i>
<p class="text-600 mt-3 mb-0">Aucun rôle disponible</p>
<small class="text-500">Créez des rôles depuis la page de gestion des rôles</small>
</div>
</div>
<div class="mt-4 surface-100 border-round p-3">
<div class="flex align-items-center gap-2">
<i class="pi pi-lightbulb text-orange-500"></i>
<div>
<div class="text-700 font-semibold text-sm">Astuce</div>
<small class="text-600">Cliquez sur <i class="pi pi-plus"></i> pour assigner un rôle à l'utilisateur</small>
</div>
</div>
</div>
</h:form>
</div>
</div>
<!-- ================================================================
ACTIONS
================================================================ -->
<div class="col-12">
<div class="card">
<h3 class="text-900 font-semibold text-lg mb-4 flex align-items-center gap-2">
<i class="pi pi-cog text-gray-500"></i>
Actions
</h3>
<h:form id="formActions">
<div class="flex flex-wrap gap-2">
<h:link outcome="/pages/user-manager/users/profile"
styleClass="p-button p-button-outlined">
<f:param name="userId" value="#{userProfilBean.userId}" />
<i class="pi pi-user mr-2"></i>
<span>Voir le Profil</span>
</h:link>
<h:link outcome="/pages/user-manager/users/edit"
styleClass="p-button p-button-outlined">
<f:param name="userId" value="#{userProfilBean.userId}" />
<i class="pi pi-pencil mr-2"></i>
<span>Modifier l'Utilisateur</span>
</h:link>
<h:link outcome="/pages/user-manager/users/list"
styleClass="p-button p-button-outlined p-button-secondary">
<i class="pi pi-list mr-2"></i>
<span>Liste des Utilisateurs</span>
</h:link>
<h:link outcome="/pages/user-manager/roles/list"
styleClass="p-button p-button-outlined p-button-info">
<i class="pi pi-shield mr-2"></i>
<span>Gérer les Rôles</span>
</h:link>
</div>
</h:form>
</div>
</div>
</h:panelGroup>
</div>
<!-- ================================================================
DIALOG DE CONFIRMATION
================================================================ -->
<p:confirmDialog global="true" showEffect="fade" hideEffect="fade"
responsive="true" width="400">
<p:commandButton value="Non" type="button"
styleClass="p-button-text"
icon="pi pi-times" />
<p:commandButton value="Oui" type="button"
styleClass="p-button-danger"
icon="pi pi-check" />
</p:confirmDialog>
</ui:define>
</ui:composition>

View File

@@ -42,164 +42,132 @@
</div>
<!-- ================================================================
FILTRES
FILTRES & KPI INTÉGRÉS (Nombre d'or φ = 1.618 → 62%/38%)
================================================================ -->
<div class="col-12">
<div class="card">
<h3 class="text-900 font-semibold text-lg mb-4 flex align-items-center gap-2">
<i class="pi pi-filter text-blue-500"></i>
Filtres
</h3>
<h:form id="formFilters">
<div class="grid">
<!-- Realm -->
<div class="col-12 md:col-4">
<div class="field mb-0">
<label for="realmFilter" class="block text-900 font-medium mb-2">
Realm
</label>
<p:selectOneMenu id="realmFilter"
value="#{roleGestionBean.realmName}"
styleClass="w-full">
<f:selectItem itemLabel="Sélectionner un realm..." itemValue="" />
<f:selectItems value="#{roleGestionBean.availableRealms}"
var="realm"
itemLabel="#{realm}"
itemValue="#{realm}" />
<p:ajax event="change"
listener="#{roleGestionBean.loadRealmRoles}"
update=":formRealmRoles :formClientRoles :formKpis" />
</p:selectOneMenu>
</div>
</div>
<!-- Client -->
<div class="col-12 md:col-4">
<div class="field mb-0">
<label for="clientFilter" class="block text-900 font-medium mb-2">
Client (optionnel)
</label>
<p:selectOneMenu id="clientFilter"
value="#{roleGestionBean.clientName}"
styleClass="w-full">
<f:selectItem itemLabel="Tous les clients" itemValue="" />
<f:selectItems value="#{roleGestionBean.availableClients}"
var="client"
itemLabel="#{client}"
itemValue="#{client}" />
<p:ajax event="change"
listener="#{roleGestionBean.loadClientRoles}"
update=":formClientRoles" />
</p:selectOneMenu>
</div>
</div>
<!-- Type -->
<div class="col-12 md:col-4">
<div class="field mb-0">
<label for="typeFilter" class="block text-900 font-medium mb-2">
Type de rôle
</label>
<p:selectOneMenu id="typeFilter"
value="#{roleGestionBean.selectedTypeRole}"
styleClass="w-full">
<f:selectItem itemLabel="Tous les types" itemValue="" />
<f:selectItems value="#{roleGestionBean.typeRoleOptions}"
var="type"
itemLabel="#{type}"
itemValue="#{type}" />
</p:selectOneMenu>
</div>
</div>
</div>
</h:form>
</div>
</div>
<!-- ================================================================
KPI CARDS
================================================================ -->
<div class="col-12">
<h:form id="formKpis">
<div class="grid">
<div class="col-12 md:col-6 lg:col-3">
<div class="card surface-0 border-round-lg">
<div class="flex align-items-start justify-content-between mb-3">
<div>
<div class="text-500 font-medium mb-1">Rôles Realm</div>
<div class="text-900 font-bold text-2xl">#{roleGestionBean.realmRoles.size()}</div>
<!-- =========== FILTRES (62%) =========== -->
<div class="col-12 lg:col-7">
<h3 class="text-900 font-semibold text-lg mb-4 flex align-items-center gap-2">
<i class="pi pi-filter text-blue-500"></i>
Filtres
</h3>
<h:form id="formFilters">
<div class="grid">
<!-- Realm -->
<div class="col-12">
<fr:fieldSelect id="realmFilter"
label="Realm"
value="#{roleGestionBean.realmName}"
iconLeft="pi pi-globe">
<f:selectItem itemLabel="Sélectionner un realm..." itemValue="" />
<f:selectItems value="#{roleGestionBean.availableRealms}"
var="realm"
itemLabel="#{realm}"
itemValue="#{realm}" />
<p:ajax event="change"
listener="#{roleGestionBean.loadRealmRoles}"
update=":formRealmRoles :formClientRoles :formKpis" />
</fr:fieldSelect>
</div>
<div class="flex align-items-center justify-content-center bg-purple-100 border-circle"
style="width: 2.5rem; height: 2.5rem">
<i class="pi pi-shield text-purple-600 text-xl"></i>
<!-- Client -->
<div class="col-12">
<fr:fieldSelect id="clientFilter"
label="Client (optionnel)"
value="#{roleGestionBean.clientName}"
iconLeft="pi pi-box">
<f:selectItem itemLabel="Tous les clients" itemValue="" />
<f:selectItems value="#{roleGestionBean.availableClients}"
var="client"
itemLabel="#{client}"
itemValue="#{client}" />
<p:ajax event="change"
listener="#{roleGestionBean.loadClientRoles}"
update=":formClientRoles" />
</fr:fieldSelect>
</div>
<!-- Type -->
<div class="col-12">
<fr:fieldSelect id="typeFilter"
label="Type de rôle"
value="#{roleGestionBean.selectedTypeRole}"
iconLeft="pi pi-filter">
<f:selectItem itemLabel="Tous les types" itemValue="" />
<f:selectItems value="#{roleGestionBean.typeRoleOptions}"
var="type"
itemLabel="#{type}"
itemValue="#{type}" />
</fr:fieldSelect>
</div>
</div>
<div class="text-500 text-sm">
<i class="pi pi-globe text-600"></i>
<span class="ml-2">Rôles du realm</span>
</div>
</div>
</h:form>
</div>
<div class="col-12 md:col-6 lg:col-3">
<div class="card surface-0 border-round-lg">
<div class="flex align-items-start justify-content-between mb-3">
<div>
<div class="text-500 font-medium mb-1">Rôles Client</div>
<div class="text-900 font-bold text-2xl">#{roleGestionBean.clientRoles.size()}</div>
</div>
<div class="flex align-items-center justify-content-center bg-blue-100 border-circle"
style="width: 2.5rem; height: 2.5rem">
<i class="pi pi-sitemap text-blue-600 text-xl"></i>
</div>
</div>
<div class="text-500 text-sm">
<i class="pi pi-box text-600"></i>
<span class="ml-2">Rôles spécifiques client</span>
</div>
</div>
</div>
<!-- =========== KPI STATISTIQUES (38%) =========== -->
<div class="col-12 lg:col-5">
<h3 class="text-900 font-semibold text-lg mb-4 flex align-items-center gap-2">
<i class="pi pi-chart-bar text-purple-500"></i>
Statistiques <small class="text-500 font-normal">(φ = 1.618)</small>
</h3>
<div class="col-12 md:col-6 lg:col-3">
<div class="card surface-0 border-round-lg">
<div class="flex align-items-start justify-content-between mb-3">
<div>
<div class="text-500 font-medium mb-1">Total Rôles</div>
<div class="text-900 font-bold text-2xl">#{roleGestionBean.allRoles.size()}</div>
<h:form id="formKpis">
<div class="grid">
<!-- KPI 1: Rôles Realm -->
<div class="col-12 md:col-6">
<div class="surface-50 border-round p-3 text-center h-full">
<div class="flex align-items-center justify-content-center bg-purple-100 border-circle mx-auto mb-2"
style="width: 2rem; height: 2rem">
<i class="pi pi-shield text-purple-600 text-sm"></i>
</div>
<div class="text-900 font-bold text-xl mb-1">#{roleGestionBean.realmRoles.size()}</div>
<div class="text-500 text-xs">Rôles Realm</div>
</div>
</div>
<div class="flex align-items-center justify-content-center bg-green-100 border-circle"
style="width: 2.5rem; height: 2.5rem">
<i class="pi pi-check-circle text-green-600 text-xl"></i>
</div>
</div>
<div class="text-500 text-sm">
<i class="pi pi-chart-bar text-600"></i>
<span class="ml-2">Tous les rôles configurés</span>
</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-3">
<div class="card surface-0 border-round-lg">
<div class="flex align-items-start justify-content-between mb-3">
<div>
<div class="text-500 font-medium mb-1">Realm Actif</div>
<div class="text-900 font-bold text-xl">#{roleGestionBean.realmName}</div>
<!-- KPI 2: Rôles Client -->
<div class="col-12 md:col-6">
<div class="surface-50 border-round p-3 text-center h-full">
<div class="flex align-items-center justify-content-center bg-blue-100 border-circle mx-auto mb-2"
style="width: 2rem; height: 2rem">
<i class="pi pi-sitemap text-blue-600 text-sm"></i>
</div>
<div class="text-900 font-bold text-xl mb-1">#{roleGestionBean.clientRoles.size()}</div>
<div class="text-500 text-xs">Rôles Client</div>
</div>
</div>
<div class="flex align-items-center justify-content-center bg-orange-100 border-circle"
style="width: 2.5rem; height: 2.5rem">
<i class="pi pi-database text-orange-600 text-xl"></i>
<!-- KPI 3: Total Rôles -->
<div class="col-12 md:col-6">
<div class="surface-50 border-round p-3 text-center h-full">
<div class="flex align-items-center justify-content-center bg-green-100 border-circle mx-auto mb-2"
style="width: 2rem; height: 2rem">
<i class="pi pi-check-circle text-green-600 text-sm"></i>
</div>
<div class="text-900 font-bold text-xl mb-1">#{roleGestionBean.allRoles.size()}</div>
<div class="text-500 text-xs">Total Rôles</div>
</div>
</div>
<!-- KPI 4: Realm Actif -->
<div class="col-12 md:col-6">
<div class="surface-50 border-round p-3 text-center h-full">
<div class="flex align-items-center justify-content-center bg-orange-100 border-circle mx-auto mb-2"
style="width: 2rem; height: 2rem">
<i class="pi pi-database text-orange-600 text-sm"></i>
</div>
<div class="text-900 font-bold text-sm mb-1" style="word-break: break-all;">#{roleGestionBean.realmName}</div>
<div class="text-500 text-xs">Realm Actif</div>
</div>
</div>
</div>
<div class="text-500 text-sm">
<i class="pi pi-server text-600"></i>
<span class="ml-2">Realm actuellement sélectionné</span>
</div>
</div>
</h:form>
</div>
</div>
</h:form>
</div>
</div>
<!-- ================================================================
@@ -237,15 +205,18 @@
</p>
</div>
<div class="flex align-items-center gap-1">
<p:commandButton icon="pi pi-trash"
styleClass="p-button-rounded p-button-text p-button-sm p-button-danger"
title="Supprimer"
action="#{roleGestionBean.deleteRealmRole(role.name)}"
update=":formRealmRoles :formKpis">
<fr:commandButton icon="pi pi-trash"
severity="danger"
rounded="true"
text="true"
size="small"
title="Supprimer"
action="#{roleGestionBean.deleteRealmRole(role.name)}"
update=":formRealmRoles :formKpis">
<p:confirm header="Confirmation"
message="Voulez-vous vraiment supprimer le rôle '#{role.name}' ?"
icon="pi pi-exclamation-triangle" />
</p:commandButton>
</fr:commandButton>
</div>
</div>
@@ -317,15 +288,18 @@
</p>
</div>
<div class="flex align-items-center gap-1">
<p:commandButton icon="pi pi-trash"
styleClass="p-button-rounded p-button-text p-button-sm p-button-danger"
title="Supprimer"
action="#{roleGestionBean.deleteClientRole(role.name)}"
update=":formClientRoles :formKpis">
<fr:commandButton icon="pi pi-trash"
severity="danger"
rounded="true"
text="true"
size="small"
title="Supprimer"
action="#{roleGestionBean.deleteClientRole(role.name)}"
update=":formClientRoles :formKpis">
<p:confirm header="Confirmation"
message="Voulez-vous vraiment supprimer le rôle '#{role.name}' ?"
icon="pi pi-exclamation-triangle" />
</p:commandButton>
</fr:commandButton>
</div>
</div>
@@ -368,150 +342,108 @@
<!-- ================================================================
DIALOG CRÉATION RÔLE REALM
================================================================ -->
<p:dialog header="Nouveau Rôle Realm"
widgetVar="createRealmRoleDialog"
modal="true"
responsive="true"
width="600"
showEffect="fade"
hideEffect="fade">
<h:form id="formCreateRealmRole">
<div class="grid">
<div class="col-12">
<div class="field mb-3">
<label for="realmRoleName" class="block text-900 font-medium mb-2">
Nom du rôle <span class="text-red-500">*</span>
</label>
<p:inputText id="realmRoleName"
value="#{roleGestionBean.newRole.name}"
styleClass="w-full"
required="true"
placeholder="ex: admin_lions">
<f:validateLength minimum="2" maximum="100" />
<f:validateRegex pattern="^[a-zA-Z0-9_-]+$" />
</p:inputText>
<small class="text-500">Lettres, chiffres, underscores et tirets uniquement</small>
</div>
<div class="field mb-0">
<label for="realmRoleDesc" class="block text-900 font-medium mb-2">
Description
</label>
<p:inputTextarea id="realmRoleDesc"
value="#{roleGestionBean.newRole.description}"
styleClass="w-full"
rows="3"
placeholder="Description du rôle...">
</p:inputTextarea>
</div>
</div>
<fr:formDialog widgetVar="createRealmRoleDialog"
header="Nouveau Rôle Realm"
formId="formCreateRealmRole"
saveLabel="Créer"
cancelLabel="Annuler"
saveAction="#{roleGestionBean.createRealmRole}"
update=":formRealmRoles :formKpis :formCreateRealmRole"
width="600">
<div class="grid">
<div class="col-12">
<fr:fieldInput id="realmRoleName"
label="Nom du rôle"
value="#{roleGestionBean.newRole.name}"
required="true"
placeholder="ex: admin_lions"
iconLeft="pi pi-tag"
helpText="Lettres, chiffres, underscores et tirets uniquement">
<f:validateLength for="input" minimum="2" maximum="100" />
<f:validateRegex for="input" pattern="^[a-zA-Z0-9_-]+$" />
</fr:fieldInput>
</div>
<p:messages id="messagesRealmRole" showDetail="true" closable="true" styleClass="mt-3">
<p:autoUpdate />
</p:messages>
</h:form>
<div class="col-12">
<fr:fieldTextarea id="realmRoleDesc"
label="Description"
value="#{roleGestionBean.newRole.description}"
rows="3"
placeholder="Description du rôle..."
iconLeft="pi pi-align-left" />
</div>
</div>
<f:facet name="footer">
<p:commandButton value="Annuler"
icon="pi pi-times"
styleClass="p-button-text"
onclick="PF('createRealmRoleDialog').hide();"
type="button" />
<p:commandButton value="Créer"
icon="pi pi-check"
styleClass="p-button-success"
action="#{roleGestionBean.createRealmRole}"
update=":formRealmRoles :formKpis :formCreateRealmRole"
oncomplete="if (args &amp;&amp; !args.validationFailed) PF('createRealmRoleDialog').hide();" />
</f:facet>
</p:dialog>
<fr:message id="messagesRealmRole" showDetail="true" closable="true">
<p:autoUpdate />
</fr:message>
</fr:formDialog>
<!-- ================================================================
DIALOG CRÉATION RÔLE CLIENT
================================================================ -->
<p:dialog header="Nouveau Rôle Client"
widgetVar="createClientRoleDialog"
modal="true"
responsive="true"
width="600"
showEffect="fade"
hideEffect="fade">
<h:form id="formCreateClientRole">
<div class="grid">
<div class="col-12">
<div class="field mb-3">
<label for="clientRoleName" class="block text-900 font-medium mb-2">
Nom du rôle <span class="text-red-500">*</span>
</label>
<p:inputText id="clientRoleName"
value="#{roleGestionBean.newRole.name}"
styleClass="w-full"
required="true"
placeholder="ex: manager">
<f:validateLength minimum="2" maximum="100" />
<f:validateRegex pattern="^[a-zA-Z0-9_-]+$" />
</p:inputText>
</div>
<div class="field mb-3">
<label for="clientName" class="block text-900 font-medium mb-2">
Client <span class="text-red-500">*</span>
</label>
<p:selectOneMenu id="clientName"
value="#{roleGestionBean.clientName}"
styleClass="w-full"
required="true">
<f:selectItem itemLabel="Sélectionner un client..." itemValue="" />
<f:selectItems value="#{roleGestionBean.availableClients}" />
</p:selectOneMenu>
</div>
<div class="field mb-0">
<label for="clientRoleDesc" class="block text-900 font-medium mb-2">
Description
</label>
<p:inputTextarea id="clientRoleDesc"
value="#{roleGestionBean.newRole.description}"
styleClass="w-full"
rows="3"
placeholder="Description du rôle...">
</p:inputTextarea>
</div>
</div>
<fr:formDialog widgetVar="createClientRoleDialog"
header="Nouveau Rôle Client"
formId="formCreateClientRole"
saveLabel="Créer"
cancelLabel="Annuler"
saveAction="#{roleGestionBean.createClientRole}"
update=":formClientRoles :formKpis :formCreateClientRole"
width="600">
<div class="grid">
<div class="col-12">
<fr:fieldInput id="clientRoleName"
label="Nom du rôle"
value="#{roleGestionBean.newRole.name}"
required="true"
placeholder="ex: manager"
iconLeft="pi pi-tag"
helpText="Lettres, chiffres, underscores et tirets uniquement">
<f:validateLength for="input" minimum="2" maximum="100" />
<f:validateRegex for="input" pattern="^[a-zA-Z0-9_-]+$" />
</fr:fieldInput>
</div>
<p:messages id="messagesClientRole" showDetail="true" closable="true" styleClass="mt-3">
<p:autoUpdate />
</p:messages>
</h:form>
<div class="col-12">
<fr:fieldSelect id="clientName"
label="Client"
value="#{roleGestionBean.clientName}"
required="true"
iconLeft="pi pi-box">
<f:selectItem itemLabel="Sélectionner un client..." itemValue="" />
<f:selectItems value="#{roleGestionBean.availableClients}" />
</fr:fieldSelect>
</div>
<f:facet name="footer">
<p:commandButton value="Annuler"
icon="pi pi-times"
styleClass="p-button-text"
onclick="PF('createClientRoleDialog').hide();"
type="button" />
<p:commandButton value="Créer"
icon="pi pi-check"
styleClass="p-button-success"
action="#{roleGestionBean.createClientRole}"
update=":formClientRoles :formKpis :formCreateClientRole"
oncomplete="if (args &amp;&amp; !args.validationFailed) PF('createClientRoleDialog').hide();" />
</f:facet>
</p:dialog>
<div class="col-12">
<fr:fieldTextarea id="clientRoleDesc"
label="Description"
value="#{roleGestionBean.newRole.description}"
rows="3"
placeholder="Description du rôle..."
iconLeft="pi pi-align-left" />
</div>
</div>
<fr:message id="messagesClientRole" showDetail="true" closable="true">
<p:autoUpdate />
</fr:message>
</fr:formDialog>
<!-- ================================================================
DIALOG DE CONFIRMATION
DIALOG DE CONFIRMATION (Freya Extension)
================================================================ -->
<!-- Le confirmDialog est géré par p:confirm dans les boutons de suppression -->
<p:confirmDialog global="true" showEffect="fade" hideEffect="fade"
responsive="true" width="400">
<p:commandButton value="Non" type="button"
styleClass="p-button-text"
icon="pi pi-times" />
<p:commandButton value="Oui" type="button"
styleClass="p-button-danger"
icon="pi pi-check" />
<fr:commandButton value="Non"
type="button"
text="true"
icon="pi pi-times" />
<fr:commandButton value="Oui"
type="button"
severity="danger"
icon="pi pi-check" />
</p:confirmDialog>
</ui:define>

View File

@@ -4,6 +4,7 @@
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:fr="http://primefaces.org/freya"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
template="/templates/main-template.xhtml">
@@ -24,31 +25,43 @@
<div class="card">
<h5>Informations du compte</h5>
<h:form id="formAccountInfo">
<p:panelGrid columns="2" styleClass="w-full" columnClasses="col-12 md:col-4, col-12 md:col-8">
<p:outputLabel for="username" value="Nom d'utilisateur" />
<p:inputText id="username"
value="#{userSessionBean.username}"
readonly="true"
styleClass="w-full" />
<div class="grid">
<div class="col-12 md:col-6">
<fr:fieldInput id="username"
label="Nom d'utilisateur"
value="#{userSessionBean.username}"
disabled="true"
iconLeft="pi pi-user"
helpText="Votre identifiant unique" />
</div>
<p:outputLabel for="email" value="Email" />
<p:inputText id="email"
value="#{userSessionBean.email}"
readonly="true"
styleClass="w-full" />
<div class="col-12 md:col-6">
<fr:fieldInput id="email"
label="Email"
value="#{userSessionBean.email}"
disabled="true"
iconLeft="pi pi-envelope"
helpText="Votre adresse email" />
</div>
<p:outputLabel for="fullName" value="Nom complet" />
<p:inputText id="fullName"
value="#{userSessionBean.fullName}"
readonly="true"
styleClass="w-full" />
<div class="col-12 md:col-6">
<fr:fieldInput id="fullName"
label="Nom complet"
value="#{userSessionBean.fullName}"
disabled="true"
iconLeft="pi pi-id-card"
helpText="Votre nom complet" />
</div>
<p:outputLabel for="mainRole" value="Rôle principal" />
<p:inputText id="mainRole"
value="#{userSessionBean.mainRole}"
readonly="true"
styleClass="w-full" />
</p:panelGrid>
<div class="col-12 md:col-6">
<fr:fieldInput id="mainRole"
label="Rôle principal"
value="#{userSessionBean.mainRole}"
disabled="true"
iconLeft="pi pi-shield"
helpText="Votre rôle dans l'application" />
</div>
</div>
</h:form>
</div>
</div>
@@ -59,35 +72,34 @@
<h5>Préférences</h5>
<h:form id="formPreferences">
<div class="flex flex-column gap-3">
<div class="flex align-items-center justify-content-between">
<span class="text-600">Thème des composants</span>
<p:selectOneMenu value="#{guestPreferences.componentTheme}"
styleClass="w-12rem">
<f:selectItems value="#{guestPreferences.componentThemes}"
var="theme"
itemLabel="#{theme.name}"
itemValue="#{theme.file}" />
<p:ajax event="change" update="@form" />
</p:selectOneMenu>
</div>
<div class="flex align-items-center justify-content-between">
<span class="text-600">Mode sombre</span>
<p:selectOneMenu value="#{guestPreferences.darkMode}"
styleClass="w-12rem">
<f:selectItem itemLabel="Clair" itemValue="light" />
<f:selectItem itemLabel="Sombre" itemValue="dark" />
<p:ajax event="change" update="@form" />
</p:selectOneMenu>
</div>
<div class="flex align-items-center justify-content-between">
<span class="text-600">Style d'input</span>
<p:selectOneMenu value="#{guestPreferences.inputStyle}"
styleClass="w-12rem">
<f:selectItem itemLabel="Outlined" itemValue="outlined" />
<f:selectItem itemLabel="Filled" itemValue="filled" />
<p:ajax event="change" update="@form" />
</p:selectOneMenu>
</div>
<fr:fieldSelect id="componentTheme"
label="Thème des composants"
value="#{guestPreferences.componentTheme}"
iconLeft="pi pi-palette">
<f:selectItems value="#{guestPreferences.componentThemes}"
var="theme"
itemLabel="#{theme.name}"
itemValue="#{theme.file}" />
<p:ajax event="change" update="@form" />
</fr:fieldSelect>
<fr:fieldSelect id="darkMode"
label="Mode sombre"
value="#{guestPreferences.darkMode}"
iconLeft="pi pi-moon">
<f:selectItem itemLabel="Clair" itemValue="light" />
<f:selectItem itemLabel="Sombre" itemValue="dark" />
<p:ajax event="change" update="@form" />
</fr:fieldSelect>
<fr:fieldSelect id="inputStyle"
label="Style d'input"
value="#{guestPreferences.inputStyle}"
iconLeft="pi pi-sliders-h">
<f:selectItem itemLabel="Outlined" itemValue="outlined" />
<f:selectItem itemLabel="Filled" itemValue="filled" />
<p:ajax event="change" update="@form" />
</fr:fieldSelect>
</div>
</h:form>
</div>
@@ -97,29 +109,26 @@
<div class="col-12">
<div class="card">
<h5>Actions</h5>
<div class="flex gap-2">
<div class="flex flex-wrap gap-2">
<h:form>
<p:commandButton
value="Rafraîchir les informations"
icon="pi pi-refresh"
styleClass="p-button-secondary"
action="#{userSessionBean.loadUserInfo}"
update="formAccountInfo" />
<fr:commandButton value="Rafraîchir les informations"
icon="pi pi-refresh"
severity="secondary"
action="#{userSessionBean.loadUserInfo}"
update="formAccountInfo" />
</h:form>
<h:form>
<p:commandButton
value="Changer le mot de passe"
icon="pi pi-key"
styleClass="p-button-info"
outcome="/pages/user-manager/users/profile" />
<fr:commandButton value="Changer le mot de passe"
icon="pi pi-key"
severity="info"
outcome="/pages/user-manager/users/profile" />
</h:form>
<h:form>
<p:commandButton
value="Sauvegarder les préférences"
icon="pi pi-save"
styleClass="p-button-success"
action="#{settingsBean.savePreferences}"
update="@form" />
<fr:commandButton value="Sauvegarder les préférences"
icon="pi pi-save"
severity="success"
action="#{settingsBean.savePreferences}"
update="@form" />
</h:form>
</div>
</div>

View File

@@ -4,6 +4,7 @@
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:fr="http://primefaces.org/freya"
template="/templates/main-template.xhtml">
<ui:define name="title">Synchronisation Keycloak - Lions User Manager</ui:define>

View File

@@ -26,16 +26,19 @@
</div>
</div>
<div class="flex gap-2">
<p:commandButton
<fr:commandButton
icon="pi pi-question-circle"
styleClass="p-button-rounded p-button-text p-button-help"
rounded="true"
text="true"
severity="help"
title="Aide"
type="button"
onclick="PF('helpDialog').show();" />
<h:link outcome="/pages/user-manager/users/list" styleClass="p-button p-button-text">
<i class="pi pi-arrow-left mr-2"></i>
Retour
</h:link>
<fr:button value="Retour"
icon="pi pi-arrow-left"
severity="secondary"
text="true"
outcome="/pages/user-manager/users/list" />
</div>
</div>
</div>
@@ -125,51 +128,46 @@
<div class="grid">
<!-- Mot de passe -->
<div class="col-12 md:col-6">
<div class="field">
<label for="password" class="block text-900 font-medium mb-2">
<i class="pi pi-lock text-500 mr-1"></i>
Mot de passe <span class="text-red-500">*</span>
</label>
<p:password id="password"
value="#{userCreationBean.password}"
styleClass="w-full"
required="true"
feedback="true"
toggleMask="true"
promptLabel="Entrez un mot de passe"
weakLabel="Faible"
goodLabel="Moyen"
strongLabel="Fort"
placeholder="Minimum 8 caractères">
<f:validateLength minimum="8" maximum="100" />
</p:password>
<small class="text-500">
<i class="pi pi-shield mr-1"></i>
Au moins 8 caractères avec lettres et chiffres
</small>
</div>
<fr:fieldPassword id="password"
label="Mot de passe"
value="#{userCreationBean.password}"
required="true"
feedback="true"
toggleMask="true"
promptLabel="Entrez un mot de passe"
weakLabel="Faible"
goodLabel="Moyen"
strongLabel="Fort"
placeholder="Minimum 8 caractères"
iconLeft="pi pi-lock"
helpText="Au moins 8 caractères avec lettres et chiffres">
<f:validateLength for="input" minimum="8" maximum="100" />
<p:ajax event="keyup"
delay="500"
listener="#{userCreationBean.validatePasswordMatch}"
update="passwordConfirm passwordConfirmMsg"
process="@this passwordConfirm" />
</fr:fieldPassword>
</div>
<!-- Confirmation mot de passe -->
<div class="col-12 md:col-6">
<div class="field">
<label for="passwordConfirm" class="block text-900 font-medium mb-2">
<i class="pi pi-lock text-500 mr-1"></i>
Confirmer le mot de passe <span class="text-red-500">*</span>
</label>
<p:password id="passwordConfirm"
value="#{userCreationBean.passwordConfirm}"
styleClass="w-full"
required="true"
feedback="false"
toggleMask="true"
placeholder="Confirmer le mot de passe">
</p:password>
<small class="text-500">
<i class="pi pi-info-circle mr-1"></i>
Doit correspondre au mot de passe
</small>
</div>
<fr:fieldPassword id="passwordConfirm"
label="Confirmer le mot de passe"
value="#{userCreationBean.passwordConfirm}"
required="true"
feedback="false"
toggleMask="true"
placeholder="Confirmer le mot de passe"
iconLeft="pi pi-lock"
helpText="Doit correspondre au mot de passe">
<p:ajax event="keyup"
delay="500"
listener="#{userCreationBean.validatePasswordMatch}"
update="passwordConfirmMsg"
process="@this password" />
</fr:fieldPassword>
<p:message id="passwordConfirmMsg" for="passwordConfirm" display="text" styleClass="mt-2" />
</div>
</div>
@@ -220,26 +218,20 @@
<div class="surface-50 border-round p-3">
<div class="flex flex-column gap-3">
<!-- Compte activé -->
<div class="flex align-items-center">
<p:selectBooleanCheckbox id="enabled"
value="#{userCreationBean.newUser.enabled}">
</p:selectBooleanCheckbox>
<label for="enabled" class="ml-2 mb-0 cursor-pointer">
<span class="font-semibold text-900">Compte activé</span>
<small class="block text-500">L'utilisateur peut se connecter immédiatement</small>
</label>
</div>
<fr:fieldCheckbox id="enabled"
label=""
value="#{userCreationBean.newUser.enabled}"
checkboxLabel="Compte activé">
<small class="block text-500 mt-1">L'utilisateur peut se connecter immédiatement</small>
</fr:fieldCheckbox>
<!-- Email vérifié -->
<div class="flex align-items-center">
<p:selectBooleanCheckbox id="emailVerified"
value="#{userCreationBean.newUser.emailVerified}">
</p:selectBooleanCheckbox>
<label for="emailVerified" class="ml-2 mb-0 cursor-pointer">
<span class="font-semibold text-900">Email vérifié</span>
<small class="block text-500">Marquer l'email comme vérifié</small>
</label>
</div>
<fr:fieldCheckbox id="emailVerified"
label=""
value="#{userCreationBean.newUser.emailVerified}"
checkboxLabel="Email vérifié">
<small class="block text-500 mt-1">Marquer l'email comme vérifié</small>
</fr:fieldCheckbox>
</div>
</div>
</div>
@@ -332,16 +324,17 @@
validateClient="true" />
<!-- Bouton Réinitialiser -->
<p:commandButton value="Réinitialiser"
<fr:commandButton value="Réinitialiser"
icon="pi pi-refresh"
styleClass="p-button-secondary p-button-outlined"
severity="secondary"
outlined="true"
action="#{userCreationBean.resetForm}"
update=":formUserCreation"
immediate="true">
<p:confirm header="Confirmation"
message="Voulez-vous vraiment réinitialiser le formulaire ?"
icon="pi pi-exclamation-triangle" />
</p:commandButton>
</fr:commandButton>
<!-- Bouton Annuler -->
<fr:commandButton value="Annuler"

View File

@@ -91,6 +91,7 @@
placeholder="ex: jean.dupont@example.com"
helpText="Adresse email valide">
<f:validateRegex for="input" pattern="^[A-Za-z0-9+_.-]+@(.+)$" />
<p:ajax event="keyup" delay="500" update="previewPanel" />
</fr:fieldInput>
</div>
@@ -103,6 +104,7 @@
placeholder="ex: Jean"
helpText="Prénom de l'utilisateur">
<f:validateLength for="input" minimum="2" maximum="100" />
<p:ajax event="keyup" delay="500" update="previewPanel" />
</fr:fieldInput>
</div>
@@ -115,6 +117,7 @@
placeholder="ex: Dupont"
helpText="Nom de famille de l'utilisateur">
<f:validateLength for="input" minimum="2" maximum="100" />
<p:ajax event="keyup" delay="500" update="previewPanel" />
</fr:fieldInput>
</div>
@@ -157,26 +160,16 @@
<div class="surface-50 border-round p-3">
<div class="flex flex-column gap-3">
<!-- Compte activé -->
<div class="flex align-items-center">
<p:selectBooleanCheckbox id="enabled"
value="#{userProfilBean.user.enabled}">
</p:selectBooleanCheckbox>
<label for="enabled" class="ml-2 mb-0 cursor-pointer">
<span class="font-semibold text-900">Compte activé</span>
<small class="block text-500">L'utilisateur peut se connecter</small>
</label>
</div>
<fr:fieldCheckbox id="enabled"
label="Compte activé"
value="#{userProfilBean.user.enabled}"
helpText="L'utilisateur peut se connecter" />
<!-- Email vérifié -->
<div class="flex align-items-center">
<p:selectBooleanCheckbox id="emailVerified"
value="#{userProfilBean.user.emailVerified}">
</p:selectBooleanCheckbox>
<label for="emailVerified" class="ml-2 mb-0 cursor-pointer">
<span class="font-semibold text-900">Email vérifié</span>
<small class="block text-500">Marquer l'email comme vérifié</small>
</label>
</div>
<fr:fieldCheckbox id="emailVerified"
label="Emailrifié"
value="#{userProfilBean.user.emailVerified}"
helpText="Marquer l'email comme vérifié" />
</div>
</div>
</div>
@@ -193,9 +186,11 @@
<div class="card sticky" style="top: 1rem;">
<div class="flex align-items-center gap-2 mb-4">
<i class="pi pi-eye text-blue-500" style="font-size: 1.5rem"></i>
<h5 class="m-0">Aperçu</h5>
<h5 class="m-0">Aperçu <small class="text-500 font-normal">(Temps réel)</small></h5>
</div>
<h:panelGroup id="previewPanel">
<!-- Avatar Preview -->
<div class="text-center mb-4">
<div style="width: 80px; height: 80px; border-radius: 50%; background: linear-gradient(135deg, var(--primary-color), var(--primary-600)); display: flex; align-items-center; justify-content: center; margin: 0 auto; font-size: 2rem; font-weight: bold; color: white; box-shadow: 0 4px 12px rgba(0,0,0,0.12);">
@@ -250,6 +245,8 @@
</div>
</div>
</div>
</h:panelGroup>
</div>
</div>
@@ -309,17 +306,9 @@
</div>
<!-- ================================================================
DIALOG DE CONFIRMATION
DIALOG DE CONFIRMATION (Freya Extension)
================================================================ -->
<p:confirmDialog global="true" showEffect="fade" hideEffect="fade"
responsive="true" width="400">
<p:commandButton value="Non" type="button"
styleClass="p-button-text"
icon="pi pi-times" />
<p:commandButton value="Oui" type="button"
styleClass="p-button-primary"
icon="pi pi-check" />
</p:confirmDialog>
<!-- Suppression désactivée - utiliser la page liste pour supprimer des utilisateurs -->
</ui:define>
</ui:composition>

View File

@@ -27,10 +27,25 @@
</div>
</div>
<div class="flex gap-2">
<fr:commandButton
value="Importer"
icon="pi pi-upload"
severity="info"
outlined="true"
onclick="PF('importUsersDialog').show()"
type="button" />
<fr:commandButton
value="Exporter"
icon="pi pi-download"
severity="secondary"
outlined="true"
action="#{userListBean.exportToCSV}"
ajax="false" />
<fr:commandButton
value="Rafraîchir"
icon="pi pi-refresh"
severity="secondary"
outlined="true"
action="#{userListBean.refreshData}"
update=":formUserList"
process="@this" />
@@ -149,76 +164,67 @@
SECTION RECHERCHE ET FILTRES
================================================================ -->
<div class="col-12">
<div class="card">
<div class="flex align-items-center gap-2 mb-3">
<i class="pi pi-search text-blue-500" style="font-size: 1.5rem"></i>
<h5 class="m-0">Recherche et Filtres</h5>
</div>
<fr:panel header="Recherche et Filtres"
toggleable="true"
collapsed="false">
<f:facet name="icons">
<h:panelGroup rendered="#{userListBean.searchText != null or userListBean.selectedStatut != null}">
<fr:tag value="Filtres actifs"
severity="info"
icon="pi pi-filter"
styleClass="mr-2" />
</h:panelGroup>
</f:facet>
<div class="grid">
<div class="col-12 md:col-6 lg:col-4">
<div class="field">
<label for="searchText" class="block text-900 font-medium mb-2">
<i class="pi pi-search text-500 mr-1"></i>
Recherche
</label>
<p:inputText id="searchText"
value="#{userListBean.searchText}"
styleClass="w-full"
placeholder="Nom, email...">
<p:ajax event="keyup"
delay="500"
update=":formUserList:userTable"
listener="#{userListBean.search}" />
</p:inputText>
</div>
<fr:fieldInput id="searchText"
label="Recherche"
value="#{userListBean.searchText}"
placeholder="Nom, email...">
<p:ajax event="keyup"
delay="500"
update=":formUserList:userTable"
listener="#{userListBean.search}" />
</fr:fieldInput>
</div>
<div class="col-12 md:col-6 lg:col-3">
<div class="field">
<label for="realmFilter" class="block text-900 font-medium mb-2">
<i class="pi pi-globe text-500 mr-1"></i>
Realm
</label>
<p:selectOneMenu id="realmFilter"
value="#{userListBean.realmName}"
styleClass="w-full">
<f:selectItems value="#{userListBean.availableRealms}" />
<p:ajax event="change"
update=":formUserList:userTable"
listener="#{userListBean.search}" />
</p:selectOneMenu>
</div>
<fr:fieldSelect id="realmFilter"
label="Realm"
value="#{userListBean.realmName}">
<f:selectItems value="#{userListBean.availableRealms}" />
<p:ajax event="change"
update=":formUserList:userTable"
listener="#{userListBean.search}" />
</fr:fieldSelect>
</div>
<div class="col-12 md:col-6 lg:col-3">
<div class="field">
<label for="statutFilter" class="block text-900 font-medium mb-2">
<i class="pi pi-filter text-500 mr-1"></i>
Statut
</label>
<p:selectOneMenu id="statutFilter"
value="#{userListBean.selectedStatut}"
styleClass="w-full">
<f:selectItem itemLabel="Tous" itemValue="#{null}" />
<f:selectItems value="#{userListBean.statutOptions}" />
<p:ajax update=":formUserList:userTable"
listener="#{userListBean.search}" />
</p:selectOneMenu>
</div>
<fr:fieldSelect id="statutFilter"
label="Statut"
value="#{userListBean.selectedStatut}">
<f:selectItem itemLabel="Tous" itemValue="#{null}" />
<f:selectItems value="#{userListBean.statutOptions}" />
<p:ajax event="change"
update=":formUserList:userTable"
listener="#{userListBean.search}" />
</fr:fieldSelect>
</div>
<div class="col-12 lg:col-2 flex align-items-end">
<div class="col-12 md:col-6 lg:col-2 flex align-items-end">
<fr:commandButton
value="Réinitialiser"
icon="pi pi-refresh"
severity="secondary"
outlined="true"
styleClass="w-full"
action="#{userListBean.resetSearch}"
update=":formUserList:userTable @form" />
update=":formUserList:userTable @form"
rendered="#{userListBean.searchText != null or userListBean.selectedStatut != null}" />
</div>
</div>
</div>
</fr:panel>
</div>
<!-- ================================================================
@@ -244,16 +250,16 @@
var="user"
rowKey="#{user.id}"
paginator="true"
rows="#{userListBean.pageSize != null ? userListBean.pageSize : 10}"
rowsPerPageTemplate="10,20,50"
rows="#{userListBean.pageSize != null ? userListBean.pageSize : 25}"
rowsPerPageTemplate="10,25,50,100"
emptyMessage="Aucun utilisateur trouvé"
reflow="true"
responsiveLayout="scroll"
styleClass="p-datatable-striped">
<p:ajax event="page" listener="#{userListBean.onPageChange}" update=":formUserList:userTable :formUserList:formMessages" />
<!-- Colonne Avatar + Username -->
<p:column headerText="Utilisateur" sortBy="#{user.username}" style="width: 250px">
<p:column headerText="Utilisateur" sortBy="#{user.username}" style="width: 250px" priority="1">
<div class="flex align-items-center gap-3">
<div class="border-circle bg-primary text-white flex align-items-center justify-content-center"
style="width: 42px; height: 42px; flex-shrink: 0;">
@@ -269,7 +275,7 @@
</p:column>
<!-- Colonne Email -->
<p:column headerText="Email" sortBy="#{user.email}" style="width: 250px">
<p:column headerText="Email" sortBy="#{user.email}" style="width: 250px" priority="2">
<div class="flex align-items-center gap-2">
<i class="pi pi-envelope text-500"></i>
<span class="text-900">#{user.email}</span>
@@ -280,13 +286,13 @@
</p:column>
<!-- Colonne Statut -->
<p:column headerText="Statut" sortBy="#{user.enabled}" style="width: 120px; text-align: center">
<p:column headerText="Statut" sortBy="#{user.enabled}" style="width: 120px; text-align: center" priority="3">
<fr:tag value="#{user.enabled ? 'ACTIF' : 'INACTIF'}"
severity="#{user.enabled ? 'success' : 'danger'}" />
</p:column>
<!-- Colonne Rôles -->
<p:column headerText="Rôles" style="width: 250px">
<p:column headerText="Rôles" style="width: 250px" priority="5">
<div class="flex flex-wrap gap-1">
<h:outputText value="Aucun rôle" styleClass="text-500 text-sm"
rendered="#{user.realmRoles == null or user.realmRoles.size() == 0}" />
@@ -304,165 +310,256 @@
</p:column>
<!-- Colonne Actions -->
<p:column headerText="Actions" style="width: 250px; text-align: center">
<div class="flex gap-1 justify-content-center flex-wrap">
<!-- Bouton Voir Profil -->
<p:column headerText="Actions" style="width: 120px; text-align: center" priority="4">
<div class="flex gap-1 justify-content-center">
<!-- Bouton Voir (Action principale) -->
<p:button icon="pi pi-eye"
styleClass="p-button-rounded p-button-text p-button-sm p-button-info"
styleClass="p-button-rounded p-button-sm p-button-info"
title="Voir le profil"
outcome="/pages/user-manager/users/view">
<f:param name="userId" value="#{user.id}" />
<f:param name="realm" value="#{userListBean.realmName}" />
</p:button>
<!-- Bouton Modifier -->
<p:button icon="pi pi-pencil"
styleClass="p-button-rounded p-button-text p-button-sm"
title="Modifier"
outcome="/pages/user-manager/users/edit">
<f:param name="userId" value="#{user.id}" />
<f:param name="realm" value="#{userListBean.realmName}" />
</p:button>
<!-- Menu Actions Secondaires -->
<p:splitButton icon="pi pi-ellipsis-v"
styleClass="p-button-rounded p-button-sm p-button-secondary"
menuStyleClass="text-left">
<p:menuitem value="Modifier"
icon="pi pi-pencil"
outcome="/pages/user-manager/users/edit">
<f:param name="userId" value="#{user.id}" />
<f:param name="realm" value="#{userListBean.realmName}" />
</p:menuitem>
<!-- Bouton Gérer les Rôles -->
<p:button icon="pi pi-key"
styleClass="p-button-rounded p-button-text p-button-sm p-button-help"
title="Gérer les rôles"
outcome="/pages/user-manager/roles/assign">
<f:param name="userId" value="#{user.id}" />
<f:param name="realm" value="#{userListBean.realmName}" />
</p:button>
<p:menuitem value="Gérer les Rôles"
icon="pi pi-key"
outcome="/pages/user-manager/users/edit">
<f:param name="userId" value="#{user.id}" />
<f:param name="realm" value="#{userListBean.realmName}" />
</p:menuitem>
<!-- Bouton Désactiver (si actif) -->
<p:commandButton icon="pi pi-ban"
styleClass="p-button-rounded p-button-text p-button-sm p-button-warning"
title="Désactiver"
action="#{userListBean.deactivateUserAction}"
update=":formUserList:userTable :formUserList:formMessages"
process="@this"
rendered="#{user.enabled}">
<f:attribute name="userId" value="#{user.id}" />
<p:confirm header="Désactiver l'utilisateur"
message="Voulez-vous vraiment désactiver l'utilisateur #{user.username} ?"
icon="pi pi-exclamation-triangle" />
</p:commandButton>
<p:divider />
<!-- Bouton Activer (si inactif) -->
<fr:commandButton icon="pi pi-check"
rounded="true"
text="true"
size="small"
severity="success"
title="Activer"
action="#{userListBean.activateUserAction}"
update=":formUserList:userTable :formUserList:formMessages"
process="@this"
rendered="#{not user.enabled}">
<f:attribute name="userId" value="#{user.id}" />
</fr:commandButton>
<!-- Désactiver (si utilisateur actif) -->
<p:menuitem value="Désactiver"
icon="pi pi-ban"
styleClass="text-orange-500"
action="#{userListBean.deactivateUserAction}"
update=":formUserList:userTable :formUserList:formMessages"
process="@this"
rendered="#{user.enabled}">
<f:attribute name="userId" value="#{user.id}" />
<p:confirm header="Désactiver l'utilisateur"
message="Voulez-vous vraiment désactiver l'utilisateur #{user.username} ?"
icon="pi pi-exclamation-triangle" />
</p:menuitem>
<!-- Bouton Supprimer -->
<p:commandButton icon="pi pi-trash"
styleClass="p-button-rounded p-button-text p-button-sm p-button-danger"
title="Supprimer"
action="#{userListBean.deleteUserAction}"
update=":formUserList:userTable :formUserList:formMessages"
process="@this">
<f:attribute name="userId" value="#{user.id}" />
<p:confirm header="Supprimer l'utilisateur"
message="Voulez-vous vraiment supprimer définitivement l'utilisateur #{user.username} ? Cette action est irréversible."
icon="pi pi-exclamation-triangle" />
</p:commandButton>
<!-- Activer (si utilisateur inactif) -->
<p:menuitem value="Activer"
icon="pi pi-check"
styleClass="text-green-500"
action="#{userListBean.activateUserAction}"
update=":formUserList:userTable :formUserList:formMessages"
process="@this"
rendered="#{!user.enabled}">
<f:attribute name="userId" value="#{user.id}" />
<p:confirm header="Activer l'utilisateur"
message="Voulez-vous vraiment activer l'utilisateur #{user.username} ?"
icon="pi pi-exclamation-triangle" />
</p:menuitem>
<p:menuitem value="Supprimer"
icon="pi pi-trash"
styleClass="text-red-500"
action="#{userListBean.deleteUserAction}"
update=":formUserList:userTable :formUserList:formMessages"
process="@this">
<f:attribute name="userId" value="#{user.id}" />
<p:confirm header="Supprimer l'utilisateur"
message="Voulez-vous vraiment supprimer définitivement l'utilisateur #{user.username} ? Cette action est IRRÉVERSIBLE."
icon="pi pi-exclamation-triangle" />
</p:menuitem>
</p:splitButton>
</div>
</p:column>
</p:dataTable>
</div>
</div>
<!-- ================================================================
ACTIONS RAPIDES
================================================================ -->
<div class="col-12">
<div class="card">
<div class="flex align-items-center gap-2 mb-3">
<i class="pi pi-bolt text-orange-500" style="font-size: 1.5rem"></i>
<h5 class="m-0">Actions Rapides</h5>
</div>
<div class="grid">
<div class="col-12 md:col-6 lg:col-3">
<fr:commandButton
value="Créer un Utilisateur"
icon="pi pi-user-plus"
severity="success"
styleClass="w-full"
outcome="/pages/user-manager/users/create" />
</div>
<div class="col-12 md:col-6 lg:col-3">
<fr:commandButton
value="Exporter la Liste"
icon="pi pi-download"
severity="secondary"
styleClass="w-full"
action="#{userListBean.exportToCSV}"
ajax="false" />
</div>
<div class="col-12 md:col-6 lg:col-3">
<fr:commandButton
value="Importer des Utilisateurs"
icon="pi pi-upload"
severity="info"
styleClass="w-full"
onclick="PF('importUsersDialog').show()"
type="button" />
</div>
<div class="col-12 md:col-6 lg:col-3">
<fr:commandButton
value="Gestion des Rôles"
icon="pi pi-shield"
severity="primary"
styleClass="w-full"
outcome="/pages/user-manager/roles/list" />
</div>
</div>
</div>
</div>
</div>
</h:form>
<!-- ================================================================
DIALOG D'IMPORT
DIALOG D'IMPORT CSV (Freya formDialog)
================================================================ -->
<p:dialog id="importUsersDialog"
widgetVar="importUsersDialog"
header="Importer des Utilisateurs"
<p:dialog widgetVar="importUsersDialog"
header="Importer des Utilisateurs depuis CSV"
modal="true"
resizable="false"
styleClass="w-full md:w-30rem">
<h:form id="formImportUsers">
<div class="flex flex-column gap-3">
<p class="text-600">
Importez des utilisateurs depuis un fichier CSV ou JSON.
</p>
<p:fileUpload mode="simple"
skinSimple="true"
accept=".csv,.json"
label="Sélectionner un fichier" />
<div class="flex justify-content-end gap-2 mt-3">
<fr:commandButton value="Annuler"
icon="pi pi-times"
severity="secondary"
onclick="PF('importUsersDialog').hide()"
type="button" />
<fr:commandButton value="Importer"
icon="pi pi-upload"
severity="success"
action="#{userListBean.importUsers}"
update=":formUserList"
oncomplete="PF('importUsersDialog').hide()" />
responsive="true"
width="600"
showEffect="fade"
hideEffect="fade"
closeOnEscape="true">
<h:form id="formImportDialog">
<!-- Instructions -->
<div class="surface-100 border-round p-3 mb-4">
<div class="flex align-items-start gap-2">
<i class="pi pi-info-circle text-blue-500 mt-1"></i>
<div>
<h6 class="mt-0 mb-2">Format du fichier CSV requis:</h6>
<ul class="text-600 text-sm mt-0 mb-0 pl-3">
<li>En-tête: <code class="bg-white px-2 py-1 border-round">username,prenom,nom,email</code></li>
<li>Encodage: UTF-8</li>
<li>Séparateur: virgule (,)</li>
</ul>
</div>
</div>
</div>
<!-- Template CSV téléchargeable -->
<div class="mb-4">
<h6 class="mb-2">Télécharger le template CSV:</h6>
<fr:commandButton value="Télécharger Template CSV"
icon="pi pi-download"
severity="info"
outlined="true"
styleClass="w-full"
action="#{userListBean.downloadCSVTemplate}"
ajax="false" />
<small class="text-500 mt-1">
Utilisez ce template pour préparer votre fichier d'import
</small>
</div>
<fr:divider />
<!-- Upload de fichier -->
<div class="mb-3">
<h6 class="mb-3">Sélectionner le fichier CSV:</h6>
<p:fileUpload id="csvFileUpload"
mode="simple"
skinSimple="true"
label="Choisir un fichier CSV"
chooseIcon="pi pi-folder-open"
accept=".csv"
listener="#{userListBean.handleFileUpload}"
update=":formUserList:userTable :formUserList:formMessages"
oncomplete="PF('importUsersDialog').hide()"
styleClass="w-full" />
</div>
<div class="surface-50 border-round p-3">
<div class="flex align-items-center gap-2">
<i class="pi pi-lightbulb text-orange-500"></i>
<small class="text-600">
<strong>Astuce:</strong> Exportez d'abord vos utilisateurs existants pour voir le format attendu
</small>
</div>
</div>
</h:form>
</p:dialog>
<!-- ================================================================
DIALOG RÉSULTATS D'IMPORT
================================================================ -->
<p:dialog widgetVar="importResultDialog"
header="Résultats de l'Import CSV"
modal="true"
responsive="true"
width="700"
showEffect="fade"
hideEffect="fade"
closeOnEscape="true">
<h:form id="formImportResult">
<h:panelGroup rendered="#{userListBean.lastImportResult != null}">
<!-- Résumé -->
<div class="mb-4">
<h6 class="mb-3">Résumé de l'import:</h6>
<div class="grid">
<div class="col-4">
<div class="surface-100 border-round p-3 text-center">
<div class="text-500 text-xs uppercase mb-1">Total lignes</div>
<div class="text-900 font-bold text-2xl">#{userListBean.lastImportResult.totalLines}</div>
</div>
</div>
<div class="col-4">
<div class="surface-100 border-round p-3 text-center border-left-3 border-green-500">
<div class="text-500 text-xs uppercase mb-1">Succès</div>
<div class="text-green-600 font-bold text-2xl">#{userListBean.lastImportResult.successCount}</div>
</div>
</div>
<div class="col-4">
<div class="surface-100 border-round p-3 text-center border-left-3 border-red-500">
<div class="text-500 text-xs uppercase mb-1">Erreurs</div>
<div class="text-red-600 font-bold text-2xl">#{userListBean.lastImportResult.errorCount}</div>
</div>
</div>
</div>
</div>
<!-- Liste des erreurs détaillées -->
<h:panelGroup rendered="#{userListBean.lastImportResult.errorCount > 0}">
<fr:divider align="left">
<span class="text-red-600 font-bold">Détails des Erreurs</span>
</fr:divider>
<p:dataTable value="#{userListBean.lastImportResult.errors}"
var="error"
paginator="true"
rows="10"
rowsPerPageTemplate="10,20,50"
emptyMessage="Aucune erreur"
styleClass="p-datatable-sm p-datatable-striped">
<p:column headerText="Ligne" style="width: 80px; text-align: center">
<fr:tag value="#{error.lineNumber}"
severity="danger"
styleClass="font-mono" />
</p:column>
<p:column headerText="Type d'erreur" style="width: 150px">
<fr:tag value="#{error.errorType}"
severity="#{error.errorType == 'VALIDATION_ERROR' ? 'warning' : 'danger'}" />
</p:column>
<p:column headerText="Champ" style="width: 120px">
<span class="font-semibold text-900">#{error.field != null ? error.field : '-'}</span>
</p:column>
<p:column headerText="Message d'erreur">
<div class="flex flex-column gap-1">
<span class="text-900">#{error.message}</span>
<h:panelGroup rendered="#{error.lineContent != null and error.lineContent.length() > 0}">
<code class="text-xs bg-red-50 text-red-900 p-2 border-round block overflow-x-auto">#{error.lineContent}</code>
</h:panelGroup>
</div>
</p:column>
</p:dataTable>
</h:panelGroup>
<!-- Message de succès complet -->
<h:panelGroup rendered="#{userListBean.lastImportResult.errorCount == 0}">
<div class="surface-100 border-left-3 border-green-500 border-round p-4 text-center">
<i class="pi pi-check-circle text-green-500" style="font-size: 3rem"></i>
<h5 class="text-green-600 mt-3 mb-2">Import réussi!</h5>
<p class="text-600 m-0">
Tous les utilisateurs ont été importés avec succès.
</p>
</div>
</h:panelGroup>
<!-- Actions -->
<div class="flex justify-content-end gap-2 mt-4">
<fr:commandButton value="Fermer"
icon="pi pi-times"
severity="secondary"
onclick="PF('importResultDialog').hide()"
type="button" />
</div>
</h:panelGroup>
</h:form>
</p:dialog>

View File

@@ -333,26 +333,29 @@
<span>Gestion du Profil</span>
</h4>
<div class="flex flex-column gap-2">
<p:commandButton value="Modifier mon profil"
icon="pi pi-pencil"
styleClass="p-button-outlined w-full justify-content-start"
disabled="true">
<fr:commandButton value="Modifier mon profil"
icon="pi pi-pencil"
outlined="true"
styleClass="w-full justify-content-start"
disabled="true">
<f:attribute name="data-tooltip" value="Fonctionnalité gérée par Keycloak"/>
</p:commandButton>
</fr:commandButton>
<p:commandButton value="Changer mon mot de passe"
icon="pi pi-key"
styleClass="p-button-outlined w-full justify-content-start"
disabled="true">
<fr:commandButton value="Changer mon mot de passe"
icon="pi pi-key"
outlined="true"
styleClass="w-full justify-content-start"
disabled="true">
<f:attribute name="data-tooltip" value="Utilisez le portail Keycloak"/>
</p:commandButton>
</fr:commandButton>
<p:commandButton value="Paramètres de sécurité"
icon="pi pi-shield"
styleClass="p-button-outlined w-full justify-content-start"
disabled="true">
<fr:commandButton value="Paramètres de sécurité"
icon="pi pi-shield"
outlined="true"
styleClass="w-full justify-content-start"
disabled="true">
<f:attribute name="data-tooltip" value="Fonctionnalité à venir"/>
</p:commandButton>
</fr:commandButton>
</div>
</div>
</div>
@@ -365,28 +368,33 @@
<span>Sessions et Sécurité</span>
</h4>
<div class="flex flex-column gap-2">
<p:commandButton value="Voir mes sessions actives"
icon="pi pi-desktop"
styleClass="p-button-outlined p-button-info w-full justify-content-start"
disabled="true">
<fr:commandButton value="Voir mes sessions actives"
icon="pi pi-desktop"
outlined="true"
severity="info"
styleClass="w-full justify-content-start"
disabled="true">
<f:attribute name="data-tooltip" value="Fonctionnalité à venir"/>
</p:commandButton>
</fr:commandButton>
<p:commandButton value="Historique des connexions"
icon="pi pi-history"
styleClass="p-button-outlined p-button-secondary w-full justify-content-start"
disabled="true">
<fr:commandButton value="Historique des connexions"
icon="pi pi-history"
outlined="true"
severity="secondary"
styleClass="w-full justify-content-start"
disabled="true">
<f:attribute name="data-tooltip" value="Fonctionnalité à venir"/>
</p:commandButton>
</fr:commandButton>
<p:commandButton value="Se déconnecter"
icon="pi pi-sign-out"
styleClass="p-button-danger w-full justify-content-start"
action="#{userSessionBean.logout}">
<fr:commandButton value="Se déconnecter"
icon="pi pi-sign-out"
severity="danger"
styleClass="w-full justify-content-start"
action="#{userSessionBean.logout}">
<p:confirm header="Confirmation de déconnexion"
message="Êtes-vous sûr de vouloir vous déconnecter ?"
icon="pi pi-exclamation-triangle" />
</p:commandButton>
</fr:commandButton>
</div>
</div>
</div>
@@ -397,16 +405,19 @@
</div>
<!-- ================================================================
DIALOG DE CONFIRMATION
DIALOG DE CONFIRMATION (Freya Extension)
================================================================ -->
<!-- Le confirmDialog est géré par p:confirm dans le bouton de déconnexion -->
<p:confirmDialog global="true" showEffect="fade" hideEffect="fade"
responsive="true" width="400">
<p:commandButton value="Non" type="button"
styleClass="p-button-text"
icon="pi pi-times" />
<p:commandButton value="Oui" type="button"
styleClass="p-button-danger"
icon="pi pi-check" />
<fr:commandButton value="Non"
type="button"
text="true"
icon="pi pi-times" />
<fr:commandButton value="Oui"
type="button"
severity="danger"
icon="pi pi-check" />
</p:confirmDialog>
<!-- Animation CSS pour le badge "Connecté" -->

View File

@@ -140,11 +140,11 @@
<div class="mb-3 pb-3 border-bottom-1 surface-border">
<label class="block text-600 font-medium mb-2 text-sm">Rôles Realm assignés</label>
<div class="flex flex-wrap gap-2">
<h:outputText value="Aucun rôle"
<h:outputText value="Aucun rôle"
styleClass="text-500 text-sm"
rendered="#{userProfilBean.user.realmRoles == null or userProfilBean.user.realmRoles.size() == 0}" />
<ui:repeat value="#{userProfilBean.user.realmRoles}" var="role">
<p:badge value="#{role}" severity="info" styleClass="text-sm"></p:badge>
<fr:tag value="#{role}" severity="info" styleClass="text-sm" />
</ui:repeat>
</div>
</div>
@@ -152,9 +152,9 @@
<div class="mb-3 pb-3 border-bottom-1 surface-border">
<label class="block text-600 font-medium mb-1 text-sm">Statut du compte</label>
<div class="flex align-items-center">
<p:tag value="#{userProfilBean.user.enabled ? 'ACTIF' : 'INACTIF'}"
severity="#{userProfilBean.user.enabled ? 'success' : 'danger'}"
styleClass="text-sm"></p:tag>
<fr:tag value="#{userProfilBean.user.enabled ? 'ACTIF' : 'INACTIF'}"
severity="#{userProfilBean.user.enabled ? 'success' : 'danger'}"
styleClass="text-sm" />
</div>
</div>
@@ -209,7 +209,7 @@
icon="pi pi-key"
severity="help"
styleClass="w-full"
outcome="/pages/user-manager/roles/assign">
outcome="/pages/user-manager/users/edit">
<f:param name="userId" value="#{userProfilBean.userId}" />
<f:param name="realm" value="#{userProfilBean.realmName}" />
</fr:commandButton>

View File

@@ -37,18 +37,18 @@
<!-- Gestion Rôles -->
<p:submenu id="m_roles" label="Gestion Rôles" icon="pi pi-shield">
<p:menuitem id="m_roles_list" value="Liste des Rôles" icon="pi pi-list" outcome="/pages/user-manager/roles/list" />
<p:menuitem id="m_roles_assign" value="Attribution Rôles" icon="pi pi-key" outcome="/pages/user-manager/roles/assign" />
</p:submenu>
<!-- Audit -->
<p:submenu id="m_audit" label="Audit" icon="pi pi-history">
<p:menuitem id="m_audit_logs" value="Journal d'Audit" icon="pi pi-file-o" outcome="/pages/user-manager/audit/logs" />
</p:submenu>
<!-- Synchronisation -->
<!-- Synchronisation - DÉSACTIVÉ: page stub non implémentée
<p:submenu id="m_sync" label="Synchronisation" icon="pi pi-sync">
<p:menuitem id="m_sync_dashboard" value="Dashboard" icon="pi pi-dashboard" outcome="/pages/user-manager/sync/dashboard" />
</p:submenu>
-->
<!-- Administration (visible uniquement pour les admins) -->
<p:submenu id="m_admin" label="Administration" icon="pi pi-cog" rendered="#{userSessionBean.hasRole('admin')}">

View File

@@ -3,6 +3,7 @@
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:fr="http://primefaces.org/freya"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<!--
@@ -44,6 +45,7 @@
</ui:include>
-->
<!-- Valeurs par défaut simplifiées (fr:commandButton gère severity/size nativement) -->
<c:set var="severity" value="#{empty severity ? 'primary' : severity}" />
<c:set var="size" value="#{empty size ? 'normal' : size}" />
<c:set var="disabled" value="#{empty disabled ? false : disabled}" />
@@ -51,47 +53,14 @@
<c:set var="hasAction" value="#{empty hasAction ? false : hasAction}" />
<c:set var="hasOutcome" value="#{empty hasOutcome ? false : hasOutcome}" />
<!-- Déterminer la classe selon la severity -->
<c:choose>
<c:when test="#{severity == 'primary'}">
<c:set var="buttonClass" value="p-button-primary" />
</c:when>
<c:when test="#{severity == 'success'}">
<c:set var="buttonClass" value="p-button-success" />
</c:when>
<c:when test="#{severity == 'warning'}">
<c:set var="buttonClass" value="p-button-warning" />
</c:when>
<c:when test="#{severity == 'danger'}">
<c:set var="buttonClass" value="p-button-danger" />
</c:when>
<c:when test="#{severity == 'info'}">
<c:set var="buttonClass" value="p-button-info" />
</c:when>
<c:otherwise>
<c:set var="buttonClass" value="p-button-secondary" />
</c:otherwise>
</c:choose>
<!-- Ajouter la taille -->
<c:if test="#{size == 'small'}">
<c:set var="buttonClass" value="#{buttonClass} p-button-sm" />
</c:if>
<c:if test="#{size == 'large'}">
<c:set var="buttonClass" value="#{buttonClass} p-button-lg" />
</c:if>
<!-- Ajouter les classes personnalisées -->
<c:if test="#{not empty styleClass}">
<c:set var="buttonClass" value="#{buttonClass} #{styleClass}" />
</c:if>
<c:choose>
<c:when test="#{hasAction}">
<p:commandButton
<fr:commandButton
value="#{value}"
icon="#{not empty icon ? icon : ''}"
styleClass="#{buttonClass}"
severity="#{severity}"
size="#{size != 'normal' ? size : null}"
styleClass="#{styleClass}"
disabled="#{disabled}"
action="#{action}"
update="#{not empty update ? update : '@form'}"
@@ -99,10 +68,12 @@
onclick="#{not empty onclick ? onclick : ''}" />
</c:when>
<c:when test="#{hasOutcome}">
<p:commandButton
<fr:commandButton
value="#{value}"
icon="#{not empty icon ? icon : ''}"
styleClass="#{buttonClass}"
severity="#{severity}"
size="#{size != 'normal' ? size : null}"
styleClass="#{styleClass}"
disabled="#{disabled}"
outcome="#{outcome}"
update="#{not empty update ? update : '@form'}"
@@ -110,19 +81,23 @@
onclick="#{not empty onclick ? onclick : ''}" />
</c:when>
<c:when test="#{not empty onclick}">
<p:commandButton
<fr:commandButton
value="#{value}"
icon="#{not empty icon ? icon : ''}"
styleClass="#{buttonClass}"
severity="#{severity}"
size="#{size != 'normal' ? size : null}"
styleClass="#{styleClass}"
disabled="#{disabled}"
type="button"
onclick="#{onclick}" />
</c:when>
<c:otherwise>
<p:commandButton
<fr:commandButton
value="#{value}"
icon="#{not empty icon ? icon : ''}"
styleClass="#{buttonClass}"
severity="#{severity}"
size="#{size != 'normal' ? size : null}"
styleClass="#{styleClass}"
disabled="true"
title="Aucune action définie" />
</c:otherwise>

View File

@@ -78,8 +78,9 @@ class DashboardBeanTest {
assertEquals(100L, dashboardBean.getTotalUsers());
assertEquals(1L, dashboardBean.getTotalRoles());
assertEquals(55L, dashboardBean.getRecentActions());
assertEquals(55L, dashboardBean.getActionsLast24h());
assertEquals("100", dashboardBean.getTotalUsersDisplay());
assertEquals("55", dashboardBean.getActionsLast24hDisplay());
}
@Test