chore(quarkus-327): bump to Quarkus 3.27.3 LTS, rename deprecated config keys

This commit is contained in:
2026-04-23 14:48:46 +00:00
parent e23ed3f451
commit be2debc6bf
122 changed files with 10918 additions and 8797 deletions

116
.gitignore vendored Normal file
View File

@@ -0,0 +1,116 @@
# ============================================
# BTPXpress Client (Quarkus JSF) .gitignore
# ============================================
# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
# Quarkus
.quarkus/
quarkus.log
# IDE
.idea/
*.iml
*.ipr
*.iws
.vscode/
.classpath
.project
.settings/
.factorypath
.apt_generated/
.apt_generated_tests/
# Eclipse
.metadata
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.loadpath
.recommenders
# IntelliJ
out/
.idea_modules/
# Logs
*.log
*.log.*
logs/
# OS
.DS_Store
Thumbs.db
*.pid
# Java
*.class
*.jar
!.mvn/wrapper/maven-wrapper.jar
*.war
*.ear
hs_err_pid*
# JSF/Faces specific
**/META-INF/resources/.faces-config.xml.jsfdia
**/javax.faces.resource/
# PrimeFaces cache
**/primefaces_resource_cache/
# Node modules (if using npm/webpack)
node_modules/
npm-debug.log
yarn-error.log
package-lock.json
yarn.lock
# Static resources compiled
src/main/resources/META-INF/resources/dist/
src/main/resources/META-INF/resources/assets/vendor/
# Application secrets
*.jks
*.p12
*.pem
*.key
*-secret.properties
application-local.properties
application-dev-override.properties
*.secret
*secret.txt
# Docker
docker-compose.override.yml
# Database
*.db
*.sqlite
*.h2.db
# Test
test-output/
.gradle/
build/
# Temporary
.tmp/
temp/
# Keycloak secrets (important!)
keycloak-secret.txt
client-secret.txt

View File

@@ -0,0 +1,263 @@
# 📊 Résumé Exécutif - Optimisations PrimeFaces BTPXpress
## 🎯 Objectif
Optimiser l'application BTPXpress en appliquant les meilleures pratiques du repository PrimeFaces officiel pour améliorer les performances, l'expérience utilisateur et la maintenabilité.
---
## 📈 Gains Attendus
### Performance
| Métrique | Avant | Après | Gain |
|----------|-------|-------|------|
| **Temps de chargement liste** | 2-3s | <500ms | **80-85%** |
| **Données transférées** | 100+ factures | 10-50 factures | **50-90%** |
| **Mémoire utilisée** | Toutes les données | Données paginées | **70-80%** |
| **Temps de réponse Ajax** | 500-1000ms | 100-200ms | **70-80%** |
### Expérience Utilisateur
- **Chargement instantané** : Pagination côté serveur
- **Filtrage en temps réel** : Résultats en <500ms
- **Validation immédiate** : Feedback client-side
- **Interface cohérente** : Composants Freya standardisés
- **Notifications claires** : Messages growl informatifs
### Maintenabilité
- **Code réutilisable** : Composants composites métier
- **Moins de duplication** : Patterns standardisés
- **Debugging facilité** : Logs structurés
- **Tests simplifiés** : Architecture modulaire
---
## 🔧 Optimisations Principales
### 1. Lazy Loading avec LazyDataModel
**Impact** : 🔥🔥🔥 Critique
**Problème actuel** :
```java
// ❌ Charge TOUTES les factures en mémoire
List<Map<String, Object>> facturesData = factureService.getAllFactures();
```
**Solution** :
```java
// ✅ Charge seulement 10-50 factures par page
LazyDataModel<Facture> lazyModel = new FactureLazyDataModel(factureService);
```
**Bénéfices** :
- Réduction de 80% du temps de chargement
- Réduction de 90% de la mémoire utilisée
- Scalabilité pour 10,000+ factures
---
### 2. Ajax Ciblé (process + update)
**Impact** : 🔥🔥 Important
**Problème actuel** :
```xml
<!-- ❌ Re-rend tout le formulaire -->
<p:commandButton update="@form" action="#{bean.filter}"/>
```
**Solution** :
```xml
<!-- ✅ Re-rend seulement la table et les messages -->
<p:commandButton process="@this filtresPanel"
update="facturesTable messages"
action="#{bean.filter}"/>
```
**Bénéfices** :
- Réduction de 70% du temps de re-rendering
- Moins de bande passante utilisée
- Interface plus réactive
---
### 3. Composants Réutilisables
**Impact** : 🔥🔥 Important
**Avant** : Code dupliqué dans chaque page
```xml
<!-- Répété 20+ fois dans le projet -->
<p:tag value="#{facture.statut}"
severity="#{facture.statut == 'PAYEE' ? 'success' : 'warning'}"/>
```
**Après** : Composant réutilisable
```xml
<!-- Utilisé partout, maintenu en un seul endroit -->
<btpx:facture-statut-badge statut="#{facture.statut}"/>
```
**Bénéfices** :
- Réduction de 60% du code XHTML
- Cohérence visuelle garantie
- Maintenance centralisée
---
### 4. Cache Intelligent
**Impact** : 🔥 Modéré
**Solution** :
```java
@ApplicationScoped
public class ReferenceDataService {
@CacheResult(cacheName = "statuts-facture")
public List<SelectItem> getStatutsFacture() {
// Appelé une seule fois, puis mis en cache
}
}
```
**Bénéfices** :
- Réduction de 95% des appels API pour données statiques
- Temps de réponse instantané
- Moins de charge sur le backend
---
### 5. Validation Côté Client
**Impact** : 🔥 Modéré
**Configuration** :
```properties
primefaces.CLIENT_SIDE_VALIDATION=true
primefaces.CSV_ENABLED=true
```
**Bénéfices** :
- Feedback immédiat pour l'utilisateur
- Réduction de 50% des appels serveur invalides
- Meilleure UX
---
## 📅 Plan de Déploiement (5 Semaines)
### Semaine 1 : Lazy Loading
- [ ] Implémenter FactureLazyDataModel
- [ ] Modifier FactureView
- [ ] Ajouter endpoints backend
- [ ] Tests et validation
- **Livrable** : Liste factures optimisée
### Semaine 2 : Ajax Optimisé
- [ ] Auditer tous les commandButton
- [ ] Ajouter process/update ciblés
- [ ] Implémenter p:ajax pour filtres
- **Livrable** : Interface plus réactive
### Semaine 3 : Composants Réutilisables
- [ ] Créer composants métier
- [ ] Migrer vers fr:dataTable
- [ ] Standardiser les patterns
- **Livrable** : Code plus maintenable
### Semaine 4 : Validation & UX
- [ ] Activer validation client
- [ ] Implémenter growl
- [ ] Ajouter confirmations
- **Livrable** : Meilleure UX
### Semaine 5 : Cache & Performance
- [ ] Implémenter cache
- [ ] Profiler et optimiser
- [ ] Tests de charge
- **Livrable** : Application optimisée
---
## 💰 ROI Estimé
### Coûts
- **Développement** : 5 semaines × 1 développeur = 5 semaines-homme
- **Tests** : 1 semaine
- **Total** : ~6 semaines-homme
### Gains
- **Performance** : 80% d'amélioration Meilleure satisfaction utilisateur
- **Scalabilité** : Support de 10x plus de données
- **Maintenance** : 60% moins de code 40% de temps de maintenance en moins
- **Bugs** : 50% moins de bugs liés aux performances
### ROI
- **Court terme** (3 mois) : Satisfaction utilisateur +30%
- **Moyen terme** (6 mois) : Temps de maintenance -40%
- **Long terme** (1 an) : Coûts d'infrastructure -20% (moins de ressources serveur)
---
## 🎓 Apprentissages Clés
### Meilleures Pratiques PrimeFaces
1. **Toujours utiliser LazyDataModel** pour les listes >50 items
2. **Spécifier process et update** de manière ciblée
3. **Créer des composants réutilisables** pour les patterns récurrents
4. **Valider côté client ET serveur**
5. **Utiliser le cache** pour les données statiques
### Patterns à Éviter
1.`update="@form"` ou `update="@all"`
2. ❌ Charger toutes les données en mémoire
3. ❌ Dupliquer le code de composants
4. ❌ Ignorer la validation côté client
5. ❌ Recharger les données de référence
---
## 📚 Ressources
### Documentation
- [Guide complet d'optimisation](./PRIMEFACES_BEST_PRACTICES_OPTIMIZATION.md)
- [Exemple d'implémentation Lazy Loading](./IMPLEMENTATION_EXAMPLE_LAZY_LOADING.md)
- [PrimeFaces Showcase](https://www.primefaces.org/showcase/)
### Support
- **PrimeFaces GitHub** : https://github.com/primefaces/primefaces
- **Documentation officielle** : https://www.primefaces.org/docs/
- **Community Forum** : https://github.com/primefaces/primefaces/discussions
---
## ✅ Critères de Succès
### Techniques
- [ ] Temps de chargement <500ms pour toutes les listes
- [ ] Score Lighthouse >90
- [ ] Couverture de tests >80%
- [ ] Zéro erreur console JavaScript
### Fonctionnels
- [ ] Pagination fluide sur toutes les listes
- [ ] Filtrage en temps réel <500ms
- [ ] Validation immédiate sur tous les formulaires
- [ ] Messages clairs pour toutes les actions
### Qualité
- [ ] Code review approuvé
- [ ] Documentation à jour
- [ ] Tests utilisateurs positifs
- [ ] Zéro régression fonctionnelle
---
## 🚀 Prochaines Actions
1. **Valider le plan** avec l'équipe technique
2. **Prioriser les modules** à optimiser en premier
3. **Commencer par Phase 1** : Lazy Loading Factures
4. **Mesurer les performances** avant/après
5. **Itérer** sur les autres modules
---
**Date** : 2025-12-29
**Auteur** : Équipe BTPXpress
**Version** : 1.0
**Statut** : Proposition

75
FOOTER_CONFIGURATION.md Normal file
View File

@@ -0,0 +1,75 @@
# 🔧 Configuration du Footer - BTPXpress
## ✅ Problème résolu
Le Footer était affiché sur **toutes les pages** de l'application, ce qui n'est pas logique pour une application métier BTP.
## 🔧 Solution implémentée
Le Footer est maintenant **conditionnel** et **désactivé par défaut** dans le template principal.
### Modification apportée
**Fichier** : `src/main/resources/META-INF/resources/WEB-INF/template.xhtml`
**Avant** :
```xhtml
<ui:include src="./footer.xhtml"/>
```
**Après** :
```xhtml
<!-- Footer conditionnel : désactivé par défaut pour application métier -->
<!-- Pour l'activer sur une page spécifique, ajouter : <ui:param name="showFooter" value="true"/> -->
<ui:fragment rendered="#{showFooter == true}">
<ui:include src="./footer.xhtml"/>
</ui:fragment>
```
## 📋 Comportement
-**Par défaut** : Le Footer n'est **PAS affiché** sur aucune page
-**Sur demande** : Pour afficher le Footer sur une page spécifique, ajouter :
```xhtml
<ui:composition template="/WEB-INF/template.xhtml">
<ui:param name="showFooter" value="true"/>
<ui:define name="content">
<!-- Contenu de la page -->
</ui:define>
</ui:composition>
```
## 🎯 Pages concernées
Le Footer n'est plus affiché sur :
- ✅ Toutes les pages de gestion (chantiers, clients, devis, factures, etc.)
- ✅ Toutes les pages de formulaire (création, édition)
- ✅ Toutes les pages de détails
- ✅ Toutes les pages de configuration
- ✅ Toutes les pages de rapports
- ✅ Toutes les pages internes de l'application
- ✅ La page dashboard (tableau de bord interne)
## ✅ Footer activé sur
Le Footer est maintenant affiché **uniquement** sur :
-**`index.xhtml`** - Page d'accueil publique (accessible sans authentification)
Cette page sert de point d'entrée public pour l'application et contient :
- Présentation de BTP Xpress
- Boutons de connexion et "En savoir plus"
- Footer complet avec liens, newsletter, etc.
## 📝 Page d'accueil publique créée
**Fichier** : `src/main/resources/META-INF/resources/index.xhtml`
Cette page a été créée pour servir de page d'accueil publique accessible à tous, avec le Footer activé.
---
**Date de modification** : 2026-01-03
**Statut** : ✅ Résolu

View File

@@ -0,0 +1,281 @@
# 🚀 Exemple d'Implémentation : Lazy Loading pour Factures
## 📋 Objectif
Transformer la liste des factures pour utiliser le lazy loading avec pagination côté serveur.
---
## 📁 Fichiers à Créer/Modifier
### 1. Créer FactureLazyDataModel.java
**Chemin** : `src/main/java/dev/lions/btpxpress/view/model/FactureLazyDataModel.java`
```java
package dev.lions.btpxpress.view.model;
import dev.lions.btpxpress.service.FactureService;
import dev.lions.btpxpress.view.FactureView.Facture;
import org.primefaces.model.FilterMeta;
import org.primefaces.model.LazyDataModel;
import org.primefaces.model.SortMeta;
import org.primefaces.model.SortOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.util.*;
public class FactureLazyDataModel extends LazyDataModel<Facture> implements Serializable {
private static final Logger LOG = LoggerFactory.getLogger(FactureLazyDataModel.class);
private final FactureService factureService;
public FactureLazyDataModel(FactureService factureService) {
this.factureService = factureService;
}
@Override
public int count(Map<String, FilterMeta> filterBy) {
try {
Map<String, Object> filterParams = buildFilterParams(filterBy);
int count = factureService.countFactures(filterParams);
LOG.debug("Count factures with filters: {}", count);
return count;
} catch (Exception e) {
LOG.error("Erreur lors du comptage des factures", e);
return 0;
}
}
@Override
public List<Facture> load(int first, int pageSize,
Map<String, SortMeta> sortBy,
Map<String, FilterMeta> filterBy) {
try {
Map<String, Object> params = new HashMap<>();
params.put("offset", first);
params.put("limit", pageSize);
// Ajouter les paramètres de tri
if (sortBy != null && !sortBy.isEmpty()) {
SortMeta sortMeta = sortBy.values().iterator().next();
params.put("sortField", sortMeta.getField());
params.put("sortOrder", sortMeta.getOrder() == SortOrder.ASCENDING ? "ASC" : "DESC");
}
// Ajouter les paramètres de filtrage
params.putAll(buildFilterParams(filterBy));
LOG.debug("Loading factures with params: {}", params);
List<Facture> factures = factureService.getFacturesLazy(params);
LOG.debug("Loaded {} factures", factures.size());
return factures;
} catch (Exception e) {
LOG.error("Erreur lors du chargement des factures", e);
return Collections.emptyList();
}
}
@Override
public String getRowKey(Facture facture) {
return facture.getId() != null ? facture.getId().toString() : null;
}
@Override
public Facture getRowData(String rowKey) {
// Cette méthode est appelée pour récupérer une facture par son ID
// Pour l'instant, on retourne null car on ne garde pas de cache local
return null;
}
private Map<String, Object> buildFilterParams(Map<String, FilterMeta> filterBy) {
Map<String, Object> params = new HashMap<>();
if (filterBy != null) {
filterBy.forEach((field, filterMeta) -> {
Object filterValue = filterMeta.getFilterValue();
if (filterValue != null && !filterValue.toString().trim().isEmpty()) {
params.put("filter_" + field, filterValue);
}
});
}
return params;
}
}
```
---
### 2. Modifier FactureService.java
**Ajouter les méthodes pour le lazy loading** :
```java
package dev.lions.btpxpress.service;
import dev.lions.btpxpress.view.FactureView.Facture;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
@ApplicationScoped
public class FactureService {
private static final Logger LOG = LoggerFactory.getLogger(FactureService.class);
@Inject
@RestClient
BtpXpressApiClient apiClient;
/**
* Récupère les factures avec pagination et filtres
*/
public List<Facture> getFacturesLazy(Map<String, Object> params) {
try {
int offset = (int) params.getOrDefault("offset", 0);
int limit = (int) params.getOrDefault("limit", 10);
String sortField = (String) params.get("sortField");
String sortOrder = (String) params.get("sortOrder");
// Extraire les filtres
String filtreNumero = (String) params.get("filter_numero");
String filtreClient = (String) params.get("filter_client");
String filtreStatut = (String) params.get("filter_statut");
// Appel API (à adapter selon votre backend)
List<Map<String, Object>> facturesData = apiClient.getFacturesLazy(
offset, limit, sortField, sortOrder,
filtreNumero, filtreClient, filtreStatut
);
// Mapper vers les objets Facture
return facturesData.stream()
.map(this::mapToFacture)
.collect(Collectors.toList());
} catch (Exception e) {
LOG.error("Erreur lors de la récupération lazy des factures", e);
return Collections.emptyList();
}
}
/**
* Compte le nombre total de factures avec filtres
*/
public int countFactures(Map<String, Object> params) {
try {
String filtreNumero = (String) params.get("filter_numero");
String filtreClient = (String) params.get("filter_client");
String filtreStatut = (String) params.get("filter_statut");
return apiClient.countFactures(filtreNumero, filtreClient, filtreStatut);
} catch (Exception e) {
LOG.error("Erreur lors du comptage des factures", e);
return 0;
}
}
private Facture mapToFacture(Map<String, Object> data) {
Facture f = new Facture();
f.setId(data.get("id") != null ? Long.valueOf(data.get("id").toString()) : null);
f.setNumero((String) data.get("numero"));
f.setObjet((String) data.get("objet"));
// Mapping du client
Object clientObj = data.get("client");
if (clientObj instanceof Map) {
Map<String, Object> clientData = (Map<String, Object>) clientObj;
String entreprise = (String) clientData.get("entreprise");
String nom = (String) clientData.get("nom");
String prenom = (String) clientData.get("prenom");
f.setClient(entreprise != null && !entreprise.trim().isEmpty() ?
entreprise : (prenom != null ? prenom + " " : "") + (nom != null ? nom : ""));
} else if (clientObj instanceof String) {
f.setClient((String) clientObj);
}
// Mapping des dates
if (data.get("dateEmission") != null) {
f.setDateEmission(LocalDate.parse(data.get("dateEmission").toString()));
}
if (data.get("dateEcheance") != null) {
f.setDateEcheance(LocalDate.parse(data.get("dateEcheance").toString()));
}
// Mapping des montants
f.setStatut((String) data.get("statut"));
f.setMontantHT(data.get("montantHT") != null ? Double.valueOf(data.get("montantHT").toString()) : 0.0);
f.setMontantTTC(data.get("montantTTC") != null ? Double.valueOf(data.get("montantTTC").toString()) : 0.0);
f.setMontantPaye(data.get("montantPaye") != null ? Double.valueOf(data.get("montantPaye").toString()) : 0.0);
return f;
}
}
```
---
### 3. Modifier BtpXpressApiClient.java
**Ajouter les endpoints pour le lazy loading** :
```java
package dev.lions.btpxpress.service;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import java.util.List;
import java.util.Map;
@Path("/api")
@RegisterRestClient(configKey = "btpxpress-api")
public interface BtpXpressApiClient {
// ... méthodes existantes ...
@GET
@Path("/factures/lazy")
@Produces(MediaType.APPLICATION_JSON)
List<Map<String, Object>> getFacturesLazy(
@QueryParam("offset") int offset,
@QueryParam("limit") int limit,
@QueryParam("sortField") String sortField,
@QueryParam("sortOrder") String sortOrder,
@QueryParam("filter_numero") String filtreNumero,
@QueryParam("filter_client") String filtreClient,
@QueryParam("filter_statut") String filtreStatut
);
@GET
@Path("/factures/count")
@Produces(MediaType.APPLICATION_JSON)
int countFactures(
@QueryParam("filter_numero") String filtreNumero,
@QueryParam("filter_client") String filtreClient,
@QueryParam("filter_statut") String filtreStatut
);
}
```
---
## ✅ Prochaines Étapes
1. Créer les fichiers ci-dessus
2. Tester avec des données de test
3. Vérifier les logs pour le debugging
4. Adapter le backend si nécessaire
5. Répliquer pour les autres modules (Devis, Clients, etc.)

View File

@@ -0,0 +1,221 @@
# 📋 Spécification Technique - Implémentation Lazy Loading Production
## 🎯 Objectif
Implémenter le lazy loading pour les factures avec une solution **production-ready** incluant :
- Gestion d'erreurs robuste
- Logging approprié
- Tests unitaires et d'intégration
- Documentation complète
- Compatibilité backend/frontend
---
## 🏗️ Architecture
### Composants à Créer/Modifier
#### Frontend (btpxpress-client)
1. **FactureLazyDataModel** - Nouveau
2. **FactureService** - Modifier (ajouter méthodes lazy)
3. **BtpXpressApiClient** - Modifier (ajouter endpoints lazy)
4. **FactureView** - Modifier (utiliser LazyDataModel)
5. **factures.xhtml** - Modifier (activer lazy loading)
#### Backend (btpxpress-server)
1. **FactureResource** - Modifier (ajouter endpoints lazy)
2. **FactureService** - Modifier (ajouter méthodes paginées)
3. **FactureRepository** - Modifier (ajouter requêtes paginées)
---
## 📊 Flux de Données
```
[factures.xhtml]
↓ (lazy=true, rows=10)
[FactureView + LazyDataModel]
↓ (load(first, pageSize, sortBy, filterBy))
[FactureService]
↓ (getFacturesLazy(params))
[BtpXpressApiClient]
↓ (HTTP GET /api/v1/factures/lazy?offset=0&limit=10&...)
[Backend FactureResource]
↓ (getFacturesLazy(offset, limit, ...))
[Backend FactureService]
↓ (findFacturesLazy(offset, limit, ...))
[Backend FactureRepository]
↓ (Panache query with pagination)
[Database]
```
---
## 🔧 Détails d'Implémentation
### 1. Backend - FactureRepository
**Méthodes à ajouter** :
```java
/**
* Compte le nombre total de factures avec filtres optionnels
*/
public long countFactures(String filtreNumero, String filtreClient, String filtreStatut);
/**
* Récupère les factures avec pagination, tri et filtres
*/
public List<Facture> findFacturesLazy(
int offset,
int limit,
String sortField,
String sortOrder,
String filtreNumero,
String filtreClient,
String filtreStatut
);
```
### 2. Backend - FactureService
**Méthodes à ajouter** :
```java
/**
* Récupère les factures avec pagination
* @return DTO contenant les factures et le total
*/
public FacturePageDTO getFacturesLazy(FactureFilterDTO filters);
/**
* Compte les factures avec filtres
*/
public long countFactures(FactureFilterDTO filters);
```
### 3. Backend - FactureResource
**Endpoints à ajouter** :
```java
@GET
@Path("/lazy")
@Produces(MediaType.APPLICATION_JSON)
public Response getFacturesLazy(
@QueryParam("offset") @DefaultValue("0") int offset,
@QueryParam("limit") @DefaultValue("10") int limit,
@QueryParam("sortField") String sortField,
@QueryParam("sortOrder") @DefaultValue("ASC") String sortOrder,
@QueryParam("filter_numero") String filtreNumero,
@QueryParam("filter_client") String filtreClient,
@QueryParam("filter_statut") String filtreStatut
);
@GET
@Path("/count")
@Produces(MediaType.APPLICATION_JSON)
public Response countFactures(
@QueryParam("filter_numero") String filtreNumero,
@QueryParam("filter_client") String filtreClient,
@QueryParam("filter_statut") String filtreStatut
);
```
---
## ✅ Critères d'Acceptation
### Fonctionnels
- [ ] La liste des factures charge seulement 10-50 items par page
- [ ] La pagination fonctionne correctement (première, précédente, suivante, dernière page)
- [ ] Le tri fonctionne sur toutes les colonnes triables
- [ ] Les filtres fonctionnent et sont appliqués côté serveur
- [ ] Le compteur total affiche le bon nombre de factures
- [ ] Les performances sont améliorées (temps de chargement <500ms)
### Techniques
- [ ] Gestion d'erreurs complète (try-catch, logging)
- [ ] Validation des paramètres (offset >= 0, limit > 0, etc.)
- [ ] Logs appropriés (DEBUG, INFO, WARN, ERROR)
- [ ] Code commenté et documenté
- [ ] Tests unitaires (couverture >80%)
- [ ] Tests d'intégration
- [ ] Pas de régression sur les fonctionnalités existantes
### Sécurité
- [ ] Validation côté serveur de tous les paramètres
- [ ] Protection contre SQL injection
- [ ] Vérification des droits d'accès
- [ ] Limitation du nombre max d'items par page (ex: 100)
---
## 🧪 Plan de Tests
### Tests Unitaires
#### Frontend
- `FactureLazyDataModel.load()` - Cas nominal
- `FactureLazyDataModel.load()` - Avec filtres
- `FactureLazyDataModel.load()` - Avec tri
- `FactureLazyDataModel.count()` - Cas nominal
- `FactureLazyDataModel` - Gestion d'erreurs
#### Backend
- `FactureRepository.findFacturesLazy()` - Pagination
- `FactureRepository.findFacturesLazy()` - Tri
- `FactureRepository.findFacturesLazy()` - Filtres
- `FactureRepository.countFactures()` - Avec/sans filtres
- `FactureService.getFacturesLazy()` - Cas nominal
- `FactureService.getFacturesLazy()` - Gestion d'erreurs
### Tests d'Intégration
- Chargement de la première page
- Navigation entre les pages
- Tri par différentes colonnes
- Application de filtres multiples
- Gestion d'erreurs réseau
- Performance (temps de réponse <500ms)
---
## 📝 Checklist d'Implémentation
### Phase 1 : Backend (Priorité 1)
- [ ] Créer DTOs (FactureFilterDTO, FacturePageDTO)
- [ ] Implémenter FactureRepository.findFacturesLazy()
- [ ] Implémenter FactureRepository.countFactures()
- [ ] Implémenter FactureService.getFacturesLazy()
- [ ] Implémenter FactureResource endpoints
- [ ] Tests unitaires backend
- [ ] Tests d'intégration backend
### Phase 2 : Frontend (Priorité 2)
- [ ] Créer FactureLazyDataModel
- [ ] Modifier BtpXpressApiClient
- [ ] Modifier FactureService
- [ ] Modifier FactureView
- [ ] Modifier factures.xhtml
- [ ] Tests unitaires frontend
### Phase 3 : Tests & Documentation (Priorité 3)
- [ ] Tests end-to-end
- [ ] Tests de performance
- [ ] Documentation technique
- [ ] Documentation utilisateur
- [ ] Revue de code
---
## 🚨 Risques et Mitigation
| Risque | Impact | Probabilité | Mitigation |
|--------|--------|-------------|------------|
| Incompatibilité backend | Élevé | Faible | Vérifier version Panache, tester avec données réelles |
| Performance dégradée | Élevé | Moyen | Indexer colonnes de tri/filtre, optimiser requêtes |
| Régression fonctionnelle | Moyen | Moyen | Tests complets avant déploiement |
| Erreurs réseau | Moyen | Faible | Gestion d'erreurs robuste, retry logic |
---
**Version** : 1.0
**Date** : 2025-12-29
**Statut** : Prêt pour implémentation

View File

@@ -0,0 +1,776 @@
# 🚀 Optimisation BTPXpress - Meilleures Pratiques PrimeFaces
## 📋 Table des Matières
1. [Analyse du Projet Actuel](#analyse-du-projet-actuel)
2. [Optimisations DataTable & Lazy Loading](#optimisations-datatable--lazy-loading)
3. [Performance Ajax & Partial Rendering](#performance-ajax--partial-rendering)
4. [Composants Réutilisables](#composants-réutilisables)
5. [Gestion d'État & ViewScoped](#gestion-détat--viewscoped)
6. [Validation & Messages](#validation--messages)
7. [Plan d'Implémentation](#plan-dimplémentation)
---
## 🔍 Analyse du Projet Actuel
### Points Forts ✅
- ✅ Utilisation de `@ViewScoped` pour les beans (bonne pratique)
- ✅ Architecture BaseListView pour la réutilisation du code
- ✅ Séparation des concerns (Service, View, Model)
- ✅ Utilisation de composants réutilisables (`liste-table.xhtml`, `liste-filters.xhtml`)
### Points à Améliorer 🔧
- ⚠️ **Pas de Lazy Loading** : Toutes les données sont chargées en mémoire
- ⚠️ **Filtrage côté client** : Le filtrage se fait en Java après récupération complète
- ⚠️ **Pas de cache** : Rechargement complet à chaque fois
- ⚠️ **Updates Ajax trop larges** : Risque de re-rendering inutile
- ⚠️ **Pas de composants composites Freya** : Utilisation directe de PrimeFaces
---
## 📊 Optimisations DataTable & Lazy Loading
### 1. Implémenter LazyDataModel
**Problème actuel** : Dans `FactureView.java`, toutes les factures sont chargées :
```java
List<Map<String, Object>> facturesData = factureService.getAllFactures();
```
**Solution** : Utiliser `LazyDataModel` de PrimeFaces
#### Créer un LazyDataModel personnalisé
```java
package dev.lions.btpxpress.view.model;
import org.primefaces.model.FilterMeta;
import org.primefaces.model.LazyDataModel;
import org.primefaces.model.SortMeta;
import dev.lions.btpxpress.service.FactureService;
import dev.lions.btpxpress.view.FactureView.Facture;
import java.util.*;
public class FactureLazyDataModel extends LazyDataModel<Facture> {
private final FactureService factureService;
public FactureLazyDataModel(FactureService factureService) {
this.factureService = factureService;
}
@Override
public int count(Map<String, FilterMeta> filterBy) {
// Appel API pour compter le nombre total avec filtres
return factureService.countFactures(buildFilterParams(filterBy));
}
@Override
public List<Facture> load(int first, int pageSize,
Map<String, SortMeta> sortBy,
Map<String, FilterMeta> filterBy) {
// Appel API avec pagination, tri et filtres
Map<String, Object> params = new HashMap<>();
params.put("offset", first);
params.put("limit", pageSize);
params.putAll(buildSortParams(sortBy));
params.putAll(buildFilterParams(filterBy));
return factureService.getFacturesLazy(params);
}
private Map<String, Object> buildSortParams(Map<String, SortMeta> sortBy) {
Map<String, Object> params = new HashMap<>();
if (sortBy != null && !sortBy.isEmpty()) {
SortMeta sort = sortBy.values().iterator().next();
params.put("sortField", sort.getField());
params.put("sortOrder", sort.getOrder().name());
}
return params;
}
private Map<String, Object> buildFilterParams(Map<String, FilterMeta> filterBy) {
Map<String, Object> params = new HashMap<>();
if (filterBy != null) {
filterBy.forEach((key, filter) -> {
if (filter.getFilterValue() != null) {
params.put("filter_" + key, filter.getFilterValue());
}
});
}
return params;
}
}
```
#### Modifier FactureView pour utiliser LazyDataModel
```java
@Named("factureView")
@ViewScoped
public class FactureView implements Serializable {
@Inject
FactureService factureService;
private LazyDataModel<Facture> lazyModel;
@PostConstruct
public void init() {
lazyModel = new FactureLazyDataModel(factureService);
}
public LazyDataModel<Facture> getLazyModel() {
return lazyModel;
}
}
```
#### Modifier factures.xhtml pour utiliser lazy loading
```xml
<p:dataTable id="facturesTable"
value="#{factureView.lazyModel}"
var="facture"
lazy="true"
paginator="true"
rows="10"
rowsPerPageTemplate="10,20,50"
paginatorPosition="both"
paginatorTemplate="{CurrentPageReport} {FirstPageLink} {PreviousPageLink} {PageLinks} {NextPageLink} {LastPageLink} {RowsPerPageDropdown}"
currentPageReportTemplate="Affichage {startRecord}-{endRecord} sur {totalRecords}"
filterDelay="500"
emptyMessage="Aucune facture trouvée">
<!-- Colonnes avec filtres intégrés -->
<p:column headerText="Numéro"
sortBy="#{facture.numero}"
filterBy="#{facture.numero}"
filterMatchMode="contains">
<h:outputText value="#{facture.numero}"/>
</p:column>
<!-- ... autres colonnes ... -->
</p:dataTable>
```
### 2. Optimiser le Service Backend
**Ajouter des endpoints paginés dans BtpXpressApiClient** :
```java
@Path("/api/factures")
public interface BtpXpressApiClient {
@GET
@Path("/lazy")
@Produces(MediaType.APPLICATION_JSON)
Response getFacturesLazy(
@QueryParam("offset") int offset,
@QueryParam("limit") int limit,
@QueryParam("sortField") String sortField,
@QueryParam("sortOrder") String sortOrder,
@QueryParam("filter_numero") String filtreNumero,
@QueryParam("filter_client") String filtreClient,
@QueryParam("filter_statut") String filtreStatut
);
@GET
@Path("/count")
@Produces(MediaType.APPLICATION_JSON)
int countFactures(
@QueryParam("filter_numero") String filtreNumero,
@QueryParam("filter_client") String filtreClient,
@QueryParam("filter_statut") String filtreStatut
);
}
```
---
## ⚡ Performance Ajax & Partial Rendering
### 1. Optimiser les Updates Ajax
**Problème** : Updates trop larges qui re-rendent des composants inutilement
**Mauvaise pratique** ❌ :
```xml
<p:commandButton value="Filtrer"
update="@form"
action="#{factureView.applyFilters}"/>
```
**Bonne pratique** ✅ :
```xml
<p:commandButton value="Filtrer"
update="facturesTable messages"
process="@this filtresPanel"
action="#{factureView.applyFilters}"/>
```
### 2. Utiliser process et update de manière ciblée
**Règles d'or** :
- `process` : Spécifie quels composants doivent être traités (validation, conversion, update model)
- `update` : Spécifie quels composants doivent être re-rendus
- Toujours utiliser les IDs spécifiques plutôt que `@form` ou `@all`
**Exemple optimisé** :
```xml
<h:form id="factureForm">
<p:panel id="filtresPanel">
<p:inputText id="filtreNumero" value="#{factureView.filtreNumero}"/>
<p:inputText id="filtreClient" value="#{factureView.filtreClient}"/>
<p:commandButton value="Rechercher"
process="@this filtresPanel"
update="facturesTable messages"
action="#{factureView.search}"/>
</p:panel>
<p:messages id="messages"/>
<p:dataTable id="facturesTable" value="#{factureView.lazyModel}" var="facture">
<!-- colonnes -->
</p:dataTable>
</h:form>
```
### 3. Utiliser p:ajax pour les événements
**Pour les changements de filtres en temps réel** :
```xml
<p:inputText id="filtreNumero" value="#{factureView.filtreNumero}">
<p:ajax event="keyup"
delay="500"
update="facturesTable"
process="@this"
listener="#{factureView.onFilterChange}"/>
</p:inputText>
```
### 4. Désactiver les auto-updates inutiles
**Éviter** :
```xml
<p:dataTable autoUpdate="true"> <!-- ❌ Mauvaise pratique -->
```
**Préférer** :
```xml
<p:dataTable id="table">
<p:ajax event="rowSelect" update="detailPanel" listener="#{bean.onRowSelect}"/>
</p:dataTable>
```
---
## 🧩 Composants Réutilisables
### 1. Migrer vers Freya Extension
**Avantages** :
- Composants pré-stylés cohérents
- Moins de code boilerplate
- Meilleure maintenabilité
#### Exemple : Remplacer p:dataTable par fr:dataTable
**Avant** :
```xml
<p:dataTable id="facturesTable"
value="#{factureView.items}"
var="facture"
paginator="true"
rows="10"
styleClass="p-datatable-striped"
emptyMessage="Aucune facture">
<p:column headerText="Numéro">
<h:outputText value="#{facture.numero}"/>
</p:column>
</p:dataTable>
```
**Après** :
```xml
<fr:dataTable value="#{factureView.lazyModel}"
var="facture"
paginator="true"
rows="10"
lazy="true"
stripedRows="true">
<p:column headerText="Numéro">
<h:outputText value="#{facture.numero}"/>
</p:column>
</fr:dataTable>
```
### 2. Créer des Composants Composites Métier
#### Créer un composant pour les badges de statut
**Fichier** : `/WEB-INF/components/facture-statut-badge.xhtml`
```xml
<?xml version="1.0" encoding="UTF-8"?>
<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:cc="http://xmlns.jcp.org/jsf/composite"
xmlns:p="http://primefaces.org/ui">
<cc:interface>
<cc:attribute name="statut" required="true" type="java.lang.String"/>
<cc:attribute name="enRetard" type="java.lang.Boolean" default="false"/>
</cc:interface>
<cc:implementation>
<p:tag value="#{cc.attrs.statut}"
severity="#{cc.attrs.statut == 'PAYEE' ? 'success' :
(cc.attrs.statut == 'ANNULEE' ? 'danger' :
(cc.attrs.enRetard ? 'danger' : 'warning'))}"
icon="#{cc.attrs.statut == 'PAYEE' ? 'pi pi-check' :
(cc.attrs.statut == 'ANNULEE' ? 'pi pi-times' :
(cc.attrs.enRetard ? 'pi pi-exclamation-triangle' : 'pi pi-clock'))}"/>
</cc:implementation>
</ui:composition>
```
**Utilisation** :
```xml
<p:column headerText="Statut">
<btpx:facture-statut-badge statut="#{facture.statut}"
enRetard="#{factureView.isEnRetard(facture)}"/>
</p:column>
```
#### Créer un composant pour les montants
**Fichier** : `/WEB-INF/components/montant-display.xhtml`
```xml
<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:cc="http://xmlns.jcp.org/jsf/composite">
<cc:interface>
<cc:attribute name="montant" required="true" type="java.lang.Double"/>
<cc:attribute name="devise" default="Fcfa"/>
<cc:attribute name="highlight" type="java.lang.Boolean" default="false"/>
<cc:attribute name="highlightColor" default="red"/>
</cc:interface>
<cc:implementation>
<span style="#{cc.attrs.highlight ? 'color: ' + cc.attrs.highlightColor + '; font-weight: bold;' : ''}">
<h:outputText value="#{cc.attrs.montant}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" #{cc.attrs.devise}"/>
</span>
</cc:implementation>
</ui:composition>
```
**Utilisation** :
```xml
<p:column headerText="Reste à payer">
<btpx:montant-display montant="#{factureView.getMontantRestant(facture)}"
highlight="#{factureView.getMontantRestant(facture) > 0}"/>
</p:column>
```
### 3. Créer un Composant de Filtre Réutilisable
**Fichier** : `/WEB-INF/components/search-filter-panel.xhtml`
```xml
<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:cc="http://xmlns.jcp.org/jsf/composite"
xmlns:p="http://primefaces.org/ui"
xmlns:fr="http://primefaces.org/freya">
<cc:interface>
<cc:attribute name="bean" required="true"/>
<cc:attribute name="tableId" required="true"/>
<cc:facet name="filters" required="true"/>
</cc:interface>
<cc:implementation>
<div class="card mb-3">
<div class="flex align-items-center justify-content-between mb-3">
<h3 class="m-0">
<i class="pi pi-filter mr-2"></i>
Filtres de recherche
</h3>
<div class="flex gap-2">
<fr:commandButton value="Rechercher"
icon="pi pi-search"
severity="primary"
process="@this @parent"
update="#{cc.attrs.tableId} messages"
action="#{cc.attrs.bean.applyFilters}"/>
<fr:commandButton value="Réinitialiser"
icon="pi pi-refresh"
severity="secondary"
outlined="true"
process="@this"
update="@parent #{cc.attrs.tableId}"
action="#{cc.attrs.bean.resetFilters}"/>
</div>
</div>
<cc:renderFacet name="filters"/>
</div>
</cc:implementation>
</ui:composition>
```
---
## 🎯 Gestion d'État & ViewScoped
### 1. Optimiser BaseListView
**Problème actuel** : Rechargement complet à chaque filtre
**Solution** : Ajouter un cache intelligent
```java
@Getter
@Setter
public abstract class BaseListView<T, ID> implements Serializable {
protected LazyDataModel<T> lazyModel;
protected T selectedItem;
protected boolean loading;
// Cache pour éviter les rechargements inutiles
private transient Map<String, Object> lastFilterParams;
@PostConstruct
public void init() {
initializeLazyModel();
}
protected abstract void initializeLazyModel();
public void applyFilters() {
Map<String, Object> currentParams = buildFilterParams();
// Vérifier si les filtres ont changé
if (!Objects.equals(lastFilterParams, currentParams)) {
lastFilterParams = new HashMap<>(currentParams);
// Le LazyDataModel se rechargera automatiquement
}
}
protected abstract Map<String, Object> buildFilterParams();
public void resetFilters() {
resetFilterFields();
lastFilterParams = null;
applyFilters();
}
protected abstract void resetFilterFields();
}
```
### 2. Utiliser @CacheResult pour les données statiques
**Pour les listes de référence (statuts, types, etc.)** :
```java
@ApplicationScoped
public class ReferenceDataService {
@CacheResult(cacheName = "statuts-facture")
public List<SelectItem> getStatutsFacture() {
return Arrays.asList(
new SelectItem("BROUILLON", "Brouillon"),
new SelectItem("EMISE", "Émise"),
new SelectItem("PAYEE", "Payée"),
// ...
);
}
}
```
**Utilisation dans le bean** :
```java
@Named("factureView")
@ViewScoped
public class FactureView implements Serializable {
@Inject
ReferenceDataService refDataService;
public List<SelectItem> getStatutsFacture() {
return refDataService.getStatutsFacture(); // Mis en cache
}
}
```
---
## ✅ Validation & Messages
### 1. Validation côté client avec PrimeFaces
**Activer la validation client** dans `application.properties` :
```properties
primefaces.CLIENT_SIDE_VALIDATION=true
primefaces.CSV_ENABLED=true
```
**Exemple de formulaire avec validation** :
```xml
<h:form id="factureForm">
<p:messages id="messages" showDetail="true" closable="true"/>
<fr:fieldInput label="Numéro de facture"
value="#{factureView.entity.numero}"
required="true"
requiredMessage="Le numéro est obligatoire">
<f:validateLength minimum="3" maximum="20"/>
</fr:fieldInput>
<fr:fieldCalendar label="Date d'émission"
value="#{factureView.entity.dateEmission}"
required="true"
showIcon="true">
<f:validator validatorId="dateValidator"/>
</fr:fieldCalendar>
<fr:commandButton value="Enregistrer"
icon="pi pi-save"
action="#{factureView.save}"
update="messages"
process="@form"
validateClient="true"/>
</h:form>
```
### 2. Messages d'erreur personnalisés
**Créer un validateur personnalisé** :
```java
@FacesValidator("dateValidator")
public class DateValidator implements Validator<LocalDate> {
@Override
public void validate(FacesContext context, UIComponent component, LocalDate value)
throws ValidatorException {
if (value != null && value.isBefore(LocalDate.now())) {
FacesMessage msg = new FacesMessage(
FacesMessage.SEVERITY_ERROR,
"Date invalide",
"La date ne peut pas être dans le passé"
);
throw new ValidatorException(msg);
}
}
}
```
### 3. Utiliser p:growl pour les notifications
**Ajouter dans le template** :
```xml
<p:growl id="growl"
life="3000"
sticky="false"
showDetail="true"/>
```
**Dans le bean** :
```java
public void save() {
try {
factureService.save(selectedItem);
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO,
"Succès",
"La facture a été enregistrée avec succès"));
} catch (Exception e) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR,
"Erreur",
"Impossible d'enregistrer la facture: " + e.getMessage()));
}
}
```
---
## 📅 Plan d'Implémentation
### Phase 1 : Optimisation des DataTables (Semaine 1)
- [ ] Créer `FactureLazyDataModel`
- [ ] Modifier `FactureView` pour utiliser LazyDataModel
- [ ] Ajouter endpoints paginés dans le backend
- [ ] Tester la pagination et le tri
- [ ] Répliquer pour Devis, Clients, Chantiers
### Phase 2 : Optimisation Ajax (Semaine 2)
- [ ] Auditer tous les `update="@form"` et les remplacer
- [ ] Ajouter `process` spécifiques sur tous les commandButton
- [ ] Implémenter `p:ajax` pour les filtres en temps réel
- [ ] Tester les performances
### Phase 3 : Composants Réutilisables (Semaine 3)
- [ ] Créer `facture-statut-badge.xhtml`
- [ ] Créer `montant-display.xhtml`
- [ ] Créer `search-filter-panel.xhtml`
- [ ] Migrer vers `fr:dataTable` pour toutes les tables
- [ ] Créer composants métier supplémentaires
### Phase 4 : Validation & UX (Semaine 4)
- [ ] Activer validation côté client
- [ ] Créer validateurs personnalisés
- [ ] Implémenter p:growl pour notifications
- [ ] Ajouter confirmations pour actions critiques
- [ ] Tests utilisateurs
### Phase 5 : Cache & Performance (Semaine 5)
- [ ] Implémenter cache pour données de référence
- [ ] Optimiser BaseListView avec cache intelligent
- [ ] Profiler et identifier les bottlenecks
- [ ] Optimiser les requêtes backend
---
## 📊 Métriques de Succès
### Avant Optimisation
- ⏱️ Temps de chargement liste factures : ~2-3s (100+ factures)
- 📦 Données transférées : Toutes les factures à chaque fois
- 🔄 Re-rendering : Formulaire complet à chaque action
- 💾 Mémoire : Toutes les données en mémoire
### Après Optimisation (Objectifs)
- ⏱️ Temps de chargement : <500ms (pagination)
- 📦 Données transférées : 10-50 factures par page
- 🔄 Re-rendering : Composants ciblés uniquement
- 💾 Mémoire : Données paginées + cache intelligent
- 🎯 Score Lighthouse : >90
---
## 🔗 Ressources
### Documentation PrimeFaces
- [PrimeFaces Showcase](https://www.primefaces.org/showcase/)
- [LazyDataModel Guide](https://www.primefaces.org/docs/guide/primefaces_user_guide_14_0_0.pdf)
- [Ajax Best Practices](https://www.primefaces.org/showcase/ui/ajax/basic.xhtml)
### Exemples de Code
- [PrimeFaces GitHub](https://github.com/primefaces/primefaces)
- [PrimeFaces Showcase Source](https://github.com/primefaces/primefaces/tree/master/primefaces-showcase)
### Articles & Tutoriels
- [PrimeFaces DataTable Lazy Loading with JPA](https://www.javacodegeeks.com/2014/01/primefaces-datatable-lazy-loading-with-pagination-filtering-and-sorting-using-jpa-criteria-viewscoped.html)
- [Optimizing JSF Performance](https://www.baeldung.com/jsf-primefaces-performance)
---
## 🎓 Bonnes Pratiques Générales
### DO ✅
- ✅ Utiliser LazyDataModel pour les grandes listes
- ✅ Spécifier `process` et `update` de manière ciblée
- ✅ Utiliser `@ViewScoped` pour les beans de vue
- ✅ Créer des composants réutilisables
- ✅ Valider côté client ET serveur
- ✅ Utiliser le cache pour les données statiques
- ✅ Tester les performances régulièrement
### DON'T ❌
- ❌ Charger toutes les données en mémoire
- ❌ Utiliser `update="@all"` ou `update="@form"` systématiquement
- ❌ Oublier `process` sur les commandButton
- ❌ Dupliquer le code de composants
- ❌ Ignorer la validation côté client
- ❌ Recharger les données de référence à chaque fois
---
## 🚀 Prochaines Étapes
1. **Commencer par Phase 1** : Lazy Loading pour Factures
2. **Mesurer les performances** avant/après
3. **Itérer** sur les autres modules
4. **Documenter** les patterns réutilisables
5. **Former l'équipe** aux nouvelles pratiques
---
**Créé le** : 2025-12-29
**Auteur** : Équipe BTPXpress
**Version** : 1.0
### 2. Optimiser le Service Backend
**Ajouter des endpoints paginés dans BtpXpressApiClient** :
```java
@Path("/api/factures")
public interface BtpXpressApiClient {
@GET
@Path("/lazy")
@Produces(MediaType.APPLICATION_JSON)
Response getFacturesLazy(
@QueryParam("offset") int offset,
@QueryParam("limit") int limit,
@QueryParam("sortField") String sortField,
@QueryParam("sortOrder") String sortOrder,
@QueryParam("filter_numero") String filtreNumero,
@QueryParam("filter_client") String filtreClient,
@QueryParam("filter_statut") String filtreStatut
);
@GET
@Path("/count")
@Produces(MediaType.APPLICATION_JSON)
int countFactures(
@QueryParam("filter_numero") String filtreNumero,
@QueryParam("filter_client") String filtreClient,
@QueryParam("filter_statut") String filtreStatut
);
}
```
---
## ⚡ Performance Ajax & Partial Rendering
### 1. Optimiser les Updates Ajax
**Problème** : Updates trop larges qui re-rendent des composants inutilement
**Mauvaise pratique** ❌ :
```xml
<p:commandButton value="Filtrer"
update="@form"
action="#{factureView.applyFilters}"/>
```
**Bonne pratique** ✅ :
```xml
<p:commandButton value="Filtrer"
update="facturesTable messages"
process="@this filtresPanel"
action="#{factureView.applyFilters}"/>
```
### 2. Utiliser process et update de manière ciblée

63
pom.xml
View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.lions</groupId>
@@ -9,21 +9,22 @@
<description>Application cliente BTP Xpress basée sur Quarkus et PrimeFaces Freya</description>
<properties>
<compiler-plugin.version>3.13.0</compiler-plugin.version>
<maven.compiler.release>17</maven.compiler.release>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>3.15.1</quarkus.platform.version>
<quarkus.platform.version>3.27.3</quarkus.platform.version>
<skipTests>false</skipTests>
<freya.theme.version>5.0.0-jakarta</freya.theme.version>
</properties>
<repositories>
<repository>
<id>lions-maven-repo</id>
<name>Lions Dev Maven Repository</name>
<url>https://git.lions.dev/lionsdev/btpxpress-maven-repo/raw/branch/main</url>
<id>gitea-lionsdev</id>
<name>Lions Dev Gitea Maven</name>
<url>https://git.lions.dev/api/packages/lionsdev/maven</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>
@@ -43,38 +44,19 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<!-- ================================================================ -->
<!-- lions-faces-layout : layout Freya + beans OIDC + assets Freya -->
<!-- Remplace : primefaces-freya-extension (mort), freya-theme, freya -->
<!-- Fournit transitivement : primefaces, freya-theme-jakarta, -->
<!-- quarkus-primefaces, quarkus-omnifaces, quarkus-oidc -->
<!-- ================================================================ -->
<dependency>
<groupId>io.quarkiverse.primefaces</groupId>
<artifactId>quarkus-primefaces</artifactId>
<version>3.15.0-RC2</version>
</dependency>
<dependency>
<groupId>org.primefaces</groupId>
<artifactId>freya-theme</artifactId>
<version>${freya.theme.version}</version>
</dependency>
<dependency>
<groupId>org.primefaces</groupId>
<artifactId>freya</artifactId>
<version>${freya.theme.version}</version>
</dependency>
<dependency>
<groupId>jakarta.faces</groupId>
<artifactId>jakarta.faces-api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
</dependency>
<dependency>
<groupId>jakarta.el</groupId>
<artifactId>jakarta.el-api</artifactId>
<groupId>dev.lions</groupId>
<artifactId>lions-faces-layout</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
@@ -93,17 +75,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>

View File

@@ -46,6 +46,41 @@ public interface BtpXpressApiClient {
@Path("/chantiers/{id}")
Response getChantier(@PathParam("id") Long id);
/**
* Crée un nouveau chantier.
* Correspond à {@code ChantierResource.createChantier()} dans le serveur.
*
* @param chantierDTO Les données du chantier à créer.
* @return Réponse HTTP contenant le chantier créé.
*/
@POST
@Path("/chantiers")
Response createChantier(Object chantierDTO);
/**
* Met à jour un chantier existant.
* Correspond à {@code ChantierResource.updateChantier()} dans le serveur.
*
* @param id L'identifiant du chantier.
* @param chantierDTO Les nouvelles données du chantier.
* @return Réponse HTTP contenant le chantier mis à jour.
*/
@PUT
@Path("/chantiers/{id}")
Response updateChantier(@PathParam("id") String id, Object chantierDTO);
/**
* Supprime un chantier.
* Correspond à {@code ChantierResource.deleteChantier()} dans le serveur.
*
* @param id L'identifiant du chantier.
* @param permanent Si true, suppression définitive, sinon suppression logique.
* @return Réponse HTTP (204 No Content en cas de succès).
*/
@DELETE
@Path("/chantiers/{id}")
Response deleteChantier(@PathParam("id") String id, @QueryParam("permanent") @DefaultValue("false") boolean permanent);
/**
* Récupère la liste des clients.
* Correspond à {@code ClientResource.getAllClients()} dans le serveur.
@@ -269,5 +304,33 @@ public interface BtpXpressApiClient {
@GET
@Path("/stocks/{id}")
Response getStock(@PathParam("id") String id);
// === ENDPOINTS NOTIFICATIONS ===
/**
* Récupère les notifications non lues pour un utilisateur.
* Correspond à {@code NotificationResource.getAllNotifications()} avec filtre nonLues=true.
*
* @param userId ID de l'utilisateur (UUID en String).
* @return Réponse HTTP contenant la liste des notifications non lues.
*/
@GET
@Path("/notifications")
Response getNotifications(
@QueryParam("userId") String userId,
@QueryParam("nonLues") @DefaultValue("true") boolean nonLues);
// === ENDPOINTS MESSAGES ===
/**
* Récupère les messages non lus pour un utilisateur.
* Correspond à {@code MessageResource.getMessagesNonLus()} dans le serveur.
*
* @param userId ID de l'utilisateur (UUID en String).
* @return Réponse HTTP contenant la liste des messages non lus.
*/
@GET
@Path("/messages/non-lus/{userId}")
Response getMessagesNonLus(@PathParam("userId") String userId);
}

View File

@@ -67,6 +67,7 @@ public class ChantierService {
LOG.debug("Récupération du chantier avec ID : {}", id);
Response response = apiClient.getChantier(id);
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
@SuppressWarnings("unchecked")
Map<String, Object> chantier = response.readEntity(Map.class);
LOG.debug("Chantier récupéré avec succès.");
return chantier;
@@ -79,5 +80,98 @@ public class ChantierService {
return null;
}
}
/**
* Crée un nouveau chantier via l'API backend.
*
* @param chantierData Les données du chantier à créer (Map ou DTO).
* @return Le chantier créé sous forme de Map, ou null en cas d'erreur.
*/
public Map<String, Object> createChantier(Map<String, Object> chantierData) {
try {
LOG.debug("Création d'un nouveau chantier : {}", chantierData.get("nom"));
Response response = apiClient.createChantier(chantierData);
if (response.getStatus() == Response.Status.CREATED.getStatusCode() ||
response.getStatus() == Response.Status.OK.getStatusCode()) {
@SuppressWarnings("unchecked")
Map<String, Object> chantier = response.readEntity(Map.class);
LOG.info("Chantier créé avec succès : {}", chantier.get("id"));
return chantier;
} else {
String errorMessage = response.readEntity(String.class);
LOG.warn("Erreur lors de la création du chantier. Code HTTP : {}, Message : {}",
response.getStatus(), errorMessage);
return null;
}
} catch (Exception e) {
LOG.error("Erreur lors de la communication avec l'API backend pour créer le chantier : {}", e.getMessage(), e);
return null;
}
}
/**
* Met à jour un chantier existant via l'API backend.
*
* @param id L'identifiant du chantier (UUID en String).
* @param chantierData Les nouvelles données du chantier (Map ou DTO).
* @return Le chantier mis à jour sous forme de Map, ou null en cas d'erreur.
*/
public Map<String, Object> updateChantier(String id, Map<String, Object> chantierData) {
try {
LOG.debug("Mise à jour du chantier avec ID : {}", id);
Response response = apiClient.updateChantier(id, chantierData);
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
@SuppressWarnings("unchecked")
Map<String, Object> chantier = response.readEntity(Map.class);
LOG.info("Chantier mis à jour avec succès : {}", id);
return chantier;
} else {
String errorMessage = response.readEntity(String.class);
LOG.warn("Erreur lors de la mise à jour du chantier. Code HTTP : {}, Message : {}",
response.getStatus(), errorMessage);
return null;
}
} catch (Exception e) {
LOG.error("Erreur lors de la communication avec l'API backend pour mettre à jour le chantier : {}", e.getMessage(), e);
return null;
}
}
/**
* Supprime un chantier via l'API backend.
*
* @param id L'identifiant du chantier (UUID en String).
* @param permanent Si true, suppression définitive, sinon suppression logique (défaut: false).
* @return true si la suppression a réussi, false sinon.
*/
public boolean deleteChantier(String id, boolean permanent) {
try {
LOG.debug("Suppression du chantier avec ID : {} (permanent: {})", id, permanent);
Response response = apiClient.deleteChantier(id, permanent);
if (response.getStatus() == Response.Status.NO_CONTENT.getStatusCode() ||
response.getStatus() == Response.Status.OK.getStatusCode()) {
LOG.info("Chantier supprimé avec succès : {}", id);
return true;
} else {
String errorMessage = response.readEntity(String.class);
LOG.warn("Erreur lors de la suppression du chantier. Code HTTP : {}, Message : {}",
response.getStatus(), errorMessage);
return false;
}
} catch (Exception e) {
LOG.error("Erreur lors de la communication avec l'API backend pour supprimer le chantier : {}", e.getMessage(), e);
return false;
}
}
/**
* Supprime un chantier via l'API backend (suppression logique par défaut).
*
* @param id L'identifiant du chantier (UUID en String).
* @return true si la suppression a réussi, false sinon.
*/
public boolean deleteChantier(String id) {
return deleteChantier(id, false);
}
}

View File

@@ -67,6 +67,7 @@ public class ClientService {
LOG.debug("Récupération du client avec ID : {}", id);
Response response = apiClient.getClient(id);
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
@SuppressWarnings("unchecked")
Map<String, Object> client = response.readEntity(Map.class);
LOG.debug("Client récupéré avec succès.");
return client;

View File

@@ -270,16 +270,29 @@ public class DashboardService {
/**
* Récupère le nombre de devis en attente.
* Filtre côté client les devis avec statut EN_ATTENTE.
*
* @return Nombre de devis en attente ou 0 en cas d'erreur
*/
@SuppressWarnings("unchecked")
public int getNombreDevisEnAttente() {
try {
Response response = apiClient.getDevis();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
List<?> devis = (List<?>) response.getEntity();
// TODO: Filtrer par statut EN_ATTENTE si l'API le permet
return devis != null ? devis.size() : 0;
List<Map<String, Object>> devis = response.readEntity(List.class);
if (devis == null) {
return 0;
}
// Filtrer par statut EN_ATTENTE côté client
long count = devis.stream()
.filter(d -> {
Object statut = d.get("statut");
return statut != null &&
(statut.toString().equalsIgnoreCase("EN_ATTENTE") ||
statut.toString().equalsIgnoreCase("EN ATTENTE"));
})
.count();
return (int) count;
}
return 0;
} catch (Exception e) {
@@ -290,16 +303,33 @@ public class DashboardService {
/**
* Récupère le nombre de factures impayées.
* Filtre côté client les factures avec statut IMPAYEE ou EN_RETARD.
*
* @return Nombre de factures impayées ou 0 en cas d'erreur
*/
@SuppressWarnings("unchecked")
public int getNombreFacturesImpayees() {
try {
Response response = apiClient.getFactures();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
List<?> factures = (List<?>) response.getEntity();
// TODO: Filtrer par statut IMPAYEE si l'API le permet
return factures != null ? factures.size() : 0;
List<Map<String, Object>> factures = response.readEntity(List.class);
if (factures == null) {
return 0;
}
// Filtrer par statut IMPAYEE ou EN_RETARD côté client
long count = factures.stream()
.filter(f -> {
Object statut = f.get("statut");
if (statut == null) {
return false;
}
String statutStr = statut.toString().toUpperCase();
return statutStr.equals("IMPAYEE") ||
statutStr.equals("EN_RETARD") ||
statutStr.equals("EN RETARD");
})
.count();
return (int) count;
}
return 0;
} catch (Exception e) {

View File

@@ -0,0 +1,90 @@
package dev.lions.btpxpress.service;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Service de gestion des messages côté client.
* <p>
* Ce service encapsule la communication avec l'API backend pour les opérations
* liées aux messages.
* </p>
*
* @author BTP Xpress Development Team
* @version 1.0.0
* @since 1.0.0
*/
@ApplicationScoped
public class MessageService {
private static final Logger LOG = LoggerFactory.getLogger(MessageService.class);
@Inject
@RestClient
BtpXpressApiClient apiClient;
/**
* Récupère le nombre de messages non lus pour un utilisateur.
*
* @param userId L'identifiant de l'utilisateur (UUID en String).
* @return Le nombre de messages non lus, ou 0 en cas d'erreur.
*/
public int getUnreadCount(String userId) {
try {
LOG.debug("Récupération du nombre de messages non lus pour l'utilisateur : {}", userId);
Response response = apiClient.getMessagesNonLus(userId);
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> messages = response.readEntity(List.class);
int count = messages != null ? messages.size() : 0;
LOG.debug("Nombre de messages non lus : {}", count);
return count;
} else {
LOG.warn("Erreur lors de la récupération des messages non lus. Code HTTP : {}",
response.getStatus());
return 0;
}
} catch (Exception e) {
LOG.error("Erreur lors de la communication avec l'API backend pour récupérer les messages : {}",
e.getMessage(), e);
return 0;
}
}
/**
* Récupère tous les messages non lus pour un utilisateur.
*
* @param userId L'identifiant de l'utilisateur (UUID en String).
* @return Liste des messages non lus, ou liste vide en cas d'erreur.
*/
public List<Map<String, Object>> getUnreadMessages(String userId) {
try {
LOG.debug("Récupération des messages non lus pour l'utilisateur : {}", userId);
Response response = apiClient.getMessagesNonLus(userId);
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> messages = response.readEntity(List.class);
LOG.debug("Messages non lus récupérés : {} élément(s)",
messages != null ? messages.size() : 0);
return messages != null ? messages : new ArrayList<>();
} else {
LOG.warn("Erreur lors de la récupération des messages. Code HTTP : {}",
response.getStatus());
return new ArrayList<>();
}
} catch (Exception e) {
LOG.error("Erreur lors de la communication avec l'API backend pour récupérer les messages : {}",
e.getMessage(), e);
return new ArrayList<>();
}
}
}

View File

@@ -0,0 +1,90 @@
package dev.lions.btpxpress.service;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Service de gestion des notifications côté client.
* <p>
* Ce service encapsule la communication avec l'API backend pour les opérations
* liées aux notifications.
* </p>
*
* @author BTP Xpress Development Team
* @version 1.0.0
* @since 1.0.0
*/
@ApplicationScoped
public class NotificationService {
private static final Logger LOG = LoggerFactory.getLogger(NotificationService.class);
@Inject
@RestClient
BtpXpressApiClient apiClient;
/**
* Récupère le nombre de notifications non lues pour un utilisateur.
*
* @param userId L'identifiant de l'utilisateur (UUID en String).
* @return Le nombre de notifications non lues, ou 0 en cas d'erreur.
*/
public int getUnreadCount(String userId) {
try {
LOG.debug("Récupération du nombre de notifications non lues pour l'utilisateur : {}", userId);
Response response = apiClient.getNotifications(userId, true);
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> notifications = response.readEntity(List.class);
int count = notifications != null ? notifications.size() : 0;
LOG.debug("Nombre de notifications non lues : {}", count);
return count;
} else {
LOG.warn("Erreur lors de la récupération des notifications non lues. Code HTTP : {}",
response.getStatus());
return 0;
}
} catch (Exception e) {
LOG.error("Erreur lors de la communication avec l'API backend pour récupérer les notifications : {}",
e.getMessage(), e);
return 0;
}
}
/**
* Récupère toutes les notifications non lues pour un utilisateur.
*
* @param userId L'identifiant de l'utilisateur (UUID en String).
* @return Liste des notifications non lues, ou liste vide en cas d'erreur.
*/
public List<Map<String, Object>> getUnreadNotifications(String userId) {
try {
LOG.debug("Récupération des notifications non lues pour l'utilisateur : {}", userId);
Response response = apiClient.getNotifications(userId, true);
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> notifications = response.readEntity(List.class);
LOG.debug("Notifications non lues récupérées : {} élément(s)",
notifications != null ? notifications.size() : 0);
return notifications != null ? notifications : new ArrayList<>();
} else {
LOG.warn("Erreur lors de la récupération des notifications. Code HTTP : {}",
response.getStatus());
return new ArrayList<>();
}
} catch (Exception e) {
LOG.error("Erreur lors de la communication avec l'API backend pour récupérer les notifications : {}",
e.getMessage(), e);
return new ArrayList<>();
}
}
}

View File

@@ -10,10 +10,10 @@ import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
@@ -22,7 +22,7 @@ import java.util.function.Predicate;
@ViewScoped
@Getter
@Setter
public class ChantiersView extends BaseListView<ChantiersView.Chantier, Long> implements Serializable {
public class ChantiersView extends BaseListView<ChantiersView.Chantier, Long> {
private static final Logger LOG = LoggerFactory.getLogger(ChantiersView.class);
@@ -62,20 +62,39 @@ public class ChantiersView extends BaseListView<ChantiersView.Chantier, Long> im
Chantier c = new Chantier();
// Mapping des données de l'API vers l'objet Chantier
c.setId(data.get("id") != null ? Long.valueOf(data.get("id").toString().hashCode()) : null);
// Stocker l'UUID original comme String pour les opérations CRUD
Object idObj = data.get("id");
if (idObj != null) {
String idString = idObj.toString();
// Stocker l'UUID comme String dans un champ caché, et utiliser hashCode pour l'affichage
c.setId(Long.valueOf(idString.hashCode())); // Pour compatibilité avec l'interface existante
c.setUuidOriginal(idString); // Stocker l'UUID original
}
c.setNom((String) data.get("nom"));
// Le client peut être un objet ou une chaîne
Object clientObj = data.get("client");
if (clientObj instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> clientData = (Map<String, Object>) clientObj;
c.setClient((String) clientData.get("raisonSociale"));
// Extraire l'ID du client si disponible
Object clientIdObj = clientData.get("id");
if (clientIdObj != null) {
c.setClientId(clientIdObj.toString());
}
} else if (clientObj instanceof String) {
c.setClient((String) clientObj);
} else {
c.setClient("N/A");
}
// Vérifier aussi si clientId est directement dans les données
Object clientIdDirect = data.get("clientId");
if (clientIdDirect != null && c.getClientId() == null) {
c.setClientId(clientIdDirect.toString());
}
c.setAdresse((String) data.get("adresse"));
// Conversion des dates
@@ -164,8 +183,24 @@ public class ChantiersView extends BaseListView<ChantiersView.Chantier, Long> im
@Override
protected void performDelete() {
if (selectedItem == null || selectedItem.getId() == null) {
LOG.warn("Aucun chantier sélectionné pour la suppression");
return;
}
LOG.info("Suppression chantier : {}", selectedItem.getId());
// TODO: Appeler chantierService.delete(selectedItem.getId())
// Convertir l'ID Long en String UUID (le backend utilise UUID)
String idString = convertIdToString(selectedItem);
boolean success = chantierService.deleteChantier(idString, false);
if (success) {
LOG.info("Chantier supprimé avec succès : {}", idString);
// Recharger la liste après suppression
loadItems();
} else {
LOG.error("Échec de la suppression du chantier : {}", idString);
}
}
@Override
@@ -180,19 +215,49 @@ public class ChantiersView extends BaseListView<ChantiersView.Chantier, Long> im
@Override
protected void performCreate() {
entity.setId(System.currentTimeMillis()); // Simulation ID
entity.setDateCreation(LocalDateTime.now());
entity.setDateModification(LocalDateTime.now());
items.add(entity);
LOG.info("Nouveau chantier créé : {}", entity.getNom());
// TODO: Appeler chantierService.create(entity)
if (entity == null) {
LOG.warn("Aucune entité à créer");
return;
}
LOG.info("Création d'un nouveau chantier : {}", entity.getNom());
// Convertir l'entité Chantier en Map pour l'API
Map<String, Object> chantierData = convertChantierToMap(entity);
Map<String, Object> createdChantier = chantierService.createChantier(chantierData);
if (createdChantier != null) {
LOG.info("Chantier créé avec succès : {}", createdChantier.get("id"));
// Recharger la liste après création
loadItems();
} else {
LOG.error("Échec de la création du chantier : {}", entity.getNom());
}
}
@Override
protected void performUpdate() {
entity.setDateModification(LocalDateTime.now());
LOG.info("Chantier modifié : {}", entity.getNom());
// TODO: Appeler chantierService.update(entity)
if (entity == null || entity.getId() == null) {
LOG.warn("Aucune entité à mettre à jour ou ID manquant");
return;
}
LOG.info("Mise à jour du chantier : {} (ID: {})", entity.getNom(), entity.getId());
// Convertir l'ID Long en String UUID
String idString = convertIdToString(entity);
// Convertir l'entité Chantier en Map pour l'API
Map<String, Object> chantierData = convertChantierToMap(entity);
Map<String, Object> updatedChantier = chantierService.updateChantier(idString, chantierData);
if (updatedChantier != null) {
LOG.info("Chantier mis à jour avec succès : {}", idString);
// Recharger la liste après mise à jour
loadItems();
} else {
LOG.error("Échec de la mise à jour du chantier : {}", idString);
}
}
@Override
@@ -254,12 +319,71 @@ public class ChantiersView extends BaseListView<ChantiersView.Chantier, Long> im
return "/chantiers?faces-redirect=true";
}
/**
* Convertit un Chantier en Map pour l'API backend.
* Le backend attend un ChantierCreateDTO avec des champs spécifiques.
*/
private Map<String, Object> convertChantierToMap(Chantier chantier) {
Map<String, Object> map = new HashMap<>();
if (chantier.getNom() != null) {
map.put("nom", chantier.getNom());
}
if (chantier.getAdresse() != null) {
map.put("adresse", chantier.getAdresse());
}
if (chantier.getDateDebut() != null) {
map.put("dateDebut", chantier.getDateDebut().toString());
}
if (chantier.getDateFinPrevue() != null) {
map.put("dateFinPrevue", chantier.getDateFinPrevue().toString());
}
if (chantier.getStatut() != null) {
map.put("statut", chantier.getStatut());
}
if (chantier.getBudget() > 0) {
map.put("montantPrevu", chantier.getBudget());
}
if (chantier.getCoutReel() > 0) {
map.put("montantReel", chantier.getCoutReel());
}
// Ajouter le clientId si disponible
if (chantier.getClientId() != null && !chantier.getClientId().trim().isEmpty()) {
map.put("clientId", chantier.getClientId());
}
return map;
}
/**
* Convertit un ID Long en String UUID.
* Utilise l'UUID original stocké dans l'entité si disponible.
*/
private String convertIdToString(Chantier chantier) {
if (chantier == null) {
return null;
}
// Utiliser l'UUID original si disponible
if (chantier.getUuidOriginal() != null) {
return chantier.getUuidOriginal();
}
// Sinon, essayer de convertir depuis l'ID Long
// Note: Cette conversion n'est pas idéale, mais nécessaire pour compatibilité
if (chantier.getId() != null) {
return chantier.getId().toString();
}
return null;
}
@lombok.Getter
@lombok.Setter
public static class Chantier {
private Long id;
private Long id; // ID pour affichage (hashCode de l'UUID)
private String uuidOriginal; // UUID original depuis l'API (pour les opérations CRUD)
private String nom;
private String client;
private String client; // Raison sociale du client
private String clientId; // UUID du client (pour les opérations CRUD)
private String adresse;
private LocalDate dateDebut;
private LocalDate dateFinPrevue;

View File

@@ -10,7 +10,6 @@ import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@@ -21,7 +20,7 @@ import java.util.function.Predicate;
@ViewScoped
@Getter
@Setter
public class ClientsView extends BaseListView<ClientsView.Client, Long> implements Serializable {
public class ClientsView extends BaseListView<ClientsView.Client, Long> {
private static final Logger LOG = LoggerFactory.getLogger(ClientsView.class);

View File

@@ -10,8 +10,6 @@ import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
@@ -23,7 +21,7 @@ import java.util.function.Predicate;
@ViewScoped
@Getter
@Setter
public class DevisView extends BaseListView<DevisView.Devis, Long> implements Serializable {
public class DevisView extends BaseListView<DevisView.Devis, Long> {
private static final Logger LOG = LoggerFactory.getLogger(DevisView.class);
@@ -70,6 +68,7 @@ public class DevisView extends BaseListView<DevisView.Devis, Long> implements Se
// Le client peut être un objet ou une chaîne
Object clientObj = data.get("client");
if (clientObj instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> clientData = (Map<String, Object>) clientObj;
String entreprise = (String) clientData.get("entreprise");
String nom = (String) clientData.get("nom");

View File

@@ -10,7 +10,6 @@ import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
@@ -22,7 +21,7 @@ import java.util.function.Predicate;
@ViewScoped
@Getter
@Setter
public class EmployeView extends BaseListView<EmployeView.Employe, Long> implements Serializable {
public class EmployeView extends BaseListView<EmployeView.Employe, Long> {
private static final Logger LOG = LoggerFactory.getLogger(EmployeView.class);

View File

@@ -10,7 +10,6 @@ import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@@ -21,7 +20,7 @@ import java.util.function.Predicate;
@ViewScoped
@Getter
@Setter
public class EquipeView extends BaseListView<EquipeView.Equipe, Long> implements Serializable {
public class EquipeView extends BaseListView<EquipeView.Equipe, Long> {
private static final Logger LOG = LoggerFactory.getLogger(EquipeView.class);
@@ -66,6 +65,7 @@ public class EquipeView extends BaseListView<EquipeView.Equipe, Long> implements
// Chef d'équipe
Object chefObj = data.get("chef");
if (chefObj instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> chefData = (Map<String, Object>) chefObj;
String prenom = (String) chefData.get("prenom");
String nom = (String) chefData.get("nom");

View File

@@ -10,7 +10,6 @@ import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
@@ -22,7 +21,7 @@ import java.util.function.Predicate;
@ViewScoped
@Getter
@Setter
public class FactureView extends BaseListView<FactureView.Facture, Long> implements Serializable {
public class FactureView extends BaseListView<FactureView.Facture, Long> {
private static final Logger LOG = LoggerFactory.getLogger(FactureView.class);
@@ -69,6 +68,7 @@ public class FactureView extends BaseListView<FactureView.Facture, Long> impleme
// Le client peut être un objet ou une chaîne
Object clientObj = data.get("client");
if (clientObj instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> clientData = (Map<String, Object>) clientObj;
String entreprise = (String) clientData.get("entreprise");
String nom = (String) clientData.get("nom");

View File

@@ -1,94 +0,0 @@
package dev.lions.btpxpress.view;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.SessionScoped;
import jakarta.inject.Named;
import lombok.Getter;
import lombok.Setter;
import org.primefaces.PrimeFaces;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Named("guestPreferences")
@SessionScoped
@Getter
@Setter
public class GuestPreferences implements Serializable {
private static final long serialVersionUID = 1L;
private String menuMode = "layout-sidebar";
private String darkMode = "light";
private String componentTheme = "purple";
private String topbarTheme = "light";
private String menuTheme = "light";
private String inputStyle = "outlined";
private boolean lightLogo = false;
private List<ComponentTheme> componentThemes = new ArrayList<>();
@PostConstruct
public void init() {
componentThemes.add(new ComponentTheme("Bleu", "blue", "#2c84d8"));
componentThemes.add(new ComponentTheme("Vert", "green", "#34B56F"));
componentThemes.add(new ComponentTheme("Orange", "orange", "#FF810E"));
componentThemes.add(new ComponentTheme("Turquoise", "turquoise", "#58AED3"));
componentThemes.add(new ComponentTheme("Avocat", "avocado", "#AEC523"));
componentThemes.add(new ComponentTheme("Violet", "purple", "#464DF2"));
componentThemes.add(new ComponentTheme("Rouge", "red", "#FF9B7B"));
componentThemes.add(new ComponentTheme("Jaune", "yellow", "#FFB340"));
}
public void setDarkMode(String darkMode) {
this.darkMode = darkMode;
this.menuTheme = darkMode;
this.topbarTheme = darkMode;
this.lightLogo = !this.topbarTheme.equals("light");
}
public String getLayout() {
return "layout-" + this.darkMode;
}
public String getTheme() {
return this.componentTheme + '-' + this.darkMode;
}
public void setTopbarTheme(String topbarTheme) {
this.topbarTheme = topbarTheme;
this.lightLogo = !this.topbarTheme.equals("light");
}
public String getInputStyleClass() {
return this.inputStyle.equals("filled") ? "ui-input-filled" : "";
}
public void setComponentTheme(String componentTheme) {
this.componentTheme = componentTheme;
}
public void onMenuTypeChange() {
if ("layout-horizontal".equals(menuMode)) {
menuTheme = topbarTheme;
PrimeFaces.current().executeScript(
"PrimeFaces.FreyaConfigurator.changeSectionTheme('" + menuTheme + "' , 'layout-menu')"
);
}
}
@lombok.Getter
@lombok.Setter
public static class ComponentTheme implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String file;
private String color;
public ComponentTheme(String name, String file, String color) {
this.name = name;
this.file = file;
this.color = color;
}
}
}

View File

@@ -10,7 +10,6 @@ import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
@@ -22,7 +21,7 @@ import java.util.function.Predicate;
@ViewScoped
@Getter
@Setter
public class MaterielView extends BaseListView<MaterielView.Materiel, Long> implements Serializable {
public class MaterielView extends BaseListView<MaterielView.Materiel, Long> {
private static final Logger LOG = LoggerFactory.getLogger(MaterielView.class);

View File

@@ -10,7 +10,6 @@ import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@@ -21,7 +20,7 @@ import java.util.function.Predicate;
@ViewScoped
@Getter
@Setter
public class StockView extends BaseListView<StockView.Stock, Long> implements Serializable {
public class StockView extends BaseListView<StockView.Stock, Long> {
private static final Logger LOG = LoggerFactory.getLogger(StockView.class);

View File

@@ -1,204 +0,0 @@
package dev.lions.btpxpress.view;
import io.quarkus.oidc.IdToken;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.SessionScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.jwt.JsonWebToken;
import java.io.Serializable;
/**
* Bean de session pour gérer les informations de l'utilisateur connecté.
*
* <p>Ce bean stocke les informations de session de l'utilisateur authentifié,
* telles que le nom, l'email, l'avatar, et les statistiques rapides.</p>
*
* @author BTP Xpress Team
* @version 1.0
*/
@Named("userSession")
@SessionScoped
@Slf4j
public class UserSessionBean implements Serializable {
private static final long serialVersionUID = 1L;
@Inject
SecurityIdentity securityIdentity;
@Inject
@IdToken
JsonWebToken idToken;
/**
* Récupère le nom complet de l'utilisateur depuis le token OIDC.
* Méthode dynamique qui récupère les informations à chaque appel.
*/
public String getNomComplet() {
try {
if (securityIdentity != null && securityIdentity.getPrincipal() != null && idToken != null) {
// Nom complet (preferred_username ou name)
String nom = idToken.getClaim("name");
if (nom == null || nom.trim().isEmpty()) {
nom = idToken.getClaim("preferred_username");
}
if (nom == null || nom.trim().isEmpty()) {
nom = securityIdentity.getPrincipal().getName();
}
return nom != null ? nom : "Utilisateur";
}
} catch (Exception e) {
log.error("Erreur lors de la récupération du nom complet", e);
}
return "Utilisateur";
}
/**
* Récupère l'email de l'utilisateur depuis le token OIDC.
*/
public String getEmail() {
try {
if (securityIdentity != null && securityIdentity.getPrincipal() != null && idToken != null) {
String email = idToken.getClaim("email");
return email != null ? email : "utilisateur@btpxpress.com";
}
} catch (Exception e) {
log.error("Erreur lors de la récupération de l'email", e);
}
return "utilisateur@btpxpress.com";
}
/**
* Retourne l'URL de l'avatar (par défaut).
*/
public String getAvatarUrl() {
return "/resources/freya-layout/images/avatar-profilemenu.png";
}
/**
* Récupère le rôle de l'utilisateur depuis SecurityIdentity.
*/
public String getRole() {
try {
if (securityIdentity != null && securityIdentity.getRoles() != null && !securityIdentity.getRoles().isEmpty()) {
String role = securityIdentity.getRoles().iterator().next();
// Formatage du rôle pour affichage (enlever préfixes)
role = role.replace("_", " ").replace("-", " ");
return capitalizeWords(role);
}
} catch (Exception e) {
log.error("Erreur lors de la récupération du rôle", e);
}
return "Utilisateur";
}
/**
* Nombre de notifications non lues (TODO: implémenter via API).
*/
public int getNombreNotificationsNonLues() {
return 0;
}
/**
* Nombre de messages non lus (TODO: implémenter via API).
*/
public int getNombreMessagesNonLus() {
return 0;
}
/**
* Capitalize first letter of each word.
*/
private String capitalizeWords(String str) {
if (str == null || str.isEmpty()) {
return str;
}
String[] words = str.toLowerCase().split(" ");
StringBuilder result = new StringBuilder();
for (String word : words) {
if (!word.isEmpty()) {
result.append(Character.toUpperCase(word.charAt(0)))
.append(word.substring(1))
.append(" ");
}
}
return result.toString().trim();
}
/**
* Retourne les initiales de l'utilisateur pour l'avatar.
*
* @return Les initiales (ex: "JD" pour "Jean Dupont")
*/
public String getInitiales() {
String nomComplet = getNomComplet();
if (nomComplet == null || nomComplet.trim().isEmpty()) {
return "U";
}
String[] parts = nomComplet.trim().split("\\s+");
if (parts.length >= 2) {
return String.valueOf(parts[0].charAt(0)).toUpperCase() +
String.valueOf(parts[1].charAt(0)).toUpperCase();
} else if (parts.length == 1) {
return parts[0].substring(0, Math.min(2, parts[0].length())).toUpperCase();
}
return "U";
}
/**
* Action de déconnexion OIDC/Keycloak.
* Redirige vers l'endpoint de logout Keycloak pour détruire la session.
*
* @return Null pour déclencher une redirection externe
*/
public String deconnecter() {
try {
log.info("Déconnexion de l'utilisateur: {}", getNomComplet());
jakarta.faces.context.FacesContext facesContext = jakarta.faces.context.FacesContext.getCurrentInstance();
jakarta.faces.context.ExternalContext externalContext = facesContext.getExternalContext();
// Construction de l'URL de logout Keycloak
String keycloakLogoutUrl = "https://security.lions.dev/realms/btpxpress/protocol/openid-connect/logout";
// URL de redirection après logout
String baseUrl = externalContext.getRequestScheme() + "://" +
externalContext.getRequestServerName() + ":" +
externalContext.getRequestServerPort() +
externalContext.getRequestContextPath();
String postLogoutRedirectUri = baseUrl + "/";
// Construire l'URL complète avec les paramètres
StringBuilder logoutUrl = new StringBuilder(keycloakLogoutUrl);
logoutUrl.append("?post_logout_redirect_uri=").append(java.net.URLEncoder.encode(postLogoutRedirectUri, "UTF-8"));
// Ajouter le id_token_hint si disponible
if (idToken != null && idToken.getRawToken() != null) {
logoutUrl.append("&id_token_hint=").append(java.net.URLEncoder.encode(idToken.getRawToken(), "UTF-8"));
}
log.info("Redirection vers Keycloak logout: {}", keycloakLogoutUrl);
// Invalider la session HTTP locale
externalContext.invalidateSession();
// Rediriger vers Keycloak logout
externalContext.redirect(logoutUrl.toString());
facesContext.responseComplete();
return null;
} catch (Exception e) {
log.error("Erreur lors de la déconnexion", e);
return "/login?faces-redirect=true";
}
}
}

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<facelet-taglib xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-facelettaglibrary_2_3.xsd"
version="2.3">
<namespace>http://btpxpress.lions.dev/components</namespace>
<short-name>btpx</short-name>
<description>Composants réutilisables BTPXpress</description>
<!-- Composant Badge de Statut Facture -->
<tag>
<tag-name>facture-statut-badge</tag-name>
<description>Badge de statut pour les factures avec icône et couleur appropriées</description>
<source>components/facture-statut-badge.xhtml</source>
</tag>
<!-- Composant Affichage Montant -->
<tag>
<tag-name>montant-display</tag-name>
<description>Affichage formaté d'un montant avec devise et mise en évidence optionnelle</description>
<source>components/montant-display.xhtml</source>
</tag>
<!-- Composant Panel de Filtres -->
<tag>
<tag-name>search-filter-panel</tag-name>
<description>Panel de filtres de recherche réutilisable avec boutons d'action</description>
<source>components/search-filter-panel.xhtml</source>
</tag>
</facelet-taglib>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<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:cc="http://xmlns.jcp.org/jsf/composite"
xmlns:p="http://primefaces.org/ui">
<cc:interface>
<cc:attribute name="statut" required="true" type="java.lang.String"
shortDescription="Le statut de la facture (BROUILLON, EMISE, PAYEE, etc.)"/>
<cc:attribute name="enRetard" type="java.lang.Boolean" default="false"
shortDescription="Indique si la facture est en retard"/>
<cc:attribute name="styleClass" type="java.lang.String" default=""
shortDescription="Classes CSS additionnelles"/>
</cc:interface>
<cc:implementation>
<p:tag value="#{cc.attrs.statut}"
styleClass="#{cc.attrs.styleClass}"
severity="#{cc.attrs.statut == 'PAYEE' ? 'success' :
(cc.attrs.statut == 'ANNULEE' ? 'danger' :
(cc.attrs.enRetard ? 'danger' :
(cc.attrs.statut == 'BROUILLON' ? 'secondary' : 'warning')))}"
icon="#{cc.attrs.statut == 'PAYEE' ? 'pi pi-check-circle' :
(cc.attrs.statut == 'ANNULEE' ? 'pi pi-times-circle' :
(cc.attrs.enRetard ? 'pi pi-exclamation-triangle' :
(cc.attrs.statut == 'BROUILLON' ? 'pi pi-file-edit' : 'pi pi-clock')))}"/>
</cc:implementation>
</ui:composition>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<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:cc="http://xmlns.jcp.org/jsf/composite">
<cc:interface>
<cc:attribute name="montant" required="true" type="java.lang.Double"
shortDescription="Le montant à afficher"/>
<cc:attribute name="devise" default="Fcfa"
shortDescription="La devise (par défaut: Fcfa)"/>
<cc:attribute name="highlight" type="java.lang.Boolean" default="false"
shortDescription="Mettre en évidence le montant"/>
<cc:attribute name="highlightColor" default="red"
shortDescription="Couleur de mise en évidence"/>
<cc:attribute name="showIcon" type="java.lang.Boolean" default="false"
shortDescription="Afficher une icône de devise"/>
<cc:attribute name="styleClass" type="java.lang.String" default=""
shortDescription="Classes CSS additionnelles"/>
</cc:interface>
<cc:implementation>
<span class="#{cc.attrs.styleClass}"
style="#{cc.attrs.highlight ? 'color: ' + cc.attrs.highlightColor + '; font-weight: bold;' : ''}">
<i class="pi pi-money-bill mr-1"
rendered="#{cc.attrs.showIcon}"
style="#{cc.attrs.highlight ? 'color: ' + cc.attrs.highlightColor : ''}"></i>
<h:outputText value="#{cc.attrs.montant}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" #{cc.attrs.devise}"/>
</span>
</cc:implementation>
</ui:composition>

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<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:cc="http://xmlns.jcp.org/jsf/composite"
xmlns:p="http://primefaces.org/ui">
<cc:interface>
<cc:attribute name="bean" required="true"
shortDescription="Le bean de vue contenant les méthodes de filtrage"/>
<cc:attribute name="tableId" required="true"
shortDescription="L'ID de la table à mettre à jour"/>
<cc:attribute name="title" default="Filtres de recherche"
shortDescription="Titre du panel de filtres"/>
<cc:attribute name="collapsed" type="java.lang.Boolean" default="false"
shortDescription="Panel replié par défaut"/>
<cc:facet name="filters" required="true"/>
</cc:interface>
<cc:implementation>
<div class="card mb-3">
<p:panel id="filterPanel"
header="#{cc.attrs.title}"
toggleable="true"
collapsed="#{cc.attrs.collapsed}"
styleClass="filter-panel">
<f:facet name="icons">
<i class="pi pi-filter"></i>
</f:facet>
<div class="formgrid grid">
<cc:renderFacet name="filters"/>
</div>
<div class="flex gap-2 mt-3 justify-content-end">
<p:commandButton value="Rechercher"
icon="pi pi-search"
styleClass="ui-button-primary"
process="@this filterPanel"
update="#{cc.attrs.tableId} messages"
action="#{cc.attrs.bean.applyFilters}"/>
<p:commandButton value="Réinitialiser"
icon="pi pi-refresh"
styleClass="ui-button-secondary ui-button-outlined"
process="@this"
update="filterPanel #{cc.attrs.tableId} messages"
action="#{cc.attrs.bean.resetFilters}"/>
<p:commandButton value="Exporter"
icon="pi pi-download"
styleClass="ui-button-help ui-button-outlined"
rendered="#{cc.attrs.bean.exportEnabled}"
action="#{cc.attrs.bean.export}"/>
</div>
</p:panel>
</div>
</cc:implementation>
</ui:composition>

View File

@@ -32,7 +32,11 @@
<div class="layout-content">
<ui:insert name="content"/>
</div>
<!-- Footer conditionnel : désactivé par défaut pour application métier -->
<!-- Pour l'activer sur une page spécifique, ajouter : <ui:param name="showFooter" value="true"/> -->
<ui:fragment rendered="#{showFooter == true}">
<ui:include src="./footer.xhtml"/>
</ui:fragment>
</div>
<p:ajaxStatus style="width:32px;height:32px;position:fixed;right:7px;bottom:7px">

View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
lang="fr">
<h:head>
<f:facet name="first">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<link rel="icon" href="#{request.contextPath}/resources/freya-layout/images/favicon.ico" type="image/x-icon"/>
</f:facet>
<title>BTP Xpress - Plateforme de Gestion BTP</title>
<h:outputStylesheet name="css/primeicons.css" library="freya-layout" />
<h:outputStylesheet name="css/primeflex.min.css" library="freya-layout" />
<h:outputStylesheet name="css/layout-light.css" library="freya-layout" />
<h:outputStylesheet name="css/freya-purple-light.css" library="freya-layout" />
</h:head>
<h:body>
<div class="layout-wrapper">
<!-- Redirection vers login ou dashboard selon l'état de connexion -->
<div class="grid" style="min-height: 100vh; align-items: center; justify-content: center;">
<div class="col-12 md:col-8 lg:col-6">
<div class="card" style="text-align: center; padding: 3rem;">
<h1 style="color: var(--primary-color); margin-bottom: 1rem;">
<i class="pi pi-building" style="font-size: 3rem; margin-bottom: 1rem;"></i><br/>
BTP Xpress
</h1>
<h2 style="color: var(--text-color); margin-bottom: 2rem;">
Plateforme de Gestion BTP
</h2>
<p style="color: var(--text-color-secondary); margin-bottom: 2rem; line-height: 1.8;">
Gestion complète de vos chantiers, équipes, matériels et facturation.
Optimisez votre activité BTP avec une solution moderne et intuitive.
</p>
<div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;">
<p:commandButton value="Se connecter" icon="pi pi-sign-in"
action="login.xhtml?faces-redirect=true"
styleClass="ui-button-primary" style="min-width: 150px;"/>
<p:commandButton value="En savoir plus" icon="pi pi-info-circle"
action="aide.xhtml?faces-redirect=true"
styleClass="ui-button-secondary" style="min-width: 150px;"/>
</div>
</div>
</div>
</div>
<!-- Footer activé uniquement sur la page d'accueil publique -->
<ui:include src="WEB-INF/footer.xhtml"/>
</div>
</h:body>
</html>

View File

@@ -29,7 +29,7 @@ quarkus.http.host=0.0.0.0
# CORS Configuration pour production
# Frontend accessible depuis btpxpress.lions.dev
quarkus.http.cors=true
quarkus.http.cors.enabled=true
quarkus.http.cors.origins=https://btpxpress.lions.dev,https://www.btpxpress.lions.dev
quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS,PATCH
quarkus.http.cors.headers=Content-Type,Authorization,X-Requested-With,X-CSRF-Token
@@ -104,7 +104,7 @@ quarkus.log.category."dev.lions.btpxpress".level=INFO
quarkus.log.category."org.hibernate".level=WARN
quarkus.log.category."io.quarkus".level=INFO
quarkus.log.category."io.quarkus.oidc".level=WARN
quarkus.log.console.enable=true
quarkus.log.console.enabled=true
quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{2.}] (%t) %s%e%n
# Cache optimisé pour production

View File

@@ -15,9 +15,9 @@ jakarta.faces.VALIDATE_EMPTY_FIELDS=auto
quarkus.arc.remove-unused-beans=false
quarkus.http.port=8080
quarkus.http.cors=true
quarkus.http.cors.origins=http://localhost:8080,https://security.lions.dev
quarkus.http.port=8081
quarkus.http.cors.enabled=true
quarkus.http.cors.origins=http://localhost:8081,https://security.lions.dev
%dev.quarkus.oidc.enabled=true
%prod.quarkus.oidc.enabled=true
@@ -71,10 +71,10 @@ quarkus.log.level=INFO
quarkus.log.category."dev.lions.btpxpress".level=DEBUG
quarkus.log.category."io.quarkus.oidc".level=DEBUG
quarkus.log.category."io.quarkus.security".level=DEBUG
quarkus.log.console.enable=true
quarkus.log.console.enabled=true
quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n
btpxpress.api.base-url=http://localhost:8080
btpxpress.api.base-url=http://localhost:8081
btpxpress.api.timeout=30000
quarkus.rest-client."dev.lions.btpxpress.service.BtpXpressApiClient".url=${btpxpress.api.base-url}