chore(quarkus-327): bump to Quarkus 3.27.3 LTS, rename deprecated config keys
This commit is contained in:
116
.gitignore
vendored
Normal file
116
.gitignore
vendored
Normal 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
|
||||
263
EXECUTIVE_SUMMARY_OPTIMIZATIONS.md
Normal file
263
EXECUTIVE_SUMMARY_OPTIMIZATIONS.md
Normal 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
75
FOOTER_CONFIGURATION.md
Normal 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
|
||||
|
||||
281
IMPLEMENTATION_EXAMPLE_LAZY_LOADING.md
Normal file
281
IMPLEMENTATION_EXAMPLE_LAZY_LOADING.md
Normal 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.)
|
||||
|
||||
221
LAZY_LOADING_IMPLEMENTATION_SPEC.md
Normal file
221
LAZY_LOADING_IMPLEMENTATION_SPEC.md
Normal 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
|
||||
|
||||
776
PRIMEFACES_BEST_PRACTICES_OPTIMIZATION.md
Normal file
776
PRIMEFACES_BEST_PRACTICES_OPTIMIZATION.md
Normal 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
63
pom.xml
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -32,7 +32,11 @@
|
||||
<div class="layout-content">
|
||||
<ui:insert name="content"/>
|
||||
</div>
|
||||
<ui:include src="./footer.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>
|
||||
</div>
|
||||
|
||||
<p:ajaxStatus style="width:32px;height:32px;position:fixed;right:7px;bottom:7px">
|
||||
|
||||
58
src/main/resources/META-INF/resources/index.xhtml
Normal file
58
src/main/resources/META-INF/resources/index.xhtml
Normal 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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user