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

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

View File

@@ -1,55 +1,55 @@
# Docker ignore pour BTP Xpress Client
# Répertoires de build et target
target/
.mvn/
.quarkus/
# Fichiers IDE
.idea/
.vscode/
*.iml
*.ipr
*.iws
.settings/
.classpath
.project
# Documentation (non nécessaire dans l'image)
*.md
!README.md
# Git
.git/
.gitignore
.gitattributes
# Fichiers de configuration locale
.env
.env.local
*.log
# Tests
src/test/
# Fichiers temporaires
*.tmp
*.bak
*.swp
*~
# OS
.DS_Store
Thumbs.db
# Scripts de déploiement (non nécessaires dans l'image)
scripts/
*.sh
*.ps1
*.bat
# Kubernetes (géré séparément)
kubernetes/
*.yaml
*.yml
# Docker ignore pour BTP Xpress Client
# Répertoires de build et target
target/
.mvn/
.quarkus/
# Fichiers IDE
.idea/
.vscode/
*.iml
*.ipr
*.iws
.settings/
.classpath
.project
# Documentation (non nécessaire dans l'image)
*.md
!README.md
# Git
.git/
.gitignore
.gitattributes
# Fichiers de configuration locale
.env
.env.local
*.log
# Tests
src/test/
# Fichiers temporaires
*.tmp
*.bak
*.swp
*~
# OS
.DS_Store
Thumbs.db
# Scripts de déploiement (non nécessaires dans l'image)
scripts/
*.sh
*.ps1
*.bat
# Kubernetes (géré séparément)
kubernetes/
*.yaml
*.yml

116
.gitignore vendored Normal file
View File

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

View File

@@ -1,186 +1,186 @@
# Audit de Configuration - BTP Xpress Client ↔ Serveur
## ✅ Résumé de l'audit effectué
Date : 2025-11-01
Portée : Configuration complète du client PrimeFaces et mapping avec le serveur backend
---
## 1. Structure du Projet Client
### ✅ Structure des fichiers
- **XHTML** : `src/main/resources/META-INF/resources/` (structure Quarkus correcte)
- **Configuration** : `src/main/resources/META-INF/web.xml` et `application.properties`
- **Beans CDI** : `src/main/java/dev/lions/btpxpress/`
- **Services** : `src/main/java/dev/lions/btpxpress/service/`
### ✅ Fichiers créés/vérifiés
-`BtpXpressApiClient.java` - Interface REST Client pour communication backend
-`ChantierService.java` - Service encapsulant les appels API chantiers
-`application.properties` - Configuration complète OIDC + REST Client
-`pom.xml` - Dépendances OIDC et JWT ajoutées
---
## 2. Configuration OIDC / Keycloak
### ✅ Client (PrimeFaces)
```properties
quarkus.oidc.enabled=true
quarkus.oidc.auth-server-url=https://security.lions.dev/realms/btpxpress
quarkus.oidc.client-id=btpxpress-frontend
quarkus.oidc.application-type=web-app
quarkus.oidc.token.issuer=https://security.lions.dev/realms/btpxpress
```
### ✅ Serveur (Backend)
```properties
mp.jwt.verify.publickey.location=https://security.lions.dev/realms/btpxpress/protocol/openid-connect/certs
mp.jwt.verify.issuer=https://security.lions.dev/realms/btpxpress
quarkus.smallrye-jwt.enabled=true
```
### ✅ Vérifications
-**Même realm** : `btpxpress`
-**Même serveur Keycloak** : `https://security.lions.dev`
-**Client ID frontend** : `btpxpress-frontend` (doit exister dans Keycloak)
-**JWT Validation** : Backend valide les tokens via certificats publics
---
## 3. Communication Client ↔ Serveur
### ✅ Configuration REST Client
```properties
btpxpress.api.base-url=http://localhost:8080
quarkus.rest-client."dev.lions.btpxpress.service.BtpXpressApiClient".url=${btpxpress.api.base-url}
```
### ✅ Endpoints mappés
| Client (Interface) | Serveur (Resource) | Endpoint | Status |
|-------------------|-------------------|----------|--------|
| `BtpXpressApiClient.getChantiers()` | `ChantierResource.getAllChantiers()` | `GET /api/v1/chantiers` | ✅ Existe |
| `BtpXpressApiClient.getChantier(id)` | `ChantierResource.getChantierById()` | `GET /api/v1/chantiers/{id}` | ✅ Existe |
| `BtpXpressApiClient.getClients()` | `ClientResource.getAllClients()` | `GET /api/v1/clients` | ✅ Existe |
| `BtpXpressApiClient.getClient(id)` | `ClientResource.getClientById()` | `GET /api/v1/clients/{id}` | ✅ Existe |
### ✅ CORS Configuration
**Serveur** (`application.properties`) :
```properties
quarkus.http.cors.origins=${CORS_ORIGINS:http://localhost:3000,http://localhost:5173,http://localhost:8081}
```
**Port 8081 ajouté** aux origines autorisées
**Client** :
```properties
quarkus.http.cors.origins=http://localhost:8080,https://security.lions.dev
```
---
## 4. Ports et URLs
| Service | Port | URL | Description |
|---------|------|-----|-------------|
| Backend | 8080 | http://localhost:8080 | API REST backend |
| Client | 8081 | http://localhost:8081 | Application PrimeFaces |
| Keycloak | - | https://security.lions.dev | Authentification OIDC |
---
## 5. Dépendances Maven
### ✅ Client (`pom.xml`)
-`quarkus-oidc` - Authentification OIDC
-`quarkus-smallrye-jwt` - Support JWT
-`quarkus-rest-client` - REST Client
-`quarkus-rest-jackson` - Sérialisation JSON
-`quarkus-primefaces` - PrimeFaces integration
-`freya-theme` - Thème PrimeFaces Freya
### ✅ Serveur (vérifié)
-`quarkus-smallrye-jwt` - Validation JWT
- ✅ CORS activé avec origine `http://localhost:8081`
---
## 6. Flux d'Authentification
```
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Client │ │ Keycloak │ │ Backend │
│ Port 8081 │ │security.lions│ │ Port 8080 │
└─────────────┘ └──────────────┘ └─────────────┘
│ │ │
│ 1. Accès page protégée │
│────────────────────────────────────────────────►│
│ │ │
│ │ 2. Redirection OIDC │
│◄────────────────────────────────────────────────│
│ │ │
│ 3. Redirect Keycloak │ │
│────────────────────────►│ │
│ │ │
│ 4. Authentification │ │
│ │ │
│ 5. Token JWT │ │
│◄────────────────────────│ │
│ │ │
│ 6. Appel API + Token │ │
│────────────────────────────────────────────────►│
│ │ │
│ │ 7. Validation token │
│ │◄───────────────────────│
│ │ │
│ 8. Réponse API │ │
│◄────────────────────────────────────────────────│
```
---
## 7. Points de Vérification Requis
### ⚠️ À vérifier dans Keycloak
1. **Client `btpxpress-frontend` existe** dans le realm `btpxpress`
2. **Redirect URIs** incluent `http://localhost:8081/*`
3. **Web Origins** incluent `http://localhost:8081`
4. **Client Secret** configuré si nécessaire (pour confidential client)
### ⚠️ À tester
1. **Authentification OIDC** : Vérifier la redirection vers Keycloak
2. **Token JWT** : Vérifier l'envoi automatique dans les requêtes REST
3. **CORS** : Vérifier que les requêtes depuis 8081 vers 8080 fonctionnent
4. **Endpoints API** : Tester les appels `GET /api/v1/chantiers` et `/api/v1/clients`
---
## 8. Configuration Complète Validée
| Composant | Configuration | Status |
|-----------|--------------|--------|
| Structure fichiers | Quarkus standard | ✅ |
| OIDC Client | `btpxpress-frontend` | ✅ |
| OIDC Server | `security.lions.dev` | ✅ |
| REST Client | `BtpXpressApiClient` | ✅ |
| Services | `ChantierService` | ✅ |
| CORS Backend | Port 8081 autorisé | ✅ |
| CORS Client | Port 8080 autorisé | ✅ |
| Endpoints mappés | Tous vérifiés | ✅ |
| Dépendances | Toutes présentes | ✅ |
---
## 🎯 Conclusion
**✅ La configuration est complète et correcte** :
- Le client PrimeFaces est correctement configuré pour communiquer avec le backend
- L'authentification OIDC est configurée avec Keycloak sur `security.lions.dev`
- Les endpoints REST sont mappés correctement
- Le CORS est configuré pour autoriser la communication bidirectionnelle
- Toutes les dépendances nécessaires sont présentes
**⚠️ Action requise** : Vérifier dans Keycloak que le client `btpxpress-frontend` existe et est correctement configuré avec les redirect URIs appropriés.
# Audit de Configuration - BTP Xpress Client ↔ Serveur
## ✅ Résumé de l'audit effectué
Date : 2025-11-01
Portée : Configuration complète du client PrimeFaces et mapping avec le serveur backend
---
## 1. Structure du Projet Client
### ✅ Structure des fichiers
- **XHTML** : `src/main/resources/META-INF/resources/` (structure Quarkus correcte)
- **Configuration** : `src/main/resources/META-INF/web.xml` et `application.properties`
- **Beans CDI** : `src/main/java/dev/lions/btpxpress/`
- **Services** : `src/main/java/dev/lions/btpxpress/service/`
### ✅ Fichiers créés/vérifiés
-`BtpXpressApiClient.java` - Interface REST Client pour communication backend
-`ChantierService.java` - Service encapsulant les appels API chantiers
-`application.properties` - Configuration complète OIDC + REST Client
-`pom.xml` - Dépendances OIDC et JWT ajoutées
---
## 2. Configuration OIDC / Keycloak
### ✅ Client (PrimeFaces)
```properties
quarkus.oidc.enabled=true
quarkus.oidc.auth-server-url=https://security.lions.dev/realms/btpxpress
quarkus.oidc.client-id=btpxpress-frontend
quarkus.oidc.application-type=web-app
quarkus.oidc.token.issuer=https://security.lions.dev/realms/btpxpress
```
### ✅ Serveur (Backend)
```properties
mp.jwt.verify.publickey.location=https://security.lions.dev/realms/btpxpress/protocol/openid-connect/certs
mp.jwt.verify.issuer=https://security.lions.dev/realms/btpxpress
quarkus.smallrye-jwt.enabled=true
```
### ✅ Vérifications
-**Même realm** : `btpxpress`
-**Même serveur Keycloak** : `https://security.lions.dev`
-**Client ID frontend** : `btpxpress-frontend` (doit exister dans Keycloak)
-**JWT Validation** : Backend valide les tokens via certificats publics
---
## 3. Communication Client ↔ Serveur
### ✅ Configuration REST Client
```properties
btpxpress.api.base-url=http://localhost:8080
quarkus.rest-client."dev.lions.btpxpress.service.BtpXpressApiClient".url=${btpxpress.api.base-url}
```
### ✅ Endpoints mappés
| Client (Interface) | Serveur (Resource) | Endpoint | Status |
|-------------------|-------------------|----------|--------|
| `BtpXpressApiClient.getChantiers()` | `ChantierResource.getAllChantiers()` | `GET /api/v1/chantiers` | ✅ Existe |
| `BtpXpressApiClient.getChantier(id)` | `ChantierResource.getChantierById()` | `GET /api/v1/chantiers/{id}` | ✅ Existe |
| `BtpXpressApiClient.getClients()` | `ClientResource.getAllClients()` | `GET /api/v1/clients` | ✅ Existe |
| `BtpXpressApiClient.getClient(id)` | `ClientResource.getClientById()` | `GET /api/v1/clients/{id}` | ✅ Existe |
### ✅ CORS Configuration
**Serveur** (`application.properties`) :
```properties
quarkus.http.cors.origins=${CORS_ORIGINS:http://localhost:3000,http://localhost:5173,http://localhost:8081}
```
**Port 8081 ajouté** aux origines autorisées
**Client** :
```properties
quarkus.http.cors.origins=http://localhost:8080,https://security.lions.dev
```
---
## 4. Ports et URLs
| Service | Port | URL | Description |
|---------|------|-----|-------------|
| Backend | 8080 | http://localhost:8080 | API REST backend |
| Client | 8081 | http://localhost:8081 | Application PrimeFaces |
| Keycloak | - | https://security.lions.dev | Authentification OIDC |
---
## 5. Dépendances Maven
### ✅ Client (`pom.xml`)
-`quarkus-oidc` - Authentification OIDC
-`quarkus-smallrye-jwt` - Support JWT
-`quarkus-rest-client` - REST Client
-`quarkus-rest-jackson` - Sérialisation JSON
-`quarkus-primefaces` - PrimeFaces integration
-`freya-theme` - Thème PrimeFaces Freya
### ✅ Serveur (vérifié)
-`quarkus-smallrye-jwt` - Validation JWT
- ✅ CORS activé avec origine `http://localhost:8081`
---
## 6. Flux d'Authentification
```
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Client │ │ Keycloak │ │ Backend │
│ Port 8081 │ │security.lions│ │ Port 8080 │
└─────────────┘ └──────────────┘ └─────────────┘
│ │ │
│ 1. Accès page protégée │
│────────────────────────────────────────────────►│
│ │ │
│ │ 2. Redirection OIDC │
│◄────────────────────────────────────────────────│
│ │ │
│ 3. Redirect Keycloak │ │
│────────────────────────►│ │
│ │ │
│ 4. Authentification │ │
│ │ │
│ 5. Token JWT │ │
│◄────────────────────────│ │
│ │ │
│ 6. Appel API + Token │ │
│────────────────────────────────────────────────►│
│ │ │
│ │ 7. Validation token │
│ │◄───────────────────────│
│ │ │
│ 8. Réponse API │ │
│◄────────────────────────────────────────────────│
```
---
## 7. Points de Vérification Requis
### ⚠️ À vérifier dans Keycloak
1. **Client `btpxpress-frontend` existe** dans le realm `btpxpress`
2. **Redirect URIs** incluent `http://localhost:8081/*`
3. **Web Origins** incluent `http://localhost:8081`
4. **Client Secret** configuré si nécessaire (pour confidential client)
### ⚠️ À tester
1. **Authentification OIDC** : Vérifier la redirection vers Keycloak
2. **Token JWT** : Vérifier l'envoi automatique dans les requêtes REST
3. **CORS** : Vérifier que les requêtes depuis 8081 vers 8080 fonctionnent
4. **Endpoints API** : Tester les appels `GET /api/v1/chantiers` et `/api/v1/clients`
---
## 8. Configuration Complète Validée
| Composant | Configuration | Status |
|-----------|--------------|--------|
| Structure fichiers | Quarkus standard | ✅ |
| OIDC Client | `btpxpress-frontend` | ✅ |
| OIDC Server | `security.lions.dev` | ✅ |
| REST Client | `BtpXpressApiClient` | ✅ |
| Services | `ChantierService` | ✅ |
| CORS Backend | Port 8081 autorisé | ✅ |
| CORS Client | Port 8080 autorisé | ✅ |
| Endpoints mappés | Tous vérifiés | ✅ |
| Dépendances | Toutes présentes | ✅ |
---
## 🎯 Conclusion
**✅ La configuration est complète et correcte** :
- Le client PrimeFaces est correctement configuré pour communiquer avec le backend
- L'authentification OIDC est configurée avec Keycloak sur `security.lions.dev`
- Les endpoints REST sont mappés correctement
- Le CORS est configuré pour autoriser la communication bidirectionnelle
- Toutes les dépendances nécessaires sont présentes
**⚠️ Action requise** : Vérifier dans Keycloak que le client `btpxpress-frontend` existe et est correctement configuré avec les redirect URIs appropriés.

View File

@@ -1,75 +1,75 @@
# Configuration BTP Xpress Client - PrimeFaces Freya
## ✅ Vérifications effectuées
### 1. Structure du projet
- ✅ Fichiers XHTML dans `src/main/resources/META-INF/resources/`
- ✅ Configuration dans `src/main/resources/META-INF/web.xml`
- ✅ Beans CDI dans `src/main/java/dev/lions/btpxpress/`
### 2. Configuration OIDC / Keycloak
-**Serveur Keycloak** : `https://security.lions.dev/realms/btpxpress`
-**Client ID** : `btpxpress-frontend`
-**Type d'application** : `web-app`
-**Redirection** : `/` (restauration du chemin après authentification)
-**Cookies** : Configurés pour la session
-**TLS** : `required` (production)
### 3. Communication avec le backend
-**URL Backend** : `http://localhost:8080`
-**Endpoints API** : `/api/v1/*`
-**REST Client** : `BtpXpressApiClient` configuré
-**Service** : `ChantierService` créé pour encapsuler les appels API
-**CORS Backend** : `http://localhost:8081` ajouté aux origines autorisées
### 4. Configuration serveur backend
-**Port** : `8080`
-**CORS Origins** : `http://localhost:3000,http://localhost:5173,http://localhost:8081`
-**JWT Validation** : `https://security.lions.dev/realms/btpxpress/protocol/openid-connect/certs`
-**Issuer** : `https://security.lions.dev/realms/btpxpress`
## 📋 Mapping Client ↔ Serveur
| Client (PrimeFaces) | Serveur (Quarkus) | Description |
|---------------------|-------------------|-------------|
| `http://localhost:8081` | `http://localhost:8080` | Communication HTTP |
| `BtpXpressApiClient` | `@Path("/api/v1/*")` | Interface REST Client |
| OIDC Client `btpxpress-frontend` | JWT Validation | Authentification |
| `ChantierService` | `ChantierResource` | Service métier chantiers |
## 🔐 Authentification
1. **Client accède à une page protégée** → Redirection vers Keycloak
2. **Keycloak (security.lions.dev)** → Authentification utilisateur
3. **Keycloak retourne le token** → Stocké dans la session du client
4. **Client fait appel API** → Token JWT envoyé dans header `Authorization`
5. **Backend valide le token** → Via les certificats Keycloak publics
## 🚀 Démarrage
1. **Backend** :
```bash
cd btpxpress-server
mvn quarkus:dev
```
→ Accessible sur http://localhost:8080
2. **Client** :
```bash
cd btpxpress-client
mvn quarkus:dev
```
→ Accessible sur http://localhost:8081
3. **Accès** :
- Page d'accueil : http://localhost:8081/
- Dashboard : http://localhost:8081/dashboard.xhtml
- Login : http://localhost:8081/login.xhtml
## ⚠️ Points d'attention
- Le client doit être configuré avec le **même realm Keycloak** que le serveur (`btpxpress`)
- Le client ID `btpxpress-frontend` doit exister dans Keycloak
- Les tokens JWT doivent être envoyés automatiquement via le REST Client
- Le backend doit accepter les requêtes CORS depuis `http://localhost:8081`
# Configuration BTP Xpress Client - PrimeFaces Freya
## ✅ Vérifications effectuées
### 1. Structure du projet
- ✅ Fichiers XHTML dans `src/main/resources/META-INF/resources/`
- ✅ Configuration dans `src/main/resources/META-INF/web.xml`
- ✅ Beans CDI dans `src/main/java/dev/lions/btpxpress/`
### 2. Configuration OIDC / Keycloak
-**Serveur Keycloak** : `https://security.lions.dev/realms/btpxpress`
-**Client ID** : `btpxpress-frontend`
-**Type d'application** : `web-app`
-**Redirection** : `/` (restauration du chemin après authentification)
-**Cookies** : Configurés pour la session
-**TLS** : `required` (production)
### 3. Communication avec le backend
-**URL Backend** : `http://localhost:8080`
-**Endpoints API** : `/api/v1/*`
-**REST Client** : `BtpXpressApiClient` configuré
-**Service** : `ChantierService` créé pour encapsuler les appels API
-**CORS Backend** : `http://localhost:8081` ajouté aux origines autorisées
### 4. Configuration serveur backend
-**Port** : `8080`
-**CORS Origins** : `http://localhost:3000,http://localhost:5173,http://localhost:8081`
-**JWT Validation** : `https://security.lions.dev/realms/btpxpress/protocol/openid-connect/certs`
-**Issuer** : `https://security.lions.dev/realms/btpxpress`
## 📋 Mapping Client ↔ Serveur
| Client (PrimeFaces) | Serveur (Quarkus) | Description |
|---------------------|-------------------|-------------|
| `http://localhost:8081` | `http://localhost:8080` | Communication HTTP |
| `BtpXpressApiClient` | `@Path("/api/v1/*")` | Interface REST Client |
| OIDC Client `btpxpress-frontend` | JWT Validation | Authentification |
| `ChantierService` | `ChantierResource` | Service métier chantiers |
## 🔐 Authentification
1. **Client accède à une page protégée** → Redirection vers Keycloak
2. **Keycloak (security.lions.dev)** → Authentification utilisateur
3. **Keycloak retourne le token** → Stocké dans la session du client
4. **Client fait appel API** → Token JWT envoyé dans header `Authorization`
5. **Backend valide le token** → Via les certificats Keycloak publics
## 🚀 Démarrage
1. **Backend** :
```bash
cd btpxpress-server
mvn quarkus:dev
```
→ Accessible sur http://localhost:8080
2. **Client** :
```bash
cd btpxpress-client
mvn quarkus:dev
```
→ Accessible sur http://localhost:8081
3. **Accès** :
- Page d'accueil : http://localhost:8081/
- Dashboard : http://localhost:8081/dashboard.xhtml
- Login : http://localhost:8081/login.xhtml
## ⚠️ Points d'attention
- Le client doit être configuré avec le **même realm Keycloak** que le serveur (`btpxpress`)
- Le client ID `btpxpress-frontend` doit exister dans Keycloak
- Les tokens JWT doivent être envoyés automatiquement via le REST Client
- Le backend doit accepter les requêtes CORS depuis `http://localhost:8081`

View File

@@ -1,47 +1,47 @@
####
# Dockerfile pour BTP Xpress Client (Frontend) - Développement
# Utilisé pour les builds de développement local
####
## Stage 1 : Build avec Maven
FROM maven:3.9.6-eclipse-temurin-17 AS build
WORKDIR /build
# Copier pom.xml et télécharger les dépendances
COPY pom.xml .
RUN mvn dependency:go-offline -B
# Copier le code source
COPY src ./src
# Build de l'application (uber-jar pour compatibilité lionsctl)
RUN mvn clean package -DskipTests -B -Dquarkus.package.type=uber-jar
## Stage 2 : Runtime image
FROM eclipse-temurin:17-jre-alpine
ENV LANGUAGE='fr_FR:fr'
# Installer curl pour les health checks
RUN apk add --no-cache curl
# Créer un utilisateur non-root pour la sécurité
RUN addgroup -g 185 -S appuser && adduser -u 185 -S appuser -G appuser
RUN mkdir -p /deployments && chown -R appuser:appuser /deployments
# Copier le JAR depuis le build (lionsctl utilise uber-jar)
# Note: Le fichier sera btpxpress-client-1.0.0-runner.jar
COPY --from=build --chown=appuser:appuser /build/target/*-runner.jar /deployments/app.jar
EXPOSE 8080
USER appuser
# Variables d'environnement JVM optimisées
ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager -XX:+UseG1GC -XX:MaxRAMPercentage=75.0 -XX:+UseStringDeduplication"
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/q/health/ready || exit 1
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /deployments/app.jar"]
####
# Dockerfile pour BTP Xpress Client (Frontend) - Développement
# Utilisé pour les builds de développement local
####
## Stage 1 : Build avec Maven
FROM maven:3.9.6-eclipse-temurin-17 AS build
WORKDIR /build
# Copier pom.xml et télécharger les dépendances
COPY pom.xml .
RUN mvn dependency:go-offline -B
# Copier le code source
COPY src ./src
# Build de l'application (uber-jar pour compatibilité lionsctl)
RUN mvn clean package -DskipTests -B -Dquarkus.package.type=uber-jar
## Stage 2 : Runtime image
FROM eclipse-temurin:17-jre-alpine
ENV LANGUAGE='fr_FR:fr'
# Installer curl pour les health checks
RUN apk add --no-cache curl
# Créer un utilisateur non-root pour la sécurité
RUN addgroup -g 185 -S appuser && adduser -u 185 -S appuser -G appuser
RUN mkdir -p /deployments && chown -R appuser:appuser /deployments
# Copier le JAR depuis le build (lionsctl utilise uber-jar)
# Note: Le fichier sera btpxpress-client-1.0.0-runner.jar
COPY --from=build --chown=appuser:appuser /build/target/*-runner.jar /deployments/app.jar
EXPOSE 8080
USER appuser
# Variables d'environnement JVM optimisées
ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager -XX:+UseG1GC -XX:MaxRAMPercentage=75.0 -XX:+UseStringDeduplication"
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/q/health/ready || exit 1
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /deployments/app.jar"]

View File

@@ -1,84 +1,84 @@
####
# Dockerfile de production pour BTP Xpress Client (Frontend)
# Multi-stage build optimisé avec sécurité renforcée
####
## Stage 1 : Build avec Maven
FROM maven:3.9.6-eclipse-temurin-17 AS builder
WORKDIR /app
# Copier pom.xml et télécharger les dépendances (cache Docker)
COPY pom.xml .
RUN mvn dependency:go-offline -B
# Copier le code source
COPY src ./src
# Build de l'application avec profil production (fast-jar par défaut)
RUN mvn clean package -DskipTests -B
## Stage 2 : Image de production optimisée et sécurisée
FROM registry.access.redhat.com/ubi8/openjdk-17:1.18
ENV LANGUAGE='fr_FR:fr'
# Variables d'environnement de production
# Ces valeurs peuvent être surchargées via docker-compose ou Kubernetes
ENV QUARKUS_PROFILE=prod
ENV QUARKUS_HTTP_PORT=8080
ENV QUARKUS_HTTP_HOST=0.0.0.0
# Configuration Keycloak/OIDC (production)
ENV QUARKUS_OIDC_AUTH_SERVER_URL=https://security.lions.dev/realms/btpxpress
ENV QUARKUS_OIDC_CLIENT_ID=btpxpress-frontend
ENV QUARKUS_OIDC_ENABLED=true
ENV QUARKUS_OIDC_TLS_VERIFICATION=required
# Configuration API Backend
ENV BTPXPRESS_API_BASE_URL=https://api.btpxpress.lions.dev
# Configuration CORS
ENV QUARKUS_HTTP_CORS_ORIGINS=https://btpxpress.lions.dev,https://www.btpxpress.lions.dev
ENV QUARKUS_HTTP_CORS_ALLOW_CREDENTIALS=true
# Installer curl pour les health checks
USER root
RUN microdnf install -y curl && \
microdnf clean all && \
rm -rf /var/cache/yum
# Créer les répertoires et permissions pour utilisateur non-root
RUN mkdir -p /deployments /app/logs && \
chown -R 185:185 /deployments /app/logs
# Passer à l'utilisateur non-root pour la sécurité
USER 185
# Copier l'application depuis le builder (format fast-jar Quarkus)
COPY --from=builder --chown=185 /app/target/quarkus-app/ /deployments/
# Exposer le port
EXPOSE 8080
# Variables JVM optimisées pour production avec sécurité
ENV JAVA_OPTS="-Xmx768m -Xms256m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+UseStringDeduplication \
-XX:+ParallelRefProcEnabled \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/app/logs/heapdump.hprof \
-Djava.security.egd=file:/dev/./urandom \
-Djava.awt.headless=true \
-Dfile.encoding=UTF-8 \
-Djava.util.logging.manager=org.jboss.logmanager.LogManager \
-Dquarkus.profile=${QUARKUS_PROFILE}"
# Health check avec endpoints Quarkus
HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \
CMD curl -f http://localhost:8080/q/health/ready || exit 1
# Point d'entrée avec profil production (format fast-jar)
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /deployments/quarkus-run.jar"]
####
# Dockerfile de production pour BTP Xpress Client (Frontend)
# Multi-stage build optimisé avec sécurité renforcée
####
## Stage 1 : Build avec Maven
FROM maven:3.9.6-eclipse-temurin-17 AS builder
WORKDIR /app
# Copier pom.xml et télécharger les dépendances (cache Docker)
COPY pom.xml .
RUN mvn dependency:go-offline -B
# Copier le code source
COPY src ./src
# Build de l'application avec profil production (fast-jar par défaut)
RUN mvn clean package -DskipTests -B
## Stage 2 : Image de production optimisée et sécurisée
FROM registry.access.redhat.com/ubi8/openjdk-17:1.18
ENV LANGUAGE='fr_FR:fr'
# Variables d'environnement de production
# Ces valeurs peuvent être surchargées via docker-compose ou Kubernetes
ENV QUARKUS_PROFILE=prod
ENV QUARKUS_HTTP_PORT=8080
ENV QUARKUS_HTTP_HOST=0.0.0.0
# Configuration Keycloak/OIDC (production)
ENV QUARKUS_OIDC_AUTH_SERVER_URL=https://security.lions.dev/realms/btpxpress
ENV QUARKUS_OIDC_CLIENT_ID=btpxpress-frontend
ENV QUARKUS_OIDC_ENABLED=true
ENV QUARKUS_OIDC_TLS_VERIFICATION=required
# Configuration API Backend
ENV BTPXPRESS_API_BASE_URL=https://api.btpxpress.lions.dev
# Configuration CORS
ENV QUARKUS_HTTP_CORS_ORIGINS=https://btpxpress.lions.dev,https://www.btpxpress.lions.dev
ENV QUARKUS_HTTP_CORS_ALLOW_CREDENTIALS=true
# Installer curl pour les health checks
USER root
RUN microdnf install -y curl && \
microdnf clean all && \
rm -rf /var/cache/yum
# Créer les répertoires et permissions pour utilisateur non-root
RUN mkdir -p /deployments /app/logs && \
chown -R 185:185 /deployments /app/logs
# Passer à l'utilisateur non-root pour la sécurité
USER 185
# Copier l'application depuis le builder (format fast-jar Quarkus)
COPY --from=builder --chown=185 /app/target/quarkus-app/ /deployments/
# Exposer le port
EXPOSE 8080
# Variables JVM optimisées pour production avec sécurité
ENV JAVA_OPTS="-Xmx768m -Xms256m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+UseStringDeduplication \
-XX:+ParallelRefProcEnabled \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/app/logs/heapdump.hprof \
-Djava.security.egd=file:/dev/./urandom \
-Djava.awt.headless=true \
-Dfile.encoding=UTF-8 \
-Djava.util.logging.manager=org.jboss.logmanager.LogManager \
-Dquarkus.profile=${QUARKUS_PROFILE}"
# Health check avec endpoints Quarkus
HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \
CMD curl -f http://localhost:8080/q/health/ready || exit 1
# Point d'entrée avec profil production (format fast-jar)
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /deployments/quarkus-run.jar"]

View File

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

View File

@@ -1,75 +1,75 @@
# Solution pour l'erreur HTTP 431 "Request Header Fields Too Large"
## Problème
L'erreur 431 se produit lorsque les en-têtes HTTP (notamment les cookies contenant les tokens OIDC/JWT) dépassent la taille maximale autorisée.
## Solutions appliquées
### 1. Configuration Quarkus HTTP
```properties
quarkus.http.max-headers-size=64K
quarkus.vertx.max-headers-size=64K
```
### 2. Optimisation OIDC Token Management
```properties
quarkus.oidc.token-state-manager.split-tokens=true
quarkus.oidc.token-state-manager.strategy=id-refresh-tokens
quarkus.oidc.token-state-manager.encryption-required=false
quarkus.oidc.token-state-manager.cookie-max-size=8192
```
Ces configurations :
- **split-tokens** : Divise les tokens en plusieurs cookies pour éviter qu'un seul cookie soit trop volumineux
- **id-refresh-tokens** : Utilise une stratégie optimisée avec refresh tokens
- **encryption-required=false** : Désactive l'encryption pour réduire la taille (développement uniquement)
- **cookie-max-size=8192** : Limite la taille d'un cookie individuel à 8KB
## Actions à effectuer
### ⚠️ IMPORTANT : Supprimer les cookies du navigateur
Les cookies existants peuvent être trop volumineux. Vous devez :
1. **Ouvrir les outils développeur** (F12)
2. **Onglet Application > Cookies**
3. **Supprimer tous les cookies** pour `http://localhost:8081`
4. **Redémarrer l'application Quarkus**
5. **Recharger la page**
Ou via la console du navigateur :
```javascript
// Supprimer tous les cookies pour localhost:8081
document.cookie.split(";").forEach(c => {
document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/");
});
```
### Redémarrer l'application
Après modification de `application.properties`, vous **devez redémarrer** l'application Quarkus :
```bash
# Arrêter l'application (Ctrl+C)
# Puis relancer
mvn quarkus:dev
```
## Vérification
Une fois les cookies supprimés et l'application redémarrée :
1. Accédez à http://localhost:8081/dashboard.xhtml
2. Vous serez redirigé vers Keycloak pour l'authentification
3. Après authentification, les nouveaux cookies (optimisés) seront créés
## Si le problème persiste
1. **Augmenter encore la limite** :
```properties
quarkus.http.max-headers-size=128K
quarkus.vertx.max-headers-size=128K
```
2. **Vérifier dans Keycloak** que le client `btpxpress-frontend` n'a pas trop de claims/roles qui gonflent le token
3. **Mode navigation privée** : Tester dans une fenêtre de navigation privée pour éviter les cookies existants
# Solution pour l'erreur HTTP 431 "Request Header Fields Too Large"
## Problème
L'erreur 431 se produit lorsque les en-têtes HTTP (notamment les cookies contenant les tokens OIDC/JWT) dépassent la taille maximale autorisée.
## Solutions appliquées
### 1. Configuration Quarkus HTTP
```properties
quarkus.http.max-headers-size=64K
quarkus.vertx.max-headers-size=64K
```
### 2. Optimisation OIDC Token Management
```properties
quarkus.oidc.token-state-manager.split-tokens=true
quarkus.oidc.token-state-manager.strategy=id-refresh-tokens
quarkus.oidc.token-state-manager.encryption-required=false
quarkus.oidc.token-state-manager.cookie-max-size=8192
```
Ces configurations :
- **split-tokens** : Divise les tokens en plusieurs cookies pour éviter qu'un seul cookie soit trop volumineux
- **id-refresh-tokens** : Utilise une stratégie optimisée avec refresh tokens
- **encryption-required=false** : Désactive l'encryption pour réduire la taille (développement uniquement)
- **cookie-max-size=8192** : Limite la taille d'un cookie individuel à 8KB
## Actions à effectuer
### ⚠️ IMPORTANT : Supprimer les cookies du navigateur
Les cookies existants peuvent être trop volumineux. Vous devez :
1. **Ouvrir les outils développeur** (F12)
2. **Onglet Application > Cookies**
3. **Supprimer tous les cookies** pour `http://localhost:8081`
4. **Redémarrer l'application Quarkus**
5. **Recharger la page**
Ou via la console du navigateur :
```javascript
// Supprimer tous les cookies pour localhost:8081
document.cookie.split(";").forEach(c => {
document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/");
});
```
### Redémarrer l'application
Après modification de `application.properties`, vous **devez redémarrer** l'application Quarkus :
```bash
# Arrêter l'application (Ctrl+C)
# Puis relancer
mvn quarkus:dev
```
## Vérification
Une fois les cookies supprimés et l'application redémarrée :
1. Accédez à http://localhost:8081/dashboard.xhtml
2. Vous serez redirigé vers Keycloak pour l'authentification
3. Après authentification, les nouveaux cookies (optimisés) seront créés
## Si le problème persiste
1. **Augmenter encore la limite** :
```properties
quarkus.http.max-headers-size=128K
quarkus.vertx.max-headers-size=128K
```
2. **Vérifier dans Keycloak** que le client `btpxpress-frontend` n'a pas trop de claims/roles qui gonflent le token
3. **Mode navigation privée** : Tester dans une fenêtre de navigation privée pour éviter les cookies existants

75
FOOTER_CONFIGURATION.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -1,335 +1,335 @@
# 🔒 Sécurisation Complète de l'Application Frontend BTP Xpress
**Date** : 2025-01-20
**Version** : 1.0.0
**Statut** : ✅ **SÉCURISÉ POUR PRODUCTION**
---
## 📋 Vue d'ensemble
L'application frontend BTP Xpress est maintenant complètement sécurisée pour la production avec :
- ✅ Headers de sécurité HTTP complets
- ✅ Configuration OIDC/Keycloak sécurisée
- ✅ CORS restreint aux domaines autorisés
- ✅ HTTPS/TLS forcé via Ingress
- ✅ Cookies sécurisés (HttpOnly, Secure, SameSite)
- ✅ Content Security Policy (CSP) stricte
- ✅ Protection contre les attaques courantes
---
## 🔐 1. Headers de Sécurité HTTP
### Filtre de Sécurité (`SecurityHeadersFilter`)
Le filtre `SecurityHeadersFilter` ajoute automatiquement les headers suivants à toutes les réponses :
| Header | Valeur | Protection |
|--------|--------|------------|
| **Strict-Transport-Security** | `max-age=31536000; includeSubDomains; preload` | Force HTTPS pendant 1 an |
| **X-Frame-Options** | `DENY` | Empêche le clickjacking |
| **X-Content-Type-Options** | `nosniff` | Empêche le MIME sniffing |
| **X-XSS-Protection** | `1; mode=block` | Active la protection XSS du navigateur |
| **Referrer-Policy** | `strict-origin-when-cross-origin` | Contrôle les informations de referrer |
| **Content-Security-Policy** | Voir ci-dessous | Politique de sécurité stricte |
| **Permissions-Policy** | Désactive geolocation, microphone, etc. | Limite les fonctionnalités du navigateur |
| **X-Permitted-Cross-Domain-Policies** | `none` | Bloque les politiques Flash/Silverlight |
| **Cross-Origin-Embedder-Policy** | `require-corp` | Protection contre les fuites de données |
| **Cross-Origin-Opener-Policy** | `same-origin` | Isolation des fenêtres |
| **Cross-Origin-Resource-Policy** | `same-origin` | Contrôle des ressources cross-origin |
### Content Security Policy (CSP)
```http
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://security.lions.dev;
style-src 'self' 'unsafe-inline' https://security.lions.dev;
img-src 'self' data: https: blob:;
font-src 'self' data: https://security.lions.dev;
connect-src 'self' https://security.lions.dev https://api.btpxpress.lions.dev https://api.lions.dev;
frame-src 'self' https://security.lions.dev;
object-src 'none';
base-uri 'self';
form-action 'self' https://security.lions.dev;
frame-ancestors 'none';
upgrade-insecure-requests;
```
**Explication** :
- Autorise uniquement les ressources depuis `self` et `security.lions.dev`
- Bloque les iframes externes (sauf Keycloak)
- Force l'upgrade vers HTTPS
- Empêche l'injection de code malveillant
---
## 🌐 2. Configuration OIDC / Keycloak
### Serveur d'authentification
- **URL** : `https://security.lions.dev/realms/btpxpress`
- **Client ID** : `btpxpress-frontend`
- **Type** : `web-app` (application publique)
- **TLS Verification** : `required` (obligatoire en production)
### Cookies de session sécurisés
-**HttpOnly** : `true` (protection XSS)
-**Secure** : `true` (HTTPS uniquement)
-**SameSite** : `strict` (protection CSRF)
-**Path** : `/`
-**Encryption** : `required` (tokens chiffrés)
-**Max Size** : `8192 bytes`
### Gestion des tokens
-**Split Tokens** : Activé (tokens divisés)
-**Strategy** : `id-refresh-tokens` (refresh automatique)
-**Session Age Extension** : `PT30M` (30 minutes)
-**Restore Path After Redirect** : `true` (navigation fluide)
---
## 🔒 3. Configuration CORS
### Origines autorisées (Production)
```properties
quarkus.http.cors.origins=https://btpxpress.lions.dev,https://www.btpxpress.lions.dev
```
### Méthodes HTTP autorisées
- `GET`, `POST`, `PUT`, `DELETE`, `OPTIONS`, `PATCH`
### Headers autorisés
- `Content-Type`
- `Authorization`
- `X-Requested-With`
- `X-CSRF-Token`
### Credentials
-**Access-Control-Allow-Credentials** : `true`
-**Max Age** : `3600 seconds` (1 heure)
---
## 🛡️ 4. Configuration Ingress Kubernetes
### TLS/HTTPS
-**SSL Redirect** : Forcé
-**Force SSL Redirect** : Activé
-**Cert Manager** : Let's Encrypt (automatique)
-**TLS Protocols** : `TLSv1.2`, `TLSv1.3`
### Headers ajoutés par Nginx Ingress
Les headers suivants sont ajoutés au niveau de l'Ingress :
- `X-Frame-Options: DENY`
- `X-Content-Type-Options: nosniff`
- `X-XSS-Protection: 1; mode=block`
- `Referrer-Policy: strict-origin-when-cross-origin`
- `Permissions-Policy: geolocation=(), microphone=(), camera=()`
- `X-Permitted-Cross-Domain-Policies: none`
### Configuration HSTS
Le header `Strict-Transport-Security` est ajouté par le filtre Java uniquement pour les connexions HTTPS détectées (via `X-Forwarded-Proto`).
---
## 🔐 5. Permissions et Accès
### Pages publiques (sans authentification)
Les ressources statiques sont accessibles sans authentification :
- `/*.css`, `/*.js`, `/*.png`, `/*.jpg`, etc.
- `/resources/*`
### Pages protégées (authentification requise)
Toutes les autres pages nécessitent une authentification OIDC :
- ✅ Redirection automatique vers Keycloak si non authentifié
- ✅ Restauration du chemin après authentification
- ✅ Session maintenue pendant 30 minutes
---
## 🔧 6. Configuration de Production
### Fichier de configuration
**Fichier** : `src/main/resources/application-prod.properties`
### Variables d'environnement requises
- `BTPXPRESS_API_BASE_URL` : URL de l'API backend (défaut: `https://api.btpxpress.lions.dev`)
### Activation en production
Pour activer la configuration de production :
```bash
export QUARKUS_PROFILE=prod
# ou
java -Dquarkus.profile=prod -jar btpxpress-client.jar
```
---
## ✅ 7. Checklist de Sécurisation
### Headers de sécurité
- [x] Strict-Transport-Security (HSTS)
- [x] X-Frame-Options
- [x] X-Content-Type-Options
- [x] X-XSS-Protection
- [x] Referrer-Policy
- [x] Content-Security-Policy (CSP)
- [x] Permissions-Policy
- [x] Cross-Origin-Embedder-Policy
- [x] Cross-Origin-Opener-Policy
- [x] Cross-Origin-Resource-Policy
### Authentification
- [x] OIDC/Keycloak configuré
- [x] TLS verification requis
- [x] Cookies sécurisés (HttpOnly, Secure, SameSite)
- [x] Tokens chiffrés
- [x] Refresh tokens automatique
### CORS
- [x] Origines restreintes à `btpxpress.lions.dev`
- [x] Credentials autorisés
- [x] Méthodes HTTP limitées
### Infrastructure
- [x] HTTPS forcé via Ingress
- [x] Certificats TLS automatiques (Let's Encrypt)
- [x] TLS 1.2+ uniquement
- [x] Headers sécurité au niveau Ingress
### Application
- [x] Filtre de sécurité activé
- [x] Configuration production séparée
- [x] Logs sécurisés (pas de secrets)
- [x] Limites HTTP configurées
---
## 🚀 8. Déploiement
### Prérequis
1. ✅ Certificat TLS configuré pour `btpxpress.lions.dev`
2. ✅ Keycloak accessible sur `https://security.lions.dev`
3. ✅ Client OIDC `btpxpress-frontend` configuré dans Keycloak
4. ✅ Ingress Kubernetes configuré avec annotations de sécurité
### Vérification post-déploiement
#### 1. Vérifier les headers de sécurité
```bash
curl -I https://btpxpress.lions.dev
# Vérifier la présence de :
# - Strict-Transport-Security
# - X-Frame-Options
# - X-Content-Type-Options
# - Content-Security-Policy
```
#### 2. Tester l'authentification
1. Accéder à `https://btpxpress.lions.dev`
2. Vérifier la redirection vers Keycloak
3. S'authentifier
4. Vérifier le retour vers l'application
#### 3. Vérifier les cookies
Dans les DevTools du navigateur :
- ✅ Cookies avec `HttpOnly`
- ✅ Cookies avec `Secure`
- ✅ Cookies avec `SameSite=Strict`
#### 4. Tester HTTPS
```bash
# Vérifier que HTTP redirige vers HTTPS
curl -I http://btpxpress.lions.dev
# Attendu : 301 ou 302 vers https://
```
#### 5. Vérifier CSP
Dans la console du navigateur :
- Aucune violation CSP
- Ressources chargées uniquement depuis les origines autorisées
---
## 📊 9. Tests de Sécurité Recommandés
### Outils en ligne
- [SSL Labs](https://www.ssllabs.com/ssltest/) : Test du certificat TLS
- [Security Headers](https://securityheaders.com/) : Vérification des headers de sécurité
- [Mozilla Observatory](https://observatory.mozilla.org/) : Audit de sécurité complet
### Commandes locales
```bash
# Test SSL
openssl s_client -connect btpxpress.lions.dev:443
# Test headers
curl -I https://btpxpress.lions.dev
# Test CSP
curl -H "Content-Security-Policy-Report-Only: default-src 'self'" https://btpxpress.lions.dev
```
---
## 🔍 10. Monitoring et Alertes
### Headers à surveiller
- Taux de violations CSP (si reporting configuré)
- Échecs d'authentification OIDC
- Erreurs de certificat TLS
- Redirections HTTP → HTTPS
### Logs à surveiller
- Erreurs d'authentification OIDC
- Violations de sécurité détectées
- Échecs de validation de token
- Tentatives d'accès non autorisées
---
## 📝 11. Maintenance
### Mise à jour des certificats
Les certificats Let's Encrypt sont renouvelés automatiquement par cert-manager.
### Mise à jour de la CSP
Si des violations CSP sont détectées en production, ajuster la CSP dans `SecurityHeadersFilter.java`.
### Mise à jour des dépendances
Maintenir les dépendances à jour pour corriger les vulnérabilités :
```bash
mvn versions:display-dependency-updates
mvn versions:display-plugin-updates
```
---
## 📚 12. Références
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [MDN Web Security](https://developer.mozilla.org/en-US/docs/Web/Security)
- [Quarkus Security Guide](https://quarkus.io/guides/security)
- [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
---
## ✅ Conclusion
L'application frontend BTP Xpress est maintenant **complètement sécurisée** pour la production avec :
-**Headers de sécurité complets** (10+ headers)
-**OIDC/Keycloak sécurisé** (TLS, cookies sécurisés)
-**CORS restreint** (btpxpress.lions.dev uniquement)
-**HTTPS forcé** (TLS 1.2+)
-**CSP stricte** (protection injection)
-**Infrastructure Kubernetes sécurisée**
**Statut** : ✅ **PRÊT POUR PRODUCTION**
---
**Auteur** : Équipe BTP Xpress
**Date de dernière mise à jour** : 2025-01-20
**Version** : 1.0.0
# 🔒 Sécurisation Complète de l'Application Frontend BTP Xpress
**Date** : 2025-01-20
**Version** : 1.0.0
**Statut** : ✅ **SÉCURISÉ POUR PRODUCTION**
---
## 📋 Vue d'ensemble
L'application frontend BTP Xpress est maintenant complètement sécurisée pour la production avec :
- ✅ Headers de sécurité HTTP complets
- ✅ Configuration OIDC/Keycloak sécurisée
- ✅ CORS restreint aux domaines autorisés
- ✅ HTTPS/TLS forcé via Ingress
- ✅ Cookies sécurisés (HttpOnly, Secure, SameSite)
- ✅ Content Security Policy (CSP) stricte
- ✅ Protection contre les attaques courantes
---
## 🔐 1. Headers de Sécurité HTTP
### Filtre de Sécurité (`SecurityHeadersFilter`)
Le filtre `SecurityHeadersFilter` ajoute automatiquement les headers suivants à toutes les réponses :
| Header | Valeur | Protection |
|--------|--------|------------|
| **Strict-Transport-Security** | `max-age=31536000; includeSubDomains; preload` | Force HTTPS pendant 1 an |
| **X-Frame-Options** | `DENY` | Empêche le clickjacking |
| **X-Content-Type-Options** | `nosniff` | Empêche le MIME sniffing |
| **X-XSS-Protection** | `1; mode=block` | Active la protection XSS du navigateur |
| **Referrer-Policy** | `strict-origin-when-cross-origin` | Contrôle les informations de referrer |
| **Content-Security-Policy** | Voir ci-dessous | Politique de sécurité stricte |
| **Permissions-Policy** | Désactive geolocation, microphone, etc. | Limite les fonctionnalités du navigateur |
| **X-Permitted-Cross-Domain-Policies** | `none` | Bloque les politiques Flash/Silverlight |
| **Cross-Origin-Embedder-Policy** | `require-corp` | Protection contre les fuites de données |
| **Cross-Origin-Opener-Policy** | `same-origin` | Isolation des fenêtres |
| **Cross-Origin-Resource-Policy** | `same-origin` | Contrôle des ressources cross-origin |
### Content Security Policy (CSP)
```http
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://security.lions.dev;
style-src 'self' 'unsafe-inline' https://security.lions.dev;
img-src 'self' data: https: blob:;
font-src 'self' data: https://security.lions.dev;
connect-src 'self' https://security.lions.dev https://api.btpxpress.lions.dev https://api.lions.dev;
frame-src 'self' https://security.lions.dev;
object-src 'none';
base-uri 'self';
form-action 'self' https://security.lions.dev;
frame-ancestors 'none';
upgrade-insecure-requests;
```
**Explication** :
- Autorise uniquement les ressources depuis `self` et `security.lions.dev`
- Bloque les iframes externes (sauf Keycloak)
- Force l'upgrade vers HTTPS
- Empêche l'injection de code malveillant
---
## 🌐 2. Configuration OIDC / Keycloak
### Serveur d'authentification
- **URL** : `https://security.lions.dev/realms/btpxpress`
- **Client ID** : `btpxpress-frontend`
- **Type** : `web-app` (application publique)
- **TLS Verification** : `required` (obligatoire en production)
### Cookies de session sécurisés
-**HttpOnly** : `true` (protection XSS)
-**Secure** : `true` (HTTPS uniquement)
-**SameSite** : `strict` (protection CSRF)
-**Path** : `/`
-**Encryption** : `required` (tokens chiffrés)
-**Max Size** : `8192 bytes`
### Gestion des tokens
-**Split Tokens** : Activé (tokens divisés)
-**Strategy** : `id-refresh-tokens` (refresh automatique)
-**Session Age Extension** : `PT30M` (30 minutes)
-**Restore Path After Redirect** : `true` (navigation fluide)
---
## 🔒 3. Configuration CORS
### Origines autorisées (Production)
```properties
quarkus.http.cors.origins=https://btpxpress.lions.dev,https://www.btpxpress.lions.dev
```
### Méthodes HTTP autorisées
- `GET`, `POST`, `PUT`, `DELETE`, `OPTIONS`, `PATCH`
### Headers autorisés
- `Content-Type`
- `Authorization`
- `X-Requested-With`
- `X-CSRF-Token`
### Credentials
-**Access-Control-Allow-Credentials** : `true`
-**Max Age** : `3600 seconds` (1 heure)
---
## 🛡️ 4. Configuration Ingress Kubernetes
### TLS/HTTPS
-**SSL Redirect** : Forcé
-**Force SSL Redirect** : Activé
-**Cert Manager** : Let's Encrypt (automatique)
-**TLS Protocols** : `TLSv1.2`, `TLSv1.3`
### Headers ajoutés par Nginx Ingress
Les headers suivants sont ajoutés au niveau de l'Ingress :
- `X-Frame-Options: DENY`
- `X-Content-Type-Options: nosniff`
- `X-XSS-Protection: 1; mode=block`
- `Referrer-Policy: strict-origin-when-cross-origin`
- `Permissions-Policy: geolocation=(), microphone=(), camera=()`
- `X-Permitted-Cross-Domain-Policies: none`
### Configuration HSTS
Le header `Strict-Transport-Security` est ajouté par le filtre Java uniquement pour les connexions HTTPS détectées (via `X-Forwarded-Proto`).
---
## 🔐 5. Permissions et Accès
### Pages publiques (sans authentification)
Les ressources statiques sont accessibles sans authentification :
- `/*.css`, `/*.js`, `/*.png`, `/*.jpg`, etc.
- `/resources/*`
### Pages protégées (authentification requise)
Toutes les autres pages nécessitent une authentification OIDC :
- ✅ Redirection automatique vers Keycloak si non authentifié
- ✅ Restauration du chemin après authentification
- ✅ Session maintenue pendant 30 minutes
---
## 🔧 6. Configuration de Production
### Fichier de configuration
**Fichier** : `src/main/resources/application-prod.properties`
### Variables d'environnement requises
- `BTPXPRESS_API_BASE_URL` : URL de l'API backend (défaut: `https://api.btpxpress.lions.dev`)
### Activation en production
Pour activer la configuration de production :
```bash
export QUARKUS_PROFILE=prod
# ou
java -Dquarkus.profile=prod -jar btpxpress-client.jar
```
---
## ✅ 7. Checklist de Sécurisation
### Headers de sécurité
- [x] Strict-Transport-Security (HSTS)
- [x] X-Frame-Options
- [x] X-Content-Type-Options
- [x] X-XSS-Protection
- [x] Referrer-Policy
- [x] Content-Security-Policy (CSP)
- [x] Permissions-Policy
- [x] Cross-Origin-Embedder-Policy
- [x] Cross-Origin-Opener-Policy
- [x] Cross-Origin-Resource-Policy
### Authentification
- [x] OIDC/Keycloak configuré
- [x] TLS verification requis
- [x] Cookies sécurisés (HttpOnly, Secure, SameSite)
- [x] Tokens chiffrés
- [x] Refresh tokens automatique
### CORS
- [x] Origines restreintes à `btpxpress.lions.dev`
- [x] Credentials autorisés
- [x] Méthodes HTTP limitées
### Infrastructure
- [x] HTTPS forcé via Ingress
- [x] Certificats TLS automatiques (Let's Encrypt)
- [x] TLS 1.2+ uniquement
- [x] Headers sécurité au niveau Ingress
### Application
- [x] Filtre de sécurité activé
- [x] Configuration production séparée
- [x] Logs sécurisés (pas de secrets)
- [x] Limites HTTP configurées
---
## 🚀 8. Déploiement
### Prérequis
1. ✅ Certificat TLS configuré pour `btpxpress.lions.dev`
2. ✅ Keycloak accessible sur `https://security.lions.dev`
3. ✅ Client OIDC `btpxpress-frontend` configuré dans Keycloak
4. ✅ Ingress Kubernetes configuré avec annotations de sécurité
### Vérification post-déploiement
#### 1. Vérifier les headers de sécurité
```bash
curl -I https://btpxpress.lions.dev
# Vérifier la présence de :
# - Strict-Transport-Security
# - X-Frame-Options
# - X-Content-Type-Options
# - Content-Security-Policy
```
#### 2. Tester l'authentification
1. Accéder à `https://btpxpress.lions.dev`
2. Vérifier la redirection vers Keycloak
3. S'authentifier
4. Vérifier le retour vers l'application
#### 3. Vérifier les cookies
Dans les DevTools du navigateur :
- ✅ Cookies avec `HttpOnly`
- ✅ Cookies avec `Secure`
- ✅ Cookies avec `SameSite=Strict`
#### 4. Tester HTTPS
```bash
# Vérifier que HTTP redirige vers HTTPS
curl -I http://btpxpress.lions.dev
# Attendu : 301 ou 302 vers https://
```
#### 5. Vérifier CSP
Dans la console du navigateur :
- Aucune violation CSP
- Ressources chargées uniquement depuis les origines autorisées
---
## 📊 9. Tests de Sécurité Recommandés
### Outils en ligne
- [SSL Labs](https://www.ssllabs.com/ssltest/) : Test du certificat TLS
- [Security Headers](https://securityheaders.com/) : Vérification des headers de sécurité
- [Mozilla Observatory](https://observatory.mozilla.org/) : Audit de sécurité complet
### Commandes locales
```bash
# Test SSL
openssl s_client -connect btpxpress.lions.dev:443
# Test headers
curl -I https://btpxpress.lions.dev
# Test CSP
curl -H "Content-Security-Policy-Report-Only: default-src 'self'" https://btpxpress.lions.dev
```
---
## 🔍 10. Monitoring et Alertes
### Headers à surveiller
- Taux de violations CSP (si reporting configuré)
- Échecs d'authentification OIDC
- Erreurs de certificat TLS
- Redirections HTTP → HTTPS
### Logs à surveiller
- Erreurs d'authentification OIDC
- Violations de sécurité détectées
- Échecs de validation de token
- Tentatives d'accès non autorisées
---
## 📝 11. Maintenance
### Mise à jour des certificats
Les certificats Let's Encrypt sont renouvelés automatiquement par cert-manager.
### Mise à jour de la CSP
Si des violations CSP sont détectées en production, ajuster la CSP dans `SecurityHeadersFilter.java`.
### Mise à jour des dépendances
Maintenir les dépendances à jour pour corriger les vulnérabilités :
```bash
mvn versions:display-dependency-updates
mvn versions:display-plugin-updates
```
---
## 📚 12. Références
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [MDN Web Security](https://developer.mozilla.org/en-US/docs/Web/Security)
- [Quarkus Security Guide](https://quarkus.io/guides/security)
- [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
---
## ✅ Conclusion
L'application frontend BTP Xpress est maintenant **complètement sécurisée** pour la production avec :
-**Headers de sécurité complets** (10+ headers)
-**OIDC/Keycloak sécurisé** (TLS, cookies sécurisés)
-**CORS restreint** (btpxpress.lions.dev uniquement)
-**HTTPS forcé** (TLS 1.2+)
-**CSP stricte** (protection injection)
-**Infrastructure Kubernetes sécurisée**
**Statut** : ✅ **PRÊT POUR PRODUCTION**
---
**Auteur** : Équipe BTP Xpress
**Date de dernière mise à jour** : 2025-01-20
**Version** : 1.0.0

279
pom.xml
View File

@@ -1,153 +1,128 @@
<?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>
<artifactId>btpxpress-client</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>BTP Xpress Client - PrimeFaces Freya</name>
<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>
<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>
<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>
</repository>
</repositories>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>${quarkus.platform.artifact-id}</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<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>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-logging-json</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client</artifactId>
</dependency>
<dependency>
<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>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
<?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>
<artifactId>btpxpress-client</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>BTP Xpress Client - PrimeFaces Freya</name>
<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>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.27.3</quarkus.platform.version>
<skipTests>false</skipTests>
</properties>
<repositories>
<repository>
<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>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>${quarkus.platform.artifact-id}</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<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>dev.lions</groupId>
<artifactId>lions-faces-layout</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-logging-json</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,87 +1,87 @@
package dev.lions.btpxpress.converter;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.convert.Converter;
import jakarta.faces.convert.FacesConverter;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
/**
* Converter personnalisé pour formater les montants en Franc CFA (Fcfa).
*
* <p>Ce converter formate les nombres avec des espaces comme séparateurs de milliers
* au lieu de virgules, conformément au format standard du Franc CFA.</p>
*
* <p>Exemple : 1234567 devient "1 234 567 Fcfa"</p>
*
* @author BTP Xpress Team
* @version 1.0
*/
@FacesConverter("fcfaConverter")
public class FcfaConverter implements Converter<Number> {
private static final DecimalFormatSymbols SYMBOLS;
static {
SYMBOLS = new DecimalFormatSymbols(Locale.FRENCH);
SYMBOLS.setGroupingSeparator(' ');
SYMBOLS.setDecimalSeparator(',');
}
/**
* Convertit une chaîne de caractères en nombre.
*
* @param context Le contexte Faces
* @param component Le composant UI
* @param value La valeur string à convertir
* @return Le nombre converti, ou null si la valeur est vide/null
*/
@Override
public Number getAsObject(FacesContext context, UIComponent component, String value) {
if (value == null || value.trim().isEmpty()) {
return null;
}
try {
// Retirer les espaces et le préfixe "Fcfa" si présent
String cleanedValue = value.replaceAll("\\s+", "")
.replace("Fcfa", "")
.replace("fcfa", "")
.trim();
return new BigDecimal(cleanedValue);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Impossible de convertir '" + value + "' en nombre", e);
}
}
/**
* Convertit un nombre en chaîne de caractères formatée.
*
* @param context Le contexte Faces
* @param component Le composant UI
* @param value Le nombre à convertir
* @return La chaîne formatée avec espaces comme séparateurs de milliers
*/
@Override
public String getAsString(FacesContext context, UIComponent component, Number value) {
if (value == null) {
return "";
}
// Formater avec espaces comme séparateurs de milliers (format Fcfa standard)
// Le pattern "#" avec groupingUsed=true utilise le groupingSeparator défini dans SYMBOLS (espace)
DecimalFormat formatter = new DecimalFormat("#", SYMBOLS);
formatter.setGroupingSize(3);
formatter.setGroupingUsed(true);
formatter.setMaximumFractionDigits(0);
long amount = value.longValue();
return formatter.format(amount);
}
}
package dev.lions.btpxpress.converter;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.convert.Converter;
import jakarta.faces.convert.FacesConverter;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
/**
* Converter personnalisé pour formater les montants en Franc CFA (Fcfa).
*
* <p>Ce converter formate les nombres avec des espaces comme séparateurs de milliers
* au lieu de virgules, conformément au format standard du Franc CFA.</p>
*
* <p>Exemple : 1234567 devient "1 234 567 Fcfa"</p>
*
* @author BTP Xpress Team
* @version 1.0
*/
@FacesConverter("fcfaConverter")
public class FcfaConverter implements Converter<Number> {
private static final DecimalFormatSymbols SYMBOLS;
static {
SYMBOLS = new DecimalFormatSymbols(Locale.FRENCH);
SYMBOLS.setGroupingSeparator(' ');
SYMBOLS.setDecimalSeparator(',');
}
/**
* Convertit une chaîne de caractères en nombre.
*
* @param context Le contexte Faces
* @param component Le composant UI
* @param value La valeur string à convertir
* @return Le nombre converti, ou null si la valeur est vide/null
*/
@Override
public Number getAsObject(FacesContext context, UIComponent component, String value) {
if (value == null || value.trim().isEmpty()) {
return null;
}
try {
// Retirer les espaces et le préfixe "Fcfa" si présent
String cleanedValue = value.replaceAll("\\s+", "")
.replace("Fcfa", "")
.replace("fcfa", "")
.trim();
return new BigDecimal(cleanedValue);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Impossible de convertir '" + value + "' en nombre", e);
}
}
/**
* Convertit un nombre en chaîne de caractères formatée.
*
* @param context Le contexte Faces
* @param component Le composant UI
* @param value Le nombre à convertir
* @return La chaîne formatée avec espaces comme séparateurs de milliers
*/
@Override
public String getAsString(FacesContext context, UIComponent component, Number value) {
if (value == null) {
return "";
}
// Formater avec espaces comme séparateurs de milliers (format Fcfa standard)
// Le pattern "#" avec groupingUsed=true utilise le groupingSeparator défini dans SYMBOLS (espace)
DecimalFormat formatter = new DecimalFormat("#", SYMBOLS);
formatter.setGroupingSize(3);
formatter.setGroupingUsed(true);
formatter.setMaximumFractionDigits(0);
long amount = value.longValue();
return formatter.format(amount);
}
}

View File

@@ -1,32 +1,32 @@
package dev.lions.btpxpress.filter;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import java.io.IOException;
public class CharacterEncodingFilter implements Filter {
private static final String DEFAULT_ENCODING = "UTF-8";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
request.setCharacterEncoding(DEFAULT_ENCODING);
response.setCharacterEncoding(DEFAULT_ENCODING);
response.setContentType("text/html; charset=" + DEFAULT_ENCODING);
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
package dev.lions.btpxpress.filter;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import java.io.IOException;
public class CharacterEncodingFilter implements Filter {
private static final String DEFAULT_ENCODING = "UTF-8";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
request.setCharacterEncoding(DEFAULT_ENCODING);
response.setCharacterEncoding(DEFAULT_ENCODING);
response.setContentType("text/html; charset=" + DEFAULT_ENCODING);
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}

View File

@@ -1,116 +1,116 @@
package dev.lions.btpxpress.filter;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Filtre de sécurité qui ajoute les headers HTTP de sécurité essentiels
* pour protéger l'application contre diverses attaques.
*/
public class SecurityHeadersFilter implements Filter {
private static final String STRICT_TRANSPORT_SECURITY = "Strict-Transport-Security";
private static final String X_FRAME_OPTIONS = "X-Frame-Options";
private static final String X_CONTENT_TYPE_OPTIONS = "X-Content-Type-Options";
private static final String X_XSS_PROTECTION = "X-XSS-Protection";
private static final String REFERRER_POLICY = "Referrer-Policy";
private static final String CONTENT_SECURITY_POLICY = "Content-Security-Policy";
private static final String PERMISSIONS_POLICY = "Permissions-Policy";
// HSTS - Force HTTPS pendant 1 an, inclut les sous-domaines
private static final String HSTS_VALUE = "max-age=31536000; includeSubDomains; preload";
// X-Frame-Options - Empêche le clickjacking
private static final String X_FRAME_OPTIONS_VALUE = "DENY";
// X-Content-Type-Options - Empêche le MIME sniffing
private static final String X_CONTENT_TYPE_OPTIONS_VALUE = "nosniff";
// X-XSS-Protection - Active la protection XSS du navigateur (legacy mais utile)
private static final String X_XSS_PROTECTION_VALUE = "1; mode=block";
// Referrer-Policy - Contrôle les informations de referrer envoyées
private static final String REFERRER_POLICY_VALUE = "strict-origin-when-cross-origin";
// Content Security Policy - Politique de sécurité stricte
// Autorise uniquement les ressources depuis le même domaine et security.lions.dev
private static final String CSP_VALUE =
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://security.lions.dev; " +
"style-src 'self' 'unsafe-inline' https://security.lions.dev; " +
"img-src 'self' data: https: blob:; " +
"font-src 'self' data: https://security.lions.dev; " +
"connect-src 'self' https://security.lions.dev https://api.btpxpress.lions.dev https://api.lions.dev; " +
"frame-src 'self' https://security.lions.dev; " +
"object-src 'none'; " +
"base-uri 'self'; " +
"form-action 'self' https://security.lions.dev; " +
"frame-ancestors 'none'; " +
"upgrade-insecure-requests;";
// Permissions Policy - Désactive les fonctionnalités non nécessaires
private static final String PERMISSIONS_POLICY_VALUE =
"geolocation=(), " +
"microphone=(), " +
"camera=(), " +
"payment=(), " +
"usb=(), " +
"magnetometer=(), " +
"gyroscope=(), " +
"speaker=()";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Initialisation non nécessaire
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// Ajouter les headers de sécurité uniquement pour les requêtes HTTPS
if (httpRequest.isSecure() ||
"https".equalsIgnoreCase(httpRequest.getHeader("X-Forwarded-Proto")) ||
"https".equalsIgnoreCase(httpRequest.getHeader("X-Forwarded-Scheme"))) {
// Strict Transport Security (HSTS)
httpResponse.setHeader(STRICT_TRANSPORT_SECURITY, HSTS_VALUE);
// Content Security Policy
httpResponse.setHeader(CONTENT_SECURITY_POLICY, CSP_VALUE);
}
// Headers de sécurité applicables même en HTTP (développement)
// Ces headers seront toujours présents
httpResponse.setHeader(X_FRAME_OPTIONS, X_FRAME_OPTIONS_VALUE);
httpResponse.setHeader(X_CONTENT_TYPE_OPTIONS, X_CONTENT_TYPE_OPTIONS_VALUE);
httpResponse.setHeader(X_XSS_PROTECTION, X_XSS_PROTECTION_VALUE);
httpResponse.setHeader(REFERRER_POLICY, REFERRER_POLICY_VALUE);
httpResponse.setHeader(PERMISSIONS_POLICY, PERMISSIONS_POLICY_VALUE);
// Headers supplémentaires pour renforcer la sécurité
httpResponse.setHeader("X-Permitted-Cross-Domain-Policies", "none");
httpResponse.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
httpResponse.setHeader("Cross-Origin-Opener-Policy", "same-origin");
httpResponse.setHeader("Cross-Origin-Resource-Policy", "same-origin");
chain.doFilter(request, response);
}
@Override
public void destroy() {
// Nettoyage non nécessaire
}
}
package dev.lions.btpxpress.filter;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Filtre de sécurité qui ajoute les headers HTTP de sécurité essentiels
* pour protéger l'application contre diverses attaques.
*/
public class SecurityHeadersFilter implements Filter {
private static final String STRICT_TRANSPORT_SECURITY = "Strict-Transport-Security";
private static final String X_FRAME_OPTIONS = "X-Frame-Options";
private static final String X_CONTENT_TYPE_OPTIONS = "X-Content-Type-Options";
private static final String X_XSS_PROTECTION = "X-XSS-Protection";
private static final String REFERRER_POLICY = "Referrer-Policy";
private static final String CONTENT_SECURITY_POLICY = "Content-Security-Policy";
private static final String PERMISSIONS_POLICY = "Permissions-Policy";
// HSTS - Force HTTPS pendant 1 an, inclut les sous-domaines
private static final String HSTS_VALUE = "max-age=31536000; includeSubDomains; preload";
// X-Frame-Options - Empêche le clickjacking
private static final String X_FRAME_OPTIONS_VALUE = "DENY";
// X-Content-Type-Options - Empêche le MIME sniffing
private static final String X_CONTENT_TYPE_OPTIONS_VALUE = "nosniff";
// X-XSS-Protection - Active la protection XSS du navigateur (legacy mais utile)
private static final String X_XSS_PROTECTION_VALUE = "1; mode=block";
// Referrer-Policy - Contrôle les informations de referrer envoyées
private static final String REFERRER_POLICY_VALUE = "strict-origin-when-cross-origin";
// Content Security Policy - Politique de sécurité stricte
// Autorise uniquement les ressources depuis le même domaine et security.lions.dev
private static final String CSP_VALUE =
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://security.lions.dev; " +
"style-src 'self' 'unsafe-inline' https://security.lions.dev; " +
"img-src 'self' data: https: blob:; " +
"font-src 'self' data: https://security.lions.dev; " +
"connect-src 'self' https://security.lions.dev https://api.btpxpress.lions.dev https://api.lions.dev; " +
"frame-src 'self' https://security.lions.dev; " +
"object-src 'none'; " +
"base-uri 'self'; " +
"form-action 'self' https://security.lions.dev; " +
"frame-ancestors 'none'; " +
"upgrade-insecure-requests;";
// Permissions Policy - Désactive les fonctionnalités non nécessaires
private static final String PERMISSIONS_POLICY_VALUE =
"geolocation=(), " +
"microphone=(), " +
"camera=(), " +
"payment=(), " +
"usb=(), " +
"magnetometer=(), " +
"gyroscope=(), " +
"speaker=()";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Initialisation non nécessaire
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// Ajouter les headers de sécurité uniquement pour les requêtes HTTPS
if (httpRequest.isSecure() ||
"https".equalsIgnoreCase(httpRequest.getHeader("X-Forwarded-Proto")) ||
"https".equalsIgnoreCase(httpRequest.getHeader("X-Forwarded-Scheme"))) {
// Strict Transport Security (HSTS)
httpResponse.setHeader(STRICT_TRANSPORT_SECURITY, HSTS_VALUE);
// Content Security Policy
httpResponse.setHeader(CONTENT_SECURITY_POLICY, CSP_VALUE);
}
// Headers de sécurité applicables même en HTTP (développement)
// Ces headers seront toujours présents
httpResponse.setHeader(X_FRAME_OPTIONS, X_FRAME_OPTIONS_VALUE);
httpResponse.setHeader(X_CONTENT_TYPE_OPTIONS, X_CONTENT_TYPE_OPTIONS_VALUE);
httpResponse.setHeader(X_XSS_PROTECTION, X_XSS_PROTECTION_VALUE);
httpResponse.setHeader(REFERRER_POLICY, REFERRER_POLICY_VALUE);
httpResponse.setHeader(PERMISSIONS_POLICY, PERMISSIONS_POLICY_VALUE);
// Headers supplémentaires pour renforcer la sécurité
httpResponse.setHeader("X-Permitted-Cross-Domain-Policies", "none");
httpResponse.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
httpResponse.setHeader("Cross-Origin-Opener-Policy", "same-origin");
httpResponse.setHeader("Cross-Origin-Resource-Policy", "same-origin");
chain.doFilter(request, response);
}
@Override
public void destroy() {
// Nettoyage non nécessaire
}
}

View File

@@ -1,273 +1,336 @@
package dev.lions.btpxpress.service;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
/**
* Interface REST Client pour communiquer avec l'API backend BTP Xpress.
* <p>
* Ce client permet au frontend PrimeFaces de communiquer avec le backend Quarkus
* en utilisant les endpoints REST exposés sur /api/v1/*. L'authentification
* est gérée automatiquement via les tokens JWT Keycloak.
* </p>
*
* @author BTP Xpress Development Team
* @version 1.0.0
* @since 1.0.0
*/
@RegisterRestClient(configKey = "btpxpress.api")
@RegisterClientHeaders
@Path("/api/v1")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public interface BtpXpressApiClient {
/**
* Récupère la liste des chantiers.
* Correspond à {@code ChantierResource.getAllChantiers()} dans le serveur.
*
* @return Réponse HTTP contenant la liste des chantiers.
*/
@GET
@Path("/chantiers")
Response getChantiers();
/**
* Récupère un chantier par son identifiant.
* Correspond à {@code ChantierResource.getChantierById()} dans le serveur.
*
* @param id L'identifiant du chantier.
* @return Réponse HTTP contenant le chantier.
*/
@GET
@Path("/chantiers/{id}")
Response getChantier(@PathParam("id") Long id);
/**
* Récupère la liste des clients.
* Correspond à {@code ClientResource.getAllClients()} dans le serveur.
*
* @return Réponse HTTP contenant la liste des clients.
*/
@GET
@Path("/clients")
Response getClients();
/**
* Récupère un client par son identifiant.
* Correspond à {@code ClientResource.getClientById()} dans le serveur.
*
* @param id L'identifiant du client.
* @return Réponse HTTP contenant le client.
*/
@GET
@Path("/clients/{id}")
Response getClient(@PathParam("id") Long id);
// === ENDPOINTS DASHBOARD ===
/**
* Récupère le dashboard principal avec les métriques globales.
* Correspond à {@code DashboardResource.getDashboardPrincipal()} dans le serveur.
*
* @return Réponse HTTP contenant les métriques du dashboard.
*/
@GET
@Path("/dashboard")
Response getDashboardPrincipal();
/**
* Récupère le dashboard des chantiers avec métriques détaillées.
* Correspond à {@code DashboardResource.getDashboardChantiers()} dans le serveur.
*
* @return Réponse HTTP contenant les métriques des chantiers.
*/
@GET
@Path("/dashboard/chantiers")
Response getDashboardChantiers();
/**
* Récupère les métriques financières.
* Correspond à {@code DashboardResource.getDashboardFinances()} dans le serveur.
*
* @param periode Période en jours (défaut: 30).
* @return Réponse HTTP contenant les métriques financières.
*/
@GET
@Path("/dashboard/finances")
Response getDashboardFinances(@QueryParam("periode") @DefaultValue("30") int periode);
/**
* Récupère les métriques de maintenance.
* Correspond à {@code DashboardResource.getDashboardMaintenance()} dans le serveur.
*
* @return Réponse HTTP contenant les métriques de maintenance.
*/
@GET
@Path("/dashboard/maintenance")
Response getDashboardMaintenance();
/**
* Récupère les métriques des ressources (équipes, employés, matériel).
* Correspond à {@code DashboardResource.getDashboardRessources()} dans le serveur.
*
* @return Réponse HTTP contenant les métriques des ressources.
*/
@GET
@Path("/dashboard/ressources")
Response getDashboardRessources();
/**
* Récupère les alertes nécessitant une attention immédiate.
* Correspond à {@code DashboardResource.getAlertes()} dans le serveur.
*
* @return Réponse HTTP contenant les alertes.
*/
@GET
@Path("/dashboard/alertes")
Response getAlertes();
/**
* Récupère les KPIs principaux.
* Correspond à {@code DashboardResource.getKPI()} dans le serveur.
*
* @param periode Période en jours (défaut: 30).
* @return Réponse HTTP contenant les KPIs.
*/
@GET
@Path("/dashboard/kpi")
Response getKPI(@QueryParam("periode") @DefaultValue("30") int periode);
/**
* Récupère les activités récentes.
* Correspond à {@code DashboardResource.getActivitesRecentes()} dans le serveur.
*
* @param limit Nombre d'activités à récupérer (défaut: 10).
* @return Réponse HTTP contenant les activités récentes.
*/
@GET
@Path("/dashboard/activites-recentes")
Response getActivitesRecentes(@QueryParam("limit") @DefaultValue("10") int limit);
/**
* Récupère le résumé quotidien.
* Correspond à {@code DashboardResource.getResumeQuotidien()} dans le serveur.
*
* @return Réponse HTTP contenant le résumé quotidien.
*/
@GET
@Path("/dashboard/resume-quotidien")
Response getResumeQuotidien();
/**
* Récupère la liste des devis.
* Correspond à {@code DevisResource.getAllDevis()} dans le serveur.
*
* @return Réponse HTTP contenant la liste des devis.
*/
@GET
@Path("/devis")
Response getDevis();
/**
* Récupère la liste des factures.
* Correspond à {@code FactureResource.getAllFactures()} dans le serveur.
*
* @return Réponse HTTP contenant la liste des factures.
*/
@GET
@Path("/factures")
Response getFactures();
// === ENDPOINTS EMPLOYÉS ===
/**
* Récupère la liste des employés.
* Correspond à {@code EmployeResource.getAllEmployes()} dans le serveur.
*
* @return Réponse HTTP contenant la liste des employés.
*/
@GET
@Path("/employes")
Response getEmployes();
/**
* Récupère un employé par son identifiant.
*
* @param id L'identifiant de l'employé.
* @return Réponse HTTP contenant l'employé.
*/
@GET
@Path("/employes/{id}")
Response getEmploye(@PathParam("id") String id);
// === ENDPOINTS ÉQUIPES ===
/**
* Récupère la liste des équipes.
* Correspond à {@code EquipeResource.getAllEquipes()} dans le serveur.
*
* @return Réponse HTTP contenant la liste des équipes.
*/
@GET
@Path("/equipes")
Response getEquipes();
/**
* Récupère une équipe par son identifiant.
*
* @param id L'identifiant de l'équipe.
* @return Réponse HTTP contenant l'équipe.
*/
@GET
@Path("/equipes/{id}")
Response getEquipe(@PathParam("id") String id);
// === ENDPOINTS MATÉRIELS ===
/**
* Récupère la liste des matériels.
* Correspond à {@code MaterielResource.getAllMateriels()} dans le serveur.
*
* @return Réponse HTTP contenant la liste des matériels.
*/
@GET
@Path("/materiels")
Response getMateriels();
/**
* Récupère un matériel par son identifiant.
*
* @param id L'identifiant du matériel.
* @return Réponse HTTP contenant le matériel.
*/
@GET
@Path("/materiels/{id}")
Response getMateriel(@PathParam("id") String id);
// === ENDPOINTS STOCKS ===
/**
* Récupère la liste des stocks.
* Correspond à {@code StockResource.getAllStocks()} dans le serveur.
*
* @return Réponse HTTP contenant la liste des stocks.
*/
@GET
@Path("/stocks")
Response getStocks();
/**
* Récupère un stock par son identifiant.
*
* @param id L'identifiant du stock.
* @return Réponse HTTP contenant le stock.
*/
@GET
@Path("/stocks/{id}")
Response getStock(@PathParam("id") String id);
}
package dev.lions.btpxpress.service;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
/**
* Interface REST Client pour communiquer avec l'API backend BTP Xpress.
* <p>
* Ce client permet au frontend PrimeFaces de communiquer avec le backend Quarkus
* en utilisant les endpoints REST exposés sur /api/v1/*. L'authentification
* est gérée automatiquement via les tokens JWT Keycloak.
* </p>
*
* @author BTP Xpress Development Team
* @version 1.0.0
* @since 1.0.0
*/
@RegisterRestClient(configKey = "btpxpress.api")
@RegisterClientHeaders
@Path("/api/v1")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public interface BtpXpressApiClient {
/**
* Récupère la liste des chantiers.
* Correspond à {@code ChantierResource.getAllChantiers()} dans le serveur.
*
* @return Réponse HTTP contenant la liste des chantiers.
*/
@GET
@Path("/chantiers")
Response getChantiers();
/**
* Récupère un chantier par son identifiant.
* Correspond à {@code ChantierResource.getChantierById()} dans le serveur.
*
* @param id L'identifiant du chantier.
* @return Réponse HTTP contenant le chantier.
*/
@GET
@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.
*
* @return Réponse HTTP contenant la liste des clients.
*/
@GET
@Path("/clients")
Response getClients();
/**
* Récupère un client par son identifiant.
* Correspond à {@code ClientResource.getClientById()} dans le serveur.
*
* @param id L'identifiant du client.
* @return Réponse HTTP contenant le client.
*/
@GET
@Path("/clients/{id}")
Response getClient(@PathParam("id") Long id);
// === ENDPOINTS DASHBOARD ===
/**
* Récupère le dashboard principal avec les métriques globales.
* Correspond à {@code DashboardResource.getDashboardPrincipal()} dans le serveur.
*
* @return Réponse HTTP contenant les métriques du dashboard.
*/
@GET
@Path("/dashboard")
Response getDashboardPrincipal();
/**
* Récupère le dashboard des chantiers avec métriques détaillées.
* Correspond à {@code DashboardResource.getDashboardChantiers()} dans le serveur.
*
* @return Réponse HTTP contenant les métriques des chantiers.
*/
@GET
@Path("/dashboard/chantiers")
Response getDashboardChantiers();
/**
* Récupère les métriques financières.
* Correspond à {@code DashboardResource.getDashboardFinances()} dans le serveur.
*
* @param periode Période en jours (défaut: 30).
* @return Réponse HTTP contenant les métriques financières.
*/
@GET
@Path("/dashboard/finances")
Response getDashboardFinances(@QueryParam("periode") @DefaultValue("30") int periode);
/**
* Récupère les métriques de maintenance.
* Correspond à {@code DashboardResource.getDashboardMaintenance()} dans le serveur.
*
* @return Réponse HTTP contenant les métriques de maintenance.
*/
@GET
@Path("/dashboard/maintenance")
Response getDashboardMaintenance();
/**
* Récupère les métriques des ressources (équipes, employés, matériel).
* Correspond à {@code DashboardResource.getDashboardRessources()} dans le serveur.
*
* @return Réponse HTTP contenant les métriques des ressources.
*/
@GET
@Path("/dashboard/ressources")
Response getDashboardRessources();
/**
* Récupère les alertes nécessitant une attention immédiate.
* Correspond à {@code DashboardResource.getAlertes()} dans le serveur.
*
* @return Réponse HTTP contenant les alertes.
*/
@GET
@Path("/dashboard/alertes")
Response getAlertes();
/**
* Récupère les KPIs principaux.
* Correspond à {@code DashboardResource.getKPI()} dans le serveur.
*
* @param periode Période en jours (défaut: 30).
* @return Réponse HTTP contenant les KPIs.
*/
@GET
@Path("/dashboard/kpi")
Response getKPI(@QueryParam("periode") @DefaultValue("30") int periode);
/**
* Récupère les activités récentes.
* Correspond à {@code DashboardResource.getActivitesRecentes()} dans le serveur.
*
* @param limit Nombre d'activités à récupérer (défaut: 10).
* @return Réponse HTTP contenant les activités récentes.
*/
@GET
@Path("/dashboard/activites-recentes")
Response getActivitesRecentes(@QueryParam("limit") @DefaultValue("10") int limit);
/**
* Récupère le résumé quotidien.
* Correspond à {@code DashboardResource.getResumeQuotidien()} dans le serveur.
*
* @return Réponse HTTP contenant le résumé quotidien.
*/
@GET
@Path("/dashboard/resume-quotidien")
Response getResumeQuotidien();
/**
* Récupère la liste des devis.
* Correspond à {@code DevisResource.getAllDevis()} dans le serveur.
*
* @return Réponse HTTP contenant la liste des devis.
*/
@GET
@Path("/devis")
Response getDevis();
/**
* Récupère la liste des factures.
* Correspond à {@code FactureResource.getAllFactures()} dans le serveur.
*
* @return Réponse HTTP contenant la liste des factures.
*/
@GET
@Path("/factures")
Response getFactures();
// === ENDPOINTS EMPLOYÉS ===
/**
* Récupère la liste des employés.
* Correspond à {@code EmployeResource.getAllEmployes()} dans le serveur.
*
* @return Réponse HTTP contenant la liste des employés.
*/
@GET
@Path("/employes")
Response getEmployes();
/**
* Récupère un employé par son identifiant.
*
* @param id L'identifiant de l'employé.
* @return Réponse HTTP contenant l'employé.
*/
@GET
@Path("/employes/{id}")
Response getEmploye(@PathParam("id") String id);
// === ENDPOINTS ÉQUIPES ===
/**
* Récupère la liste des équipes.
* Correspond à {@code EquipeResource.getAllEquipes()} dans le serveur.
*
* @return Réponse HTTP contenant la liste des équipes.
*/
@GET
@Path("/equipes")
Response getEquipes();
/**
* Récupère une équipe par son identifiant.
*
* @param id L'identifiant de l'équipe.
* @return Réponse HTTP contenant l'équipe.
*/
@GET
@Path("/equipes/{id}")
Response getEquipe(@PathParam("id") String id);
// === ENDPOINTS MATÉRIELS ===
/**
* Récupère la liste des matériels.
* Correspond à {@code MaterielResource.getAllMateriels()} dans le serveur.
*
* @return Réponse HTTP contenant la liste des matériels.
*/
@GET
@Path("/materiels")
Response getMateriels();
/**
* Récupère un matériel par son identifiant.
*
* @param id L'identifiant du matériel.
* @return Réponse HTTP contenant le matériel.
*/
@GET
@Path("/materiels/{id}")
Response getMateriel(@PathParam("id") String id);
// === ENDPOINTS STOCKS ===
/**
* Récupère la liste des stocks.
* Correspond à {@code StockResource.getAllStocks()} dans le serveur.
*
* @return Réponse HTTP contenant la liste des stocks.
*/
@GET
@Path("/stocks")
Response getStocks();
/**
* Récupère un stock par son identifiant.
*
* @param id L'identifiant du stock.
* @return Réponse HTTP contenant le stock.
*/
@GET
@Path("/stocks/{id}")
Response getStock(@PathParam("id") String id);
// === ENDPOINTS NOTIFICATIONS ===
/**
* Récupère les notifications non lues pour un utilisateur.
* Correspond à {@code NotificationResource.getAllNotifications()} avec filtre nonLues=true.
*
* @param userId ID de l'utilisateur (UUID en String).
* @return Réponse HTTP contenant la liste des notifications non lues.
*/
@GET
@Path("/notifications")
Response getNotifications(
@QueryParam("userId") String userId,
@QueryParam("nonLues") @DefaultValue("true") boolean nonLues);
// === ENDPOINTS MESSAGES ===
/**
* Récupère les messages non lus pour un utilisateur.
* Correspond à {@code MessageResource.getMessagesNonLus()} dans le serveur.
*
* @param userId ID de l'utilisateur (UUID en String).
* @return Réponse HTTP contenant la liste des messages non lus.
*/
@GET
@Path("/messages/non-lus/{userId}")
Response getMessagesNonLus(@PathParam("userId") String userId);
}

View File

@@ -1,83 +1,177 @@
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 chantiers côté client.
* <p>
* Ce service encapsule la communication avec l'API backend pour les opérations
* liées aux chantiers. Il utilise le REST Client pour effectuer les appels HTTP
* vers le backend.
* </p>
*
* @author BTP Xpress Development Team
* @version 1.0.0
* @since 1.0.0
*/
@ApplicationScoped
public class ChantierService {
private static final Logger LOG = LoggerFactory.getLogger(ChantierService.class);
@Inject
@RestClient
BtpXpressApiClient apiClient;
/**
* Récupère tous les chantiers depuis l'API backend.
*
* @return Liste des chantiers, ou liste vide en cas d'erreur.
*/
public List<Map<String, Object>> getAllChantiers() {
try {
LOG.debug("Récupération de la liste des chantiers depuis l'API backend.");
Response response = apiClient.getChantiers();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> chantiers = response.readEntity(List.class);
LOG.debug("Chantiers récupérés avec succès : {} élément(s)", chantiers != null ? chantiers.size() : 0);
return chantiers != null ? chantiers : new ArrayList<>();
} else {
LOG.warn("Erreur lors de la récupération des chantiers. 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 chantiers : {}", e.getMessage(), e);
return new ArrayList<>();
}
}
/**
* Récupère un chantier par son identifiant depuis l'API backend.
*
* @param id L'identifiant du chantier.
* @return Le chantier sous forme de Map, ou null en cas d'erreur.
*/
public Map<String, Object> getChantierById(Long id) {
try {
LOG.debug("Récupération du chantier avec ID : {}", id);
Response response = apiClient.getChantier(id);
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Map<String, Object> chantier = response.readEntity(Map.class);
LOG.debug("Chantier récupéré avec succès.");
return chantier;
} else {
LOG.warn("Erreur lors de la récupération du chantier. Code HTTP : {}", response.getStatus());
return null;
}
} catch (Exception e) {
LOG.error("Erreur lors de la communication avec l'API backend pour récupérer le chantier : {}", e.getMessage(), e);
return null;
}
}
}
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 chantiers côté client.
* <p>
* Ce service encapsule la communication avec l'API backend pour les opérations
* liées aux chantiers. Il utilise le REST Client pour effectuer les appels HTTP
* vers le backend.
* </p>
*
* @author BTP Xpress Development Team
* @version 1.0.0
* @since 1.0.0
*/
@ApplicationScoped
public class ChantierService {
private static final Logger LOG = LoggerFactory.getLogger(ChantierService.class);
@Inject
@RestClient
BtpXpressApiClient apiClient;
/**
* Récupère tous les chantiers depuis l'API backend.
*
* @return Liste des chantiers, ou liste vide en cas d'erreur.
*/
public List<Map<String, Object>> getAllChantiers() {
try {
LOG.debug("Récupération de la liste des chantiers depuis l'API backend.");
Response response = apiClient.getChantiers();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> chantiers = response.readEntity(List.class);
LOG.debug("Chantiers récupérés avec succès : {} élément(s)", chantiers != null ? chantiers.size() : 0);
return chantiers != null ? chantiers : new ArrayList<>();
} else {
LOG.warn("Erreur lors de la récupération des chantiers. 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 chantiers : {}", e.getMessage(), e);
return new ArrayList<>();
}
}
/**
* Récupère un chantier par son identifiant depuis l'API backend.
*
* @param id L'identifiant du chantier.
* @return Le chantier sous forme de Map, ou null en cas d'erreur.
*/
public Map<String, Object> getChantierById(Long id) {
try {
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;
} else {
LOG.warn("Erreur lors de la récupération du chantier. Code HTTP : {}", response.getStatus());
return null;
}
} catch (Exception e) {
LOG.error("Erreur lors de la communication avec l'API backend pour récupérer le chantier : {}", e.getMessage(), e);
return null;
}
}
/**
* Crée un nouveau chantier via l'API backend.
*
* @param chantierData Les données du chantier à créer (Map ou DTO).
* @return Le chantier créé sous forme de Map, ou null en cas d'erreur.
*/
public Map<String, Object> createChantier(Map<String, Object> chantierData) {
try {
LOG.debug("Création d'un nouveau chantier : {}", chantierData.get("nom"));
Response response = apiClient.createChantier(chantierData);
if (response.getStatus() == Response.Status.CREATED.getStatusCode() ||
response.getStatus() == Response.Status.OK.getStatusCode()) {
@SuppressWarnings("unchecked")
Map<String, Object> chantier = response.readEntity(Map.class);
LOG.info("Chantier créé avec succès : {}", chantier.get("id"));
return chantier;
} else {
String errorMessage = response.readEntity(String.class);
LOG.warn("Erreur lors de la création du chantier. Code HTTP : {}, Message : {}",
response.getStatus(), errorMessage);
return null;
}
} catch (Exception e) {
LOG.error("Erreur lors de la communication avec l'API backend pour créer le chantier : {}", e.getMessage(), e);
return null;
}
}
/**
* Met à jour un chantier existant via l'API backend.
*
* @param id L'identifiant du chantier (UUID en String).
* @param chantierData Les nouvelles données du chantier (Map ou DTO).
* @return Le chantier mis à jour sous forme de Map, ou null en cas d'erreur.
*/
public Map<String, Object> updateChantier(String id, Map<String, Object> chantierData) {
try {
LOG.debug("Mise à jour du chantier avec ID : {}", id);
Response response = apiClient.updateChantier(id, chantierData);
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
@SuppressWarnings("unchecked")
Map<String, Object> chantier = response.readEntity(Map.class);
LOG.info("Chantier mis à jour avec succès : {}", id);
return chantier;
} else {
String errorMessage = response.readEntity(String.class);
LOG.warn("Erreur lors de la mise à jour du chantier. Code HTTP : {}, Message : {}",
response.getStatus(), errorMessage);
return null;
}
} catch (Exception e) {
LOG.error("Erreur lors de la communication avec l'API backend pour mettre à jour le chantier : {}", e.getMessage(), e);
return null;
}
}
/**
* Supprime un chantier via l'API backend.
*
* @param id L'identifiant du chantier (UUID en String).
* @param permanent Si true, suppression définitive, sinon suppression logique (défaut: false).
* @return true si la suppression a réussi, false sinon.
*/
public boolean deleteChantier(String id, boolean permanent) {
try {
LOG.debug("Suppression du chantier avec ID : {} (permanent: {})", id, permanent);
Response response = apiClient.deleteChantier(id, permanent);
if (response.getStatus() == Response.Status.NO_CONTENT.getStatusCode() ||
response.getStatus() == Response.Status.OK.getStatusCode()) {
LOG.info("Chantier supprimé avec succès : {}", id);
return true;
} else {
String errorMessage = response.readEntity(String.class);
LOG.warn("Erreur lors de la suppression du chantier. Code HTTP : {}, Message : {}",
response.getStatus(), errorMessage);
return false;
}
} catch (Exception e) {
LOG.error("Erreur lors de la communication avec l'API backend pour supprimer le chantier : {}", e.getMessage(), e);
return false;
}
}
/**
* Supprime un chantier via l'API backend (suppression logique par défaut).
*
* @param id L'identifiant du chantier (UUID en String).
* @return true si la suppression a réussi, false sinon.
*/
public boolean deleteChantier(String id) {
return deleteChantier(id, false);
}
}

View File

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

View File

@@ -1,311 +1,341 @@
package dev.lions.btpxpress.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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.List;
import java.util.Map;
/**
* Service pour récupérer et transformer les données du dashboard depuis l'API backend.
*
* <p>Ce service encapsule tous les appels à l'API dashboard et transforme
* les réponses JSON en objets Java utilisables par les vues JSF.</p>
*
* @author BTP Xpress Team
* @version 1.0
*/
@ApplicationScoped
public class DashboardService {
private static final Logger logger = LoggerFactory.getLogger(DashboardService.class);
@Inject
@RestClient
BtpXpressApiClient apiClient;
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* Récupère les métriques du dashboard principal.
*
* @return JsonNode contenant les métriques ou null en cas d'erreur
*/
public JsonNode getDashboardPrincipal() {
try {
Response response = apiClient.getDashboardPrincipal();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity();
if (entity == null) {
logger.warn("Réponse vide du dashboard principal");
return null;
}
// REST Client avec Jackson désérialise déjà en Map/Object
return convertToJsonNode(entity);
} else {
logger.error("Erreur API dashboard principal: status {}", response.getStatus());
return null;
}
} catch (Exception e) {
logger.error("Erreur lors de la récupération du dashboard principal", e);
return null;
}
}
/**
* Convertit un objet en JsonNode, quel que soit son type (String, Map, Object, etc.).
*/
private JsonNode convertToJsonNode(Object entity) {
try {
if (entity instanceof String) {
return objectMapper.readTree((String) entity);
} else if (entity instanceof Map || entity instanceof List) {
// Map ou List sont déjà désérialisés par REST Client
return objectMapper.valueToTree(entity);
} else {
// Pour les autres objets, conversion via ObjectMapper
return objectMapper.valueToTree(entity);
}
} catch (Exception e) {
logger.error("Erreur lors de la conversion en JsonNode", e);
return null;
}
}
/**
* Récupère les métriques des chantiers.
*
* @return JsonNode contenant les métriques des chantiers ou null en cas d'erreur
*/
public JsonNode getDashboardChantiers() {
try {
Response response = apiClient.getDashboardChantiers();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity();
return convertToJsonNode(entity);
} else {
logger.error("Erreur API dashboard chantiers: status {}", response.getStatus());
return null;
}
} catch (Exception e) {
logger.error("Erreur lors de la récupération du dashboard chantiers", e);
return null;
}
}
/**
* Récupère les métriques financières.
*
* @param periode Période en jours (défaut: 30)
* @return JsonNode contenant les métriques financières ou null en cas d'erreur
*/
public JsonNode getDashboardFinances(int periode) {
try {
Response response = apiClient.getDashboardFinances(periode);
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else {
logger.error("Erreur API dashboard finances: status {}", response.getStatus());
return null;
}
} catch (Exception e) {
logger.error("Erreur lors de la récupération du dashboard finances", e);
return null;
}
}
/**
* Récupère les métriques de maintenance.
*
* @return JsonNode contenant les métriques de maintenance ou null en cas d'erreur
*/
public JsonNode getDashboardMaintenance() {
try {
Response response = apiClient.getDashboardMaintenance();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else {
logger.error("Erreur API dashboard maintenance: status {}", response.getStatus());
return null;
}
} catch (Exception e) {
logger.error("Erreur lors de la récupération du dashboard maintenance", e);
return null;
}
}
/**
* Récupère les métriques des ressources.
*
* @return JsonNode contenant les métriques des ressources ou null en cas d'erreur
*/
public JsonNode getDashboardRessources() {
try {
Response response = apiClient.getDashboardRessources();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else {
logger.error("Erreur API dashboard ressources: status {}", response.getStatus());
return null;
}
} catch (Exception e) {
logger.error("Erreur lors de la récupération du dashboard ressources", e);
return null;
}
}
/**
* Récupère les alertes.
*
* @return JsonNode contenant les alertes ou null en cas d'erreur
*/
public JsonNode getAlertes() {
try {
Response response = apiClient.getAlertes();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else {
logger.error("Erreur API alertes: status {}", response.getStatus());
return null;
}
} catch (Exception e) {
logger.error("Erreur lors de la récupération des alertes", e);
return null;
}
}
/**
* Récupère les KPIs.
*
* @param periode Période en jours (défaut: 30)
* @return JsonNode contenant les KPIs ou null en cas d'erreur
*/
public JsonNode getKPI(int periode) {
try {
Response response = apiClient.getKPI(periode);
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else {
logger.error("Erreur API KPI: status {}", response.getStatus());
return null;
}
} catch (Exception e) {
logger.error("Erreur lors de la récupération des KPIs", e);
return null;
}
}
/**
* Récupère les activités récentes.
*
* @param limit Nombre d'activités à récupérer
* @return JsonNode contenant les activités récentes ou null en cas d'erreur
*/
public JsonNode getActivitesRecentes(int limit) {
try {
Response response = apiClient.getActivitesRecentes(limit);
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else {
logger.error("Erreur API activités récentes: status {}", response.getStatus());
return null;
}
} catch (Exception e) {
logger.error("Erreur lors de la récupération des activités récentes", e);
return null;
}
}
/**
* Récupère le résumé quotidien.
*
* @return JsonNode contenant le résumé quotidien ou null en cas d'erreur
*/
public JsonNode getResumeQuotidien() {
try {
Response response = apiClient.getResumeQuotidien();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else {
logger.error("Erreur API résumé quotidien: status {}", response.getStatus());
return null;
}
} catch (Exception e) {
logger.error("Erreur lors de la récupération du résumé quotidien", e);
return null;
}
}
/**
* Récupère le nombre de clients.
*
* @return Nombre de clients ou 0 en cas d'erreur
*/
public int getNombreClients() {
try {
Response response = apiClient.getClients();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
List<?> clients = (List<?>) response.getEntity();
return clients != null ? clients.size() : 0;
}
return 0;
} catch (Exception e) {
logger.error("Erreur lors de la récupération du nombre de clients", e);
return 0;
}
}
/**
* Récupère le nombre de devis en attente.
*
* @return Nombre de devis en attente ou 0 en cas d'erreur
*/
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;
}
return 0;
} catch (Exception e) {
logger.error("Erreur lors de la récupération du nombre de devis", e);
return 0;
}
}
/**
* Récupère le nombre de factures impayées.
*
* @return Nombre de factures impayées ou 0 en cas d'erreur
*/
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;
}
return 0;
} catch (Exception e) {
logger.error("Erreur lors de la récupération du nombre de factures", e);
return 0;
}
}
}
package dev.lions.btpxpress.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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.List;
import java.util.Map;
/**
* Service pour récupérer et transformer les données du dashboard depuis l'API backend.
*
* <p>Ce service encapsule tous les appels à l'API dashboard et transforme
* les réponses JSON en objets Java utilisables par les vues JSF.</p>
*
* @author BTP Xpress Team
* @version 1.0
*/
@ApplicationScoped
public class DashboardService {
private static final Logger logger = LoggerFactory.getLogger(DashboardService.class);
@Inject
@RestClient
BtpXpressApiClient apiClient;
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* Récupère les métriques du dashboard principal.
*
* @return JsonNode contenant les métriques ou null en cas d'erreur
*/
public JsonNode getDashboardPrincipal() {
try {
Response response = apiClient.getDashboardPrincipal();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity();
if (entity == null) {
logger.warn("Réponse vide du dashboard principal");
return null;
}
// REST Client avec Jackson désérialise déjà en Map/Object
return convertToJsonNode(entity);
} else {
logger.error("Erreur API dashboard principal: status {}", response.getStatus());
return null;
}
} catch (Exception e) {
logger.error("Erreur lors de la récupération du dashboard principal", e);
return null;
}
}
/**
* Convertit un objet en JsonNode, quel que soit son type (String, Map, Object, etc.).
*/
private JsonNode convertToJsonNode(Object entity) {
try {
if (entity instanceof String) {
return objectMapper.readTree((String) entity);
} else if (entity instanceof Map || entity instanceof List) {
// Map ou List sont déjà désérialisés par REST Client
return objectMapper.valueToTree(entity);
} else {
// Pour les autres objets, conversion via ObjectMapper
return objectMapper.valueToTree(entity);
}
} catch (Exception e) {
logger.error("Erreur lors de la conversion en JsonNode", e);
return null;
}
}
/**
* Récupère les métriques des chantiers.
*
* @return JsonNode contenant les métriques des chantiers ou null en cas d'erreur
*/
public JsonNode getDashboardChantiers() {
try {
Response response = apiClient.getDashboardChantiers();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity();
return convertToJsonNode(entity);
} else {
logger.error("Erreur API dashboard chantiers: status {}", response.getStatus());
return null;
}
} catch (Exception e) {
logger.error("Erreur lors de la récupération du dashboard chantiers", e);
return null;
}
}
/**
* Récupère les métriques financières.
*
* @param periode Période en jours (défaut: 30)
* @return JsonNode contenant les métriques financières ou null en cas d'erreur
*/
public JsonNode getDashboardFinances(int periode) {
try {
Response response = apiClient.getDashboardFinances(periode);
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else {
logger.error("Erreur API dashboard finances: status {}", response.getStatus());
return null;
}
} catch (Exception e) {
logger.error("Erreur lors de la récupération du dashboard finances", e);
return null;
}
}
/**
* Récupère les métriques de maintenance.
*
* @return JsonNode contenant les métriques de maintenance ou null en cas d'erreur
*/
public JsonNode getDashboardMaintenance() {
try {
Response response = apiClient.getDashboardMaintenance();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else {
logger.error("Erreur API dashboard maintenance: status {}", response.getStatus());
return null;
}
} catch (Exception e) {
logger.error("Erreur lors de la récupération du dashboard maintenance", e);
return null;
}
}
/**
* Récupère les métriques des ressources.
*
* @return JsonNode contenant les métriques des ressources ou null en cas d'erreur
*/
public JsonNode getDashboardRessources() {
try {
Response response = apiClient.getDashboardRessources();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else {
logger.error("Erreur API dashboard ressources: status {}", response.getStatus());
return null;
}
} catch (Exception e) {
logger.error("Erreur lors de la récupération du dashboard ressources", e);
return null;
}
}
/**
* Récupère les alertes.
*
* @return JsonNode contenant les alertes ou null en cas d'erreur
*/
public JsonNode getAlertes() {
try {
Response response = apiClient.getAlertes();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else {
logger.error("Erreur API alertes: status {}", response.getStatus());
return null;
}
} catch (Exception e) {
logger.error("Erreur lors de la récupération des alertes", e);
return null;
}
}
/**
* Récupère les KPIs.
*
* @param periode Période en jours (défaut: 30)
* @return JsonNode contenant les KPIs ou null en cas d'erreur
*/
public JsonNode getKPI(int periode) {
try {
Response response = apiClient.getKPI(periode);
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else {
logger.error("Erreur API KPI: status {}", response.getStatus());
return null;
}
} catch (Exception e) {
logger.error("Erreur lors de la récupération des KPIs", e);
return null;
}
}
/**
* Récupère les activités récentes.
*
* @param limit Nombre d'activités à récupérer
* @return JsonNode contenant les activités récentes ou null en cas d'erreur
*/
public JsonNode getActivitesRecentes(int limit) {
try {
Response response = apiClient.getActivitesRecentes(limit);
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else {
logger.error("Erreur API activités récentes: status {}", response.getStatus());
return null;
}
} catch (Exception e) {
logger.error("Erreur lors de la récupération des activités récentes", e);
return null;
}
}
/**
* Récupère le résumé quotidien.
*
* @return JsonNode contenant le résumé quotidien ou null en cas d'erreur
*/
public JsonNode getResumeQuotidien() {
try {
Response response = apiClient.getResumeQuotidien();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else {
logger.error("Erreur API résumé quotidien: status {}", response.getStatus());
return null;
}
} catch (Exception e) {
logger.error("Erreur lors de la récupération du résumé quotidien", e);
return null;
}
}
/**
* Récupère le nombre de clients.
*
* @return Nombre de clients ou 0 en cas d'erreur
*/
public int getNombreClients() {
try {
Response response = apiClient.getClients();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
List<?> clients = (List<?>) response.getEntity();
return clients != null ? clients.size() : 0;
}
return 0;
} catch (Exception e) {
logger.error("Erreur lors de la récupération du nombre de clients", e);
return 0;
}
}
/**
* 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<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) {
logger.error("Erreur lors de la récupération du nombre de devis", e);
return 0;
}
}
/**
* 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<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) {
logger.error("Erreur lors de la récupération du nombre de factures", e);
return 0;
}
}
}

View File

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

View File

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

View File

@@ -1,383 +1,383 @@
package dev.lions.btpxpress.view;
import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* Classe de base pour les vues de type liste/CRUD.
*
* Fonctionnalités:
* - Chargement et affichage de listes
* - Filtrage multi-critères
* - Tri (ascendant/descendant)
* - Pagination
* - CRUD complet (Create, Read, Update, Delete)
* - Sélection simple/multiple
* - Messages utilisateur (succès, erreur, warning)
* - Lazy loading pour grandes listes
*
* Principe DRY: Toute la logique commune des écrans de liste est centralisée ici.
*
* @param <T> Type d'entité
* @param <ID> Type de l'identifiant
*/
@Getter
@Setter
public abstract class BaseListView<T, ID> implements Serializable {
protected static final Logger LOG = LoggerFactory.getLogger(BaseListView.class);
private static final long serialVersionUID = 1L;
// ========== Données ==========
protected List<T> items = new ArrayList<>();
protected List<T> filteredItems = new ArrayList<>();
protected T selectedItem;
protected List<T> selectedItems = new ArrayList<>();
protected T entity; // Pour les formulaires create/edit
// ========== États ==========
protected boolean loading = false;
protected boolean editing = false; // Mode édition vs création
protected String globalFilter; // Recherche globale
// ========== Pagination ==========
protected int first = 0; // Index de départ
protected int pageSize = 10; // Taille de page
protected int totalRecords = 0; // Nombre total d'enregistrements
// ========== Tri ==========
protected String sortField; // Champ de tri
protected boolean sortAscending = true; // Ordre de tri
// ========== Sélection ==========
protected String selectionMode = "single"; // single, multiple, checkbox
/**
* Initialisation du bean au chargement de la page.
*/
@PostConstruct
public void init() {
LOG.debug("Initialisation de {}", getClass().getSimpleName());
try {
initializeFields();
loadItems();
} catch (Exception e) {
LOG.error("Erreur lors de l'initialisation", e);
addErrorMessage("Erreur lors du chargement des données");
}
}
/**
* Initialiser les champs spécifiques de la vue.
* Override si nécessaire.
*/
protected void initializeFields() {
// À surcharger dans les classes filles si besoin
}
/**
* Charger les items depuis la source de données.
* DOIT être implémenté par les classes filles.
*/
public abstract void loadItems();
/**
* Recharger les données (alias pour loadItems).
*/
public void refresh() {
LOG.debug("Rafraîchissement des données");
loadItems();
}
// ========== Filtrage ==========
/**
* Appliquer les filtres à la liste d'items.
*/
protected void applyFilters(List<T> sourceItems, List<Predicate<T>> filters) {
if (filters == null || filters.isEmpty()) {
filteredItems = new ArrayList<>(sourceItems);
return;
}
filteredItems = sourceItems.stream()
.filter(filters.stream().reduce(Predicate::and).orElse(x -> true))
.collect(Collectors.toList());
}
/**
* Recherche avec les critères de filtrage actuels.
*/
public void search() {
LOG.debug("Recherche lancée pour {}", getClass().getSimpleName());
first = 0; // Retour à la première page
loadItems();
}
/**
* Réinitialiser tous les filtres.
*/
public void resetFilters() {
LOG.debug("Réinitialisation des filtres pour {}", getClass().getSimpleName());
globalFilter = null;
sortField = null;
sortAscending = true;
first = 0;
resetFilterFields();
loadItems();
}
/**
* Réinitialiser les champs de filtre spécifiques.
* DOIT être implémenté par les classes filles.
*/
protected abstract void resetFilterFields();
// ========== Tri ==========
/**
* Trier la liste par un champ donné.
*/
public void sort(String field) {
if (field.equals(sortField)) {
sortAscending = !sortAscending;
} else {
sortField = field;
sortAscending = true;
}
LOG.debug("Tri par {} ({})", field, sortAscending ? "ASC" : "DESC");
loadItems();
}
// ========== Navigation ==========
/**
* Naviguer vers la page de détails d'un item.
*/
public String viewDetails(ID id) {
LOG.debug("Redirection vers détails : {}", id);
return getDetailsPath() + "?id=" + id + "&faces-redirect=true";
}
/**
* Naviguer vers la page de détails de l'item sélectionné.
*/
public String viewSelectedDetails() {
if (selectedItem == null) {
addWarningMessage("Aucun élément sélectionné");
return null;
}
return viewDetails(getEntityId(selectedItem));
}
/**
* Obtenir le chemin de la page de détails.
*/
protected abstract String getDetailsPath();
/**
* Naviguer vers la page de création.
*/
public String createNew() {
LOG.debug("Redirection vers création");
return getCreatePath() + "?faces-redirect=true";
}
/**
* Obtenir le chemin de la page de création.
*/
protected abstract String getCreatePath();
// ========== CRUD ==========
/**
* Préparer un nouvel item pour création.
*/
public void prepareNew() {
LOG.debug("Préparation nouvelle entité");
entity = createNewEntity();
editing = false;
}
/**
* Créer une nouvelle instance de l'entité.
* DOIT être implémenté par les classes filles.
*/
protected abstract T createNewEntity();
/**
* Préparer un item pour édition.
*/
public void prepareEdit(T item) {
LOG.debug("Préparation édition : {}", item);
entity = item;
editing = true;
}
/**
* Sauvegarder l'entité (création ou modification).
*/
public void save() {
try {
loading = true;
if (editing) {
performUpdate();
addSuccessMessage("Modification réussie");
} else {
performCreate();
addSuccessMessage("Création réussie");
}
loadItems();
entity = null;
editing = false;
} catch (Exception e) {
LOG.error("Erreur lors de la sauvegarde", e);
addErrorMessage("Erreur lors de la sauvegarde : " + e.getMessage());
} finally {
loading = false;
}
}
/**
* Créer une nouvelle entité.
* DOIT être implémenté par les classes filles.
*/
protected abstract void performCreate();
/**
* Mettre à jour une entité existante.
* DOIT être implémenté par les classes filles.
*/
protected abstract void performUpdate();
/**
* Supprimer l'item sélectionné.
*/
public void delete() {
if (selectedItem == null) {
addWarningMessage("Aucun élément sélectionné");
return;
}
try {
loading = true;
LOG.info("Suppression : {}", selectedItem);
performDelete();
items.remove(selectedItem);
selectedItem = null;
addSuccessMessage("Suppression réussie");
loadItems();
} catch (Exception e) {
LOG.error("Erreur lors de la suppression", e);
addErrorMessage("Erreur lors de la suppression : " + e.getMessage());
} finally {
loading = false;
}
}
/**
* Supprimer les items sélectionnés (sélection multiple).
*/
public void deleteSelected() {
if (selectedItems == null || selectedItems.isEmpty()) {
addWarningMessage("Aucun élément sélectionné");
return;
}
try {
loading = true;
int count = selectedItems.size();
for (T item : selectedItems) {
selectedItem = item;
performDelete();
}
loadItems();
selectedItems.clear();
selectedItem = null;
addSuccessMessage(count + " élément(s) supprimé(s)");
} catch (Exception e) {
LOG.error("Erreur lors de la suppression multiple", e);
addErrorMessage("Erreur lors de la suppression");
} finally {
loading = false;
}
}
/**
* Effectuer la suppression réelle.
* DOIT être implémenté par les classes filles.
*/
protected abstract void performDelete();
/**
* Obtenir l'ID d'une entité.
* DOIT être implémenté par les classes filles.
*/
protected abstract ID getEntityId(T entity);
// ========== Messages utilisateur ==========
protected void addSuccessMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message));
}
protected void addErrorMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
}
protected void addWarningMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_WARN, "Attention", message));
}
protected void addInfoMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Information", message));
}
// ========== Utilitaires ==========
/**
* Vérifier si la liste est vide.
*/
public boolean isEmpty() {
return items == null || items.isEmpty();
}
/**
* Obtenir le nombre d'items.
*/
public int getItemCount() {
return items == null ? 0 : items.size();
}
/**
* Vérifier si un item est sélectionné.
*/
public boolean hasSelection() {
return selectedItem != null;
}
/**
* Vérifier si plusieurs items sont sélectionnés.
*/
public boolean hasMultipleSelection() {
return selectedItems != null && !selectedItems.isEmpty();
}
}
package dev.lions.btpxpress.view;
import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* Classe de base pour les vues de type liste/CRUD.
*
* Fonctionnalités:
* - Chargement et affichage de listes
* - Filtrage multi-critères
* - Tri (ascendant/descendant)
* - Pagination
* - CRUD complet (Create, Read, Update, Delete)
* - Sélection simple/multiple
* - Messages utilisateur (succès, erreur, warning)
* - Lazy loading pour grandes listes
*
* Principe DRY: Toute la logique commune des écrans de liste est centralisée ici.
*
* @param <T> Type d'entité
* @param <ID> Type de l'identifiant
*/
@Getter
@Setter
public abstract class BaseListView<T, ID> implements Serializable {
protected static final Logger LOG = LoggerFactory.getLogger(BaseListView.class);
private static final long serialVersionUID = 1L;
// ========== Données ==========
protected List<T> items = new ArrayList<>();
protected List<T> filteredItems = new ArrayList<>();
protected T selectedItem;
protected List<T> selectedItems = new ArrayList<>();
protected T entity; // Pour les formulaires create/edit
// ========== États ==========
protected boolean loading = false;
protected boolean editing = false; // Mode édition vs création
protected String globalFilter; // Recherche globale
// ========== Pagination ==========
protected int first = 0; // Index de départ
protected int pageSize = 10; // Taille de page
protected int totalRecords = 0; // Nombre total d'enregistrements
// ========== Tri ==========
protected String sortField; // Champ de tri
protected boolean sortAscending = true; // Ordre de tri
// ========== Sélection ==========
protected String selectionMode = "single"; // single, multiple, checkbox
/**
* Initialisation du bean au chargement de la page.
*/
@PostConstruct
public void init() {
LOG.debug("Initialisation de {}", getClass().getSimpleName());
try {
initializeFields();
loadItems();
} catch (Exception e) {
LOG.error("Erreur lors de l'initialisation", e);
addErrorMessage("Erreur lors du chargement des données");
}
}
/**
* Initialiser les champs spécifiques de la vue.
* Override si nécessaire.
*/
protected void initializeFields() {
// À surcharger dans les classes filles si besoin
}
/**
* Charger les items depuis la source de données.
* DOIT être implémenté par les classes filles.
*/
public abstract void loadItems();
/**
* Recharger les données (alias pour loadItems).
*/
public void refresh() {
LOG.debug("Rafraîchissement des données");
loadItems();
}
// ========== Filtrage ==========
/**
* Appliquer les filtres à la liste d'items.
*/
protected void applyFilters(List<T> sourceItems, List<Predicate<T>> filters) {
if (filters == null || filters.isEmpty()) {
filteredItems = new ArrayList<>(sourceItems);
return;
}
filteredItems = sourceItems.stream()
.filter(filters.stream().reduce(Predicate::and).orElse(x -> true))
.collect(Collectors.toList());
}
/**
* Recherche avec les critères de filtrage actuels.
*/
public void search() {
LOG.debug("Recherche lancée pour {}", getClass().getSimpleName());
first = 0; // Retour à la première page
loadItems();
}
/**
* Réinitialiser tous les filtres.
*/
public void resetFilters() {
LOG.debug("Réinitialisation des filtres pour {}", getClass().getSimpleName());
globalFilter = null;
sortField = null;
sortAscending = true;
first = 0;
resetFilterFields();
loadItems();
}
/**
* Réinitialiser les champs de filtre spécifiques.
* DOIT être implémenté par les classes filles.
*/
protected abstract void resetFilterFields();
// ========== Tri ==========
/**
* Trier la liste par un champ donné.
*/
public void sort(String field) {
if (field.equals(sortField)) {
sortAscending = !sortAscending;
} else {
sortField = field;
sortAscending = true;
}
LOG.debug("Tri par {} ({})", field, sortAscending ? "ASC" : "DESC");
loadItems();
}
// ========== Navigation ==========
/**
* Naviguer vers la page de détails d'un item.
*/
public String viewDetails(ID id) {
LOG.debug("Redirection vers détails : {}", id);
return getDetailsPath() + "?id=" + id + "&faces-redirect=true";
}
/**
* Naviguer vers la page de détails de l'item sélectionné.
*/
public String viewSelectedDetails() {
if (selectedItem == null) {
addWarningMessage("Aucun élément sélectionné");
return null;
}
return viewDetails(getEntityId(selectedItem));
}
/**
* Obtenir le chemin de la page de détails.
*/
protected abstract String getDetailsPath();
/**
* Naviguer vers la page de création.
*/
public String createNew() {
LOG.debug("Redirection vers création");
return getCreatePath() + "?faces-redirect=true";
}
/**
* Obtenir le chemin de la page de création.
*/
protected abstract String getCreatePath();
// ========== CRUD ==========
/**
* Préparer un nouvel item pour création.
*/
public void prepareNew() {
LOG.debug("Préparation nouvelle entité");
entity = createNewEntity();
editing = false;
}
/**
* Créer une nouvelle instance de l'entité.
* DOIT être implémenté par les classes filles.
*/
protected abstract T createNewEntity();
/**
* Préparer un item pour édition.
*/
public void prepareEdit(T item) {
LOG.debug("Préparation édition : {}", item);
entity = item;
editing = true;
}
/**
* Sauvegarder l'entité (création ou modification).
*/
public void save() {
try {
loading = true;
if (editing) {
performUpdate();
addSuccessMessage("Modification réussie");
} else {
performCreate();
addSuccessMessage("Création réussie");
}
loadItems();
entity = null;
editing = false;
} catch (Exception e) {
LOG.error("Erreur lors de la sauvegarde", e);
addErrorMessage("Erreur lors de la sauvegarde : " + e.getMessage());
} finally {
loading = false;
}
}
/**
* Créer une nouvelle entité.
* DOIT être implémenté par les classes filles.
*/
protected abstract void performCreate();
/**
* Mettre à jour une entité existante.
* DOIT être implémenté par les classes filles.
*/
protected abstract void performUpdate();
/**
* Supprimer l'item sélectionné.
*/
public void delete() {
if (selectedItem == null) {
addWarningMessage("Aucun élément sélectionné");
return;
}
try {
loading = true;
LOG.info("Suppression : {}", selectedItem);
performDelete();
items.remove(selectedItem);
selectedItem = null;
addSuccessMessage("Suppression réussie");
loadItems();
} catch (Exception e) {
LOG.error("Erreur lors de la suppression", e);
addErrorMessage("Erreur lors de la suppression : " + e.getMessage());
} finally {
loading = false;
}
}
/**
* Supprimer les items sélectionnés (sélection multiple).
*/
public void deleteSelected() {
if (selectedItems == null || selectedItems.isEmpty()) {
addWarningMessage("Aucun élément sélectionné");
return;
}
try {
loading = true;
int count = selectedItems.size();
for (T item : selectedItems) {
selectedItem = item;
performDelete();
}
loadItems();
selectedItems.clear();
selectedItem = null;
addSuccessMessage(count + " élément(s) supprimé(s)");
} catch (Exception e) {
LOG.error("Erreur lors de la suppression multiple", e);
addErrorMessage("Erreur lors de la suppression");
} finally {
loading = false;
}
}
/**
* Effectuer la suppression réelle.
* DOIT être implémenté par les classes filles.
*/
protected abstract void performDelete();
/**
* Obtenir l'ID d'une entité.
* DOIT être implémenté par les classes filles.
*/
protected abstract ID getEntityId(T entity);
// ========== Messages utilisateur ==========
protected void addSuccessMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message));
}
protected void addErrorMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
}
protected void addWarningMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_WARN, "Attention", message));
}
protected void addInfoMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Information", message));
}
// ========== Utilitaires ==========
/**
* Vérifier si la liste est vide.
*/
public boolean isEmpty() {
return items == null || items.isEmpty();
}
/**
* Obtenir le nombre d'items.
*/
public int getItemCount() {
return items == null ? 0 : items.size();
}
/**
* Vérifier si un item est sélectionné.
*/
public boolean hasSelection() {
return selectedItem != null;
}
/**
* Vérifier si plusieurs items sont sélectionnés.
*/
public boolean hasMultipleSelection() {
return selectedItems != null && !selectedItems.isEmpty();
}
}

View File

@@ -1,273 +1,397 @@
package dev.lions.btpxpress.view;
import dev.lions.btpxpress.service.ChantierService;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.Getter;
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.List;
import java.util.Map;
import java.util.function.Predicate;
@Named("chantiersView")
@ViewScoped
@Getter
@Setter
public class ChantiersView extends BaseListView<ChantiersView.Chantier, Long> implements Serializable {
private static final Logger LOG = LoggerFactory.getLogger(ChantiersView.class);
@Inject
ChantierService chantierService;
private String filtreNom;
private String filtreClient;
private String filtreStatut;
private Long chantierId;
@PostConstruct
public void init() {
if (filtreStatut == null) {
filtreStatut = "TOUS";
}
loadItems();
}
/**
* Définit le filtre de statut (utilisé depuis les pages filtrées).
*/
public void setFiltreStatut(String statut) {
this.filtreStatut = statut;
}
@Override
public void loadItems() {
loading = true;
try {
items = new ArrayList<>();
// Récupération depuis l'API backend
List<Map<String, Object>> chantiersData = chantierService.getAllChantiers();
for (Map<String, Object> data : chantiersData) {
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);
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) {
Map<String, Object> clientData = (Map<String, Object>) clientObj;
c.setClient((String) clientData.get("raisonSociale"));
} else if (clientObj instanceof String) {
c.setClient((String) clientObj);
} else {
c.setClient("N/A");
}
c.setAdresse((String) data.get("adresse"));
// Conversion des dates
if (data.get("dateDebut") != null) {
c.setDateDebut(LocalDate.parse(data.get("dateDebut").toString()));
}
if (data.get("dateFinPrevue") != null) {
c.setDateFinPrevue(LocalDate.parse(data.get("dateFinPrevue").toString()));
}
c.setStatut((String) data.get("statut"));
// Avancement en pourcentage
Object avancementObj = data.get("avancement");
if (avancementObj != null) {
c.setAvancement(avancementObj instanceof Integer ?
(Integer) avancementObj :
Integer.parseInt(avancementObj.toString()));
} else {
c.setAvancement(0);
}
// Budget et coût réel
Object montantObj = data.get("montant");
if (montantObj != null) {
c.setBudget(montantObj instanceof Number ?
((Number) montantObj).doubleValue() :
Double.parseDouble(montantObj.toString()));
} else {
c.setBudget(0.0);
}
Object coutReelObj = data.get("coutReel");
if (coutReelObj != null) {
c.setCoutReel(coutReelObj instanceof Number ?
((Number) coutReelObj).doubleValue() :
Double.parseDouble(coutReelObj.toString()));
} else {
c.setCoutReel(0.0);
}
items.add(c);
}
LOG.info("Chantiers chargés depuis l'API : {} élément(s)", items.size());
applyFilters(items, buildFilters());
} catch (Exception e) {
LOG.error("Erreur chargement chantiers depuis l'API", e);
// En cas d'erreur, on garde une liste vide
items = new ArrayList<>();
} finally {
loading = false;
}
}
private List<Predicate<Chantier>> buildFilters() {
List<Predicate<Chantier>> filters = new ArrayList<>();
if (filtreNom != null && !filtreNom.trim().isEmpty()) {
filters.add(c -> c.getNom().toLowerCase().contains(filtreNom.toLowerCase()));
}
if (filtreClient != null && !filtreClient.trim().isEmpty()) {
filters.add(c -> c.getClient().toLowerCase().contains(filtreClient.toLowerCase()));
}
if (filtreStatut != null && !filtreStatut.trim().isEmpty() && !"TOUS".equals(filtreStatut)) {
filters.add(c -> c.getStatut().equals(filtreStatut));
}
return filters;
}
@Override
protected void resetFilterFields() {
filtreNom = null;
filtreClient = null;
filtreStatut = "TOUS";
}
@Override
protected String getDetailsPath() {
return "/chantiers/";
}
@Override
protected String getCreatePath() {
return "/chantiers/nouveau";
}
@Override
protected void performDelete() {
LOG.info("Suppression chantier : {}", selectedItem.getId());
// TODO: Appeler chantierService.delete(selectedItem.getId())
}
@Override
protected Chantier createNewEntity() {
Chantier c = new Chantier();
c.setStatut("PLANIFIE");
c.setAvancement(0);
c.setDateDebut(LocalDate.now());
c.setDateCreation(LocalDateTime.now());
return c;
}
@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)
}
@Override
protected void performUpdate() {
entity.setDateModification(LocalDateTime.now());
LOG.info("Chantier modifié : {}", entity.getNom());
// TODO: Appeler chantierService.update(entity)
}
@Override
protected Long getEntityId(Chantier chantier) {
return chantier.getId();
}
/**
* Initialise un nouveau chantier pour la création.
*/
@Override
public String createNew() {
prepareNew();
return getCreatePath() + "?faces-redirect=true";
}
/**
* Sauvegarde un nouveau chantier.
*/
public String saveNew() {
if (selectedItem == null) {
selectedItem = new Chantier();
}
selectedItem.setId(System.currentTimeMillis()); // Simulation ID
selectedItem.setDateCreation(LocalDateTime.now());
selectedItem.setDateModification(LocalDateTime.now());
items.add(selectedItem);
LOG.info("Nouveau chantier créé : {}", selectedItem.getNom());
return "/chantiers?faces-redirect=true";
}
/**
* Charge un chantier par son ID depuis les paramètres de la requête.
*/
public void loadChantierById() {
if (chantierId != null) {
loadItems(); // S'assurer que les items sont chargés
selectedItem = items.stream()
.filter(c -> c.getId().equals(chantierId))
.findFirst()
.orElse(null);
if (selectedItem == null) {
LOG.warn("Chantier avec ID {} non trouvé", chantierId);
}
}
}
/**
* Affiche les détails d'un chantier.
*/
public String viewDetails(Long id) {
selectedItem = items.stream()
.filter(c -> c.getId().equals(id))
.findFirst()
.orElse(null);
if (selectedItem != null) {
return getDetailsPath() + "details?id=" + id + "&faces-redirect=true";
}
return "/chantiers?faces-redirect=true";
}
@lombok.Getter
@lombok.Setter
public static class Chantier {
private Long id;
private String nom;
private String client;
private String adresse;
private LocalDate dateDebut;
private LocalDate dateFinPrevue;
private String statut;
private int avancement;
private double budget;
private double coutReel;
private LocalDateTime dateCreation;
private LocalDateTime dateModification;
}
}
package dev.lions.btpxpress.view;
import dev.lions.btpxpress.service.ChantierService;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
@Named("chantiersView")
@ViewScoped
@Getter
@Setter
public class ChantiersView extends BaseListView<ChantiersView.Chantier, Long> {
private static final Logger LOG = LoggerFactory.getLogger(ChantiersView.class);
@Inject
ChantierService chantierService;
private String filtreNom;
private String filtreClient;
private String filtreStatut;
private Long chantierId;
@PostConstruct
public void init() {
if (filtreStatut == null) {
filtreStatut = "TOUS";
}
loadItems();
}
/**
* Définit le filtre de statut (utilisé depuis les pages filtrées).
*/
public void setFiltreStatut(String statut) {
this.filtreStatut = statut;
}
@Override
public void loadItems() {
loading = true;
try {
items = new ArrayList<>();
// Récupération depuis l'API backend
List<Map<String, Object>> chantiersData = chantierService.getAllChantiers();
for (Map<String, Object> data : chantiersData) {
Chantier c = new Chantier();
// Mapping des données de l'API vers l'objet Chantier
// 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
if (data.get("dateDebut") != null) {
c.setDateDebut(LocalDate.parse(data.get("dateDebut").toString()));
}
if (data.get("dateFinPrevue") != null) {
c.setDateFinPrevue(LocalDate.parse(data.get("dateFinPrevue").toString()));
}
c.setStatut((String) data.get("statut"));
// Avancement en pourcentage
Object avancementObj = data.get("avancement");
if (avancementObj != null) {
c.setAvancement(avancementObj instanceof Integer ?
(Integer) avancementObj :
Integer.parseInt(avancementObj.toString()));
} else {
c.setAvancement(0);
}
// Budget et coût réel
Object montantObj = data.get("montant");
if (montantObj != null) {
c.setBudget(montantObj instanceof Number ?
((Number) montantObj).doubleValue() :
Double.parseDouble(montantObj.toString()));
} else {
c.setBudget(0.0);
}
Object coutReelObj = data.get("coutReel");
if (coutReelObj != null) {
c.setCoutReel(coutReelObj instanceof Number ?
((Number) coutReelObj).doubleValue() :
Double.parseDouble(coutReelObj.toString()));
} else {
c.setCoutReel(0.0);
}
items.add(c);
}
LOG.info("Chantiers chargés depuis l'API : {} élément(s)", items.size());
applyFilters(items, buildFilters());
} catch (Exception e) {
LOG.error("Erreur chargement chantiers depuis l'API", e);
// En cas d'erreur, on garde une liste vide
items = new ArrayList<>();
} finally {
loading = false;
}
}
private List<Predicate<Chantier>> buildFilters() {
List<Predicate<Chantier>> filters = new ArrayList<>();
if (filtreNom != null && !filtreNom.trim().isEmpty()) {
filters.add(c -> c.getNom().toLowerCase().contains(filtreNom.toLowerCase()));
}
if (filtreClient != null && !filtreClient.trim().isEmpty()) {
filters.add(c -> c.getClient().toLowerCase().contains(filtreClient.toLowerCase()));
}
if (filtreStatut != null && !filtreStatut.trim().isEmpty() && !"TOUS".equals(filtreStatut)) {
filters.add(c -> c.getStatut().equals(filtreStatut));
}
return filters;
}
@Override
protected void resetFilterFields() {
filtreNom = null;
filtreClient = null;
filtreStatut = "TOUS";
}
@Override
protected String getDetailsPath() {
return "/chantiers/";
}
@Override
protected String getCreatePath() {
return "/chantiers/nouveau";
}
@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());
// 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
protected Chantier createNewEntity() {
Chantier c = new Chantier();
c.setStatut("PLANIFIE");
c.setAvancement(0);
c.setDateDebut(LocalDate.now());
c.setDateCreation(LocalDateTime.now());
return c;
}
@Override
protected void performCreate() {
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() {
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
protected Long getEntityId(Chantier chantier) {
return chantier.getId();
}
/**
* Initialise un nouveau chantier pour la création.
*/
@Override
public String createNew() {
prepareNew();
return getCreatePath() + "?faces-redirect=true";
}
/**
* Sauvegarde un nouveau chantier.
*/
public String saveNew() {
if (selectedItem == null) {
selectedItem = new Chantier();
}
selectedItem.setId(System.currentTimeMillis()); // Simulation ID
selectedItem.setDateCreation(LocalDateTime.now());
selectedItem.setDateModification(LocalDateTime.now());
items.add(selectedItem);
LOG.info("Nouveau chantier créé : {}", selectedItem.getNom());
return "/chantiers?faces-redirect=true";
}
/**
* Charge un chantier par son ID depuis les paramètres de la requête.
*/
public void loadChantierById() {
if (chantierId != null) {
loadItems(); // S'assurer que les items sont chargés
selectedItem = items.stream()
.filter(c -> c.getId().equals(chantierId))
.findFirst()
.orElse(null);
if (selectedItem == null) {
LOG.warn("Chantier avec ID {} non trouvé", chantierId);
}
}
}
/**
* Affiche les détails d'un chantier.
*/
public String viewDetails(Long id) {
selectedItem = items.stream()
.filter(c -> c.getId().equals(id))
.findFirst()
.orElse(null);
if (selectedItem != null) {
return getDetailsPath() + "details?id=" + id + "&faces-redirect=true";
}
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; // 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; // Raison sociale du client
private String clientId; // UUID du client (pour les opérations CRUD)
private String adresse;
private LocalDate dateDebut;
private LocalDate dateFinPrevue;
private String statut;
private int avancement;
private double budget;
private double coutReel;
private LocalDateTime dateCreation;
private LocalDateTime dateModification;
}
}

View File

@@ -1,250 +1,249 @@
package dev.lions.btpxpress.view;
import dev.lions.btpxpress.service.ClientService;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.Getter;
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;
import java.util.Map;
import java.util.function.Predicate;
@Named("clientsView")
@ViewScoped
@Getter
@Setter
public class ClientsView extends BaseListView<ClientsView.Client, Long> implements Serializable {
private static final Logger LOG = LoggerFactory.getLogger(ClientsView.class);
@Inject
ClientService clientService;
private String filtreNom;
private String filtreEmail;
private String filtreVille;
private Long clientId;
@PostConstruct
public void init() {
loadItems();
}
@Override
public void loadItems() {
loading = true;
try {
items = new ArrayList<>();
// Récupération depuis l'API backend
List<Map<String, Object>> clientsData = clientService.getAllClients();
for (Map<String, Object> data : clientsData) {
Client c = new Client();
// Mapping des données de l'API vers l'objet Client
c.setId(data.get("id") != null ? Long.valueOf(data.get("id").toString().hashCode()) : null);
// Raison sociale : entreprise ou "Particulier" si vide
String entreprise = (String) data.get("entreprise");
c.setRaisonSociale(entreprise != null && !entreprise.trim().isEmpty() ?
entreprise : "Particulier");
// Nom complet du contact : prénom + nom
String prenom = (String) data.get("prenom");
String nom = (String) data.get("nom");
c.setNomContact((prenom != null ? prenom + " " : "") + (nom != null ? nom : ""));
c.setEmail((String) data.get("email"));
c.setTelephone((String) data.get("telephone"));
c.setAdresse((String) data.get("adresse"));
c.setVille((String) data.get("ville"));
c.setCodePostal((String) data.get("codePostal"));
// Nombre de chantiers (relation)
Object chantiersObj = data.get("chantiers");
if (chantiersObj instanceof List) {
c.setNombreChantiers(((List<?>) chantiersObj).size());
} else {
c.setNombreChantiers(0);
}
// Chiffre d'affaires total (à calculer ou récupérer)
// Pour l'instant, on met 0 car cette donnée n'est pas dans l'API
c.setChiffreAffairesTotal(0.0);
// Date de création
if (data.get("dateCreation") != null) {
c.setDateCreation(LocalDateTime.parse(data.get("dateCreation").toString()));
}
// Date de modification
if (data.get("dateModification") != null) {
c.setDateModification(LocalDateTime.parse(data.get("dateModification").toString()));
}
items.add(c);
}
LOG.info("Clients chargés depuis l'API : {} élément(s)", items.size());
applyFilters(items, buildFilters());
} catch (Exception e) {
LOG.error("Erreur chargement clients depuis l'API", e);
// En cas d'erreur, on garde une liste vide
items = new ArrayList<>();
} finally {
loading = false;
}
}
private List<Predicate<Client>> buildFilters() {
List<Predicate<Client>> filters = new ArrayList<>();
if (filtreNom != null && !filtreNom.trim().isEmpty()) {
filters.add(c -> c.getRaisonSociale().toLowerCase().contains(filtreNom.toLowerCase()) ||
c.getNomContact().toLowerCase().contains(filtreNom.toLowerCase()));
}
if (filtreEmail != null && !filtreEmail.trim().isEmpty()) {
filters.add(c -> c.getEmail().toLowerCase().contains(filtreEmail.toLowerCase()));
}
if (filtreVille != null && !filtreVille.trim().isEmpty()) {
filters.add(c -> c.getVille().toLowerCase().contains(filtreVille.toLowerCase()));
}
return filters;
}
@Override
protected void resetFilterFields() {
filtreNom = null;
filtreEmail = null;
filtreVille = null;
}
@Override
protected String getDetailsPath() {
return "/clients/";
}
@Override
protected String getCreatePath() {
return "/clients/nouveau";
}
@Override
protected void performDelete() {
LOG.info("Suppression client : {}", selectedItem.getId());
}
@Override
protected Client createNewEntity() {
Client client = new Client();
client.setDateCreation(LocalDateTime.now());
client.setDateModification(LocalDateTime.now());
client.setNombreChantiers(0);
client.setChiffreAffairesTotal(0.0);
return client;
}
@Override
protected void performCreate() {
if (selectedItem.getId() == null) {
selectedItem.setId(System.currentTimeMillis());
}
selectedItem.setDateCreation(LocalDateTime.now());
selectedItem.setDateModification(LocalDateTime.now());
items.add(selectedItem);
LOG.info("Created: {}", selectedItem);
}
@Override
protected void performUpdate() {
selectedItem.setDateModification(LocalDateTime.now());
LOG.info("Updated: {}", selectedItem);
}
@Override
protected Long getEntityId(Client entity) {
return entity.getId();
}
/**
* Initialise un nouveau client pour la création.
*/
@Override
public String createNew() {
selectedItem = new Client();
return getCreatePath() + "?faces-redirect=true";
}
/**
* Sauvegarde un nouveau client.
*/
public String saveNew() {
if (selectedItem == null) {
selectedItem = new Client();
}
selectedItem.setId(System.currentTimeMillis());
selectedItem.setDateCreation(LocalDateTime.now());
selectedItem.setDateModification(LocalDateTime.now());
selectedItem.setNombreChantiers(0);
selectedItem.setChiffreAffairesTotal(0.0);
items.add(selectedItem);
LOG.info("Nouveau client créé : {}", selectedItem.getRaisonSociale());
return "/clients?faces-redirect=true";
}
/**
* Affiche les détails d'un client.
*/
public String viewDetails(Long id) {
selectedItem = items.stream()
.filter(c -> c.getId().equals(id))
.findFirst()
.orElse(null);
if (selectedItem != null) {
return getDetailsPath() + "details?id=" + id + "&faces-redirect=true";
}
return "/clients?faces-redirect=true";
}
/**
* Charge un client par son ID depuis les paramètres de la requête.
*/
public void loadClientById() {
if (clientId != null) {
loadItems(); // S'assurer que les items sont chargés
selectedItem = items.stream()
.filter(c -> c.getId().equals(clientId))
.findFirst()
.orElse(null);
if (selectedItem == null) {
LOG.warn("Client avec ID {} non trouvé", clientId);
}
}
}
@lombok.Getter
@lombok.Setter
public static class Client {
private Long id;
private String raisonSociale;
private String nomContact;
private String email;
private String telephone;
private String adresse;
private String ville;
private String codePostal;
private String pays;
private int nombreChantiers;
private double chiffreAffairesTotal;
private LocalDateTime dateCreation;
private LocalDateTime dateModification;
}
}
package dev.lions.btpxpress.view;
import dev.lions.btpxpress.service.ClientService;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
@Named("clientsView")
@ViewScoped
@Getter
@Setter
public class ClientsView extends BaseListView<ClientsView.Client, Long> {
private static final Logger LOG = LoggerFactory.getLogger(ClientsView.class);
@Inject
ClientService clientService;
private String filtreNom;
private String filtreEmail;
private String filtreVille;
private Long clientId;
@PostConstruct
public void init() {
loadItems();
}
@Override
public void loadItems() {
loading = true;
try {
items = new ArrayList<>();
// Récupération depuis l'API backend
List<Map<String, Object>> clientsData = clientService.getAllClients();
for (Map<String, Object> data : clientsData) {
Client c = new Client();
// Mapping des données de l'API vers l'objet Client
c.setId(data.get("id") != null ? Long.valueOf(data.get("id").toString().hashCode()) : null);
// Raison sociale : entreprise ou "Particulier" si vide
String entreprise = (String) data.get("entreprise");
c.setRaisonSociale(entreprise != null && !entreprise.trim().isEmpty() ?
entreprise : "Particulier");
// Nom complet du contact : prénom + nom
String prenom = (String) data.get("prenom");
String nom = (String) data.get("nom");
c.setNomContact((prenom != null ? prenom + " " : "") + (nom != null ? nom : ""));
c.setEmail((String) data.get("email"));
c.setTelephone((String) data.get("telephone"));
c.setAdresse((String) data.get("adresse"));
c.setVille((String) data.get("ville"));
c.setCodePostal((String) data.get("codePostal"));
// Nombre de chantiers (relation)
Object chantiersObj = data.get("chantiers");
if (chantiersObj instanceof List) {
c.setNombreChantiers(((List<?>) chantiersObj).size());
} else {
c.setNombreChantiers(0);
}
// Chiffre d'affaires total (à calculer ou récupérer)
// Pour l'instant, on met 0 car cette donnée n'est pas dans l'API
c.setChiffreAffairesTotal(0.0);
// Date de création
if (data.get("dateCreation") != null) {
c.setDateCreation(LocalDateTime.parse(data.get("dateCreation").toString()));
}
// Date de modification
if (data.get("dateModification") != null) {
c.setDateModification(LocalDateTime.parse(data.get("dateModification").toString()));
}
items.add(c);
}
LOG.info("Clients chargés depuis l'API : {} élément(s)", items.size());
applyFilters(items, buildFilters());
} catch (Exception e) {
LOG.error("Erreur chargement clients depuis l'API", e);
// En cas d'erreur, on garde une liste vide
items = new ArrayList<>();
} finally {
loading = false;
}
}
private List<Predicate<Client>> buildFilters() {
List<Predicate<Client>> filters = new ArrayList<>();
if (filtreNom != null && !filtreNom.trim().isEmpty()) {
filters.add(c -> c.getRaisonSociale().toLowerCase().contains(filtreNom.toLowerCase()) ||
c.getNomContact().toLowerCase().contains(filtreNom.toLowerCase()));
}
if (filtreEmail != null && !filtreEmail.trim().isEmpty()) {
filters.add(c -> c.getEmail().toLowerCase().contains(filtreEmail.toLowerCase()));
}
if (filtreVille != null && !filtreVille.trim().isEmpty()) {
filters.add(c -> c.getVille().toLowerCase().contains(filtreVille.toLowerCase()));
}
return filters;
}
@Override
protected void resetFilterFields() {
filtreNom = null;
filtreEmail = null;
filtreVille = null;
}
@Override
protected String getDetailsPath() {
return "/clients/";
}
@Override
protected String getCreatePath() {
return "/clients/nouveau";
}
@Override
protected void performDelete() {
LOG.info("Suppression client : {}", selectedItem.getId());
}
@Override
protected Client createNewEntity() {
Client client = new Client();
client.setDateCreation(LocalDateTime.now());
client.setDateModification(LocalDateTime.now());
client.setNombreChantiers(0);
client.setChiffreAffairesTotal(0.0);
return client;
}
@Override
protected void performCreate() {
if (selectedItem.getId() == null) {
selectedItem.setId(System.currentTimeMillis());
}
selectedItem.setDateCreation(LocalDateTime.now());
selectedItem.setDateModification(LocalDateTime.now());
items.add(selectedItem);
LOG.info("Created: {}", selectedItem);
}
@Override
protected void performUpdate() {
selectedItem.setDateModification(LocalDateTime.now());
LOG.info("Updated: {}", selectedItem);
}
@Override
protected Long getEntityId(Client entity) {
return entity.getId();
}
/**
* Initialise un nouveau client pour la création.
*/
@Override
public String createNew() {
selectedItem = new Client();
return getCreatePath() + "?faces-redirect=true";
}
/**
* Sauvegarde un nouveau client.
*/
public String saveNew() {
if (selectedItem == null) {
selectedItem = new Client();
}
selectedItem.setId(System.currentTimeMillis());
selectedItem.setDateCreation(LocalDateTime.now());
selectedItem.setDateModification(LocalDateTime.now());
selectedItem.setNombreChantiers(0);
selectedItem.setChiffreAffairesTotal(0.0);
items.add(selectedItem);
LOG.info("Nouveau client créé : {}", selectedItem.getRaisonSociale());
return "/clients?faces-redirect=true";
}
/**
* Affiche les détails d'un client.
*/
public String viewDetails(Long id) {
selectedItem = items.stream()
.filter(c -> c.getId().equals(id))
.findFirst()
.orElse(null);
if (selectedItem != null) {
return getDetailsPath() + "details?id=" + id + "&faces-redirect=true";
}
return "/clients?faces-redirect=true";
}
/**
* Charge un client par son ID depuis les paramètres de la requête.
*/
public void loadClientById() {
if (clientId != null) {
loadItems(); // S'assurer que les items sont chargés
selectedItem = items.stream()
.filter(c -> c.getId().equals(clientId))
.findFirst()
.orElse(null);
if (selectedItem == null) {
LOG.warn("Client avec ID {} non trouvé", clientId);
}
}
}
@lombok.Getter
@lombok.Setter
public static class Client {
private Long id;
private String raisonSociale;
private String nomContact;
private String email;
private String telephone;
private String adresse;
private String ville;
private String codePostal;
private String pays;
private int nombreChantiers;
private double chiffreAffairesTotal;
private LocalDateTime dateCreation;
private LocalDateTime dateModification;
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,53 +1,53 @@
package dev.lions.btpxpress.view;
import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Named;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Named("loginView")
@RequestScoped
@Getter
@Setter
public class LoginView implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private String password;
private boolean rememberMe = false;
public String login() {
if (username == null || username.trim().isEmpty()) {
addErrorMessage("Le nom d'utilisateur est requis");
return null;
}
if (password == null || password.trim().isEmpty()) {
addErrorMessage("Le mot de passe est requis");
return null;
}
if ("admin".equals(username) && "admin".equals(password)) {
addInfoMessage("Connexion réussie !");
return "/dashboard?faces-redirect=true";
} else {
addErrorMessage("Nom d'utilisateur ou mot de passe incorrect");
return null;
}
}
private void addErrorMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
}
private void addInfoMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Information", message));
}
}
package dev.lions.btpxpress.view;
import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Named;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Named("loginView")
@RequestScoped
@Getter
@Setter
public class LoginView implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private String password;
private boolean rememberMe = false;
public String login() {
if (username == null || username.trim().isEmpty()) {
addErrorMessage("Le nom d'utilisateur est requis");
return null;
}
if (password == null || password.trim().isEmpty()) {
addErrorMessage("Le mot de passe est requis");
return null;
}
if ("admin".equals(username) && "admin".equals(password)) {
addInfoMessage("Connexion réussie !");
return "/dashboard?faces-redirect=true";
} else {
addErrorMessage("Nom d'utilisateur ou mot de passe incorrect");
return null;
}
}
private void addErrorMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
}
private void addInfoMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Information", message));
}
}

View File

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

View File

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

View File

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

View File

@@ -1,31 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<faces-config version="4.0"
xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-facesconfig_4_0.xsd">
<name>btpxpress_freya</name>
<application>
<locale-config>
<default-locale>fr</default-locale>
<supported-locale>fr</supported-locale>
<supported-locale>en</supported-locale>
</locale-config>
</application>
<component>
<component-type>org.primefaces.component.FreyaMenu</component-type>
<component-class>org.primefaces.freya.component.FreyaMenu</component-class>
</component>
<render-kit>
<renderer>
<component-family>org.primefaces.component</component-family>
<renderer-type>org.primefaces.component.FreyaMenuRenderer</renderer-type>
<renderer-class>org.primefaces.freya.component.FreyaMenuRenderer</renderer-class>
</renderer>
</render-kit>
</faces-config>
<?xml version="1.0" encoding="UTF-8"?>
<faces-config version="4.0"
xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-facesconfig_4_0.xsd">
<name>btpxpress_freya</name>
<application>
<locale-config>
<default-locale>fr</default-locale>
<supported-locale>fr</supported-locale>
<supported-locale>en</supported-locale>
</locale-config>
</application>
<component>
<component-type>org.primefaces.component.FreyaMenu</component-type>
<component-class>org.primefaces.freya.component.FreyaMenu</component-class>
</component>
<render-kit>
<renderer>
<component-family>org.primefaces.component</component-family>
<renderer-type>org.primefaces.component.FreyaMenuRenderer</renderer-type>
<renderer-class>org.primefaces.freya.component.FreyaMenuRenderer</renderer-class>
</renderer>
</render-kit>
</faces-config>

View File

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

View File

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

View File

@@ -1,33 +1,33 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value=""/>
<ui:param name="tableId" value=""/>
<div class="card">
<h5>Recherche et filtres</h5>
<h:form id="#{formId}">
<ui:insert name="filter-fields">
<!-- Les champs de filtre spécifiques à chaque page -->
</ui:insert>
<div class="col-12">
<p:commandButton value="Rechercher"
icon="pi pi-search"
action="#{viewBean.search()}"
update="#{tableId}"
styleClass="ui-button-primary"/>
<p:commandButton value="Réinitialiser"
icon="pi pi-refresh"
action="#{viewBean.resetFilters()}"
update="#{tableId} #{formId}"
styleClass="ui-button-secondary"/>
</div>
</h:form>
</div>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value=""/>
<ui:param name="tableId" value=""/>
<div class="card">
<h5>Recherche et filtres</h5>
<h:form id="#{formId}">
<ui:insert name="filter-fields">
<!-- Les champs de filtre spécifiques à chaque page -->
</ui:insert>
<div class="col-12">
<p:commandButton value="Rechercher"
icon="pi pi-search"
action="#{viewBean.search()}"
update="#{tableId}"
styleClass="ui-button-primary"/>
<p:commandButton value="Réinitialiser"
icon="pi pi-refresh"
action="#{viewBean.resetFilters()}"
update="#{tableId} #{formId}"
styleClass="ui-button-secondary"/>
</div>
</h:form>
</div>
</ui:composition>

View File

@@ -1,44 +1,44 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h5>#{title}</h5>
<p:commandButton value="Nouveau"
icon="pi pi-plus"
action="#{viewBean.createNew()}"
styleClass="ui-button-primary"
rendered="#{not empty createPath}"/>
</div>
<h:form id="#{formId}">
<p:dataTable id="#{tableId}"
value="#{viewBean.items}"
var="#{var}"
rowKey="id"
paginator="true"
rows="10"
rowsPerPageTemplate="10,20,50"
emptyMessage="Aucun résultat trouvé"
loading="#{viewBean.loading}"
selection="#{viewBean.selectedItem}"
selectionMode="single">
<f:facet name="header">
<div class="flex align-items-center justify-content-between">
<span>Total : #{viewBean.items.size()} élément(s)</span>
<p:commandButton icon="pi pi-trash"
title="Supprimer"
disabled="#{empty viewBean.selectedItem}"
action="#{viewBean.delete()}"
update="#{tableId}"
styleClass="ui-button-danger ui-button-text"/>
</div>
</f:facet>
<ui:insert name="columns"/>
</p:dataTable>
</h:form>
</div>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h5>#{title}</h5>
<p:commandButton value="Nouveau"
icon="pi pi-plus"
action="#{viewBean.createNew()}"
styleClass="ui-button-primary"
rendered="#{not empty createPath}"/>
</div>
<h:form id="#{formId}">
<p:dataTable id="#{tableId}"
value="#{viewBean.items}"
var="#{var}"
rowKey="id"
paginator="true"
rows="10"
rowsPerPageTemplate="10,20,50"
emptyMessage="Aucun résultat trouvé"
loading="#{viewBean.loading}"
selection="#{viewBean.selectedItem}"
selectionMode="single">
<f:facet name="header">
<div class="flex align-items-center justify-content-between">
<span>Total : #{viewBean.items.size()} élément(s)</span>
<p:commandButton icon="pi pi-trash"
title="Supprimer"
disabled="#{empty viewBean.selectedItem}"
action="#{viewBean.delete()}"
update="#{tableId}"
styleClass="ui-button-danger ui-button-text"/>
</div>
</f:facet>
<ui:insert name="columns"/>
</p:dataTable>
</h:form>
</div>
</ui:composition>

View File

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

View File

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

View File

@@ -1,94 +1,94 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui">
<a href="#" id="layout-config-button" class="layout-config-button">
<i class="pi pi-cog"/>
</a>
<div id="layout-config" class="layout-config">
<h:form id="config-form" styleClass="layout-config-form">
<h5 style="margin-top: 0">Type de Menu</h5>
<p:selectOneRadio value="#{guestPreferences.menuMode}" layout="pageDirection"
onchange="PrimeFaces.FreyaConfigurator.changeMenuMode(event.target.value)" >
<f:selectItem itemLabel="Horizontal" itemValue="layout-horizontal" />
<f:selectItem itemLabel="Sidebar" itemValue="layout-sidebar" />
<f:selectItem itemLabel="Slim" itemValue="layout-slim" />
<p:ajax listener="#{guestPreferences.onMenuTypeChange}" update="config-form" />
</p:selectOneRadio>
<hr/>
<h5>Schéma de Couleurs</h5>
<p:selectOneRadio value="#{guestPreferences.darkMode}" layout="pageDirection"
onchange="PrimeFaces.FreyaConfigurator.changeLayout('#{guestPreferences.componentTheme}', event.target.value)" >
<f:selectItem itemLabel="Clair" itemValue="light" />
<f:selectItem itemLabel="Sombre" itemValue="dark" />
<p:ajax onstart="PrimeFaces.FreyaConfigurator.beforeResourceChange()" update="config-form logolink"/>
</p:selectOneRadio>
<p:outputPanel rendered="#{guestPreferences.menuMode eq 'layout-horizontal'}">
<hr/>
<h5>Mode Topbar et Menu</h5>
<p:selectOneRadio value="#{guestPreferences.topbarTheme}" layout="pageDirection"
onchange="PrimeFaces.FreyaConfigurator.changeSectionTheme(event.target.value , 'layout-topbar');PrimeFaces.FreyaConfigurator.changeSectionTheme(event.target.value , 'layout-menu')" >
<f:selectItem itemLabel="Clair" itemValue="light" itemDisabled="#{guestPreferences.darkMode != 'light'}" />
<f:selectItem itemLabel="Sombre" itemValue="dark" itemDisabled="#{guestPreferences.darkMode != 'light'}"/>
<p:ajax update="logolink config-form"/>
</p:selectOneRadio>
</p:outputPanel>
<p:outputPanel rendered="#{guestPreferences.menuMode != 'layout-horizontal'}">
<hr/>
<h5>Mode Topbar</h5>
<p:selectOneRadio value="#{guestPreferences.topbarTheme}" layout="pageDirection"
onchange="PrimeFaces.FreyaConfigurator.changeSectionTheme(event.target.value , 'layout-topbar')" >
<f:selectItem itemLabel="Clair" itemValue="light" itemDisabled="#{guestPreferences.darkMode != 'light'}" />
<f:selectItem itemLabel="Sombre" itemValue="dark" itemDisabled="#{guestPreferences.darkMode != 'light'}"/>
<p:ajax update="logolink config-form"/>
</p:selectOneRadio>
</p:outputPanel>
<p:outputPanel rendered="#{guestPreferences.menuMode != 'layout-horizontal'}">
<hr/>
<h5>Mode Menu</h5>
<p:selectOneRadio value="#{guestPreferences.menuTheme}" layout="pageDirection"
onchange="PrimeFaces.FreyaConfigurator.changeSectionTheme(event.target.value , 'layout-menu')" >
<f:selectItem itemLabel="Clair" itemValue="light" itemDisabled="#{guestPreferences.darkMode != 'light'}" />
<f:selectItem itemLabel="Sombre" itemValue="dark" itemDisabled="#{guestPreferences.darkMode != 'light'}"/>
<p:ajax update="logolink config-form"/>
</p:selectOneRadio>
</p:outputPanel>
<hr/>
<h5>Style d'Input</h5>
<p:selectOneRadio value="#{guestPreferences.inputStyle}" layout="pageDirection"
onchange="PrimeFaces.FreyaConfigurator.updateInputStyle(event.target.value)">
<f:selectItem itemLabel="Outlined" itemValue="outlined" />
<f:selectItem itemLabel="Filled" itemValue="filled" />
<p:ajax />
</p:selectOneRadio>
<hr/>
<h5>Couleurs du Thème</h5>
<div class="layout-themes">
<ui:repeat value="#{guestPreferences.componentThemes}" var="componentTheme">
<div>
<p:commandLink actionListener="#{guestPreferences.setComponentTheme(componentTheme.file)}"
style="background-color: #{componentTheme.color};" title="#{componentTheme.name}"
process="@this"
update="config-form"
onstart="PrimeFaces.FreyaConfigurator.beforeResourceChange()"
oncomplete="PrimeFaces.FreyaConfigurator.changeComponentsTheme('#{componentTheme.file}', '#{guestPreferences.darkMode}')">
</p:commandLink>
</div>
</ui:repeat>
</div>
</h:form>
</div>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui">
<a href="#" id="layout-config-button" class="layout-config-button">
<i class="pi pi-cog"/>
</a>
<div id="layout-config" class="layout-config">
<h:form id="config-form" styleClass="layout-config-form">
<h5 style="margin-top: 0">Type de Menu</h5>
<p:selectOneRadio value="#{guestPreferences.menuMode}" layout="pageDirection"
onchange="PrimeFaces.FreyaConfigurator.changeMenuMode(event.target.value)" >
<f:selectItem itemLabel="Horizontal" itemValue="layout-horizontal" />
<f:selectItem itemLabel="Sidebar" itemValue="layout-sidebar" />
<f:selectItem itemLabel="Slim" itemValue="layout-slim" />
<p:ajax listener="#{guestPreferences.onMenuTypeChange}" update="config-form" />
</p:selectOneRadio>
<hr/>
<h5>Schéma de Couleurs</h5>
<p:selectOneRadio value="#{guestPreferences.darkMode}" layout="pageDirection"
onchange="PrimeFaces.FreyaConfigurator.changeLayout('#{guestPreferences.componentTheme}', event.target.value)" >
<f:selectItem itemLabel="Clair" itemValue="light" />
<f:selectItem itemLabel="Sombre" itemValue="dark" />
<p:ajax onstart="PrimeFaces.FreyaConfigurator.beforeResourceChange()" update="config-form logolink"/>
</p:selectOneRadio>
<p:outputPanel rendered="#{guestPreferences.menuMode eq 'layout-horizontal'}">
<hr/>
<h5>Mode Topbar et Menu</h5>
<p:selectOneRadio value="#{guestPreferences.topbarTheme}" layout="pageDirection"
onchange="PrimeFaces.FreyaConfigurator.changeSectionTheme(event.target.value , 'layout-topbar');PrimeFaces.FreyaConfigurator.changeSectionTheme(event.target.value , 'layout-menu')" >
<f:selectItem itemLabel="Clair" itemValue="light" itemDisabled="#{guestPreferences.darkMode != 'light'}" />
<f:selectItem itemLabel="Sombre" itemValue="dark" itemDisabled="#{guestPreferences.darkMode != 'light'}"/>
<p:ajax update="logolink config-form"/>
</p:selectOneRadio>
</p:outputPanel>
<p:outputPanel rendered="#{guestPreferences.menuMode != 'layout-horizontal'}">
<hr/>
<h5>Mode Topbar</h5>
<p:selectOneRadio value="#{guestPreferences.topbarTheme}" layout="pageDirection"
onchange="PrimeFaces.FreyaConfigurator.changeSectionTheme(event.target.value , 'layout-topbar')" >
<f:selectItem itemLabel="Clair" itemValue="light" itemDisabled="#{guestPreferences.darkMode != 'light'}" />
<f:selectItem itemLabel="Sombre" itemValue="dark" itemDisabled="#{guestPreferences.darkMode != 'light'}"/>
<p:ajax update="logolink config-form"/>
</p:selectOneRadio>
</p:outputPanel>
<p:outputPanel rendered="#{guestPreferences.menuMode != 'layout-horizontal'}">
<hr/>
<h5>Mode Menu</h5>
<p:selectOneRadio value="#{guestPreferences.menuTheme}" layout="pageDirection"
onchange="PrimeFaces.FreyaConfigurator.changeSectionTheme(event.target.value , 'layout-menu')" >
<f:selectItem itemLabel="Clair" itemValue="light" itemDisabled="#{guestPreferences.darkMode != 'light'}" />
<f:selectItem itemLabel="Sombre" itemValue="dark" itemDisabled="#{guestPreferences.darkMode != 'light'}"/>
<p:ajax update="logolink config-form"/>
</p:selectOneRadio>
</p:outputPanel>
<hr/>
<h5>Style d'Input</h5>
<p:selectOneRadio value="#{guestPreferences.inputStyle}" layout="pageDirection"
onchange="PrimeFaces.FreyaConfigurator.updateInputStyle(event.target.value)">
<f:selectItem itemLabel="Outlined" itemValue="outlined" />
<f:selectItem itemLabel="Filled" itemValue="filled" />
<p:ajax />
</p:selectOneRadio>
<hr/>
<h5>Couleurs du Thème</h5>
<div class="layout-themes">
<ui:repeat value="#{guestPreferences.componentThemes}" var="componentTheme">
<div>
<p:commandLink actionListener="#{guestPreferences.setComponentTheme(componentTheme.file)}"
style="background-color: #{componentTheme.color};" title="#{componentTheme.name}"
process="@this"
update="config-form"
onstart="PrimeFaces.FreyaConfigurator.beforeResourceChange()"
oncomplete="PrimeFaces.FreyaConfigurator.changeComponentsTheme('#{componentTheme.file}', '#{guestPreferences.darkMode}')">
</p:commandLink>
</div>
</ui:repeat>
</div>
</h:form>
</div>
</ui:composition>

View File

@@ -1,65 +1,65 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui">
<div class="layout-rightpanel">
<div class="rightpanel-wrapper">
<div class="rightpanel-section tasks-section">
<div class="section-header">
<h6>Mes Tâches</h6>
<h:form>
<p:commandButton type="button" icon="pi pi-plus" styleClass="ui-button-secondary ui-button-flat rounded-button" />
</h:form>
</div>
<ul>
<li>
<div class="task-info">
<h6>Réviser le devis pour le chantier A</h6>
<span>-Validation budgétaire</span>
<span>-Vérification matériaux</span>
</div>
</li>
<li>
<div class="task-info">
<h6>Planifier la maintenance préventive</h6>
<span>Matériel : Pelleteuse BX-2024</span>
</div>
</li>
<li class="done">
<div class="task-info">
<h6>Finaliser le rapport hebdomadaire</h6>
</div>
<i class="pi pi-check"></i>
</li>
</ul>
</div>
<div class="rightpanel-section favorites-section">
<div class="section-header">
<h6>Favoris</h6>
</div>
<div class="favorite-items">
<a href="dashboard.xhtml" class="favorite-item">
<i class="pi pi-home" style="font-size: 1.5rem;"></i>
</a>
<a href="chantiers.xhtml" class="favorite-item">
<i class="pi pi-building" style="font-size: 1.5rem;"></i>
</a>
<a href="clients.xhtml" class="favorite-item">
<i class="pi pi-users" style="font-size: 1.5rem;"></i>
</a>
<a href="rapports.xhtml" class="favorite-item">
<i class="pi pi-chart-bar" style="font-size: 1.5rem;"></i>
</a>
<a href="#" class="add-item">
<i class="pi pi-plus"></i>
</a>
</div>
</div>
</div>
</div>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui">
<div class="layout-rightpanel">
<div class="rightpanel-wrapper">
<div class="rightpanel-section tasks-section">
<div class="section-header">
<h6>Mes Tâches</h6>
<h:form>
<p:commandButton type="button" icon="pi pi-plus" styleClass="ui-button-secondary ui-button-flat rounded-button" />
</h:form>
</div>
<ul>
<li>
<div class="task-info">
<h6>Réviser le devis pour le chantier A</h6>
<span>-Validation budgétaire</span>
<span>-Vérification matériaux</span>
</div>
</li>
<li>
<div class="task-info">
<h6>Planifier la maintenance préventive</h6>
<span>Matériel : Pelleteuse BX-2024</span>
</div>
</li>
<li class="done">
<div class="task-info">
<h6>Finaliser le rapport hebdomadaire</h6>
</div>
<i class="pi pi-check"></i>
</li>
</ul>
</div>
<div class="rightpanel-section favorites-section">
<div class="section-header">
<h6>Favoris</h6>
</div>
<div class="favorite-items">
<a href="dashboard.xhtml" class="favorite-item">
<i class="pi pi-home" style="font-size: 1.5rem;"></i>
</a>
<a href="chantiers.xhtml" class="favorite-item">
<i class="pi pi-building" style="font-size: 1.5rem;"></i>
</a>
<a href="clients.xhtml" class="favorite-item">
<i class="pi pi-users" style="font-size: 1.5rem;"></i>
</a>
<a href="rapports.xhtml" class="favorite-item">
<i class="pi pi-chart-bar" style="font-size: 1.5rem;"></i>
</a>
<a href="#" class="add-item">
<i class="pi pi-plus"></i>
</a>
</div>
</div>
</div>
</div>
</ui:composition>

View File

@@ -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">

View File

@@ -1,130 +1,130 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:po="http://primefaces.org/freya">
<div class="layout-topbar">
<div class="layout-topbar-wrapper">
<div class="layout-topbar-left">
<a href="#" class="menu-button">
<i class="pi pi-bars"/>
</a>
<h:link id="logolink" outcome="/dashboard" styleClass="layout-topbar-logo">
<p:graphicImage name="images/#{ guestPreferences.lightLogo ? 'logo-freya-white.svg' : 'logo-freya.svg'}" library="freya-layout" />
</h:link>
</div>
<ui:include src="./menu.xhtml" />
<div class="layout-topbar-right">
<ul class="layout-topbar-actions">
<li class="topbar-item search-item">
<a href="#">
<i class="topbar-icon pi pi-search"/>
</a>
<h:form>
<h:panelGroup styleClass="search-input-wrapper">
<p:inputText placeholder="Search..." />
<i class="pi pi-search"/>
</h:panelGroup>
</h:form>
<ul>
<h:form onsubmit="return false;">
<h:panelGroup styleClass="search-input-wrapper">
<p:inputText placeholder="Search..." />
<i class="pi pi-search"/>
</h:panelGroup>
</h:form>
</ul>
</li>
<li class="topbar-item notifications-item">
<a href="/notifications">
<i class="topbar-icon pi pi-bell"/>
<p:outputPanel rendered="#{userSession.nombreNotificationsNonLues > 0}">
<span class="ui-badge">#{userSession.nombreNotificationsNonLues}</span>
</p:outputPanel>
</a>
</li>
<li class="topbar-item messages-item">
<a href="/messages">
<i class="topbar-icon pi pi-envelope"/>
<p:outputPanel rendered="#{userSession.nombreMessagesNonLus > 0}">
<span class="ui-badge">#{userSession.nombreMessagesNonLus}</span>
</p:outputPanel>
</a>
</li>
<li class="topbar-item user-profile">
<a href="#">
<p:graphicImage name="images/avatar-profilemenu.png" library="freya-layout" />
</a>
<ul>
<li class="user-profile-header">
<div class="user-info">
<p:graphicImage name="images/avatar-profilemenu.png" library="freya-layout" styleClass="profile-avatar-small" />
<div class="user-details">
<span class="user-name">#{userSession.nomComplet}</span>
<span class="user-role">#{userSession.role}</span>
</div>
</div>
</li>
<li class="user-profile-divider">
<hr/>
</li>
<li>
<a href="/profile">
<i class="pi pi-user"></i>
<span>Profile</span>
</a>
</li>
<li>
<a href="#">
<i class="pi pi-cog"></i>
<span>Settings</span>
</a>
</li>
<li>
<a href="/messages">
<i class="pi pi-envelope"></i>
<span>Messages</span>
<p:outputPanel rendered="#{userSession.nombreMessagesNonLus > 0}">
<span class="ui-badge">#{userSession.nombreMessagesNonLus}</span>
</p:outputPanel>
</a>
</li>
<li>
<a href="/notifications">
<i class="pi pi-bell"></i>
<span>Notifications</span>
<p:outputPanel rendered="#{userSession.nombreNotificationsNonLues > 0}">
<span class="ui-badge">#{userSession.nombreNotificationsNonLues}</span>
</p:outputPanel>
</a>
</li>
<li class="user-profile-divider">
<hr/>
</li>
<li>
<h:form>
<p:commandButton action="#{userSession.deconnecter()}"
value="Logout"
styleClass="logout-link p-button-text"
ajax="false"
icon="pi pi-sign-out"/>
</h:form>
</li>
</ul>
</li>
</ul>
<a href="#" class="layout-rightpanel-button">
<i class="pi pi-arrow-left"/>
</a>
</div>
</div>
</div>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:po="http://primefaces.org/freya">
<div class="layout-topbar">
<div class="layout-topbar-wrapper">
<div class="layout-topbar-left">
<a href="#" class="menu-button">
<i class="pi pi-bars"/>
</a>
<h:link id="logolink" outcome="/dashboard" styleClass="layout-topbar-logo">
<p:graphicImage name="images/#{ guestPreferences.lightLogo ? 'logo-freya-white.svg' : 'logo-freya.svg'}" library="freya-layout" />
</h:link>
</div>
<ui:include src="./menu.xhtml" />
<div class="layout-topbar-right">
<ul class="layout-topbar-actions">
<li class="topbar-item search-item">
<a href="#">
<i class="topbar-icon pi pi-search"/>
</a>
<h:form>
<h:panelGroup styleClass="search-input-wrapper">
<p:inputText placeholder="Search..." />
<i class="pi pi-search"/>
</h:panelGroup>
</h:form>
<ul>
<h:form onsubmit="return false;">
<h:panelGroup styleClass="search-input-wrapper">
<p:inputText placeholder="Search..." />
<i class="pi pi-search"/>
</h:panelGroup>
</h:form>
</ul>
</li>
<li class="topbar-item notifications-item">
<a href="/notifications">
<i class="topbar-icon pi pi-bell"/>
<p:outputPanel rendered="#{userSession.nombreNotificationsNonLues > 0}">
<span class="ui-badge">#{userSession.nombreNotificationsNonLues}</span>
</p:outputPanel>
</a>
</li>
<li class="topbar-item messages-item">
<a href="/messages">
<i class="topbar-icon pi pi-envelope"/>
<p:outputPanel rendered="#{userSession.nombreMessagesNonLus > 0}">
<span class="ui-badge">#{userSession.nombreMessagesNonLus}</span>
</p:outputPanel>
</a>
</li>
<li class="topbar-item user-profile">
<a href="#">
<p:graphicImage name="images/avatar-profilemenu.png" library="freya-layout" />
</a>
<ul>
<li class="user-profile-header">
<div class="user-info">
<p:graphicImage name="images/avatar-profilemenu.png" library="freya-layout" styleClass="profile-avatar-small" />
<div class="user-details">
<span class="user-name">#{userSession.nomComplet}</span>
<span class="user-role">#{userSession.role}</span>
</div>
</div>
</li>
<li class="user-profile-divider">
<hr/>
</li>
<li>
<a href="/profile">
<i class="pi pi-user"></i>
<span>Profile</span>
</a>
</li>
<li>
<a href="#">
<i class="pi pi-cog"></i>
<span>Settings</span>
</a>
</li>
<li>
<a href="/messages">
<i class="pi pi-envelope"></i>
<span>Messages</span>
<p:outputPanel rendered="#{userSession.nombreMessagesNonLus > 0}">
<span class="ui-badge">#{userSession.nombreMessagesNonLus}</span>
</p:outputPanel>
</a>
</li>
<li>
<a href="/notifications">
<i class="pi pi-bell"></i>
<span>Notifications</span>
<p:outputPanel rendered="#{userSession.nombreNotificationsNonLues > 0}">
<span class="ui-badge">#{userSession.nombreNotificationsNonLues}</span>
</p:outputPanel>
</a>
</li>
<li class="user-profile-divider">
<hr/>
</li>
<li>
<h:form>
<p:commandButton action="#{userSession.deconnecter()}"
value="Logout"
styleClass="logout-link p-button-text"
ajax="false"
icon="pi pi-sign-out"/>
</h:form>
</li>
</ul>
</li>
</ul>
<a href="#" class="layout-rightpanel-button">
<i class="pi pi-arrow-left"/>
</a>
</div>
</div>
</div>
</ui:composition>

View File

@@ -1,28 +1,28 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Bons de commande - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Bons de commande</h6>
<p class="subtitle">Gestion des bons de commande</p>
</div>
</div>
<p>Page en développement</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Bons de commande - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Bons de commande</h6>
<p class="subtitle">Gestion des bons de commande</p>
</div>
</div>
<p>Page en développement</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,28 +1,28 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Budgets - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Budgets</h6>
<p class="subtitle">Gestion des budgets</p>
</div>
</div>
<p>Page en développement</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Budgets - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Budgets</h6>
<p class="subtitle">Gestion des budgets</p>
</div>
</div>
<p>Page en développement</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,103 +1,103 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Chantiers - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Gestion des Chantiers</h1>
<p:commandButton value="Nouveau chantier" icon="pi pi-plus"
action="#{chantiersView.createNew()}"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{chantiersView}"/>
<ui:param name="tableId" value="chantiersTable"/>
<ui:define name="filter-fields">
<div class="grid">
<div class="col-12 md:col-4">
<h:outputLabel for="filtreNom" value="Nom du chantier"/>
<p:inputText id="filtreNom" value="#{chantiersView.filtreNom}"
placeholder="Rechercher par nom..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreClient" value="Client"/>
<p:inputText id="filtreClient" value="#{chantiersView.filtreClient}"
placeholder="Rechercher par client..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreStatut" value="Statut"/>
<p:selectOneMenu id="filtreStatut" value="#{chantiersView.filtreStatut}" style="width: 100%;">
<f:selectItem itemLabel="Tous" itemValue="TOUS"/>
<f:selectItem itemLabel="En cours" itemValue="EN_COURS"/>
<f:selectItem itemLabel="Terminés" itemValue="TERMINE"/>
<f:selectItem itemLabel="Planifiés" itemValue="PLANIFIE"/>
</p:selectOneMenu>
</div>
</div>
</ui:define>
</ui:include>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="chantiersForm"/>
<ui:param name="tableId" value="chantiersTable"/>
<ui:param name="viewBean" value="#{chantiersView}"/>
<ui:param name="var" value="chantier"/>
<ui:param name="title" value="Liste des chantiers"/>
<ui:param name="createPath" value="/chantiers/nouveau"/>
<ui:define name="columns">
<p:column headerText="Nom" sortBy="#{chantier.nom}">
<h:outputText value="#{chantier.nom}"/>
</p:column>
<p:column headerText="Client" sortBy="#{chantier.client}">
<h:outputText value="#{chantier.client}"/>
</p:column>
<p:column headerText="Adresse">
<h:outputText value="#{chantier.adresse}"/>
</p:column>
<p:column headerText="Date début" sortBy="#{chantier.dateDebut}">
<h:outputText value="#{chantier.dateDebut}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Statut" sortBy="#{chantier.statut}">
<p:tag value="#{chantier.statut}"
severity="#{chantier.statut == 'TERMINE' ? 'success' : (chantier.statut == 'EN_COURS' ? 'info' : 'warning')}"/>
</p:column>
<p:column headerText="Avancement">
<p:progressBar value="#{chantier.avancement}" showValue="true"
styleClass="ui-progressbar-success"/>
</p:column>
<p:column headerText="Budget">
<h:outputText value="#{chantier.budget}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa" style="margin-left: 4px;"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{chantiersView.viewDetails(chantier.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Chantiers - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Gestion des Chantiers</h1>
<p:commandButton value="Nouveau chantier" icon="pi pi-plus"
action="#{chantiersView.createNew()}"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{chantiersView}"/>
<ui:param name="tableId" value="chantiersTable"/>
<ui:define name="filter-fields">
<div class="grid">
<div class="col-12 md:col-4">
<h:outputLabel for="filtreNom" value="Nom du chantier"/>
<p:inputText id="filtreNom" value="#{chantiersView.filtreNom}"
placeholder="Rechercher par nom..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreClient" value="Client"/>
<p:inputText id="filtreClient" value="#{chantiersView.filtreClient}"
placeholder="Rechercher par client..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreStatut" value="Statut"/>
<p:selectOneMenu id="filtreStatut" value="#{chantiersView.filtreStatut}" style="width: 100%;">
<f:selectItem itemLabel="Tous" itemValue="TOUS"/>
<f:selectItem itemLabel="En cours" itemValue="EN_COURS"/>
<f:selectItem itemLabel="Terminés" itemValue="TERMINE"/>
<f:selectItem itemLabel="Planifiés" itemValue="PLANIFIE"/>
</p:selectOneMenu>
</div>
</div>
</ui:define>
</ui:include>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="chantiersForm"/>
<ui:param name="tableId" value="chantiersTable"/>
<ui:param name="viewBean" value="#{chantiersView}"/>
<ui:param name="var" value="chantier"/>
<ui:param name="title" value="Liste des chantiers"/>
<ui:param name="createPath" value="/chantiers/nouveau"/>
<ui:define name="columns">
<p:column headerText="Nom" sortBy="#{chantier.nom}">
<h:outputText value="#{chantier.nom}"/>
</p:column>
<p:column headerText="Client" sortBy="#{chantier.client}">
<h:outputText value="#{chantier.client}"/>
</p:column>
<p:column headerText="Adresse">
<h:outputText value="#{chantier.adresse}"/>
</p:column>
<p:column headerText="Date début" sortBy="#{chantier.dateDebut}">
<h:outputText value="#{chantier.dateDebut}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Statut" sortBy="#{chantier.statut}">
<p:tag value="#{chantier.statut}"
severity="#{chantier.statut == 'TERMINE' ? 'success' : (chantier.statut == 'EN_COURS' ? 'info' : 'warning')}"/>
</p:column>
<p:column headerText="Avancement">
<p:progressBar value="#{chantier.avancement}" showValue="true"
styleClass="ui-progressbar-success"/>
</p:column>
<p:column headerText="Budget">
<h:outputText value="#{chantier.budget}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa" style="margin-left: 4px;"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{chantiersView.viewDetails(chantier.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,307 +1,307 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Détails du chantier - BTP Xpress</ui:define>
<f:metadata>
<f:viewParam name="id" value="#{chantiersView.chantierId}"/>
<f:event type="preRenderView" listener="#{chantiersView.loadChantierById()}"/>
</f:metadata>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<!-- En-tête avec actions -->
<div class="card mb-3">
<div class="flex align-items-start justify-content-between flex-wrap gap-3">
<div class="flex-grow-1">
<div class="flex align-items-center gap-3 mb-2">
<h2 class="text-900 font-bold m-0">#{chantiersView.selectedItem.nom}</h2>
<ui:include src="/WEB-INF/components/status-badge.xhtml">
<ui:param name="value" value="#{chantiersView.selectedItem.statut}"/>
</ui:include>
</div>
<p class="text-600 mt-0 mb-2">
<i class="pi pi-building mr-2"></i>#{chantiersView.selectedItem.client}
<span class="mx-2"></span>
<i class="pi pi-map-marker mr-2"></i>#{chantiersView.selectedItem.adresse}
</p>
<div class="flex align-items-center gap-3 text-sm">
<span class="text-600">
<i class="pi pi-calendar mr-1"></i>
Début: <h:outputText value="#{chantiersView.selectedItem.dateDebut}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</span>
<span class="text-600">
<i class="pi pi-calendar-times mr-1"></i>
Fin prévue: <h:outputText value="#{chantiersView.selectedItem.dateFinPrevue}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</span>
</div>
</div>
<div class="flex gap-2">
<p:commandButton value="Retour"
icon="pi pi-arrow-left"
outcome="/chantiers"
styleClass="ui-button-secondary ui-button-outlined"/>
<p:splitButton value="Modifier"
icon="pi pi-pencil"
styleClass="ui-button-primary"
model="#{chantiersView.chantierActions}"/>
</div>
</div>
</div>
<!-- KPI Cards -->
<div class="grid mb-3">
<div class="col-12 md:col-6 lg:col-3">
<ui:include src="/WEB-INF/components/detail-card.xhtml">
<ui:param name="title" value="Avancement"/>
<ui:param name="value" value="#{chantiersView.selectedItem.avancement}%"/>
<ui:param name="icon" value="pi-chart-line"/>
<ui:param name="iconColor" value="primary"/>
<ui:param name="trend" value="+5%"/>
<ui:param name="footer" value="vs semaine dernière"/>
</ui:include>
</div>
<div class="col-12 md:col-6 lg:col-3">
<div class="card mb-0">
<div class="flex flex-column">
<span class="text-600 font-medium text-sm mb-2">Budget total</span>
<div class="text-900 font-bold text-2xl mb-2">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{chantiersView.selectedItem.budget}"/>
<ui:param name="size" value="normal"/>
<ui:param name="bold" value="true"/>
</ui:include>
</div>
<span class="text-500 text-xs">Alloué au projet</span>
</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-3">
<div class="card mb-0">
<div class="flex flex-column">
<span class="text-600 font-medium text-sm mb-2">Coût réel</span>
<div class="text-900 font-bold text-2xl mb-2">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{chantiersView.selectedItem.coutReel}"/>
<ui:param name="color" value="#{chantiersView.selectedItem.coutReel > chantiersView.selectedItem.budget ? 'danger' : 'success'}"/>
<ui:param name="size" value="normal"/>
<ui:param name="bold" value="true"/>
</ui:include>
</div>
<span class="text-500 text-xs">Dépensé à ce jour</span>
</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-3">
<div class="card mb-0">
<div class="flex flex-column">
<span class="text-600 font-medium text-sm mb-2">Reste disponible</span>
<div class="text-900 font-bold text-2xl mb-2">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{chantiersView.selectedItem.budget - chantiersView.selectedItem.coutReel}"/>
<ui:param name="color" value="#{(chantiersView.selectedItem.budget - chantiersView.selectedItem.coutReel) < 0 ? 'danger' : 'success'}"/>
<ui:param name="size" value="normal"/>
<ui:param name="bold" value="true"/>
</ui:include>
</div>
<span class="text-500 text-xs">
#{(chantiersView.selectedItem.budget - chantiersView.selectedItem.coutReel) >= 0 ? 'Excédent' : 'Dépassement'}
</span>
</div>
</div>
</div>
</div>
<!-- Onglets détaillés -->
<div class="card">
<p:tabView dynamic="true" cache="false">
<!-- ONGLET 1: Vue d'ensemble -->
<p:tab title="Vue d'ensemble" icon="pi pi-home">
<div class="grid">
<!-- Informations générales -->
<div class="col-12 lg:col-6">
<h5 class="text-900 font-bold mb-3">Informations générales</h5>
<div class="surface-50 border-round p-3 mb-3">
<div class="grid">
<div class="col-6">
<span class="text-600 text-sm">Nom du chantier</span>
<p class="text-900 font-medium mt-1 mb-0">#{chantiersView.selectedItem.nom}</p>
</div>
<div class="col-6">
<span class="text-600 text-sm">Client</span>
<p class="text-900 font-medium mt-1 mb-0">#{chantiersView.selectedItem.client}</p>
</div>
<div class="col-12">
<span class="text-600 text-sm">Adresse</span>
<p class="text-900 font-medium mt-1 mb-0">#{chantiersView.selectedItem.adresse}</p>
</div>
<div class="col-6">
<span class="text-600 text-sm">Statut</span>
<div class="mt-1">
<ui:include src="/WEB-INF/components/status-badge.xhtml">
<ui:param name="value" value="#{chantiersView.selectedItem.statut}"/>
</ui:include>
</div>
</div>
<div class="col-6">
<span class="text-600 text-sm">Avancement</span>
<p class="text-900 font-bold text-xl mt-1 mb-0">#{chantiersView.selectedItem.avancement}%</p>
</div>
</div>
</div>
</div>
<!-- Progression visuelle -->
<div class="col-12 lg:col-6">
<h5 class="text-900 font-bold mb-3">Progression du chantier</h5>
<div class="surface-50 border-round p-3 mb-3">
<ui:include src="/WEB-INF/components/progress-indicator.xhtml">
<ui:param name="value" value="#{chantiersView.selectedItem.avancement}"/>
<ui:param name="label" value="Réalisation globale"/>
<ui:param name="height" value="1.5rem"/>
</ui:include>
</div>
</div>
<!-- Analyse budgétaire -->
<div class="col-12">
<h5 class="text-900 font-bold mb-3">Analyse budgétaire</h5>
<div class="surface-50 border-round p-3">
<div class="grid">
<div class="col-12 md:col-4">
<div class="text-center">
<span class="text-600 text-sm">Budget prévu</span>
<div class="text-primary font-bold text-xl mt-2">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{chantiersView.selectedItem.budget}"/>
</ui:include>
</div>
</div>
</div>
<div class="col-12 md:col-4">
<div class="text-center">
<span class="text-600 text-sm">Dépensé</span>
<div class="font-bold text-xl mt-2"
style="color: #{chantiersView.selectedItem.coutReel > chantiersView.selectedItem.budget ? '#EF4444' : '#10B981'}">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{chantiersView.selectedItem.coutReel}"/>
</ui:include>
</div>
</div>
</div>
<div class="col-12 md:col-4">
<div class="text-center">
<span class="text-600 text-sm">
#{(chantiersView.selectedItem.budget - chantiersView.selectedItem.coutReel) >= 0 ? 'Reste' : 'Dépassement'}
</span>
<div class="font-bold text-xl mt-2"
style="color: #{(chantiersView.selectedItem.budget - chantiersView.selectedItem.coutReel) >= 0 ? '#10B981' : '#EF4444'}">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{chantiersView.selectedItem.budget - chantiersView.selectedItem.coutReel}"/>
</ui:include>
</div>
</div>
</div>
<div class="col-12 mt-3">
<ui:include src="/WEB-INF/components/progress-indicator.xhtml">
<ui:param name="value" value="#{(chantiersView.selectedItem.coutReel / chantiersView.selectedItem.budget) * 100}"/>
<ui:param name="label" value="Utilisation du budget"/>
<ui:param name="labelPosition" value="top"/>
</ui:include>
</div>
</div>
</div>
</div>
</div>
</p:tab>
<!-- ONGLET 2: Phases -->
<p:tab title="Phases" icon="pi pi-sitemap">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<h5 class="text-900 font-bold m-0">Phases du chantier</h5>
<p:commandButton value="Ajouter une phase"
icon="pi pi-plus"
styleClass="ui-button-success ui-button-sm"/>
</div>
<p:message severity="info" text="Fonctionnalité de gestion des phases en cours de développement"/>
</div>
</p:tab>
<!-- ONGLET 3: Équipes -->
<p:tab title="Équipes" icon="pi pi-users">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<h5 class="text-900 font-bold m-0">Équipes affectées</h5>
<p:commandButton value="Affecter une équipe"
icon="pi pi-plus"
styleClass="ui-button-success ui-button-sm"/>
</div>
<p:message severity="info" text="Fonctionnalité d'affectation des équipes en cours de développement"/>
</div>
</p:tab>
<!-- ONGLET 4: Matériels -->
<p:tab title="Matériels" icon="pi pi-wrench">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<h5 class="text-900 font-bold m-0">Matériels utilisés</h5>
<p:commandButton value="Ajouter du matériel"
icon="pi pi-plus"
styleClass="ui-button-success ui-button-sm"/>
</div>
<p:message severity="info" text="Fonctionnalité de gestion des matériels en cours de développement"/>
</div>
</p:tab>
<!-- ONGLET 5: Documents -->
<p:tab title="Documents" icon="pi pi-folder">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<h5 class="text-900 font-bold m-0">Documents du chantier</h5>
<p:commandButton value="Ajouter un document"
icon="pi pi-upload"
styleClass="ui-button-success ui-button-sm"/>
</div>
<p:message severity="info" text="Fonctionnalité de gestion documentaire en cours de développement"/>
</div>
</p:tab>
<!-- ONGLET 6: Historique -->
<p:tab title="Historique" icon="pi pi-history">
<div class="p-3">
<h5 class="text-900 font-bold mb-3">Historique des modifications</h5>
<p:timeline value="#{chantiersView.chantierHistory}" align="alternate">
<p:templateSlot name="marker">
<i class="pi pi-circle-fill text-primary"></i>
</p:templateSlot>
<p:templateSlot name="content">
<small class="text-600">Fonctionnalité en cours de développement</small>
</p:templateSlot>
</p:timeline>
</div>
</p:tab>
</p:tabView>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Détails du chantier - BTP Xpress</ui:define>
<f:metadata>
<f:viewParam name="id" value="#{chantiersView.chantierId}"/>
<f:event type="preRenderView" listener="#{chantiersView.loadChantierById()}"/>
</f:metadata>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<!-- En-tête avec actions -->
<div class="card mb-3">
<div class="flex align-items-start justify-content-between flex-wrap gap-3">
<div class="flex-grow-1">
<div class="flex align-items-center gap-3 mb-2">
<h2 class="text-900 font-bold m-0">#{chantiersView.selectedItem.nom}</h2>
<ui:include src="/WEB-INF/components/status-badge.xhtml">
<ui:param name="value" value="#{chantiersView.selectedItem.statut}"/>
</ui:include>
</div>
<p class="text-600 mt-0 mb-2">
<i class="pi pi-building mr-2"></i>#{chantiersView.selectedItem.client}
<span class="mx-2"></span>
<i class="pi pi-map-marker mr-2"></i>#{chantiersView.selectedItem.adresse}
</p>
<div class="flex align-items-center gap-3 text-sm">
<span class="text-600">
<i class="pi pi-calendar mr-1"></i>
Début: <h:outputText value="#{chantiersView.selectedItem.dateDebut}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</span>
<span class="text-600">
<i class="pi pi-calendar-times mr-1"></i>
Fin prévue: <h:outputText value="#{chantiersView.selectedItem.dateFinPrevue}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</span>
</div>
</div>
<div class="flex gap-2">
<p:commandButton value="Retour"
icon="pi pi-arrow-left"
outcome="/chantiers"
styleClass="ui-button-secondary ui-button-outlined"/>
<p:splitButton value="Modifier"
icon="pi pi-pencil"
styleClass="ui-button-primary"
model="#{chantiersView.chantierActions}"/>
</div>
</div>
</div>
<!-- KPI Cards -->
<div class="grid mb-3">
<div class="col-12 md:col-6 lg:col-3">
<ui:include src="/WEB-INF/components/detail-card.xhtml">
<ui:param name="title" value="Avancement"/>
<ui:param name="value" value="#{chantiersView.selectedItem.avancement}%"/>
<ui:param name="icon" value="pi-chart-line"/>
<ui:param name="iconColor" value="primary"/>
<ui:param name="trend" value="+5%"/>
<ui:param name="footer" value="vs semaine dernière"/>
</ui:include>
</div>
<div class="col-12 md:col-6 lg:col-3">
<div class="card mb-0">
<div class="flex flex-column">
<span class="text-600 font-medium text-sm mb-2">Budget total</span>
<div class="text-900 font-bold text-2xl mb-2">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{chantiersView.selectedItem.budget}"/>
<ui:param name="size" value="normal"/>
<ui:param name="bold" value="true"/>
</ui:include>
</div>
<span class="text-500 text-xs">Alloué au projet</span>
</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-3">
<div class="card mb-0">
<div class="flex flex-column">
<span class="text-600 font-medium text-sm mb-2">Coût réel</span>
<div class="text-900 font-bold text-2xl mb-2">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{chantiersView.selectedItem.coutReel}"/>
<ui:param name="color" value="#{chantiersView.selectedItem.coutReel > chantiersView.selectedItem.budget ? 'danger' : 'success'}"/>
<ui:param name="size" value="normal"/>
<ui:param name="bold" value="true"/>
</ui:include>
</div>
<span class="text-500 text-xs">Dépensé à ce jour</span>
</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-3">
<div class="card mb-0">
<div class="flex flex-column">
<span class="text-600 font-medium text-sm mb-2">Reste disponible</span>
<div class="text-900 font-bold text-2xl mb-2">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{chantiersView.selectedItem.budget - chantiersView.selectedItem.coutReel}"/>
<ui:param name="color" value="#{(chantiersView.selectedItem.budget - chantiersView.selectedItem.coutReel) < 0 ? 'danger' : 'success'}"/>
<ui:param name="size" value="normal"/>
<ui:param name="bold" value="true"/>
</ui:include>
</div>
<span class="text-500 text-xs">
#{(chantiersView.selectedItem.budget - chantiersView.selectedItem.coutReel) >= 0 ? 'Excédent' : 'Dépassement'}
</span>
</div>
</div>
</div>
</div>
<!-- Onglets détaillés -->
<div class="card">
<p:tabView dynamic="true" cache="false">
<!-- ONGLET 1: Vue d'ensemble -->
<p:tab title="Vue d'ensemble" icon="pi pi-home">
<div class="grid">
<!-- Informations générales -->
<div class="col-12 lg:col-6">
<h5 class="text-900 font-bold mb-3">Informations générales</h5>
<div class="surface-50 border-round p-3 mb-3">
<div class="grid">
<div class="col-6">
<span class="text-600 text-sm">Nom du chantier</span>
<p class="text-900 font-medium mt-1 mb-0">#{chantiersView.selectedItem.nom}</p>
</div>
<div class="col-6">
<span class="text-600 text-sm">Client</span>
<p class="text-900 font-medium mt-1 mb-0">#{chantiersView.selectedItem.client}</p>
</div>
<div class="col-12">
<span class="text-600 text-sm">Adresse</span>
<p class="text-900 font-medium mt-1 mb-0">#{chantiersView.selectedItem.adresse}</p>
</div>
<div class="col-6">
<span class="text-600 text-sm">Statut</span>
<div class="mt-1">
<ui:include src="/WEB-INF/components/status-badge.xhtml">
<ui:param name="value" value="#{chantiersView.selectedItem.statut}"/>
</ui:include>
</div>
</div>
<div class="col-6">
<span class="text-600 text-sm">Avancement</span>
<p class="text-900 font-bold text-xl mt-1 mb-0">#{chantiersView.selectedItem.avancement}%</p>
</div>
</div>
</div>
</div>
<!-- Progression visuelle -->
<div class="col-12 lg:col-6">
<h5 class="text-900 font-bold mb-3">Progression du chantier</h5>
<div class="surface-50 border-round p-3 mb-3">
<ui:include src="/WEB-INF/components/progress-indicator.xhtml">
<ui:param name="value" value="#{chantiersView.selectedItem.avancement}"/>
<ui:param name="label" value="Réalisation globale"/>
<ui:param name="height" value="1.5rem"/>
</ui:include>
</div>
</div>
<!-- Analyse budgétaire -->
<div class="col-12">
<h5 class="text-900 font-bold mb-3">Analyse budgétaire</h5>
<div class="surface-50 border-round p-3">
<div class="grid">
<div class="col-12 md:col-4">
<div class="text-center">
<span class="text-600 text-sm">Budget prévu</span>
<div class="text-primary font-bold text-xl mt-2">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{chantiersView.selectedItem.budget}"/>
</ui:include>
</div>
</div>
</div>
<div class="col-12 md:col-4">
<div class="text-center">
<span class="text-600 text-sm">Dépensé</span>
<div class="font-bold text-xl mt-2"
style="color: #{chantiersView.selectedItem.coutReel > chantiersView.selectedItem.budget ? '#EF4444' : '#10B981'}">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{chantiersView.selectedItem.coutReel}"/>
</ui:include>
</div>
</div>
</div>
<div class="col-12 md:col-4">
<div class="text-center">
<span class="text-600 text-sm">
#{(chantiersView.selectedItem.budget - chantiersView.selectedItem.coutReel) >= 0 ? 'Reste' : 'Dépassement'}
</span>
<div class="font-bold text-xl mt-2"
style="color: #{(chantiersView.selectedItem.budget - chantiersView.selectedItem.coutReel) >= 0 ? '#10B981' : '#EF4444'}">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{chantiersView.selectedItem.budget - chantiersView.selectedItem.coutReel}"/>
</ui:include>
</div>
</div>
</div>
<div class="col-12 mt-3">
<ui:include src="/WEB-INF/components/progress-indicator.xhtml">
<ui:param name="value" value="#{(chantiersView.selectedItem.coutReel / chantiersView.selectedItem.budget) * 100}"/>
<ui:param name="label" value="Utilisation du budget"/>
<ui:param name="labelPosition" value="top"/>
</ui:include>
</div>
</div>
</div>
</div>
</div>
</p:tab>
<!-- ONGLET 2: Phases -->
<p:tab title="Phases" icon="pi pi-sitemap">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<h5 class="text-900 font-bold m-0">Phases du chantier</h5>
<p:commandButton value="Ajouter une phase"
icon="pi pi-plus"
styleClass="ui-button-success ui-button-sm"/>
</div>
<p:message severity="info" text="Fonctionnalité de gestion des phases en cours de développement"/>
</div>
</p:tab>
<!-- ONGLET 3: Équipes -->
<p:tab title="Équipes" icon="pi pi-users">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<h5 class="text-900 font-bold m-0">Équipes affectées</h5>
<p:commandButton value="Affecter une équipe"
icon="pi pi-plus"
styleClass="ui-button-success ui-button-sm"/>
</div>
<p:message severity="info" text="Fonctionnalité d'affectation des équipes en cours de développement"/>
</div>
</p:tab>
<!-- ONGLET 4: Matériels -->
<p:tab title="Matériels" icon="pi pi-wrench">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<h5 class="text-900 font-bold m-0">Matériels utilisés</h5>
<p:commandButton value="Ajouter du matériel"
icon="pi pi-plus"
styleClass="ui-button-success ui-button-sm"/>
</div>
<p:message severity="info" text="Fonctionnalité de gestion des matériels en cours de développement"/>
</div>
</p:tab>
<!-- ONGLET 5: Documents -->
<p:tab title="Documents" icon="pi pi-folder">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<h5 class="text-900 font-bold m-0">Documents du chantier</h5>
<p:commandButton value="Ajouter un document"
icon="pi pi-upload"
styleClass="ui-button-success ui-button-sm"/>
</div>
<p:message severity="info" text="Fonctionnalité de gestion documentaire en cours de développement"/>
</div>
</p:tab>
<!-- ONGLET 6: Historique -->
<p:tab title="Historique" icon="pi pi-history">
<div class="p-3">
<h5 class="text-900 font-bold mb-3">Historique des modifications</h5>
<p:timeline value="#{chantiersView.chantierHistory}" align="alternate">
<p:templateSlot name="marker">
<i class="pi pi-circle-fill text-primary"></i>
</p:templateSlot>
<p:templateSlot name="content">
<small class="text-600">Fonctionnalité en cours de développement</small>
</p:templateSlot>
</p:timeline>
</div>
</p:tab>
</p:tabView>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,97 +1,97 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Chantiers en cours - BTP Xpress</ui:define>
<f:metadata>
<f:event type="preRenderView" listener="#{chantiersView.setFiltreStatut('EN_COURS')}"/>
<f:event type="preRenderView" listener="#{chantiersView.init()}"/>
</f:metadata>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Chantiers en cours</h1>
<p:commandButton value="Nouveau chantier" icon="pi pi-plus"
action="#{chantiersView.createNew()}"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{chantiersView}"/>
<ui:param name="tableId" value="chantiersTable"/>
<ui:define name="filter-fields">
<div class="grid">
<div class="col-12 md:col-6">
<h:outputLabel for="filtreNom" value="Nom du chantier"/>
<p:inputText id="filtreNom" value="#{chantiersView.filtreNom}"
placeholder="Rechercher par nom..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-6">
<h:outputLabel for="filtreClient" value="Client"/>
<p:inputText id="filtreClient" value="#{chantiersView.filtreClient}"
placeholder="Rechercher par client..." style="width: 100%;"/>
</div>
</div>
</ui:define>
</ui:include>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="chantiersForm"/>
<ui:param name="tableId" value="chantiersTable"/>
<ui:param name="viewBean" value="#{chantiersView}"/>
<ui:param name="var" value="chantier"/>
<ui:param name="title" value="Liste des chantiers en cours"/>
<ui:param name="createPath" value="/chantiers/nouveau"/>
<ui:define name="columns">
<p:column headerText="Nom" sortBy="#{chantier.nom}">
<h:outputText value="#{chantier.nom}"/>
</p:column>
<p:column headerText="Client" sortBy="#{chantier.client}">
<h:outputText value="#{chantier.client}"/>
</p:column>
<p:column headerText="Adresse">
<h:outputText value="#{chantier.adresse}"/>
</p:column>
<p:column headerText="Date début" sortBy="#{chantier.dateDebut}">
<h:outputText value="#{chantier.dateDebut}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Avancement">
<p:progressBar value="#{chantier.avancement}"
showValue="true"
styleClass="ui-progressbar-success"/>
</p:column>
<p:column headerText="Budget">
<h:outputText value="#{chantier.budget}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{chantiersView.viewDetails(chantier.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Chantiers en cours - BTP Xpress</ui:define>
<f:metadata>
<f:event type="preRenderView" listener="#{chantiersView.setFiltreStatut('EN_COURS')}"/>
<f:event type="preRenderView" listener="#{chantiersView.init()}"/>
</f:metadata>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Chantiers en cours</h1>
<p:commandButton value="Nouveau chantier" icon="pi pi-plus"
action="#{chantiersView.createNew()}"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{chantiersView}"/>
<ui:param name="tableId" value="chantiersTable"/>
<ui:define name="filter-fields">
<div class="grid">
<div class="col-12 md:col-6">
<h:outputLabel for="filtreNom" value="Nom du chantier"/>
<p:inputText id="filtreNom" value="#{chantiersView.filtreNom}"
placeholder="Rechercher par nom..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-6">
<h:outputLabel for="filtreClient" value="Client"/>
<p:inputText id="filtreClient" value="#{chantiersView.filtreClient}"
placeholder="Rechercher par client..." style="width: 100%;"/>
</div>
</div>
</ui:define>
</ui:include>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="chantiersForm"/>
<ui:param name="tableId" value="chantiersTable"/>
<ui:param name="viewBean" value="#{chantiersView}"/>
<ui:param name="var" value="chantier"/>
<ui:param name="title" value="Liste des chantiers en cours"/>
<ui:param name="createPath" value="/chantiers/nouveau"/>
<ui:define name="columns">
<p:column headerText="Nom" sortBy="#{chantier.nom}">
<h:outputText value="#{chantier.nom}"/>
</p:column>
<p:column headerText="Client" sortBy="#{chantier.client}">
<h:outputText value="#{chantier.client}"/>
</p:column>
<p:column headerText="Adresse">
<h:outputText value="#{chantier.adresse}"/>
</p:column>
<p:column headerText="Date début" sortBy="#{chantier.dateDebut}">
<h:outputText value="#{chantier.dateDebut}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Avancement">
<p:progressBar value="#{chantier.avancement}"
showValue="true"
styleClass="ui-progressbar-success"/>
</p:column>
<p:column headerText="Budget">
<h:outputText value="#{chantier.budget}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{chantiersView.viewDetails(chantier.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,236 +1,236 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Nouveau chantier - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<!-- En-tête avec breadcrumb -->
<div class="flex align-items-center justify-content-between mb-4">
<div>
<h2 class="text-900 font-bold mb-2">Créer un nouveau chantier</h2>
<p class="text-600 mt-0">Remplissez les informations du chantier à créer</p>
</div>
<p:commandButton value="Retour à la liste"
icon="pi pi-arrow-left"
outcome="/chantiers"
styleClass="ui-button-secondary ui-button-outlined"/>
</div>
<p:messages id="messages" showDetail="true" closable="true"/>
<h:form id="nouveauChantierForm" styleClass="p-fluid">
<!-- SECTION 1: Informations générales -->
<p:panel header="Informations générales" toggleable="true" collapsed="false" class="mb-4">
<div class="formgrid grid">
<!-- Nom du chantier -->
<div class="field col-12 md:col-6">
<label for="nom" class="font-bold">Nom du chantier <span class="text-red-500">*</span></label>
<p:inputText id="nom"
value="#{chantiersView.entity.nom}"
required="true"
requiredMessage="Le nom du chantier est obligatoire"
placeholder="Ex: Construction Immeuble R+3">
<f:validateLength minimum="3" maximum="200"/>
</p:inputText>
<small class="text-600">Nom descriptif du projet de construction</small>
</div>
<!-- Client -->
<div class="field col-12 md:col-6">
<label for="client" class="font-bold">Client <span class="text-red-500">*</span></label>
<p:inputText id="client"
value="#{chantiersView.entity.client}"
required="true"
requiredMessage="Le client est obligatoire"
placeholder="Ex: Société ABC">
<f:validateLength minimum="2" maximum="200"/>
</p:inputText>
<small class="text-600">Nom du client ou de l'entreprise</small>
</div>
<!-- Adresse complète -->
<div class="field col-12">
<label for="adresse" class="font-bold">Adresse du chantier</label>
<p:inputTextarea id="adresse"
value="#{chantiersView.entity.adresse}"
rows="3"
placeholder="Ex: Quartier Résidentiel, Avenue de la Paix, Lot 245"
autoResize="false">
<f:validateLength maximum="500"/>
</p:inputTextarea>
<small class="text-600">Localisation précise du chantier</small>
</div>
<!-- Statut -->
<div class="field col-12 md:col-4">
<label for="statut" class="font-bold">Statut <span class="text-red-500">*</span></label>
<p:selectOneMenu id="statut"
value="#{chantiersView.entity.statut}"
required="true">
<f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true"/>
<f:selectItem itemLabel="Planifié" itemValue="PLANIFIE"/>
<f:selectItem itemLabel="En cours" itemValue="EN_COURS"/>
<f:selectItem itemLabel="Suspendu" itemValue="SUSPENDU"/>
<f:selectItem itemLabel="Terminé" itemValue="TERMINE"/>
</p:selectOneMenu>
</div>
<!-- Avancement initial -->
<div class="field col-12 md:col-4">
<label for="avancement" class="font-bold">Avancement (%)</label>
<p:inputNumber id="avancement"
value="#{chantiersView.entity.avancement}"
minValue="0"
maxValue="100"
suffix=" %"
decimalPlaces="0">
</p:inputNumber>
<small class="text-600">Pourcentage de réalisation (0-100%)</small>
</div>
</div>
</p:panel>
<!-- SECTION 2: Planification -->
<p:panel header="Planification" toggleable="true" collapsed="false" class="mb-4">
<div class="formgrid grid">
<!-- Date de début -->
<div class="field col-12 md:col-4">
<label for="dateDebut" class="font-bold">Date de début <span class="text-red-500">*</span></label>
<p:calendar id="dateDebut"
value="#{chantiersView.entity.dateDebut}"
pattern="dd/MM/yyyy"
locale="fr"
required="true"
requiredMessage="La date de début est obligatoire"
showIcon="true"
showButtonBar="true"
monthNavigator="true"
yearNavigator="true"
yearRange="2020:2030"
placeholder="Sélectionner une date">
</p:calendar>
</div>
<!-- Date de fin prévue -->
<div class="field col-12 md:col-4">
<label for="dateFinPrevue" class="font-bold">Date de fin prévue <span class="text-red-500">*</span></label>
<p:calendar id="dateFinPrevue"
value="#{chantiersView.entity.dateFinPrevue}"
pattern="dd/MM/yyyy"
locale="fr"
required="true"
requiredMessage="La date de fin est obligatoire"
showIcon="true"
showButtonBar="true"
monthNavigator="true"
yearNavigator="true"
yearRange="2020:2035"
mindate="#{chantiersView.entity.dateDebut}"
placeholder="Sélectionner une date">
</p:calendar>
<small class="text-600">Doit être postérieure à la date de début</small>
</div>
<!-- Durée estimée (calculée automatiquement) -->
<div class="field col-12 md:col-4">
<label class="font-bold">Durée estimée</label>
<div class="p-inputgroup">
<span class="p-inputgroup-addon">
<i class="pi pi-calendar"></i>
</span>
<p:inputText value="Calculée automatiquement"
disabled="true"
styleClass="text-center font-bold"/>
</div>
<small class="text-600">Basé sur dates début et fin</small>
</div>
</div>
</p:panel>
<!-- SECTION 3: Budget -->
<p:panel header="Budget et coûts" toggleable="true" collapsed="false" class="mb-4">
<div class="formgrid grid">
<!-- Budget total -->
<div class="field col-12 md:col-6">
<label for="budget" class="font-bold">Budget total (FCFA) <span class="text-red-500">*</span></label>
<p:inputNumber id="budget"
value="#{chantiersView.entity.budget}"
required="true"
requiredMessage="Le budget est obligatoire"
minValue="0"
decimalPlaces="0"
thousandSeparator=" "
suffix=" FCFA"
placeholder="0">
</p:inputNumber>
<small class="text-600">Budget total alloué au chantier</small>
</div>
<!-- Coût réel (initialement 0) -->
<div class="field col-12 md:col-6">
<label for="coutReel" class="font-bold">Coût réel (FCFA)</label>
<p:inputNumber id="coutReel"
value="#{chantiersView.entity.coutReel}"
minValue="0"
decimalPlaces="0"
thousandSeparator=" "
suffix=" FCFA"
placeholder="0">
</p:inputNumber>
<small class="text-600">Coût réel dépensé (actualisé régulièrement)</small>
</div>
<!-- Indicateur budgétaire visuel -->
<div class="field col-12">
<div class="surface-100 border-round p-3">
<div class="flex align-items-center justify-content-between mb-2">
<span class="text-900 font-medium">État budgétaire</span>
<span class="text-600 text-sm">Budget: #{chantiersView.entity.budget} FCFA | Dépensé: #{chantiersView.entity.coutReel} FCFA</span>
</div>
<p:progressBar value="#{chantiersView.entity.coutReel / chantiersView.entity.budget * 100}"
displayValue="true"
labelTemplate="{value}% du budget utilisé"
styleClass="#{chantiersView.entity.coutReel > chantiersView.entity.budget ? 'bg-red-500' : 'bg-green-500'}"/>
</div>
</div>
</div>
</p:panel>
<!-- Boutons d'action -->
<div class="flex align-items-center justify-content-between pt-4 border-top-1 surface-border">
<div>
<span class="text-600 text-sm">Les champs marqués d'un </span>
<span class="text-red-500 font-bold">*</span>
<span class="text-600 text-sm"> sont obligatoires</span>
</div>
<div class="flex gap-2">
<p:commandButton value="Annuler"
icon="pi pi-times"
action="/chantiers?faces-redirect=true"
styleClass="ui-button-secondary"
immediate="true"/>
<p:commandButton value="Enregistrer le chantier"
icon="pi pi-save"
action="#{chantiersView.save}"
update="@form messages"
oncomplete="if (args &amp;&amp; !args.validationFailed) window.location.href='/chantiers.xhtml';"
styleClass="ui-button-primary"/>
</div>
</div>
</h:form>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Nouveau chantier - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<!-- En-tête avec breadcrumb -->
<div class="flex align-items-center justify-content-between mb-4">
<div>
<h2 class="text-900 font-bold mb-2">Créer un nouveau chantier</h2>
<p class="text-600 mt-0">Remplissez les informations du chantier à créer</p>
</div>
<p:commandButton value="Retour à la liste"
icon="pi pi-arrow-left"
outcome="/chantiers"
styleClass="ui-button-secondary ui-button-outlined"/>
</div>
<p:messages id="messages" showDetail="true" closable="true"/>
<h:form id="nouveauChantierForm" styleClass="p-fluid">
<!-- SECTION 1: Informations générales -->
<p:panel header="Informations générales" toggleable="true" collapsed="false" class="mb-4">
<div class="formgrid grid">
<!-- Nom du chantier -->
<div class="field col-12 md:col-6">
<label for="nom" class="font-bold">Nom du chantier <span class="text-red-500">*</span></label>
<p:inputText id="nom"
value="#{chantiersView.entity.nom}"
required="true"
requiredMessage="Le nom du chantier est obligatoire"
placeholder="Ex: Construction Immeuble R+3">
<f:validateLength minimum="3" maximum="200"/>
</p:inputText>
<small class="text-600">Nom descriptif du projet de construction</small>
</div>
<!-- Client -->
<div class="field col-12 md:col-6">
<label for="client" class="font-bold">Client <span class="text-red-500">*</span></label>
<p:inputText id="client"
value="#{chantiersView.entity.client}"
required="true"
requiredMessage="Le client est obligatoire"
placeholder="Ex: Société ABC">
<f:validateLength minimum="2" maximum="200"/>
</p:inputText>
<small class="text-600">Nom du client ou de l'entreprise</small>
</div>
<!-- Adresse complète -->
<div class="field col-12">
<label for="adresse" class="font-bold">Adresse du chantier</label>
<p:inputTextarea id="adresse"
value="#{chantiersView.entity.adresse}"
rows="3"
placeholder="Ex: Quartier Résidentiel, Avenue de la Paix, Lot 245"
autoResize="false">
<f:validateLength maximum="500"/>
</p:inputTextarea>
<small class="text-600">Localisation précise du chantier</small>
</div>
<!-- Statut -->
<div class="field col-12 md:col-4">
<label for="statut" class="font-bold">Statut <span class="text-red-500">*</span></label>
<p:selectOneMenu id="statut"
value="#{chantiersView.entity.statut}"
required="true">
<f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true"/>
<f:selectItem itemLabel="Planifié" itemValue="PLANIFIE"/>
<f:selectItem itemLabel="En cours" itemValue="EN_COURS"/>
<f:selectItem itemLabel="Suspendu" itemValue="SUSPENDU"/>
<f:selectItem itemLabel="Terminé" itemValue="TERMINE"/>
</p:selectOneMenu>
</div>
<!-- Avancement initial -->
<div class="field col-12 md:col-4">
<label for="avancement" class="font-bold">Avancement (%)</label>
<p:inputNumber id="avancement"
value="#{chantiersView.entity.avancement}"
minValue="0"
maxValue="100"
suffix=" %"
decimalPlaces="0">
</p:inputNumber>
<small class="text-600">Pourcentage de réalisation (0-100%)</small>
</div>
</div>
</p:panel>
<!-- SECTION 2: Planification -->
<p:panel header="Planification" toggleable="true" collapsed="false" class="mb-4">
<div class="formgrid grid">
<!-- Date de début -->
<div class="field col-12 md:col-4">
<label for="dateDebut" class="font-bold">Date de début <span class="text-red-500">*</span></label>
<p:calendar id="dateDebut"
value="#{chantiersView.entity.dateDebut}"
pattern="dd/MM/yyyy"
locale="fr"
required="true"
requiredMessage="La date de début est obligatoire"
showIcon="true"
showButtonBar="true"
monthNavigator="true"
yearNavigator="true"
yearRange="2020:2030"
placeholder="Sélectionner une date">
</p:calendar>
</div>
<!-- Date de fin prévue -->
<div class="field col-12 md:col-4">
<label for="dateFinPrevue" class="font-bold">Date de fin prévue <span class="text-red-500">*</span></label>
<p:calendar id="dateFinPrevue"
value="#{chantiersView.entity.dateFinPrevue}"
pattern="dd/MM/yyyy"
locale="fr"
required="true"
requiredMessage="La date de fin est obligatoire"
showIcon="true"
showButtonBar="true"
monthNavigator="true"
yearNavigator="true"
yearRange="2020:2035"
mindate="#{chantiersView.entity.dateDebut}"
placeholder="Sélectionner une date">
</p:calendar>
<small class="text-600">Doit être postérieure à la date de début</small>
</div>
<!-- Durée estimée (calculée automatiquement) -->
<div class="field col-12 md:col-4">
<label class="font-bold">Durée estimée</label>
<div class="p-inputgroup">
<span class="p-inputgroup-addon">
<i class="pi pi-calendar"></i>
</span>
<p:inputText value="Calculée automatiquement"
disabled="true"
styleClass="text-center font-bold"/>
</div>
<small class="text-600">Basé sur dates début et fin</small>
</div>
</div>
</p:panel>
<!-- SECTION 3: Budget -->
<p:panel header="Budget et coûts" toggleable="true" collapsed="false" class="mb-4">
<div class="formgrid grid">
<!-- Budget total -->
<div class="field col-12 md:col-6">
<label for="budget" class="font-bold">Budget total (FCFA) <span class="text-red-500">*</span></label>
<p:inputNumber id="budget"
value="#{chantiersView.entity.budget}"
required="true"
requiredMessage="Le budget est obligatoire"
minValue="0"
decimalPlaces="0"
thousandSeparator=" "
suffix=" FCFA"
placeholder="0">
</p:inputNumber>
<small class="text-600">Budget total alloué au chantier</small>
</div>
<!-- Coût réel (initialement 0) -->
<div class="field col-12 md:col-6">
<label for="coutReel" class="font-bold">Coût réel (FCFA)</label>
<p:inputNumber id="coutReel"
value="#{chantiersView.entity.coutReel}"
minValue="0"
decimalPlaces="0"
thousandSeparator=" "
suffix=" FCFA"
placeholder="0">
</p:inputNumber>
<small class="text-600">Coût réel dépensé (actualisé régulièrement)</small>
</div>
<!-- Indicateur budgétaire visuel -->
<div class="field col-12">
<div class="surface-100 border-round p-3">
<div class="flex align-items-center justify-content-between mb-2">
<span class="text-900 font-medium">État budgétaire</span>
<span class="text-600 text-sm">Budget: #{chantiersView.entity.budget} FCFA | Dépensé: #{chantiersView.entity.coutReel} FCFA</span>
</div>
<p:progressBar value="#{chantiersView.entity.coutReel / chantiersView.entity.budget * 100}"
displayValue="true"
labelTemplate="{value}% du budget utilisé"
styleClass="#{chantiersView.entity.coutReel > chantiersView.entity.budget ? 'bg-red-500' : 'bg-green-500'}"/>
</div>
</div>
</div>
</p:panel>
<!-- Boutons d'action -->
<div class="flex align-items-center justify-content-between pt-4 border-top-1 surface-border">
<div>
<span class="text-600 text-sm">Les champs marqués d'un </span>
<span class="text-red-500 font-bold">*</span>
<span class="text-600 text-sm"> sont obligatoires</span>
</div>
<div class="flex gap-2">
<p:commandButton value="Annuler"
icon="pi pi-times"
action="/chantiers?faces-redirect=true"
styleClass="ui-button-secondary"
immediate="true"/>
<p:commandButton value="Enregistrer le chantier"
icon="pi pi-save"
action="#{chantiersView.save}"
update="@form messages"
oncomplete="if (args &amp;&amp; !args.validationFailed) window.location.href='/chantiers.xhtml';"
styleClass="ui-button-primary"/>
</div>
</div>
</h:form>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,94 +1,94 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Chantiers planifiés - BTP Xpress</ui:define>
<f:metadata>
<f:event type="preRenderView" listener="#{chantiersView.setFiltreStatut('PLANIFIE')}"/>
<f:event type="preRenderView" listener="#{chantiersView.init()}"/>
</f:metadata>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Chantiers planifiés</h1>
<p:commandButton value="Nouveau chantier" icon="pi pi-plus"
action="#{chantiersView.createNew()}"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{chantiersView}"/>
<ui:param name="tableId" value="chantiersTable"/>
<ui:define name="filter-fields">
<div class="grid">
<div class="col-12 md:col-6">
<h:outputLabel for="filtreNom" value="Nom du chantier"/>
<p:inputText id="filtreNom" value="#{chantiersView.filtreNom}"
placeholder="Rechercher par nom..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-6">
<h:outputLabel for="filtreClient" value="Client"/>
<p:inputText id="filtreClient" value="#{chantiersView.filtreClient}"
placeholder="Rechercher par client..." style="width: 100%;"/>
</div>
</div>
</ui:define>
</ui:include>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="chantiersForm"/>
<ui:param name="tableId" value="chantiersTable"/>
<ui:param name="viewBean" value="#{chantiersView}"/>
<ui:param name="var" value="chantier"/>
<ui:param name="title" value="Liste des chantiers planifiés"/>
<ui:param name="createPath" value="/chantiers/nouveau"/>
<ui:define name="columns">
<p:column headerText="Nom" sortBy="#{chantier.nom}">
<h:outputText value="#{chantier.nom}"/>
</p:column>
<p:column headerText="Client" sortBy="#{chantier.client}">
<h:outputText value="#{chantier.client}"/>
</p:column>
<p:column headerText="Date début prévue" sortBy="#{chantier.dateDebut}">
<h:outputText value="#{chantier.dateDebut}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Date fin prévue" sortBy="#{chantier.dateFinPrevue}">
<h:outputText value="#{chantier.dateFinPrevue}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Budget">
<h:outputText value="#{chantier.budget}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{chantiersView.viewDetails(chantier.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Chantiers planifiés - BTP Xpress</ui:define>
<f:metadata>
<f:event type="preRenderView" listener="#{chantiersView.setFiltreStatut('PLANIFIE')}"/>
<f:event type="preRenderView" listener="#{chantiersView.init()}"/>
</f:metadata>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Chantiers planifiés</h1>
<p:commandButton value="Nouveau chantier" icon="pi pi-plus"
action="#{chantiersView.createNew()}"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{chantiersView}"/>
<ui:param name="tableId" value="chantiersTable"/>
<ui:define name="filter-fields">
<div class="grid">
<div class="col-12 md:col-6">
<h:outputLabel for="filtreNom" value="Nom du chantier"/>
<p:inputText id="filtreNom" value="#{chantiersView.filtreNom}"
placeholder="Rechercher par nom..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-6">
<h:outputLabel for="filtreClient" value="Client"/>
<p:inputText id="filtreClient" value="#{chantiersView.filtreClient}"
placeholder="Rechercher par client..." style="width: 100%;"/>
</div>
</div>
</ui:define>
</ui:include>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="chantiersForm"/>
<ui:param name="tableId" value="chantiersTable"/>
<ui:param name="viewBean" value="#{chantiersView}"/>
<ui:param name="var" value="chantier"/>
<ui:param name="title" value="Liste des chantiers planifiés"/>
<ui:param name="createPath" value="/chantiers/nouveau"/>
<ui:define name="columns">
<p:column headerText="Nom" sortBy="#{chantier.nom}">
<h:outputText value="#{chantier.nom}"/>
</p:column>
<p:column headerText="Client" sortBy="#{chantier.client}">
<h:outputText value="#{chantier.client}"/>
</p:column>
<p:column headerText="Date début prévue" sortBy="#{chantier.dateDebut}">
<h:outputText value="#{chantier.dateDebut}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Date fin prévue" sortBy="#{chantier.dateFinPrevue}">
<h:outputText value="#{chantier.dateFinPrevue}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Budget">
<h:outputText value="#{chantier.budget}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{chantiersView.viewDetails(chantier.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,94 +1,94 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Chantiers terminés - BTP Xpress</ui:define>
<f:metadata>
<f:event type="preRenderView" listener="#{chantiersView.setFiltreStatut('TERMINE')}"/>
<f:event type="preRenderView" listener="#{chantiersView.init()}"/>
</f:metadata>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Chantiers terminés</h1>
<p:commandButton value="Nouveau chantier" icon="pi pi-plus"
action="#{chantiersView.createNew()}"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{chantiersView}"/>
<ui:param name="tableId" value="chantiersTable"/>
<ui:define name="filter-fields">
<div class="grid">
<div class="col-12 md:col-6">
<h:outputLabel for="filtreNom" value="Nom du chantier"/>
<p:inputText id="filtreNom" value="#{chantiersView.filtreNom}"
placeholder="Rechercher par nom..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-6">
<h:outputLabel for="filtreClient" value="Client"/>
<p:inputText id="filtreClient" value="#{chantiersView.filtreClient}"
placeholder="Rechercher par client..." style="width: 100%;"/>
</div>
</div>
</ui:define>
</ui:include>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="chantiersForm"/>
<ui:param name="tableId" value="chantiersTable"/>
<ui:param name="viewBean" value="#{chantiersView}"/>
<ui:param name="var" value="chantier"/>
<ui:param name="title" value="Liste des chantiers terminés"/>
<ui:param name="createPath" value="/chantiers/nouveau"/>
<ui:define name="columns">
<p:column headerText="Nom" sortBy="#{chantier.nom}">
<h:outputText value="#{chantier.nom}"/>
</p:column>
<p:column headerText="Client" sortBy="#{chantier.client}">
<h:outputText value="#{chantier.client}"/>
</p:column>
<p:column headerText="Date début" sortBy="#{chantier.dateDebut}">
<h:outputText value="#{chantier.dateDebut}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Date fin prévue" sortBy="#{chantier.dateFinPrevue}">
<h:outputText value="#{chantier.dateFinPrevue}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Budget">
<h:outputText value="#{chantier.budget}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{chantiersView.viewDetails(chantier.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Chantiers terminés - BTP Xpress</ui:define>
<f:metadata>
<f:event type="preRenderView" listener="#{chantiersView.setFiltreStatut('TERMINE')}"/>
<f:event type="preRenderView" listener="#{chantiersView.init()}"/>
</f:metadata>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Chantiers terminés</h1>
<p:commandButton value="Nouveau chantier" icon="pi pi-plus"
action="#{chantiersView.createNew()}"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{chantiersView}"/>
<ui:param name="tableId" value="chantiersTable"/>
<ui:define name="filter-fields">
<div class="grid">
<div class="col-12 md:col-6">
<h:outputLabel for="filtreNom" value="Nom du chantier"/>
<p:inputText id="filtreNom" value="#{chantiersView.filtreNom}"
placeholder="Rechercher par nom..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-6">
<h:outputLabel for="filtreClient" value="Client"/>
<p:inputText id="filtreClient" value="#{chantiersView.filtreClient}"
placeholder="Rechercher par client..." style="width: 100%;"/>
</div>
</div>
</ui:define>
</ui:include>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="chantiersForm"/>
<ui:param name="tableId" value="chantiersTable"/>
<ui:param name="viewBean" value="#{chantiersView}"/>
<ui:param name="var" value="chantier"/>
<ui:param name="title" value="Liste des chantiers terminés"/>
<ui:param name="createPath" value="/chantiers/nouveau"/>
<ui:define name="columns">
<p:column headerText="Nom" sortBy="#{chantier.nom}">
<h:outputText value="#{chantier.nom}"/>
</p:column>
<p:column headerText="Client" sortBy="#{chantier.client}">
<h:outputText value="#{chantier.client}"/>
</p:column>
<p:column headerText="Date début" sortBy="#{chantier.dateDebut}">
<h:outputText value="#{chantier.dateDebut}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Date fin prévue" sortBy="#{chantier.dateFinPrevue}">
<h:outputText value="#{chantier.dateFinPrevue}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Budget">
<h:outputText value="#{chantier.budget}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{chantiersView.viewDetails(chantier.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,95 +1,95 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Clients - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Gestion des Clients</h1>
<p:commandButton value="Nouveau client" icon="pi pi-user-plus"
action="#{clientsView.createNew()}"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{clientsView}"/>
<ui:param name="tableId" value="clientsTable"/>
<ui:define name="filter-fields">
<div class="grid">
<div class="col-12 md:col-4">
<h:outputLabel for="filtreNom" value="Nom / Raison sociale"/>
<p:inputText id="filtreNom" value="#{clientsView.filtreNom}"
placeholder="Rechercher par nom..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreEmail" value="Email"/>
<p:inputText id="filtreEmail" value="#{clientsView.filtreEmail}"
placeholder="Rechercher par email..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreVille" value="Ville"/>
<p:inputText id="filtreVille" value="#{clientsView.filtreVille}"
placeholder="Rechercher par ville..." style="width: 100%;"/>
</div>
</div>
</ui:define>
</ui:include>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="clientsForm"/>
<ui:param name="tableId" value="clientsTable"/>
<ui:param name="viewBean" value="#{clientsView}"/>
<ui:param name="var" value="client"/>
<ui:param name="title" value="Liste des clients"/>
<ui:param name="createPath" value="/clients/nouveau"/>
<ui:define name="columns">
<p:column headerText="Raison sociale" sortBy="#{client.raisonSociale}">
<h:outputText value="#{client.raisonSociale}"/>
</p:column>
<p:column headerText="Contact" sortBy="#{client.nomContact}">
<h:outputText value="#{client.nomContact}"/>
</p:column>
<p:column headerText="Email" sortBy="#{client.email}">
<h:outputText value="#{client.email}"/>
</p:column>
<p:column headerText="Téléphone">
<h:outputText value="#{client.telephone}"/>
</p:column>
<p:column headerText="Ville" sortBy="#{client.ville}">
<h:outputText value="#{client.ville}"/>
</p:column>
<p:column headerText="Chantiers">
<p:tag value="#{client.nombreChantiers}" severity="info"/>
</p:column>
<p:column headerText="Chiffre d'affaires">
<h:outputText value="#{client.chiffreAffairesTotal}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{clientsView.viewDetails(client.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Clients - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Gestion des Clients</h1>
<p:commandButton value="Nouveau client" icon="pi pi-user-plus"
action="#{clientsView.createNew()}"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{clientsView}"/>
<ui:param name="tableId" value="clientsTable"/>
<ui:define name="filter-fields">
<div class="grid">
<div class="col-12 md:col-4">
<h:outputLabel for="filtreNom" value="Nom / Raison sociale"/>
<p:inputText id="filtreNom" value="#{clientsView.filtreNom}"
placeholder="Rechercher par nom..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreEmail" value="Email"/>
<p:inputText id="filtreEmail" value="#{clientsView.filtreEmail}"
placeholder="Rechercher par email..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreVille" value="Ville"/>
<p:inputText id="filtreVille" value="#{clientsView.filtreVille}"
placeholder="Rechercher par ville..." style="width: 100%;"/>
</div>
</div>
</ui:define>
</ui:include>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="clientsForm"/>
<ui:param name="tableId" value="clientsTable"/>
<ui:param name="viewBean" value="#{clientsView}"/>
<ui:param name="var" value="client"/>
<ui:param name="title" value="Liste des clients"/>
<ui:param name="createPath" value="/clients/nouveau"/>
<ui:define name="columns">
<p:column headerText="Raison sociale" sortBy="#{client.raisonSociale}">
<h:outputText value="#{client.raisonSociale}"/>
</p:column>
<p:column headerText="Contact" sortBy="#{client.nomContact}">
<h:outputText value="#{client.nomContact}"/>
</p:column>
<p:column headerText="Email" sortBy="#{client.email}">
<h:outputText value="#{client.email}"/>
</p:column>
<p:column headerText="Téléphone">
<h:outputText value="#{client.telephone}"/>
</p:column>
<p:column headerText="Ville" sortBy="#{client.ville}">
<h:outputText value="#{client.ville}"/>
</p:column>
<p:column headerText="Chantiers">
<p:tag value="#{client.nombreChantiers}" severity="info"/>
</p:column>
<p:column headerText="Chiffre d'affaires">
<h:outputText value="#{client.chiffreAffairesTotal}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{clientsView.viewDetails(client.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,85 +1,85 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Détails du client - BTP Xpress</ui:define>
<f:metadata>
<f:viewParam name="id" value="#{clientsView.clientId}"/>
<f:event type="preRenderView" listener="#{clientsView.loadClientById()}"/>
</f:metadata>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Détails du client</h1>
<p:commandButton value="Retour" icon="pi pi-arrow-left"
outcome="/clients"
styleClass="ui-button-secondary"/>
</div>
<h:form id="detailsClientForm">
<div class="grid" rendered="#{not empty clientsView.selectedItem}">
<div class="col-12">
<p:panel header="Informations générales">
<div class="grid">
<div class="col-12 md:col-6">
<p><strong>Raison sociale :</strong> #{clientsView.selectedItem.raisonSociale}</p>
</div>
<div class="col-12 md:col-6">
<p><strong>Nom du contact :</strong> #{clientsView.selectedItem.nomContact}</p>
</div>
<div class="col-12 md:col-6">
<p><strong>Email :</strong> #{clientsView.selectedItem.email}</p>
</div>
<div class="col-12 md:col-6">
<p><strong>Téléphone :</strong> #{clientsView.selectedItem.telephone}</p>
</div>
</div>
</p:panel>
</div>
<div class="col-12 md:col-6">
<p:panel header="Adresse">
<p><strong>Adresse :</strong> #{clientsView.selectedItem.adresse}</p>
<p><strong>Ville :</strong> #{clientsView.selectedItem.ville}</p>
<p><strong>Code postal :</strong> #{clientsView.selectedItem.codePostal}</p>
<p><strong>Pays :</strong> #{clientsView.selectedItem.pays}</p>
</p:panel>
</div>
<div class="col-12 md:col-6">
<p:panel header="Statistiques">
<p><strong>Nombre de chantiers :</strong>
<p:tag value="#{clientsView.selectedItem.nombreChantiers}" severity="info"/>
</p>
<p><strong>Chiffre d'affaires total :</strong>
<h:outputText value="#{clientsView.selectedItem.chiffreAffairesTotal}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p>
<p><strong>Date de création :</strong>
<h:outputText value="#{clientsView.selectedItem.dateCreation}">
<f:convertDateTime pattern="dd/MM/yyyy HH:mm"/>
</h:outputText>
</p>
</p:panel>
</div>
</div>
<p:message rendered="#{empty clientsView.selectedItem}" severity="warn"
summary="Client introuvable"/>
</h:form>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Détails du client - BTP Xpress</ui:define>
<f:metadata>
<f:viewParam name="id" value="#{clientsView.clientId}"/>
<f:event type="preRenderView" listener="#{clientsView.loadClientById()}"/>
</f:metadata>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Détails du client</h1>
<p:commandButton value="Retour" icon="pi pi-arrow-left"
outcome="/clients"
styleClass="ui-button-secondary"/>
</div>
<h:form id="detailsClientForm">
<div class="grid" rendered="#{not empty clientsView.selectedItem}">
<div class="col-12">
<p:panel header="Informations générales">
<div class="grid">
<div class="col-12 md:col-6">
<p><strong>Raison sociale :</strong> #{clientsView.selectedItem.raisonSociale}</p>
</div>
<div class="col-12 md:col-6">
<p><strong>Nom du contact :</strong> #{clientsView.selectedItem.nomContact}</p>
</div>
<div class="col-12 md:col-6">
<p><strong>Email :</strong> #{clientsView.selectedItem.email}</p>
</div>
<div class="col-12 md:col-6">
<p><strong>Téléphone :</strong> #{clientsView.selectedItem.telephone}</p>
</div>
</div>
</p:panel>
</div>
<div class="col-12 md:col-6">
<p:panel header="Adresse">
<p><strong>Adresse :</strong> #{clientsView.selectedItem.adresse}</p>
<p><strong>Ville :</strong> #{clientsView.selectedItem.ville}</p>
<p><strong>Code postal :</strong> #{clientsView.selectedItem.codePostal}</p>
<p><strong>Pays :</strong> #{clientsView.selectedItem.pays}</p>
</p:panel>
</div>
<div class="col-12 md:col-6">
<p:panel header="Statistiques">
<p><strong>Nombre de chantiers :</strong>
<p:tag value="#{clientsView.selectedItem.nombreChantiers}" severity="info"/>
</p>
<p><strong>Chiffre d'affaires total :</strong>
<h:outputText value="#{clientsView.selectedItem.chiffreAffairesTotal}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p>
<p><strong>Date de création :</strong>
<h:outputText value="#{clientsView.selectedItem.dateCreation}">
<f:convertDateTime pattern="dd/MM/yyyy HH:mm"/>
</h:outputText>
</p>
</p:panel>
</div>
</div>
<p:message rendered="#{empty clientsView.selectedItem}" severity="warn"
summary="Client introuvable"/>
</h:form>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,95 +1,95 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Nouveau client - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Créer un nouveau client</h1>
<p:commandButton value="Retour" icon="pi pi-arrow-left"
outcome="/clients"
styleClass="ui-button-secondary"/>
</div>
<h:form id="nouveauClientForm">
<div class="grid">
<div class="col-12 md:col-6">
<h:outputLabel for="raisonSociale" value="Raison sociale *"/>
<p:inputText id="raisonSociale" value="#{clientsView.selectedItem.raisonSociale}"
required="true" requiredMessage="La raison sociale est obligatoire"
style="width: 100%;"/>
</div>
<div class="col-12 md:col-6">
<h:outputLabel for="nomContact" value="Nom du contact *"/>
<p:inputText id="nomContact" value="#{clientsView.selectedItem.nomContact}"
required="true" requiredMessage="Le nom du contact est obligatoire"
style="width: 100%;"/>
</div>
<div class="col-12 md:col-6">
<h:outputLabel for="email" value="Email *"/>
<p:inputText id="email" value="#{clientsView.selectedItem.email}"
required="true" requiredMessage="L'email est obligatoire"
style="width: 100%;"/>
<p:message for="email"/>
</div>
<div class="col-12 md:col-6">
<h:outputLabel for="telephone" value="Téléphone"/>
<p:inputText id="telephone" value="#{clientsView.selectedItem.telephone}"
style="width: 100%;"/>
</div>
<div class="col-12">
<h:outputLabel for="adresse" value="Adresse"/>
<p:inputTextarea id="adresse" value="#{clientsView.selectedItem.adresse}"
rows="3" style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="ville" value="Ville"/>
<p:inputText id="ville" value="#{clientsView.selectedItem.ville}"
style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="codePostal" value="Code postal"/>
<p:inputText id="codePostal" value="#{clientsView.selectedItem.codePostal}"
style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="pays" value="Pays"/>
<p:inputText id="pays" value="#{clientsView.selectedItem.pays}"
style="width: 100%;"/>
</div>
<div class="col-12">
<div class="flex justify-content-end gap-2 mt-3">
<p:commandButton value="Annuler" icon="pi pi-times"
outcome="/clients"
styleClass="ui-button-secondary"/>
<p:commandButton value="Enregistrer" icon="pi pi-check"
action="#{clientsView.saveNew()}"
update="@form"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
</h:form>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Nouveau client - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Créer un nouveau client</h1>
<p:commandButton value="Retour" icon="pi pi-arrow-left"
outcome="/clients"
styleClass="ui-button-secondary"/>
</div>
<h:form id="nouveauClientForm">
<div class="grid">
<div class="col-12 md:col-6">
<h:outputLabel for="raisonSociale" value="Raison sociale *"/>
<p:inputText id="raisonSociale" value="#{clientsView.selectedItem.raisonSociale}"
required="true" requiredMessage="La raison sociale est obligatoire"
style="width: 100%;"/>
</div>
<div class="col-12 md:col-6">
<h:outputLabel for="nomContact" value="Nom du contact *"/>
<p:inputText id="nomContact" value="#{clientsView.selectedItem.nomContact}"
required="true" requiredMessage="Le nom du contact est obligatoire"
style="width: 100%;"/>
</div>
<div class="col-12 md:col-6">
<h:outputLabel for="email" value="Email *"/>
<p:inputText id="email" value="#{clientsView.selectedItem.email}"
required="true" requiredMessage="L'email est obligatoire"
style="width: 100%;"/>
<p:message for="email"/>
</div>
<div class="col-12 md:col-6">
<h:outputLabel for="telephone" value="Téléphone"/>
<p:inputText id="telephone" value="#{clientsView.selectedItem.telephone}"
style="width: 100%;"/>
</div>
<div class="col-12">
<h:outputLabel for="adresse" value="Adresse"/>
<p:inputTextarea id="adresse" value="#{clientsView.selectedItem.adresse}"
rows="3" style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="ville" value="Ville"/>
<p:inputText id="ville" value="#{clientsView.selectedItem.ville}"
style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="codePostal" value="Code postal"/>
<p:inputText id="codePostal" value="#{clientsView.selectedItem.codePostal}"
style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="pays" value="Pays"/>
<p:inputText id="pays" value="#{clientsView.selectedItem.pays}"
style="width: 100%;"/>
</div>
<div class="col-12">
<div class="flex justify-content-end gap-2 mt-3">
<p:commandButton value="Annuler" icon="pi pi-times"
outcome="/clients"
styleClass="ui-button-secondary"/>
<p:commandButton value="Enregistrer" icon="pi pi-check"
action="#{clientsView.saveNew()}"
update="@form"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
</h:form>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,96 +1,96 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Recherche de clients - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Recherche avancée de clients</h1>
<p:commandButton value="Retour" icon="pi pi-arrow-left"
outcome="/clients"
styleClass="ui-button-secondary"/>
</div>
<h:form id="rechercheClientForm">
<div class="grid">
<div class="col-12">
<p:panel header="Critères de recherche">
<div class="grid">
<div class="col-12 md:col-4">
<h:outputLabel for="rechercheNom" value="Nom / Raison sociale"/>
<p:inputText id="rechercheNom" value="#{clientsView.filtreNom}"
placeholder="Rechercher..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="rechercheEmail" value="Email"/>
<p:inputText id="rechercheEmail" value="#{clientsView.filtreEmail}"
placeholder="Rechercher..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="rechercheVille" value="Ville"/>
<p:inputText id="rechercheVille" value="#{clientsView.filtreVille}"
placeholder="Rechercher..." style="width: 100%;"/>
</div>
</div>
<div class="flex justify-content-end gap-2 mt-3">
<p:commandButton value="Réinitialiser" icon="pi pi-refresh"
action="#{clientsView.resetFilters()}"
update="@form"
styleClass="ui-button-secondary"/>
<p:commandButton value="Rechercher" icon="pi pi-search"
action="#{clientsView.search()}"
update="@form,clientsTable"
styleClass="ui-button-primary"/>
</div>
</p:panel>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="clientsForm"/>
<ui:param name="tableId" value="clientsTable"/>
<ui:param name="viewBean" value="#{clientsView}"/>
<ui:param name="var" value="client"/>
<ui:param name="title" value="Résultats de recherche"/>
<ui:param name="createPath" value=""/>
<ui:define name="columns">
<p:column headerText="Raison sociale" sortBy="#{client.raisonSociale}">
<h:outputText value="#{client.raisonSociale}"/>
</p:column>
<p:column headerText="Contact" sortBy="#{client.nomContact}">
<h:outputText value="#{client.nomContact}"/>
</p:column>
<p:column headerText="Email" sortBy="#{client.email}">
<h:outputText value="#{client.email}"/>
</p:column>
<p:column headerText="Téléphone">
<h:outputText value="#{client.telephone}"/>
</p:column>
<p:column headerText="Ville" sortBy="#{client.ville}">
<h:outputText value="#{client.ville}"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{clientsView.viewDetails(client.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</h:form>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Recherche de clients - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Recherche avancée de clients</h1>
<p:commandButton value="Retour" icon="pi pi-arrow-left"
outcome="/clients"
styleClass="ui-button-secondary"/>
</div>
<h:form id="rechercheClientForm">
<div class="grid">
<div class="col-12">
<p:panel header="Critères de recherche">
<div class="grid">
<div class="col-12 md:col-4">
<h:outputLabel for="rechercheNom" value="Nom / Raison sociale"/>
<p:inputText id="rechercheNom" value="#{clientsView.filtreNom}"
placeholder="Rechercher..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="rechercheEmail" value="Email"/>
<p:inputText id="rechercheEmail" value="#{clientsView.filtreEmail}"
placeholder="Rechercher..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="rechercheVille" value="Ville"/>
<p:inputText id="rechercheVille" value="#{clientsView.filtreVille}"
placeholder="Rechercher..." style="width: 100%;"/>
</div>
</div>
<div class="flex justify-content-end gap-2 mt-3">
<p:commandButton value="Réinitialiser" icon="pi pi-refresh"
action="#{clientsView.resetFilters()}"
update="@form"
styleClass="ui-button-secondary"/>
<p:commandButton value="Rechercher" icon="pi pi-search"
action="#{clientsView.search()}"
update="@form,clientsTable"
styleClass="ui-button-primary"/>
</div>
</p:panel>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="clientsForm"/>
<ui:param name="tableId" value="clientsTable"/>
<ui:param name="viewBean" value="#{clientsView}"/>
<ui:param name="var" value="client"/>
<ui:param name="title" value="Résultats de recherche"/>
<ui:param name="createPath" value=""/>
<ui:define name="columns">
<p:column headerText="Raison sociale" sortBy="#{client.raisonSociale}">
<h:outputText value="#{client.raisonSociale}"/>
</p:column>
<p:column headerText="Contact" sortBy="#{client.nomContact}">
<h:outputText value="#{client.nomContact}"/>
</p:column>
<p:column headerText="Email" sortBy="#{client.email}">
<h:outputText value="#{client.email}"/>
</p:column>
<p:column headerText="Téléphone">
<h:outputText value="#{client.telephone}"/>
</p:column>
<p:column headerText="Ville" sortBy="#{client.ville}">
<h:outputText value="#{client.ville}"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{clientsView.viewDetails(client.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</h:form>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,488 +1,488 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Tableau de bord - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<h1>Tableau de bord - BTP Xpress</h1>
<p>Bean dashboardView disponible: #{not empty dashboardView}</p>
<p>Chantiers actifs: #{dashboardView.chantiersActifs}</p>
<p>Test de contenu simple</p>
</div>
</div>
</div>
<!-- ========================================================================
BARRE D'ALERTES (affichée uniquement si alertes critiques)
======================================================================== -->
<p:outputPanel rendered="#{dashboardView.alerteCritique}" styleClass="col-12">
<div class="notification notification-danger">
<i class="pi pi-exclamation-triangle"></i>
<strong>#{dashboardView.totalAlertes} alertes</strong> nécessitent votre attention immédiate
<span style="margin-left: 1rem; opacity: 0.9;">
Maintenance: #{dashboardView.alertesMaintenanceCount} •
Chantiers: #{dashboardView.alertesChantiersCount} •
Disponibilités: #{dashboardView.alertesDisponibilitesCount}
</span>
<p:commandButton value="Rafraîchir"
icon="pi pi-refresh"
action="#{dashboardView.rafraichir}"
update="@form"
styleClass="ui-button-text"
style="float: right;"/>
</div>
</p:outputPanel>
<div class="grid">
<!-- ====================================================================
KPIs PRINCIPAUX (3 cartes en ligne)
==================================================================== -->
<div class="col-12">
<div class="grid" style="margin: -0.5rem;">
<!-- KPI 1: Chantiers Actifs -->
<div class="col-12 md:col-6 xl:col-4">
<div class="card overview-box white">
<div class="overview-info">
<h6>Chantiers actifs</h6>
<h1>#{dashboardView.chantiersActifs}</h1>
<p class="subtitle">
Sur #{dashboardView.nombreChantiers} au total
</p>
<p:progressBar value="#{dashboardView.tauxActiviteChantiers}"
showValue="true"
displayValue="#{dashboardView.tauxActiviteChantiers}%"
styleClass="ui-progressbar-info"/>
</div>
<i class="pi pi-building"></i>
</div>
</div>
<!-- KPI 2: Équipes Disponibles -->
<div class="col-12 md:col-6 xl:col-4">
<div class="card overview-box blue">
<div class="overview-info">
<h6>Équipes disponibles</h6>
<h1>#{dashboardView.equipesDisponibles}/#{dashboardView.nombreEquipes}</h1>
<p class="subtitle">Taux de disponibilité</p>
<p:progressBar value="#{dashboardView.tauxDisponibiliteEquipes}"
showValue="true"
displayValue="#{dashboardView.tauxDisponibiliteEquipes}%"
style="background: rgba(255,255,255,0.3);"/>
</div>
<i class="pi pi-users"></i>
</div>
</div>
<!-- KPI 3: Maintenances Critiques -->
<div class="col-12 md:col-12 xl:col-4">
<div class="card overview-box #{dashboardView.alerteRetardMaintenance ? 'red' : 'green'}">
<div class="overview-info">
<h6>Maintenances en retard</h6>
<h1>#{dashboardView.maintenancesEnRetard}</h1>
<p class="subtitle">#{dashboardView.maintenancesPlanifiees} planifiées</p>
<p:badge value="#{dashboardView.alerteRetardMaintenance ? 'URGENT' : 'OK'}"
severity="#{dashboardView.alerteRetardMaintenance ? 'danger' : 'success'}"
style="margin-top: 0.5rem;"/>
</div>
<i class="pi pi-wrench"></i>
</div>
</div>
</div>
</div>
<!-- ====================================================================
SECTION CENTRALE : Graphique + KPIs Ressources
==================================================================== -->
<!-- Colonne gauche: Statistiques chantiers (placeholder pour graphique futur) -->
<div class="col-12 xl:col-8">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Vue d'ensemble</h6>
<p class="subtitle">Statistiques globales</p>
</div>
</div>
<div class="grid">
<!-- Chantiers actifs avec progression -->
<div class="col-12 md:col-6">
<div class="statistic-item" style="padding: 1.5rem; background: var(--blue-50); border-radius: var(--border-radius); border-left: 4px solid var(--blue-500);">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 0.75rem;">
<i class="pi pi-building" style="font-size: 2rem; color: var(--blue-500);"></i>
<div style="flex: 1;">
<h5 style="margin: 0; font-size: 1.75rem; color: var(--blue-600);">#{dashboardView.chantiersActifs}</h5>
<h6 style="margin: 0.25rem 0 0 0; font-weight: 500; color: var(--text-color-secondary);">Chantiers actifs</h6>
</div>
</div>
<p:progressBar value="#{dashboardView.tauxActiviteChantiers}"
showValue="true"
displayValue="#{dashboardView.tauxActiviteChantiers}% d'activité"
styleClass="ui-progressbar-info"
style="height: 1rem;"/>
</div>
</div>
<!-- Chantiers en retard -->
<div class="col-12 md:col-6">
<div class="statistic-item" style="padding: 1.5rem; background: var(--orange-50); border-radius: var(--border-radius); border-left: 4px solid var(--orange-500);">
<div style="display: flex; align-items: center; gap: 1rem;">
<i class="pi pi-exclamation-triangle" style="font-size: 2rem; color: var(--orange-500);"></i>
<div style="flex: 1;">
<h5 style="margin: 0; font-size: 1.75rem; color: var(--orange-600);">#{dashboardView.chantiersEnRetardList.size()}</h5>
<h6 style="margin: 0.25rem 0 0 0; font-weight: 500; color: var(--text-color-secondary);">Chantiers en retard</h6>
</div>
</div>
<p:outputPanel rendered="#{dashboardView.chantiersEnRetardList.size() > 0}">
<small style="display: block; margin-top: 0.75rem; color: var(--orange-700);">
<i class="pi pi-info-circle"></i> Attention requise
</small>
</p:outputPanel>
</div>
</div>
<!-- Événements aujourd'hui -->
<div class="col-12 md:col-6">
<div class="statistic-item" style="padding: 1.5rem; background: var(--purple-50); border-radius: var(--border-radius); border-left: 4px solid var(--purple-500);">
<div style="display: flex; align-items: center; gap: 1rem;">
<i class="pi pi-calendar" style="font-size: 2rem; color: var(--purple-500);"></i>
<div style="flex: 1;">
<h5 style="margin: 0; font-size: 1.75rem; color: var(--purple-600);">#{dashboardView.evenementsAujourdhui}</h5>
<h6 style="margin: 0.25rem 0 0 0; font-weight: 500; color: var(--text-color-secondary);">Événements aujourd'hui</h6>
</div>
</div>
</div>
</div>
<!-- Documents totaux -->
<div class="col-12 md:col-6">
<div class="statistic-item" style="padding: 1.5rem; background: var(--cyan-50); border-radius: var(--border-radius); border-left: 4px solid var(--cyan-500);">
<div style="display: flex; align-items: center; gap: 1rem;">
<i class="pi pi-file" style="font-size: 2rem; color: var(--cyan-500);"></i>
<div style="flex: 1;">
<h5 style="margin: 0; font-size: 1.75rem; color: var(--cyan-600);">#{dashboardView.nombreDocuments}</h5>
<h6 style="margin: 0.25rem 0 0 0; font-weight: 500; color: var(--text-color-secondary);">Documents totaux</h6>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Colonne droite: KPIs Ressources -->
<div class="col-12 xl:col-4">
<div class="card" style="height: 100%;">
<div class="card-header">
<div class="card-title">
<h6>Ressources</h6>
<p class="subtitle">État actuel des ressources</p>
</div>
</div>
<div style="padding: 1rem; display: flex; flex-direction: column; gap: 1.5rem;">
<!-- Employés actifs -->
<div class="statistic-item" style="padding: 1.25rem; background: var(--green-50); border-radius: var(--border-radius); border-left: 4px solid var(--green-500);">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 0.75rem;">
<i class="pi pi-users" style="font-size: 1.75rem; color: var(--green-500);"></i>
<div style="flex: 1;">
<h5 style="margin: 0; font-size: 1.5rem; color: var(--green-600);">
#{dashboardView.employesActifs}<span style="font-size: 1rem; color: var(--text-color-secondary);">/#{dashboardView.nombreEmployes}</span>
</h5>
<h6 style="margin: 0.25rem 0 0 0; font-weight: 500; color: var(--text-color-secondary);">Employés actifs</h6>
</div>
</div>
<p:progressBar value="#{dashboardView.tauxActiviteEmployes}"
showValue="true"
displayValue="#{dashboardView.tauxActiviteEmployes}%"
styleClass="ui-progressbar-#{dashboardView.tauxActiviteEmployes > 80 ? 'success' : (dashboardView.tauxActiviteEmployes > 60 ? 'warning' : 'danger')}"
style="height: 1rem;"/>
</div>
<!-- Matériel disponible -->
<div class="statistic-item" style="padding: 1.25rem; background: var(--teal-50); border-radius: var(--border-radius); border-left: 4px solid var(--teal-500);">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 0.75rem;">
<i class="pi pi-cog" style="font-size: 1.75rem; color: var(--teal-500);"></i>
<div style="flex: 1;">
<h5 style="margin: 0; font-size: 1.5rem; color: var(--teal-600);">
#{dashboardView.materielDisponible}<span style="font-size: 1rem; color: var(--text-color-secondary);">/#{dashboardView.nombreMateriel}</span>
</h5>
<h6 style="margin: 0.25rem 0 0 0; font-weight: 500; color: var(--text-color-secondary);">Matériel disponible</h6>
</div>
</div>
<p:progressBar value="#{dashboardView.tauxDisponibiliteMateriel}"
showValue="true"
displayValue="#{dashboardView.tauxDisponibiliteMateriel}%"
styleClass="ui-progressbar-success"
style="height: 1rem;"/>
</div>
<!-- Taux d'utilisation global -->
<div class="statistic-item" style="padding: 1.25rem; background: var(--indigo-50); border-radius: var(--border-radius); border-left: 4px solid var(--indigo-500); flex: 1; display: flex; flex-direction: column; justify-content: center;">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 0.5rem;">
<i class="pi pi-chart-line" style="font-size: 1.75rem; color: var(--indigo-500);"></i>
<div style="flex: 1;">
<h5 style="margin: 0; font-size: 1.75rem; color: var(--indigo-600);">#{dashboardView.tauxUtilisationGlobal}%</h5>
<h6 style="margin: 0.25rem 0 0 0; font-weight: 500; color: var(--text-color-secondary);">Taux d'utilisation global</h6>
</div>
</div>
<small style="display: block; color: var(--text-color-secondary); font-style: italic; padding-left: 2.75rem;">
<i class="pi pi-info-circle" style="font-size: 0.875rem;"></i>
Moyenne chantiers, employés et matériel
</small>
</div>
</div>
</div>
</div>
<!-- ====================================================================
TABLEAU CHANTIERS ACTIFS
==================================================================== -->
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Chantiers actifs</h6>
<p class="subtitle">#{dashboardView.chantiersActifsList.size()} chantiers en cours</p>
</div>
<p:commandButton value="Voir tout"
icon="pi pi-arrow-right"
outcome="/chantiers"
styleClass="ui-button-text"/>
</div>
<p:dataTable value="#{dashboardView.chantiersActifsList}"
var="chantier"
emptyMessage="Aucun chantier actif pour le moment"
styleClass="p-datatable-sm"
paginator="true"
rows="10"
paginatorPosition="bottom">
<p:column headerText="Nom" sortBy="#{chantier.nom}">
<h:outputText value="#{chantier.nom}"/>
</p:column>
<p:column headerText="Client" sortBy="#{chantier.client}">
<h:outputText value="#{chantier.client}"/>
</p:column>
<p:column headerText="Date début" sortBy="#{chantier.dateDebut}">
<h:outputText value="#{chantier.dateDebutFormatee}"/>
</p:column>
<p:column headerText="Fin prévue" sortBy="#{chantier.dateFinPrevue}">
<h:outputText value="#{chantier.dateFinPrevueFormatee}"/>
</p:column>
<p:column headerText="Avancement">
<p:progressBar value="#{chantier.avancement}"
showValue="true"
displayValue="#{chantier.avancement}%"
styleClass="ui-progressbar-success"/>
</p:column>
<p:column headerText="Budget" sortBy="#{chantier.budget}">
<h:outputText value="#{chantier.budget}">
<f:convertNumber type="number" groupingUsed="true"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p:column>
<p:column headerText="Coût réel" sortBy="#{chantier.coutReel}">
<h:outputText value="#{chantier.coutReel}"
style="#{chantier.depassementBudget ? 'color: var(--red-500); font-weight: bold;' : ''}">
<f:convertNumber type="number" groupingUsed="true"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
<p:badge value="!" severity="danger"
style="margin-left: 0.5rem;"
rendered="#{chantier.depassementBudget}"/>
</p:column>
<p:column headerText="Statut">
<p:badge value="#{chantier.statut}"
severity="#{chantier.statut == 'EN_COURS' ? 'info' : 'success'}"/>
</p:column>
</p:dataTable>
</div>
</div>
<!-- ====================================================================
SECTION BAS : Chantiers en retard + Maintenances en retard
==================================================================== -->
<!-- Chantiers en retard -->
<div class="col-12 md:col-6">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Chantiers en retard</h6>
<p class="subtitle">#{dashboardView.chantiersEnRetardList.size()} chantiers en retard</p>
</div>
</div>
<ui:repeat value="#{dashboardView.chantiersEnRetardList}" var="chantier">
<div class="chantier-retard-item" style="padding: 1rem; border-bottom: 1px solid var(--surface-border); background: var(--orange-50);">
<div style="display: flex; justify-content: space-between; align-items: start;">
<div>
<h6 style="margin: 0 0 0.5rem 0;">
<i class="pi pi-building" style="color: var(--orange-500);"></i>
#{chantier.nom}
</h6>
<p style="margin: 0.25rem 0; font-size: 0.9rem;">
<strong>Date fin prévue:</strong> #{chantier.dateFinPrevueFormatee}
</p>
</div>
<p:badge value="+#{chantier.joursRetard}j" severity="warning" size="large"/>
</div>
</div>
</ui:repeat>
<p:outputPanel rendered="#{empty dashboardView.chantiersEnRetardList}">
<div style="padding: 2rem; text-align: center; color: var(--green-500);">
<i class="pi pi-check-circle" style="font-size: 3rem;"></i>
<p style="margin-top: 1rem;">Tous les chantiers sont dans les temps</p>
</div>
</p:outputPanel>
</div>
</div>
<!-- Maintenances en retard -->
<div class="col-12 md:col-6">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Maintenances en retard</h6>
<p class="subtitle">#{dashboardView.maintenancesEnRetardList.size()} maintenances urgentes</p>
</div>
</div>
<ui:repeat value="#{dashboardView.maintenancesEnRetardList}" var="maintenance">
<div class="maintenance-item" style="padding: 1rem; border-bottom: 1px solid var(--surface-border); background: var(--red-50);">
<div style="display: flex; justify-content: space-between; align-items: start;">
<div>
<h6 style="margin: 0 0 0.5rem 0;">
<i class="pi pi-wrench" style="color: var(--red-500);"></i>
#{maintenance.materiel}
</h6>
<p style="margin: 0.25rem 0; font-size: 0.9rem;">
<strong>Type:</strong> #{maintenance.type} •
<strong>Prévue:</strong> #{maintenance.datePrevueFormatee}
</p>
<p style="margin: 0.25rem 0; font-size: 0.85rem; color: var(--text-color-secondary);">
#{maintenance.description}
</p>
</div>
<p:badge value="+#{maintenance.joursRetard}j" severity="danger" size="large"/>
</div>
</div>
</ui:repeat>
<p:outputPanel rendered="#{empty dashboardView.maintenancesEnRetardList}">
<div style="padding: 2rem; text-align: center; color: var(--green-500);">
<i class="pi pi-check-circle" style="font-size: 3rem;"></i>
<p style="margin-top: 1rem;">Toutes les maintenances sont à jour</p>
</div>
</p:outputPanel>
</div>
</div>
<!-- ====================================================================
SECTION BAS 2 : Disponibilités en attente + Documents récents
==================================================================== -->
<!-- Disponibilités en attente -->
<div class="col-12 md:col-6">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Disponibilités en attente</h6>
<p class="subtitle">#{dashboardView.disponibilitesEnAttenteList.size()} demandes à valider</p>
</div>
</div>
<ui:repeat value="#{dashboardView.disponibilitesEnAttenteList}" var="dispo">
<div class="disponibilite-card" style="padding: 1rem; border-bottom: 1px solid var(--surface-border);">
<div style="display: flex; justify-content: space-between; align-items: start;">
<div style="flex: 1;">
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
<i class="pi pi-user"></i>
<strong>#{dispo.employe}</strong>
</div>
<div style="margin: 0.5rem 0;">
<p:badge value="#{dispo.type}"
severity="#{dashboardView.getSeveriteDisponibilite(dispo.type)}"/>
<span style="margin-left: 0.5rem; font-size: 0.9rem;">
Du #{dispo.dateDebutFormatee} au #{dispo.dateFinFormatee}
(#{dispo.nombreJours} jours)
</span>
</div>
<small style="color: var(--text-color-secondary);">
<strong>Motif:</strong> #{dispo.motif}
</small>
</div>
</div>
</div>
</ui:repeat>
<p:outputPanel rendered="#{empty dashboardView.disponibilitesEnAttenteList}">
<div style="padding: 2rem; text-align: center; color: var(--text-color-secondary);">
<i class="pi pi-inbox" style="font-size: 2rem;"></i>
<p style="margin-top: 1rem;">Aucune demande de disponibilité en attente</p>
</div>
</p:outputPanel>
</div>
</div>
<!-- Documents récents -->
<div class="col-12 md:col-6">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Documents récents</h6>
<p class="subtitle">5 derniers documents ajoutés</p>
</div>
</div>
<ul class="documents-list" style="list-style: none; padding: 0; margin: 0;">
<ui:repeat value="#{dashboardView.documentsRecentsList}" var="doc">
<li style="padding: 1rem; border-bottom: 1px solid var(--surface-border); display: flex; align-items: center; gap: 1rem;">
<i class="#{dashboardView.getIconeDocument(doc.type)}"
style="font-size: 2rem; color: var(--primary-color);"></i>
<div style="flex: 1;">
<div style="font-weight: 500;">#{doc.nom}</div>
<small style="color: var(--text-color-secondary);">
#{doc.type} • Ajouté le #{doc.dateCreationFormatee}
</small>
</div>
<p:button icon="pi pi-download" styleClass="ui-button-text ui-button-sm"/>
</li>
</ui:repeat>
</ul>
<p:outputPanel rendered="#{empty dashboardView.documentsRecentsList}">
<div style="padding: 2rem; text-align: center; color: var(--text-color-secondary);">
<i class="pi pi-file" style="font-size: 2rem;"></i>
<p style="margin-top: 1rem;">Aucun document récent</p>
</div>
</p:outputPanel>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Tableau de bord - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<h1>Tableau de bord - BTP Xpress</h1>
<p>Bean dashboardView disponible: #{not empty dashboardView}</p>
<p>Chantiers actifs: #{dashboardView.chantiersActifs}</p>
<p>Test de contenu simple</p>
</div>
</div>
</div>
<!-- ========================================================================
BARRE D'ALERTES (affichée uniquement si alertes critiques)
======================================================================== -->
<p:outputPanel rendered="#{dashboardView.alerteCritique}" styleClass="col-12">
<div class="notification notification-danger">
<i class="pi pi-exclamation-triangle"></i>
<strong>#{dashboardView.totalAlertes} alertes</strong> nécessitent votre attention immédiate
<span style="margin-left: 1rem; opacity: 0.9;">
Maintenance: #{dashboardView.alertesMaintenanceCount} •
Chantiers: #{dashboardView.alertesChantiersCount} •
Disponibilités: #{dashboardView.alertesDisponibilitesCount}
</span>
<p:commandButton value="Rafraîchir"
icon="pi pi-refresh"
action="#{dashboardView.rafraichir}"
update="@form"
styleClass="ui-button-text"
style="float: right;"/>
</div>
</p:outputPanel>
<div class="grid">
<!-- ====================================================================
KPIs PRINCIPAUX (3 cartes en ligne)
==================================================================== -->
<div class="col-12">
<div class="grid" style="margin: -0.5rem;">
<!-- KPI 1: Chantiers Actifs -->
<div class="col-12 md:col-6 xl:col-4">
<div class="card overview-box white">
<div class="overview-info">
<h6>Chantiers actifs</h6>
<h1>#{dashboardView.chantiersActifs}</h1>
<p class="subtitle">
Sur #{dashboardView.nombreChantiers} au total
</p>
<p:progressBar value="#{dashboardView.tauxActiviteChantiers}"
showValue="true"
displayValue="#{dashboardView.tauxActiviteChantiers}%"
styleClass="ui-progressbar-info"/>
</div>
<i class="pi pi-building"></i>
</div>
</div>
<!-- KPI 2: Équipes Disponibles -->
<div class="col-12 md:col-6 xl:col-4">
<div class="card overview-box blue">
<div class="overview-info">
<h6>Équipes disponibles</h6>
<h1>#{dashboardView.equipesDisponibles}/#{dashboardView.nombreEquipes}</h1>
<p class="subtitle">Taux de disponibilité</p>
<p:progressBar value="#{dashboardView.tauxDisponibiliteEquipes}"
showValue="true"
displayValue="#{dashboardView.tauxDisponibiliteEquipes}%"
style="background: rgba(255,255,255,0.3);"/>
</div>
<i class="pi pi-users"></i>
</div>
</div>
<!-- KPI 3: Maintenances Critiques -->
<div class="col-12 md:col-12 xl:col-4">
<div class="card overview-box #{dashboardView.alerteRetardMaintenance ? 'red' : 'green'}">
<div class="overview-info">
<h6>Maintenances en retard</h6>
<h1>#{dashboardView.maintenancesEnRetard}</h1>
<p class="subtitle">#{dashboardView.maintenancesPlanifiees} planifiées</p>
<p:badge value="#{dashboardView.alerteRetardMaintenance ? 'URGENT' : 'OK'}"
severity="#{dashboardView.alerteRetardMaintenance ? 'danger' : 'success'}"
style="margin-top: 0.5rem;"/>
</div>
<i class="pi pi-wrench"></i>
</div>
</div>
</div>
</div>
<!-- ====================================================================
SECTION CENTRALE : Graphique + KPIs Ressources
==================================================================== -->
<!-- Colonne gauche: Statistiques chantiers (placeholder pour graphique futur) -->
<div class="col-12 xl:col-8">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Vue d'ensemble</h6>
<p class="subtitle">Statistiques globales</p>
</div>
</div>
<div class="grid">
<!-- Chantiers actifs avec progression -->
<div class="col-12 md:col-6">
<div class="statistic-item" style="padding: 1.5rem; background: var(--blue-50); border-radius: var(--border-radius); border-left: 4px solid var(--blue-500);">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 0.75rem;">
<i class="pi pi-building" style="font-size: 2rem; color: var(--blue-500);"></i>
<div style="flex: 1;">
<h5 style="margin: 0; font-size: 1.75rem; color: var(--blue-600);">#{dashboardView.chantiersActifs}</h5>
<h6 style="margin: 0.25rem 0 0 0; font-weight: 500; color: var(--text-color-secondary);">Chantiers actifs</h6>
</div>
</div>
<p:progressBar value="#{dashboardView.tauxActiviteChantiers}"
showValue="true"
displayValue="#{dashboardView.tauxActiviteChantiers}% d'activité"
styleClass="ui-progressbar-info"
style="height: 1rem;"/>
</div>
</div>
<!-- Chantiers en retard -->
<div class="col-12 md:col-6">
<div class="statistic-item" style="padding: 1.5rem; background: var(--orange-50); border-radius: var(--border-radius); border-left: 4px solid var(--orange-500);">
<div style="display: flex; align-items: center; gap: 1rem;">
<i class="pi pi-exclamation-triangle" style="font-size: 2rem; color: var(--orange-500);"></i>
<div style="flex: 1;">
<h5 style="margin: 0; font-size: 1.75rem; color: var(--orange-600);">#{dashboardView.chantiersEnRetardList.size()}</h5>
<h6 style="margin: 0.25rem 0 0 0; font-weight: 500; color: var(--text-color-secondary);">Chantiers en retard</h6>
</div>
</div>
<p:outputPanel rendered="#{dashboardView.chantiersEnRetardList.size() > 0}">
<small style="display: block; margin-top: 0.75rem; color: var(--orange-700);">
<i class="pi pi-info-circle"></i> Attention requise
</small>
</p:outputPanel>
</div>
</div>
<!-- Événements aujourd'hui -->
<div class="col-12 md:col-6">
<div class="statistic-item" style="padding: 1.5rem; background: var(--purple-50); border-radius: var(--border-radius); border-left: 4px solid var(--purple-500);">
<div style="display: flex; align-items: center; gap: 1rem;">
<i class="pi pi-calendar" style="font-size: 2rem; color: var(--purple-500);"></i>
<div style="flex: 1;">
<h5 style="margin: 0; font-size: 1.75rem; color: var(--purple-600);">#{dashboardView.evenementsAujourdhui}</h5>
<h6 style="margin: 0.25rem 0 0 0; font-weight: 500; color: var(--text-color-secondary);">Événements aujourd'hui</h6>
</div>
</div>
</div>
</div>
<!-- Documents totaux -->
<div class="col-12 md:col-6">
<div class="statistic-item" style="padding: 1.5rem; background: var(--cyan-50); border-radius: var(--border-radius); border-left: 4px solid var(--cyan-500);">
<div style="display: flex; align-items: center; gap: 1rem;">
<i class="pi pi-file" style="font-size: 2rem; color: var(--cyan-500);"></i>
<div style="flex: 1;">
<h5 style="margin: 0; font-size: 1.75rem; color: var(--cyan-600);">#{dashboardView.nombreDocuments}</h5>
<h6 style="margin: 0.25rem 0 0 0; font-weight: 500; color: var(--text-color-secondary);">Documents totaux</h6>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Colonne droite: KPIs Ressources -->
<div class="col-12 xl:col-4">
<div class="card" style="height: 100%;">
<div class="card-header">
<div class="card-title">
<h6>Ressources</h6>
<p class="subtitle">État actuel des ressources</p>
</div>
</div>
<div style="padding: 1rem; display: flex; flex-direction: column; gap: 1.5rem;">
<!-- Employés actifs -->
<div class="statistic-item" style="padding: 1.25rem; background: var(--green-50); border-radius: var(--border-radius); border-left: 4px solid var(--green-500);">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 0.75rem;">
<i class="pi pi-users" style="font-size: 1.75rem; color: var(--green-500);"></i>
<div style="flex: 1;">
<h5 style="margin: 0; font-size: 1.5rem; color: var(--green-600);">
#{dashboardView.employesActifs}<span style="font-size: 1rem; color: var(--text-color-secondary);">/#{dashboardView.nombreEmployes}</span>
</h5>
<h6 style="margin: 0.25rem 0 0 0; font-weight: 500; color: var(--text-color-secondary);">Employés actifs</h6>
</div>
</div>
<p:progressBar value="#{dashboardView.tauxActiviteEmployes}"
showValue="true"
displayValue="#{dashboardView.tauxActiviteEmployes}%"
styleClass="ui-progressbar-#{dashboardView.tauxActiviteEmployes > 80 ? 'success' : (dashboardView.tauxActiviteEmployes > 60 ? 'warning' : 'danger')}"
style="height: 1rem;"/>
</div>
<!-- Matériel disponible -->
<div class="statistic-item" style="padding: 1.25rem; background: var(--teal-50); border-radius: var(--border-radius); border-left: 4px solid var(--teal-500);">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 0.75rem;">
<i class="pi pi-cog" style="font-size: 1.75rem; color: var(--teal-500);"></i>
<div style="flex: 1;">
<h5 style="margin: 0; font-size: 1.5rem; color: var(--teal-600);">
#{dashboardView.materielDisponible}<span style="font-size: 1rem; color: var(--text-color-secondary);">/#{dashboardView.nombreMateriel}</span>
</h5>
<h6 style="margin: 0.25rem 0 0 0; font-weight: 500; color: var(--text-color-secondary);">Matériel disponible</h6>
</div>
</div>
<p:progressBar value="#{dashboardView.tauxDisponibiliteMateriel}"
showValue="true"
displayValue="#{dashboardView.tauxDisponibiliteMateriel}%"
styleClass="ui-progressbar-success"
style="height: 1rem;"/>
</div>
<!-- Taux d'utilisation global -->
<div class="statistic-item" style="padding: 1.25rem; background: var(--indigo-50); border-radius: var(--border-radius); border-left: 4px solid var(--indigo-500); flex: 1; display: flex; flex-direction: column; justify-content: center;">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 0.5rem;">
<i class="pi pi-chart-line" style="font-size: 1.75rem; color: var(--indigo-500);"></i>
<div style="flex: 1;">
<h5 style="margin: 0; font-size: 1.75rem; color: var(--indigo-600);">#{dashboardView.tauxUtilisationGlobal}%</h5>
<h6 style="margin: 0.25rem 0 0 0; font-weight: 500; color: var(--text-color-secondary);">Taux d'utilisation global</h6>
</div>
</div>
<small style="display: block; color: var(--text-color-secondary); font-style: italic; padding-left: 2.75rem;">
<i class="pi pi-info-circle" style="font-size: 0.875rem;"></i>
Moyenne chantiers, employés et matériel
</small>
</div>
</div>
</div>
</div>
<!-- ====================================================================
TABLEAU CHANTIERS ACTIFS
==================================================================== -->
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Chantiers actifs</h6>
<p class="subtitle">#{dashboardView.chantiersActifsList.size()} chantiers en cours</p>
</div>
<p:commandButton value="Voir tout"
icon="pi pi-arrow-right"
outcome="/chantiers"
styleClass="ui-button-text"/>
</div>
<p:dataTable value="#{dashboardView.chantiersActifsList}"
var="chantier"
emptyMessage="Aucun chantier actif pour le moment"
styleClass="p-datatable-sm"
paginator="true"
rows="10"
paginatorPosition="bottom">
<p:column headerText="Nom" sortBy="#{chantier.nom}">
<h:outputText value="#{chantier.nom}"/>
</p:column>
<p:column headerText="Client" sortBy="#{chantier.client}">
<h:outputText value="#{chantier.client}"/>
</p:column>
<p:column headerText="Date début" sortBy="#{chantier.dateDebut}">
<h:outputText value="#{chantier.dateDebutFormatee}"/>
</p:column>
<p:column headerText="Fin prévue" sortBy="#{chantier.dateFinPrevue}">
<h:outputText value="#{chantier.dateFinPrevueFormatee}"/>
</p:column>
<p:column headerText="Avancement">
<p:progressBar value="#{chantier.avancement}"
showValue="true"
displayValue="#{chantier.avancement}%"
styleClass="ui-progressbar-success"/>
</p:column>
<p:column headerText="Budget" sortBy="#{chantier.budget}">
<h:outputText value="#{chantier.budget}">
<f:convertNumber type="number" groupingUsed="true"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p:column>
<p:column headerText="Coût réel" sortBy="#{chantier.coutReel}">
<h:outputText value="#{chantier.coutReel}"
style="#{chantier.depassementBudget ? 'color: var(--red-500); font-weight: bold;' : ''}">
<f:convertNumber type="number" groupingUsed="true"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
<p:badge value="!" severity="danger"
style="margin-left: 0.5rem;"
rendered="#{chantier.depassementBudget}"/>
</p:column>
<p:column headerText="Statut">
<p:badge value="#{chantier.statut}"
severity="#{chantier.statut == 'EN_COURS' ? 'info' : 'success'}"/>
</p:column>
</p:dataTable>
</div>
</div>
<!-- ====================================================================
SECTION BAS : Chantiers en retard + Maintenances en retard
==================================================================== -->
<!-- Chantiers en retard -->
<div class="col-12 md:col-6">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Chantiers en retard</h6>
<p class="subtitle">#{dashboardView.chantiersEnRetardList.size()} chantiers en retard</p>
</div>
</div>
<ui:repeat value="#{dashboardView.chantiersEnRetardList}" var="chantier">
<div class="chantier-retard-item" style="padding: 1rem; border-bottom: 1px solid var(--surface-border); background: var(--orange-50);">
<div style="display: flex; justify-content: space-between; align-items: start;">
<div>
<h6 style="margin: 0 0 0.5rem 0;">
<i class="pi pi-building" style="color: var(--orange-500);"></i>
#{chantier.nom}
</h6>
<p style="margin: 0.25rem 0; font-size: 0.9rem;">
<strong>Date fin prévue:</strong> #{chantier.dateFinPrevueFormatee}
</p>
</div>
<p:badge value="+#{chantier.joursRetard}j" severity="warning" size="large"/>
</div>
</div>
</ui:repeat>
<p:outputPanel rendered="#{empty dashboardView.chantiersEnRetardList}">
<div style="padding: 2rem; text-align: center; color: var(--green-500);">
<i class="pi pi-check-circle" style="font-size: 3rem;"></i>
<p style="margin-top: 1rem;">Tous les chantiers sont dans les temps</p>
</div>
</p:outputPanel>
</div>
</div>
<!-- Maintenances en retard -->
<div class="col-12 md:col-6">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Maintenances en retard</h6>
<p class="subtitle">#{dashboardView.maintenancesEnRetardList.size()} maintenances urgentes</p>
</div>
</div>
<ui:repeat value="#{dashboardView.maintenancesEnRetardList}" var="maintenance">
<div class="maintenance-item" style="padding: 1rem; border-bottom: 1px solid var(--surface-border); background: var(--red-50);">
<div style="display: flex; justify-content: space-between; align-items: start;">
<div>
<h6 style="margin: 0 0 0.5rem 0;">
<i class="pi pi-wrench" style="color: var(--red-500);"></i>
#{maintenance.materiel}
</h6>
<p style="margin: 0.25rem 0; font-size: 0.9rem;">
<strong>Type:</strong> #{maintenance.type} •
<strong>Prévue:</strong> #{maintenance.datePrevueFormatee}
</p>
<p style="margin: 0.25rem 0; font-size: 0.85rem; color: var(--text-color-secondary);">
#{maintenance.description}
</p>
</div>
<p:badge value="+#{maintenance.joursRetard}j" severity="danger" size="large"/>
</div>
</div>
</ui:repeat>
<p:outputPanel rendered="#{empty dashboardView.maintenancesEnRetardList}">
<div style="padding: 2rem; text-align: center; color: var(--green-500);">
<i class="pi pi-check-circle" style="font-size: 3rem;"></i>
<p style="margin-top: 1rem;">Toutes les maintenances sont à jour</p>
</div>
</p:outputPanel>
</div>
</div>
<!-- ====================================================================
SECTION BAS 2 : Disponibilités en attente + Documents récents
==================================================================== -->
<!-- Disponibilités en attente -->
<div class="col-12 md:col-6">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Disponibilités en attente</h6>
<p class="subtitle">#{dashboardView.disponibilitesEnAttenteList.size()} demandes à valider</p>
</div>
</div>
<ui:repeat value="#{dashboardView.disponibilitesEnAttenteList}" var="dispo">
<div class="disponibilite-card" style="padding: 1rem; border-bottom: 1px solid var(--surface-border);">
<div style="display: flex; justify-content: space-between; align-items: start;">
<div style="flex: 1;">
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
<i class="pi pi-user"></i>
<strong>#{dispo.employe}</strong>
</div>
<div style="margin: 0.5rem 0;">
<p:badge value="#{dispo.type}"
severity="#{dashboardView.getSeveriteDisponibilite(dispo.type)}"/>
<span style="margin-left: 0.5rem; font-size: 0.9rem;">
Du #{dispo.dateDebutFormatee} au #{dispo.dateFinFormatee}
(#{dispo.nombreJours} jours)
</span>
</div>
<small style="color: var(--text-color-secondary);">
<strong>Motif:</strong> #{dispo.motif}
</small>
</div>
</div>
</div>
</ui:repeat>
<p:outputPanel rendered="#{empty dashboardView.disponibilitesEnAttenteList}">
<div style="padding: 2rem; text-align: center; color: var(--text-color-secondary);">
<i class="pi pi-inbox" style="font-size: 2rem;"></i>
<p style="margin-top: 1rem;">Aucune demande de disponibilité en attente</p>
</div>
</p:outputPanel>
</div>
</div>
<!-- Documents récents -->
<div class="col-12 md:col-6">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Documents récents</h6>
<p class="subtitle">5 derniers documents ajoutés</p>
</div>
</div>
<ul class="documents-list" style="list-style: none; padding: 0; margin: 0;">
<ui:repeat value="#{dashboardView.documentsRecentsList}" var="doc">
<li style="padding: 1rem; border-bottom: 1px solid var(--surface-border); display: flex; align-items: center; gap: 1rem;">
<i class="#{dashboardView.getIconeDocument(doc.type)}"
style="font-size: 2rem; color: var(--primary-color);"></i>
<div style="flex: 1;">
<div style="font-weight: 500;">#{doc.nom}</div>
<small style="color: var(--text-color-secondary);">
#{doc.type} • Ajouté le #{doc.dateCreationFormatee}
</small>
</div>
<p:button icon="pi pi-download" styleClass="ui-button-text ui-button-sm"/>
</li>
</ui:repeat>
</ul>
<p:outputPanel rendered="#{empty dashboardView.documentsRecentsList}">
<div style="padding: 2rem; text-align: center; color: var(--text-color-secondary);">
<i class="pi pi-file" style="font-size: 2rem;"></i>
<p style="margin-top: 1rem;">Aucun document récent</p>
</div>
</p:outputPanel>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,112 +1,112 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Devis - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Gestion des Devis</h1>
<p:commandButton value="Nouveau devis" icon="pi pi-plus"
action="#{devisView.createNew()}"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{devisView}"/>
<ui:param name="tableId" value="devisTable"/>
<ui:define name="filter-fields">
<div class="grid">
<div class="col-12 md:col-4">
<h:outputLabel for="filtreNumero" value="Numéro"/>
<p:inputText id="filtreNumero" value="#{devisView.filtreNumero}"
placeholder="Rechercher par numéro..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreClient" value="Client"/>
<p:inputText id="filtreClient" value="#{devisView.filtreClient}"
placeholder="Rechercher par client..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreStatut" value="Statut"/>
<p:selectOneMenu id="filtreStatut" value="#{devisView.filtreStatut}" style="width: 100%;">
<f:selectItem itemLabel="Tous" itemValue="TOUS"/>
<f:selectItem itemLabel="Brouillon" itemValue="BROUILLON"/>
<f:selectItem itemLabel="En attente" itemValue="EN_ATTENTE"/>
<f:selectItem itemLabel="Accepté" itemValue="ACCEPTE"/>
<f:selectItem itemLabel="Refusé" itemValue="REFUSE"/>
<f:selectItem itemLabel="Expiré" itemValue="EXPIRE"/>
</p:selectOneMenu>
</div>
</div>
</ui:define>
</ui:include>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="devisForm"/>
<ui:param name="tableId" value="devisTable"/>
<ui:param name="viewBean" value="#{devisView}"/>
<ui:param name="var" value="devis"/>
<ui:param name="title" value="Liste des devis"/>
<ui:param name="createPath" value="/devis/nouveau"/>
<ui:define name="columns">
<p:column headerText="Numéro" sortBy="#{devis.numero}">
<h:outputText value="#{devis.numero}"/>
</p:column>
<p:column headerText="Objet" sortBy="#{devis.objet}">
<h:outputText value="#{devis.objet}"/>
</p:column>
<p:column headerText="Client" sortBy="#{devis.client}">
<h:outputText value="#{devis.client}"/>
</p:column>
<p:column headerText="Date émission" sortBy="#{devis.dateEmission}">
<h:outputText value="#{devis.dateEmission}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Date validité" sortBy="#{devis.dateValidite}">
<h:outputText value="#{devis.dateValidite}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Montant HT">
<h:outputText value="#{devis.montantHT}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p:column>
<p:column headerText="Montant TTC">
<h:outputText value="#{devis.montantTTC}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p:column>
<p:column headerText="Statut" sortBy="#{devis.statut}">
<p:tag value="#{devis.statut}"
severity="#{devis.statut == 'ACCEPTE' ? 'success' : (devis.statut == 'REFUSE' ? 'danger' : (devis.statut == 'EXPIRE' ? 'warning' : 'info'))}"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{devisView.viewDetails(devis.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Devis - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Gestion des Devis</h1>
<p:commandButton value="Nouveau devis" icon="pi pi-plus"
action="#{devisView.createNew()}"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{devisView}"/>
<ui:param name="tableId" value="devisTable"/>
<ui:define name="filter-fields">
<div class="grid">
<div class="col-12 md:col-4">
<h:outputLabel for="filtreNumero" value="Numéro"/>
<p:inputText id="filtreNumero" value="#{devisView.filtreNumero}"
placeholder="Rechercher par numéro..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreClient" value="Client"/>
<p:inputText id="filtreClient" value="#{devisView.filtreClient}"
placeholder="Rechercher par client..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreStatut" value="Statut"/>
<p:selectOneMenu id="filtreStatut" value="#{devisView.filtreStatut}" style="width: 100%;">
<f:selectItem itemLabel="Tous" itemValue="TOUS"/>
<f:selectItem itemLabel="Brouillon" itemValue="BROUILLON"/>
<f:selectItem itemLabel="En attente" itemValue="EN_ATTENTE"/>
<f:selectItem itemLabel="Accepté" itemValue="ACCEPTE"/>
<f:selectItem itemLabel="Refusé" itemValue="REFUSE"/>
<f:selectItem itemLabel="Expiré" itemValue="EXPIRE"/>
</p:selectOneMenu>
</div>
</div>
</ui:define>
</ui:include>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="devisForm"/>
<ui:param name="tableId" value="devisTable"/>
<ui:param name="viewBean" value="#{devisView}"/>
<ui:param name="var" value="devis"/>
<ui:param name="title" value="Liste des devis"/>
<ui:param name="createPath" value="/devis/nouveau"/>
<ui:define name="columns">
<p:column headerText="Numéro" sortBy="#{devis.numero}">
<h:outputText value="#{devis.numero}"/>
</p:column>
<p:column headerText="Objet" sortBy="#{devis.objet}">
<h:outputText value="#{devis.objet}"/>
</p:column>
<p:column headerText="Client" sortBy="#{devis.client}">
<h:outputText value="#{devis.client}"/>
</p:column>
<p:column headerText="Date émission" sortBy="#{devis.dateEmission}">
<h:outputText value="#{devis.dateEmission}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Date validité" sortBy="#{devis.dateValidite}">
<h:outputText value="#{devis.dateValidite}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Montant HT">
<h:outputText value="#{devis.montantHT}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p:column>
<p:column headerText="Montant TTC">
<h:outputText value="#{devis.montantTTC}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p:column>
<p:column headerText="Statut" sortBy="#{devis.statut}">
<p:tag value="#{devis.statut}"
severity="#{devis.statut == 'ACCEPTE' ? 'success' : (devis.statut == 'REFUSE' ? 'danger' : (devis.statut == 'EXPIRE' ? 'warning' : 'info'))}"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{devisView.viewDetails(devis.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,27 +1,27 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Devis acceptés - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Devis acceptés</h6>
<p class="subtitle">Devis acceptés par les clients</p>
</div>
</div>
<p>Page en développement</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Devis acceptés - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Devis acceptés</h6>
<p class="subtitle">Devis acceptés par les clients</p>
</div>
</div>
<p>Page en développement</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1 +1 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Attente - DEVIS - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Attente</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/devis" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Attente - DEVIS - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Attente</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/devis" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>

View File

@@ -1,27 +1,27 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Devis expirés - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Devis expirés</h6>
<p class="subtitle">Devis dont la validité est expirée</p>
</div>
</div>
<p>Page en développement</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Devis expirés - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Devis expirés</h6>
<p class="subtitle">Devis dont la validité est expirée</p>
</div>
</div>
<p>Page en développement</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,312 +1,312 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Nouveau devis - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<!-- En-tête avec breadcrumb -->
<div class="flex align-items-center justify-content-between mb-4">
<div>
<h2 class="text-900 font-bold mb-2">Créer un nouveau devis</h2>
<p class="text-600 mt-0">Établissez un devis détaillé pour votre client</p>
</div>
<p:commandButton value="Retour à la liste"
icon="pi pi-arrow-left"
outcome="/devis"
styleClass="ui-button-secondary ui-button-outlined"/>
</div>
<p:messages id="messages" showDetail="true" closable="true"/>
<h:form id="nouveauDevisForm" styleClass="p-fluid">
<!-- SECTION 1: Informations générales -->
<p:panel header="Informations générales" toggleable="true" collapsed="false" class="mb-4">
<div class="formgrid grid">
<!-- Numéro (auto-généré) -->
<div class="field col-12 md:col-4">
<label for="numero" class="font-bold">Numéro de devis</label>
<div class="p-inputgroup">
<span class="p-inputgroup-addon">
<i class="pi pi-hashtag"></i>
</span>
<p:inputText id="numero"
value="#{devisView.entity.numero}"
disabled="true"
placeholder="Auto-généré"
styleClass="text-center font-bold"/>
</div>
<small class="text-600">Généré automatiquement lors de l'enregistrement</small>
</div>
<!-- Statut -->
<div class="field col-12 md:col-4">
<label for="statut" class="font-bold">Statut <span class="text-red-500">*</span></label>
<p:selectOneMenu id="statut"
value="#{devisView.entity.statut}"
required="true">
<f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true"/>
<f:selectItem itemLabel="Brouillon" itemValue="BROUILLON"/>
<f:selectItem itemLabel="En attente" itemValue="ATTENTE"/>
<f:selectItem itemLabel="Accepté" itemValue="ACCEPTE"/>
<f:selectItem itemLabel="Refusé" itemValue="REFUSE"/>
<f:selectItem itemLabel="Expiré" itemValue="EXPIRE"/>
</p:selectOneMenu>
</div>
<!-- Date d'émission -->
<div class="field col-12 md:col-4">
<label for="dateEmission" class="font-bold">Date d'émission <span class="text-red-500">*</span></label>
<p:calendar id="dateEmission"
value="#{devisView.entity.dateEmission}"
pattern="dd/MM/yyyy"
locale="fr"
required="true"
requiredMessage="La date d'émission est obligatoire"
showIcon="true"
showButtonBar="true"
monthNavigator="true"
yearNavigator="true"
yearRange="2020:2030"
placeholder="Sélectionner une date">
</p:calendar>
</div>
<!-- Client -->
<div class="field col-12 md:col-8">
<label for="client" class="font-bold">Client <span class="text-red-500">*</span></label>
<p:inputText id="client"
value="#{devisView.entity.client}"
required="true"
requiredMessage="Le client est obligatoire"
placeholder="Ex: Entreprise ABC SARL">
<f:validateLength minimum="2" maximum="200"/>
</p:inputText>
<small class="text-600">Nom du client ou de l'entreprise</small>
</div>
<!-- Date de validité -->
<div class="field col-12 md:col-4">
<label for="dateValidite" class="font-bold">Date de validité <span class="text-red-500">*</span></label>
<p:calendar id="dateValidite"
value="#{devisView.entity.dateValidite}"
pattern="dd/MM/yyyy"
locale="fr"
required="true"
requiredMessage="La date de validité est obligatoire"
showIcon="true"
showButtonBar="true"
monthNavigator="true"
yearNavigator="true"
yearRange="2020:2035"
mindate="#{devisView.entity.dateEmission}"
placeholder="Sélectionner une date">
</p:calendar>
<small class="text-600">Date limite de validité du devis (généralement 30 jours)</small>
</div>
<!-- Objet du devis -->
<div class="field col-12">
<label for="objet" class="font-bold">Objet du devis <span class="text-red-500">*</span></label>
<p:inputTextarea id="objet"
value="#{devisView.entity.objet}"
required="true"
requiredMessage="L'objet du devis est obligatoire"
rows="3"
placeholder="Ex: Construction d'un immeuble R+3 à usage résidentiel"
autoResize="false">
<f:validateLength minimum="10" maximum="500"/>
</p:inputTextarea>
<small class="text-600">Description détaillée de la prestation</small>
</div>
</div>
</p:panel>
<!-- SECTION 2: Lignes du devis -->
<p:panel header="Détail du devis" toggleable="true" collapsed="false" class="mb-4">
<div class="mb-3">
<div class="surface-100 border-round p-3">
<div class="flex align-items-center gap-2 mb-2">
<i class="pi pi-info-circle text-blue-500"></i>
<span class="text-900 font-medium">Lignes de devis</span>
</div>
<p class="text-600 text-sm mt-0 mb-0">
Ajoutez les différentes prestations, fournitures et main d'œuvre.
Cette fonctionnalité sera disponible dans une prochaine version.
</p>
</div>
</div>
<!-- Placeholder pour table de lignes -->
<div class="surface-50 border-round p-4 text-center">
<i class="pi pi-list text-400 mb-3" style="font-size: 3rem;"></i>
<p class="text-600 mt-0 mb-3">Gestion des lignes de devis en cours de développement</p>
<p class="text-500 text-sm">
Bientôt disponible: ajout de lignes avec désignation, quantité, prix unitaire, TVA, etc.
</p>
</div>
</p:panel>
<!-- SECTION 3: Montants et totaux -->
<p:panel header="Montants" toggleable="true" collapsed="false" class="mb-4">
<div class="formgrid grid">
<!-- Montant HT -->
<div class="field col-12 md:col-6">
<label for="montantHT" class="font-bold">Montant HT (FCFA) <span class="text-red-500">*</span></label>
<p:inputNumber id="montantHT"
value="#{devisView.entity.montantHT}"
required="true"
requiredMessage="Le montant HT est obligatoire"
minValue="0"
decimalPlaces="0"
thousandSeparator=" "
suffix=" FCFA"
placeholder="0">
</p:inputNumber>
<small class="text-600">Montant hors taxes</small>
</div>
<!-- TVA (calculée) -->
<div class="field col-12 md:col-6">
<label class="font-bold">TVA (18%)</label>
<div class="p-inputgroup">
<span class="p-inputgroup-addon">
<i class="pi pi-percentage"></i>
</span>
<p:inputNumber value="#{devisView.entity.montantHT * 0.18}"
disabled="true"
decimalPlaces="0"
thousandSeparator=" "
suffix=" FCFA"
styleClass="text-center font-medium"/>
</div>
<small class="text-600">Calculé automatiquement (18% du montant HT)</small>
</div>
<!-- Montant TTC (calculé) -->
<div class="field col-12">
<label class="font-bold">Montant TTC (FCFA)</label>
<div class="p-inputgroup">
<span class="p-inputgroup-addon bg-primary">
<i class="pi pi-dollar text-white"></i>
</span>
<p:inputNumber value="#{devisView.entity.montantHT * 1.18}"
disabled="true"
decimalPlaces="0"
thousandSeparator=" "
suffix=" FCFA"
styleClass="text-center font-bold text-xl text-primary"/>
</div>
<small class="text-600">Montant toutes taxes comprises (HT + TVA)</small>
</div>
<!-- Récapitulatif visuel -->
<div class="field col-12">
<div class="surface-100 border-round p-3">
<div class="grid">
<div class="col-12 md:col-4">
<div class="text-center">
<span class="text-600 text-sm block mb-2">Montant HT</span>
<div class="text-900 font-bold text-xl">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{devisView.entity.montantHT}"/>
<ui:param name="size" value="normal"/>
</ui:include>
</div>
</div>
</div>
<div class="col-12 md:col-4">
<div class="text-center">
<span class="text-600 text-sm block mb-2">TVA (18%)</span>
<div class="text-orange-600 font-bold text-xl">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{devisView.entity.montantHT * 0.18}"/>
<ui:param name="size" value="normal"/>
</ui:include>
</div>
</div>
</div>
<div class="col-12 md:col-4">
<div class="text-center">
<span class="text-600 text-sm block mb-2">Total TTC</span>
<div class="text-primary font-bold text-2xl">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{devisView.entity.montantHT * 1.18}"/>
<ui:param name="size" value="large"/>
<ui:param name="bold" value="true"/>
</ui:include>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</p:panel>
<!-- SECTION 4: Conditions (optionnel) -->
<p:panel header="Conditions et remarques" toggleable="true" collapsed="true" class="mb-4">
<div class="formgrid grid">
<div class="field col-12">
<label for="conditions" class="font-bold">Conditions de paiement</label>
<p:inputTextarea id="conditions"
rows="3"
placeholder="Ex: Paiement en 3 fois : 30% à la commande, 40% à mi-parcours, 30% à la livraison"
autoResize="false">
</p:inputTextarea>
<small class="text-600">Détaillez les modalités de paiement</small>
</div>
<div class="field col-12">
<label for="remarques" class="font-bold">Remarques</label>
<p:inputTextarea id="remarques"
rows="3"
placeholder="Toutes remarques ou précisions supplémentaires"
autoResize="false">
</p:inputTextarea>
</div>
</div>
</p:panel>
<!-- Boutons d'action -->
<div class="flex align-items-center justify-content-between pt-4 border-top-1 surface-border">
<div>
<span class="text-600 text-sm">Les champs marqués d'un </span>
<span class="text-red-500 font-bold">*</span>
<span class="text-600 text-sm"> sont obligatoires</span>
</div>
<div class="flex gap-2">
<p:commandButton value="Annuler"
icon="pi pi-times"
action="/devis?faces-redirect=true"
styleClass="ui-button-secondary"
immediate="true"/>
<p:commandButton value="Enregistrer comme brouillon"
icon="pi pi-save"
action="#{devisView.save}"
update="@form messages"
oncomplete="if (args &amp;&amp; !args.validationFailed) window.location.href='/devis.xhtml';"
styleClass="ui-button-secondary"/>
<p:commandButton value="Enregistrer et envoyer"
icon="pi pi-send"
action="#{devisView.save}"
update="@form messages"
oncomplete="if (args &amp;&amp; !args.validationFailed) window.location.href='/devis.xhtml';"
styleClass="ui-button-primary"/>
</div>
</div>
</h:form>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Nouveau devis - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<!-- En-tête avec breadcrumb -->
<div class="flex align-items-center justify-content-between mb-4">
<div>
<h2 class="text-900 font-bold mb-2">Créer un nouveau devis</h2>
<p class="text-600 mt-0">Établissez un devis détaillé pour votre client</p>
</div>
<p:commandButton value="Retour à la liste"
icon="pi pi-arrow-left"
outcome="/devis"
styleClass="ui-button-secondary ui-button-outlined"/>
</div>
<p:messages id="messages" showDetail="true" closable="true"/>
<h:form id="nouveauDevisForm" styleClass="p-fluid">
<!-- SECTION 1: Informations générales -->
<p:panel header="Informations générales" toggleable="true" collapsed="false" class="mb-4">
<div class="formgrid grid">
<!-- Numéro (auto-généré) -->
<div class="field col-12 md:col-4">
<label for="numero" class="font-bold">Numéro de devis</label>
<div class="p-inputgroup">
<span class="p-inputgroup-addon">
<i class="pi pi-hashtag"></i>
</span>
<p:inputText id="numero"
value="#{devisView.entity.numero}"
disabled="true"
placeholder="Auto-généré"
styleClass="text-center font-bold"/>
</div>
<small class="text-600">Généré automatiquement lors de l'enregistrement</small>
</div>
<!-- Statut -->
<div class="field col-12 md:col-4">
<label for="statut" class="font-bold">Statut <span class="text-red-500">*</span></label>
<p:selectOneMenu id="statut"
value="#{devisView.entity.statut}"
required="true">
<f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true"/>
<f:selectItem itemLabel="Brouillon" itemValue="BROUILLON"/>
<f:selectItem itemLabel="En attente" itemValue="ATTENTE"/>
<f:selectItem itemLabel="Accepté" itemValue="ACCEPTE"/>
<f:selectItem itemLabel="Refusé" itemValue="REFUSE"/>
<f:selectItem itemLabel="Expiré" itemValue="EXPIRE"/>
</p:selectOneMenu>
</div>
<!-- Date d'émission -->
<div class="field col-12 md:col-4">
<label for="dateEmission" class="font-bold">Date d'émission <span class="text-red-500">*</span></label>
<p:calendar id="dateEmission"
value="#{devisView.entity.dateEmission}"
pattern="dd/MM/yyyy"
locale="fr"
required="true"
requiredMessage="La date d'émission est obligatoire"
showIcon="true"
showButtonBar="true"
monthNavigator="true"
yearNavigator="true"
yearRange="2020:2030"
placeholder="Sélectionner une date">
</p:calendar>
</div>
<!-- Client -->
<div class="field col-12 md:col-8">
<label for="client" class="font-bold">Client <span class="text-red-500">*</span></label>
<p:inputText id="client"
value="#{devisView.entity.client}"
required="true"
requiredMessage="Le client est obligatoire"
placeholder="Ex: Entreprise ABC SARL">
<f:validateLength minimum="2" maximum="200"/>
</p:inputText>
<small class="text-600">Nom du client ou de l'entreprise</small>
</div>
<!-- Date de validité -->
<div class="field col-12 md:col-4">
<label for="dateValidite" class="font-bold">Date de validité <span class="text-red-500">*</span></label>
<p:calendar id="dateValidite"
value="#{devisView.entity.dateValidite}"
pattern="dd/MM/yyyy"
locale="fr"
required="true"
requiredMessage="La date de validité est obligatoire"
showIcon="true"
showButtonBar="true"
monthNavigator="true"
yearNavigator="true"
yearRange="2020:2035"
mindate="#{devisView.entity.dateEmission}"
placeholder="Sélectionner une date">
</p:calendar>
<small class="text-600">Date limite de validité du devis (généralement 30 jours)</small>
</div>
<!-- Objet du devis -->
<div class="field col-12">
<label for="objet" class="font-bold">Objet du devis <span class="text-red-500">*</span></label>
<p:inputTextarea id="objet"
value="#{devisView.entity.objet}"
required="true"
requiredMessage="L'objet du devis est obligatoire"
rows="3"
placeholder="Ex: Construction d'un immeuble R+3 à usage résidentiel"
autoResize="false">
<f:validateLength minimum="10" maximum="500"/>
</p:inputTextarea>
<small class="text-600">Description détaillée de la prestation</small>
</div>
</div>
</p:panel>
<!-- SECTION 2: Lignes du devis -->
<p:panel header="Détail du devis" toggleable="true" collapsed="false" class="mb-4">
<div class="mb-3">
<div class="surface-100 border-round p-3">
<div class="flex align-items-center gap-2 mb-2">
<i class="pi pi-info-circle text-blue-500"></i>
<span class="text-900 font-medium">Lignes de devis</span>
</div>
<p class="text-600 text-sm mt-0 mb-0">
Ajoutez les différentes prestations, fournitures et main d'œuvre.
Cette fonctionnalité sera disponible dans une prochaine version.
</p>
</div>
</div>
<!-- Placeholder pour table de lignes -->
<div class="surface-50 border-round p-4 text-center">
<i class="pi pi-list text-400 mb-3" style="font-size: 3rem;"></i>
<p class="text-600 mt-0 mb-3">Gestion des lignes de devis en cours de développement</p>
<p class="text-500 text-sm">
Bientôt disponible: ajout de lignes avec désignation, quantité, prix unitaire, TVA, etc.
</p>
</div>
</p:panel>
<!-- SECTION 3: Montants et totaux -->
<p:panel header="Montants" toggleable="true" collapsed="false" class="mb-4">
<div class="formgrid grid">
<!-- Montant HT -->
<div class="field col-12 md:col-6">
<label for="montantHT" class="font-bold">Montant HT (FCFA) <span class="text-red-500">*</span></label>
<p:inputNumber id="montantHT"
value="#{devisView.entity.montantHT}"
required="true"
requiredMessage="Le montant HT est obligatoire"
minValue="0"
decimalPlaces="0"
thousandSeparator=" "
suffix=" FCFA"
placeholder="0">
</p:inputNumber>
<small class="text-600">Montant hors taxes</small>
</div>
<!-- TVA (calculée) -->
<div class="field col-12 md:col-6">
<label class="font-bold">TVA (18%)</label>
<div class="p-inputgroup">
<span class="p-inputgroup-addon">
<i class="pi pi-percentage"></i>
</span>
<p:inputNumber value="#{devisView.entity.montantHT * 0.18}"
disabled="true"
decimalPlaces="0"
thousandSeparator=" "
suffix=" FCFA"
styleClass="text-center font-medium"/>
</div>
<small class="text-600">Calculé automatiquement (18% du montant HT)</small>
</div>
<!-- Montant TTC (calculé) -->
<div class="field col-12">
<label class="font-bold">Montant TTC (FCFA)</label>
<div class="p-inputgroup">
<span class="p-inputgroup-addon bg-primary">
<i class="pi pi-dollar text-white"></i>
</span>
<p:inputNumber value="#{devisView.entity.montantHT * 1.18}"
disabled="true"
decimalPlaces="0"
thousandSeparator=" "
suffix=" FCFA"
styleClass="text-center font-bold text-xl text-primary"/>
</div>
<small class="text-600">Montant toutes taxes comprises (HT + TVA)</small>
</div>
<!-- Récapitulatif visuel -->
<div class="field col-12">
<div class="surface-100 border-round p-3">
<div class="grid">
<div class="col-12 md:col-4">
<div class="text-center">
<span class="text-600 text-sm block mb-2">Montant HT</span>
<div class="text-900 font-bold text-xl">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{devisView.entity.montantHT}"/>
<ui:param name="size" value="normal"/>
</ui:include>
</div>
</div>
</div>
<div class="col-12 md:col-4">
<div class="text-center">
<span class="text-600 text-sm block mb-2">TVA (18%)</span>
<div class="text-orange-600 font-bold text-xl">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{devisView.entity.montantHT * 0.18}"/>
<ui:param name="size" value="normal"/>
</ui:include>
</div>
</div>
</div>
<div class="col-12 md:col-4">
<div class="text-center">
<span class="text-600 text-sm block mb-2">Total TTC</span>
<div class="text-primary font-bold text-2xl">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{devisView.entity.montantHT * 1.18}"/>
<ui:param name="size" value="large"/>
<ui:param name="bold" value="true"/>
</ui:include>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</p:panel>
<!-- SECTION 4: Conditions (optionnel) -->
<p:panel header="Conditions et remarques" toggleable="true" collapsed="true" class="mb-4">
<div class="formgrid grid">
<div class="field col-12">
<label for="conditions" class="font-bold">Conditions de paiement</label>
<p:inputTextarea id="conditions"
rows="3"
placeholder="Ex: Paiement en 3 fois : 30% à la commande, 40% à mi-parcours, 30% à la livraison"
autoResize="false">
</p:inputTextarea>
<small class="text-600">Détaillez les modalités de paiement</small>
</div>
<div class="field col-12">
<label for="remarques" class="font-bold">Remarques</label>
<p:inputTextarea id="remarques"
rows="3"
placeholder="Toutes remarques ou précisions supplémentaires"
autoResize="false">
</p:inputTextarea>
</div>
</div>
</p:panel>
<!-- Boutons d'action -->
<div class="flex align-items-center justify-content-between pt-4 border-top-1 surface-border">
<div>
<span class="text-600 text-sm">Les champs marqués d'un </span>
<span class="text-red-500 font-bold">*</span>
<span class="text-600 text-sm"> sont obligatoires</span>
</div>
<div class="flex gap-2">
<p:commandButton value="Annuler"
icon="pi pi-times"
action="/devis?faces-redirect=true"
styleClass="ui-button-secondary"
immediate="true"/>
<p:commandButton value="Enregistrer comme brouillon"
icon="pi pi-save"
action="#{devisView.save}"
update="@form messages"
oncomplete="if (args &amp;&amp; !args.validationFailed) window.location.href='/devis.xhtml';"
styleClass="ui-button-secondary"/>
<p:commandButton value="Enregistrer et envoyer"
icon="pi pi-send"
action="#{devisView.save}"
update="@form messages"
oncomplete="if (args &amp;&amp; !args.validationFailed) window.location.href='/devis.xhtml';"
styleClass="ui-button-primary"/>
</div>
</div>
</h:form>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,24 +1,24 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Documentation - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<h1>Documentation</h1>
<p>Documentation de l'application BTP Xpress</p>
<p>Module en cours de développement...</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Documentation - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<h1>Documentation</h1>
<p>Documentation de l'application BTP Xpress</p>
<p>Module en cours de développement...</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,28 +1,28 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Documents - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Documents</h6>
<p class="subtitle">Gestion des documents</p>
</div>
</div>
<p>Page en développement</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Documents - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Documents</h6>
<p class="subtitle">Gestion des documents</p>
</div>
</div>
<p>Page en développement</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,102 +1,102 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Employés - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Gestion des Employés</h1>
<p:commandButton value="Nouvel employé" icon="pi pi-user-plus"
action="#{employeView.createNew()}"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{employeView}"/>
<ui:param name="tableId" value="employesTable"/>
<ui:define name="filter-fields">
<div class="grid">
<div class="col-12 md:col-4">
<h:outputLabel for="filtreNom" value="Nom"/>
<p:inputText id="filtreNom" value="#{employeView.filtreNom}"
placeholder="Rechercher par nom..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtrePoste" value="Poste"/>
<p:inputText id="filtrePoste" value="#{employeView.filtrePoste}"
placeholder="Rechercher par poste..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreStatut" value="Statut"/>
<p:selectOneMenu id="filtreStatut" value="#{employeView.filtreStatut}" style="width: 100%;">
<f:selectItem itemLabel="Tous" itemValue="TOUS"/>
<f:selectItem itemLabel="Actif" itemValue="ACTIF"/>
<f:selectItem itemLabel="Inactif" itemValue="INACTIF"/>
<f:selectItem itemLabel="En congé" itemValue="EN_CONGE"/>
</p:selectOneMenu>
</div>
</div>
</ui:define>
</ui:include>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="employesForm"/>
<ui:param name="tableId" value="employesTable"/>
<ui:param name="viewBean" value="#{employeView}"/>
<ui:param name="var" value="employe"/>
<ui:param name="title" value="Liste des employés"/>
<ui:param name="createPath" value="/employes/nouveau"/>
<ui:define name="columns">
<p:column headerText="Nom complet" sortBy="#{employe.nomComplet}">
<h:outputText value="#{employe.nomComplet}"/>
</p:column>
<p:column headerText="Email" sortBy="#{employe.email}">
<h:outputText value="#{employe.email}"/>
</p:column>
<p:column headerText="Téléphone">
<h:outputText value="#{employe.telephone}"/>
</p:column>
<p:column headerText="Poste" sortBy="#{employe.poste}">
<h:outputText value="#{employe.poste}"/>
</p:column>
<p:column headerText="Taux horaire">
<h:outputText value="#{employe.tauxHoraire}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa/h"/>
</p:column>
<p:column headerText="Date embauche" sortBy="#{employe.dateEmbauche}">
<h:outputText value="#{employe.dateEmbauche}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Statut" sortBy="#{employe.statut}">
<p:tag value="#{employe.statut}"
severity="#{employe.statut == 'ACTIF' ? 'success' : 'warning'}"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{employeView.viewDetails(employe.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Employés - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Gestion des Employés</h1>
<p:commandButton value="Nouvel employé" icon="pi pi-user-plus"
action="#{employeView.createNew()}"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{employeView}"/>
<ui:param name="tableId" value="employesTable"/>
<ui:define name="filter-fields">
<div class="grid">
<div class="col-12 md:col-4">
<h:outputLabel for="filtreNom" value="Nom"/>
<p:inputText id="filtreNom" value="#{employeView.filtreNom}"
placeholder="Rechercher par nom..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtrePoste" value="Poste"/>
<p:inputText id="filtrePoste" value="#{employeView.filtrePoste}"
placeholder="Rechercher par poste..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreStatut" value="Statut"/>
<p:selectOneMenu id="filtreStatut" value="#{employeView.filtreStatut}" style="width: 100%;">
<f:selectItem itemLabel="Tous" itemValue="TOUS"/>
<f:selectItem itemLabel="Actif" itemValue="ACTIF"/>
<f:selectItem itemLabel="Inactif" itemValue="INACTIF"/>
<f:selectItem itemLabel="En congé" itemValue="EN_CONGE"/>
</p:selectOneMenu>
</div>
</div>
</ui:define>
</ui:include>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="employesForm"/>
<ui:param name="tableId" value="employesTable"/>
<ui:param name="viewBean" value="#{employeView}"/>
<ui:param name="var" value="employe"/>
<ui:param name="title" value="Liste des employés"/>
<ui:param name="createPath" value="/employes/nouveau"/>
<ui:define name="columns">
<p:column headerText="Nom complet" sortBy="#{employe.nomComplet}">
<h:outputText value="#{employe.nomComplet}"/>
</p:column>
<p:column headerText="Email" sortBy="#{employe.email}">
<h:outputText value="#{employe.email}"/>
</p:column>
<p:column headerText="Téléphone">
<h:outputText value="#{employe.telephone}"/>
</p:column>
<p:column headerText="Poste" sortBy="#{employe.poste}">
<h:outputText value="#{employe.poste}"/>
</p:column>
<p:column headerText="Taux horaire">
<h:outputText value="#{employe.tauxHoraire}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa/h"/>
</p:column>
<p:column headerText="Date embauche" sortBy="#{employe.dateEmbauche}">
<h:outputText value="#{employe.dateEmbauche}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Statut" sortBy="#{employe.statut}">
<p:tag value="#{employe.statut}"
severity="#{employe.statut == 'ACTIF' ? 'success' : 'warning'}"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{employeView.viewDetails(employe.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1 +1 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Actifs - EMPLOYES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Actifs</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/employes" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Actifs - EMPLOYES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Actifs</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/employes" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>

View File

@@ -1 +1 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Disponibles - EMPLOYES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Disponibles</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/employes" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Disponibles - EMPLOYES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Disponibles</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/employes" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>

View File

@@ -1 +1 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">EMPLOYES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>EMPLOYES</h1><p>Module en cours de développement...</p></div></div></div></div></ui:define></ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">EMPLOYES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>EMPLOYES</h1><p>Module en cours de développement...</p></div></div></div></div></ui:define></ui:composition>

View File

@@ -1,93 +1,93 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Équipes - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Gestion des Équipes</h1>
<p:commandButton value="Nouvelle équipe" icon="pi pi-users"
action="#{equipeView.createNew()}"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{equipeView}"/>
<ui:param name="tableId" value="equipesTable"/>
<ui:define name="filter-fields">
<div class="grid">
<div class="col-12 md:col-4">
<h:outputLabel for="filtreNom" value="Nom de l'équipe"/>
<p:inputText id="filtreNom" value="#{equipeView.filtreNom}"
placeholder="Rechercher par nom..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreSpecialite" value="Spécialité"/>
<p:inputText id="filtreSpecialite" value="#{equipeView.filtreSpecialite}"
placeholder="Rechercher par spécialité..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreStatut" value="Statut"/>
<p:selectOneMenu id="filtreStatut" value="#{equipeView.filtreStatut}" style="width: 100%;">
<f:selectItem itemLabel="Tous" itemValue="TOUS"/>
<f:selectItem itemLabel="Active" itemValue="ACTIVE"/>
<f:selectItem itemLabel="Inactive" itemValue="INACTIVE"/>
</p:selectOneMenu>
</div>
</div>
</ui:define>
</ui:include>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="equipesForm"/>
<ui:param name="tableId" value="equipesTable"/>
<ui:param name="viewBean" value="#{equipeView}"/>
<ui:param name="var" value="equipe"/>
<ui:param name="title" value="Liste des équipes"/>
<ui:param name="createPath" value="/equipes/nouveau"/>
<ui:define name="columns">
<p:column headerText="Nom" sortBy="#{equipe.nom}">
<h:outputText value="#{equipe.nom}"/>
</p:column>
<p:column headerText="Chef d'équipe" sortBy="#{equipe.chef}">
<h:outputText value="#{equipe.chef}"/>
</p:column>
<p:column headerText="Spécialité" sortBy="#{equipe.specialite}">
<h:outputText value="#{equipe.specialite}"/>
</p:column>
<p:column headerText="Nombre de membres">
<p:tag value="#{equipe.nombreMembres}" severity="info"/>
</p:column>
<p:column headerText="Description">
<h:outputText value="#{equipe.description}"/>
</p:column>
<p:column headerText="Statut" sortBy="#{equipe.statut}">
<p:tag value="#{equipe.statut}"
severity="#{equipe.statut == 'ACTIVE' ? 'success' : 'warning'}"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{equipeView.viewDetails(equipe.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Équipes - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Gestion des Équipes</h1>
<p:commandButton value="Nouvelle équipe" icon="pi pi-users"
action="#{equipeView.createNew()}"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{equipeView}"/>
<ui:param name="tableId" value="equipesTable"/>
<ui:define name="filter-fields">
<div class="grid">
<div class="col-12 md:col-4">
<h:outputLabel for="filtreNom" value="Nom de l'équipe"/>
<p:inputText id="filtreNom" value="#{equipeView.filtreNom}"
placeholder="Rechercher par nom..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreSpecialite" value="Spécialité"/>
<p:inputText id="filtreSpecialite" value="#{equipeView.filtreSpecialite}"
placeholder="Rechercher par spécialité..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreStatut" value="Statut"/>
<p:selectOneMenu id="filtreStatut" value="#{equipeView.filtreStatut}" style="width: 100%;">
<f:selectItem itemLabel="Tous" itemValue="TOUS"/>
<f:selectItem itemLabel="Active" itemValue="ACTIVE"/>
<f:selectItem itemLabel="Inactive" itemValue="INACTIVE"/>
</p:selectOneMenu>
</div>
</div>
</ui:define>
</ui:include>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="equipesForm"/>
<ui:param name="tableId" value="equipesTable"/>
<ui:param name="viewBean" value="#{equipeView}"/>
<ui:param name="var" value="equipe"/>
<ui:param name="title" value="Liste des équipes"/>
<ui:param name="createPath" value="/equipes/nouveau"/>
<ui:define name="columns">
<p:column headerText="Nom" sortBy="#{equipe.nom}">
<h:outputText value="#{equipe.nom}"/>
</p:column>
<p:column headerText="Chef d'équipe" sortBy="#{equipe.chef}">
<h:outputText value="#{equipe.chef}"/>
</p:column>
<p:column headerText="Spécialité" sortBy="#{equipe.specialite}">
<h:outputText value="#{equipe.specialite}"/>
</p:column>
<p:column headerText="Nombre de membres">
<p:tag value="#{equipe.nombreMembres}" severity="info"/>
</p:column>
<p:column headerText="Description">
<h:outputText value="#{equipe.description}"/>
</p:column>
<p:column headerText="Statut" sortBy="#{equipe.statut}">
<p:tag value="#{equipe.statut}"
severity="#{equipe.statut == 'ACTIVE' ? 'success' : 'warning'}"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{equipeView.viewDetails(equipe.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1 +1 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Disponibles - EQUIPES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Disponibles</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/equipes" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Disponibles - EQUIPES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Disponibles</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/equipes" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>

View File

@@ -1 +1 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">EQUIPES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>EQUIPES</h1><p>Module en cours de développement...</p></div></div></div></div></ui:define></ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">EQUIPES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>EQUIPES</h1><p>Module en cours de développement...</p></div></div></div></div></ui:define></ui:composition>

View File

@@ -1 +1 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Specialites - EQUIPES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Specialites</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/equipes" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Specialites - EQUIPES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Specialites</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/equipes" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>

View File

@@ -1,122 +1,122 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Factures - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Gestion des Factures</h1>
<p:commandButton value="Nouvelle facture" icon="pi pi-plus"
action="#{factureView.createNew()}"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{factureView}"/>
<ui:param name="tableId" value="facturesTable"/>
<ui:define name="filter-fields">
<div class="grid">
<div class="col-12 md:col-4">
<h:outputLabel for="filtreNumero" value="Numéro"/>
<p:inputText id="filtreNumero" value="#{factureView.filtreNumero}"
placeholder="Rechercher par numéro..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreClient" value="Client"/>
<p:inputText id="filtreClient" value="#{factureView.filtreClient}"
placeholder="Rechercher par client..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreStatut" value="Statut"/>
<p:selectOneMenu id="filtreStatut" value="#{factureView.filtreStatut}" style="width: 100%;">
<f:selectItem itemLabel="Tous" itemValue="TOUS"/>
<f:selectItem itemLabel="Brouillon" itemValue="BROUILLON"/>
<f:selectItem itemLabel="Émise" itemValue="EMISE"/>
<f:selectItem itemLabel="Envoyée" itemValue="ENVOYEE"/>
<f:selectItem itemLabel="Payée" itemValue="PAYEE"/>
<f:selectItem itemLabel="En retard" itemValue="EN_RETARD"/>
<f:selectItem itemLabel="Annulée" itemValue="ANNULEE"/>
</p:selectOneMenu>
</div>
</div>
</ui:define>
</ui:include>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="facturesForm"/>
<ui:param name="tableId" value="facturesTable"/>
<ui:param name="viewBean" value="#{factureView}"/>
<ui:param name="var" value="facture"/>
<ui:param name="title" value="Liste des factures"/>
<ui:param name="createPath" value="/factures/nouveau"/>
<ui:define name="columns">
<p:column headerText="Numéro" sortBy="#{facture.numero}">
<h:outputText value="#{facture.numero}"/>
</p:column>
<p:column headerText="Objet" sortBy="#{facture.objet}">
<h:outputText value="#{facture.objet}"/>
</p:column>
<p:column headerText="Client" sortBy="#{facture.client}">
<h:outputText value="#{facture.client}"/>
</p:column>
<p:column headerText="Date émission" sortBy="#{facture.dateEmission}">
<h:outputText value="#{facture.dateEmission}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Date échéance" sortBy="#{facture.dateEcheance}">
<h:outputText value="#{facture.dateEcheance}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
<h:outputText value=" ⚠️" rendered="#{factureView.isEnRetard(facture)}"
title="Facture en retard" style="color: red;"/>
</p:column>
<p:column headerText="Montant TTC">
<h:outputText value="#{facture.montantTTC}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p:column>
<p:column headerText="Montant payé">
<h:outputText value="#{facture.montantPaye}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p:column>
<p:column headerText="Reste à payer">
<h:outputText value="#{factureView.getMontantRestant(facture)}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"
style="#{factureView.getMontantRestant(facture) > 0 ? 'color: red; font-weight: bold;' : ''}"/>
</p:column>
<p:column headerText="Statut" sortBy="#{facture.statut}">
<p:tag value="#{facture.statut}"
severity="#{facture.statut == 'PAYEE' ? 'success' : (facture.statut == 'ANNULEE' ? 'danger' : (factureView.isEnRetard(facture) ? 'danger' : 'warning'))}"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{factureView.viewDetails(facture.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Factures - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Gestion des Factures</h1>
<p:commandButton value="Nouvelle facture" icon="pi pi-plus"
action="#{factureView.createNew()}"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{factureView}"/>
<ui:param name="tableId" value="facturesTable"/>
<ui:define name="filter-fields">
<div class="grid">
<div class="col-12 md:col-4">
<h:outputLabel for="filtreNumero" value="Numéro"/>
<p:inputText id="filtreNumero" value="#{factureView.filtreNumero}"
placeholder="Rechercher par numéro..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreClient" value="Client"/>
<p:inputText id="filtreClient" value="#{factureView.filtreClient}"
placeholder="Rechercher par client..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreStatut" value="Statut"/>
<p:selectOneMenu id="filtreStatut" value="#{factureView.filtreStatut}" style="width: 100%;">
<f:selectItem itemLabel="Tous" itemValue="TOUS"/>
<f:selectItem itemLabel="Brouillon" itemValue="BROUILLON"/>
<f:selectItem itemLabel="Émise" itemValue="EMISE"/>
<f:selectItem itemLabel="Envoyée" itemValue="ENVOYEE"/>
<f:selectItem itemLabel="Payée" itemValue="PAYEE"/>
<f:selectItem itemLabel="En retard" itemValue="EN_RETARD"/>
<f:selectItem itemLabel="Annulée" itemValue="ANNULEE"/>
</p:selectOneMenu>
</div>
</div>
</ui:define>
</ui:include>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="facturesForm"/>
<ui:param name="tableId" value="facturesTable"/>
<ui:param name="viewBean" value="#{factureView}"/>
<ui:param name="var" value="facture"/>
<ui:param name="title" value="Liste des factures"/>
<ui:param name="createPath" value="/factures/nouveau"/>
<ui:define name="columns">
<p:column headerText="Numéro" sortBy="#{facture.numero}">
<h:outputText value="#{facture.numero}"/>
</p:column>
<p:column headerText="Objet" sortBy="#{facture.objet}">
<h:outputText value="#{facture.objet}"/>
</p:column>
<p:column headerText="Client" sortBy="#{facture.client}">
<h:outputText value="#{facture.client}"/>
</p:column>
<p:column headerText="Date émission" sortBy="#{facture.dateEmission}">
<h:outputText value="#{facture.dateEmission}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Date échéance" sortBy="#{facture.dateEcheance}">
<h:outputText value="#{facture.dateEcheance}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
<h:outputText value=" ⚠️" rendered="#{factureView.isEnRetard(facture)}"
title="Facture en retard" style="color: red;"/>
</p:column>
<p:column headerText="Montant TTC">
<h:outputText value="#{facture.montantTTC}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p:column>
<p:column headerText="Montant payé">
<h:outputText value="#{facture.montantPaye}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p:column>
<p:column headerText="Reste à payer">
<h:outputText value="#{factureView.getMontantRestant(facture)}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"
style="#{factureView.getMontantRestant(facture) > 0 ? 'color: red; font-weight: bold;' : ''}"/>
</p:column>
<p:column headerText="Statut" sortBy="#{facture.statut}">
<p:tag value="#{facture.statut}"
severity="#{facture.statut == 'PAYEE' ? 'success' : (facture.statut == 'ANNULEE' ? 'danger' : (factureView.isEnRetard(facture) ? 'danger' : 'warning'))}"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{factureView.viewDetails(facture.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1 +1 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Impayees - FACTURES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Impayees</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/factures" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Impayees - FACTURES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Impayees</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/factures" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>

View File

@@ -1,392 +1,392 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Nouvelle facture - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<!-- En-tête avec breadcrumb -->
<div class="flex align-items-center justify-content-between mb-4">
<div>
<h2 class="text-900 font-bold mb-2">Créer une nouvelle facture</h2>
<p class="text-600 mt-0">Émettez une facture pour un chantier ou prestation réalisée</p>
</div>
<p:commandButton value="Retour à la liste"
icon="pi pi-arrow-left"
outcome="/factures"
styleClass="ui-button-secondary ui-button-outlined"/>
</div>
<p:messages id="messages" showDetail="true" closable="true"/>
<h:form id="nouvelleFactureForm" styleClass="p-fluid">
<!-- SECTION 1: Informations générales -->
<p:panel header="Informations générales" toggleable="true" collapsed="false" class="mb-4">
<div class="formgrid grid">
<!-- Numéro (auto-généré) -->
<div class="field col-12 md:col-4">
<label for="numero" class="font-bold">Numéro de facture</label>
<div class="p-inputgroup">
<span class="p-inputgroup-addon bg-primary">
<i class="pi pi-hashtag text-white"></i>
</span>
<p:inputText id="numero"
value="#{factureView.entity.numero}"
disabled="true"
placeholder="Auto-généré"
styleClass="text-center font-bold text-primary"/>
</div>
<small class="text-600">Généré automatiquement selon la séquence configurée</small>
</div>
<!-- Statut -->
<div class="field col-12 md:col-4">
<label for="statut" class="font-bold">Statut <span class="text-red-500">*</span></label>
<p:selectOneMenu id="statut"
value="#{factureView.entity.statut}"
required="true">
<f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true"/>
<f:selectItem itemLabel="Brouillon" itemValue="BROUILLON"/>
<f:selectItem itemLabel="Émise" itemValue="EMISE"/>
<f:selectItem itemLabel="Payée" itemValue="PAYEE"/>
<f:selectItem itemLabel="Impayée" itemValue="IMPAYEE"/>
<f:selectItem itemLabel="En retard" itemValue="EN_RETARD"/>
<f:selectItem itemLabel="Annulée" itemValue="ANNULEE"/>
</p:selectOneMenu>
</div>
<!-- Type de facture -->
<div class="field col-12 md:col-4">
<label for="typeFacture" class="font-bold">Type de facture</label>
<p:selectOneMenu id="typeFacture">
<f:selectItem itemLabel="Standard" itemValue="STANDARD"/>
<f:selectItem itemLabel="Acompte" itemValue="ACOMPTE"/>
<f:selectItem itemLabel="Solde" itemValue="SOLDE"/>
<f:selectItem itemLabel="Avoir" itemValue="AVOIR"/>
</p:selectOneMenu>
<small class="text-600">Nature de la facturation</small>
</div>
<!-- Client -->
<div class="field col-12 md:col-8">
<label for="client" class="font-bold">Client <span class="text-red-500">*</span></label>
<p:inputText id="client"
value="#{factureView.entity.client}"
required="true"
requiredMessage="Le client est obligatoire"
placeholder="Ex: Entreprise ABC SARL">
<f:validateLength minimum="2" maximum="200"/>
</p:inputText>
<small class="text-600">Nom du client ou de l'entreprise</small>
</div>
<!-- Date d'émission -->
<div class="field col-12 md:col-4">
<label for="dateEmission" class="font-bold">Date d'émission <span class="text-red-500">*</span></label>
<p:calendar id="dateEmission"
value="#{factureView.entity.dateEmission}"
pattern="dd/MM/yyyy"
locale="fr"
required="true"
requiredMessage="La date d'émission est obligatoire"
showIcon="true"
showButtonBar="true"
monthNavigator="true"
yearNavigator="true"
yearRange="2020:2030"
placeholder="Sélectionner une date">
</p:calendar>
</div>
<!-- Objet -->
<div class="field col-12">
<label for="objet" class="font-bold">Objet de la facture <span class="text-red-500">*</span></label>
<p:inputTextarea id="objet"
value="#{factureView.entity.objet}"
required="true"
requiredMessage="L'objet de la facture est obligatoire"
rows="2"
placeholder="Ex: Travaux de construction immeuble R+3 - Phase gros œuvre"
autoResize="false">
<f:validateLength minimum="10" maximum="500"/>
</p:inputTextarea>
<small class="text-600">Description des prestations facturées</small>
</div>
<!-- Date d'échéance -->
<div class="field col-12 md:col-6">
<label for="dateEcheance" class="font-bold">Date d'échéance <span class="text-red-500">*</span></label>
<p:calendar id="dateEcheance"
value="#{factureView.entity.dateEcheance}"
pattern="dd/MM/yyyy"
locale="fr"
required="true"
requiredMessage="La date d'échéance est obligatoire"
showIcon="true"
showButtonBar="true"
monthNavigator="true"
yearNavigator="true"
yearRange="2020:2035"
mindate="#{factureView.entity.dateEmission}"
placeholder="Sélectionner une date">
</p:calendar>
<small class="text-600">Date limite de paiement (généralement 30 jours)</small>
</div>
<!-- Date de paiement (optionnel) -->
<div class="field col-12 md:col-6">
<label for="datePaiement" class="font-bold">Date de paiement</label>
<p:calendar id="datePaiement"
value="#{factureView.entity.datePaiement}"
pattern="dd/MM/yyyy"
locale="fr"
showIcon="true"
showButtonBar="true"
monthNavigator="true"
yearNavigator="true"
yearRange="2020:2030"
placeholder="À renseigner après paiement">
</p:calendar>
<small class="text-600">Date effective du paiement (optionnel)</small>
</div>
</div>
</p:panel>
<!-- SECTION 2: Lignes de facture -->
<p:panel header="Détail de la facture" toggleable="true" collapsed="false" class="mb-4">
<div class="mb-3">
<div class="surface-100 border-round p-3">
<div class="flex align-items-center gap-2 mb-2">
<i class="pi pi-info-circle text-blue-500"></i>
<span class="text-900 font-medium">Lignes de facturation</span>
</div>
<p class="text-600 text-sm mt-0 mb-0">
Ajoutez les différentes prestations, fournitures et quantités facturées.
Cette fonctionnalité sera disponible dans une prochaine version.
</p>
</div>
</div>
<!-- Placeholder pour table de lignes -->
<div class="surface-50 border-round p-4 text-center">
<i class="pi pi-list text-400 mb-3" style="font-size: 3rem;"></i>
<p class="text-600 mt-0 mb-3">Gestion des lignes de facture en cours de développement</p>
<p class="text-500 text-sm">
Bientôt disponible: ajout de lignes avec désignation, quantité, prix unitaire, remise, etc.
</p>
</div>
</p:panel>
<!-- SECTION 3: Montants -->
<p:panel header="Montants et totaux" toggleable="true" collapsed="false" class="mb-4">
<div class="formgrid grid">
<!-- Montant HT -->
<div class="field col-12 md:col-6">
<label for="montantHT" class="font-bold">Montant HT (FCFA) <span class="text-red-500">*</span></label>
<p:inputNumber id="montantHT"
value="#{factureView.entity.montantHT}"
required="true"
requiredMessage="Le montant HT est obligatoire"
minValue="0"
decimalPlaces="0"
thousandSeparator=" "
suffix=" FCFA"
placeholder="0">
</p:inputNumber>
<small class="text-600">Montant hors taxes</small>
</div>
<!-- TVA (calculée) -->
<div class="field col-12 md:col-6">
<label class="font-bold">TVA (18%)</label>
<div class="p-inputgroup">
<span class="p-inputgroup-addon bg-orange-100">
<i class="pi pi-percentage text-orange-600"></i>
</span>
<p:inputNumber value="#{factureView.entity.montantHT * 0.18}"
disabled="true"
decimalPlaces="0"
thousandSeparator=" "
suffix=" FCFA"
styleClass="text-center font-medium text-orange-600"/>
</div>
<small class="text-600">Calculé automatiquement (18% du montant HT)</small>
</div>
<!-- Montant TTC (calculé) -->
<div class="field col-12">
<label class="font-bold">Montant TTC (FCFA)</label>
<div class="p-inputgroup">
<span class="p-inputgroup-addon bg-primary">
<i class="pi pi-dollar text-white"></i>
</span>
<p:inputNumber value="#{factureView.entity.montantHT * 1.18}"
disabled="true"
decimalPlaces="0"
thousandSeparator=" "
suffix=" FCFA"
styleClass="text-center font-bold text-xl text-primary"/>
</div>
<small class="text-600">Montant toutes taxes comprises (HT + TVA)</small>
</div>
<!-- Montant payé -->
<div class="field col-12 md:col-6">
<label for="montantPaye" class="font-bold">Montant payé (FCFA)</label>
<p:inputNumber id="montantPaye"
value="#{factureView.entity.montantPaye}"
minValue="0"
decimalPlaces="0"
thousandSeparator=" "
suffix=" FCFA"
placeholder="0">
</p:inputNumber>
<small class="text-600">Montant déjà encaissé</small>
</div>
<!-- Montant restant (calculé) -->
<div class="field col-12 md:col-6">
<label class="font-bold">Montant restant (FCFA)</label>
<div class="p-inputgroup">
<span class="p-inputgroup-addon">
<i class="pi pi-exclamation-triangle"></i>
</span>
<p:inputNumber value="#{(factureView.entity.montantHT * 1.18) - factureView.entity.montantPaye}"
disabled="true"
decimalPlaces="0"
thousandSeparator=" "
suffix=" FCFA"
styleClass="text-center font-bold #{((factureView.entity.montantHT * 1.18) - factureView.entity.montantPaye) > 0 ? 'text-red-600' : 'text-green-600'}"/>
</div>
<small class="text-600">Reste à encaisser</small>
</div>
<!-- Récapitulatif visuel -->
<div class="field col-12">
<div class="surface-100 border-round p-3">
<div class="grid">
<div class="col-12 md:col-3">
<div class="text-center">
<span class="text-600 text-sm block mb-2">Montant HT</span>
<div class="text-900 font-bold text-xl">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{factureView.entity.montantHT}"/>
<ui:param name="size" value="normal"/>
</ui:include>
</div>
</div>
</div>
<div class="col-12 md:col-3">
<div class="text-center">
<span class="text-600 text-sm block mb-2">TVA (18%)</span>
<div class="text-orange-600 font-bold text-xl">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{factureView.entity.montantHT * 0.18}"/>
<ui:param name="size" value="normal"/>
</ui:include>
</div>
</div>
</div>
<div class="col-12 md:col-3">
<div class="text-center">
<span class="text-600 text-sm block mb-2">Total TTC</span>
<div class="text-primary font-bold text-2xl">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{factureView.entity.montantHT * 1.18}"/>
<ui:param name="size" value="large"/>
<ui:param name="bold" value="true"/>
</ui:include>
</div>
</div>
</div>
<div class="col-12 md:col-3">
<div class="text-center">
<span class="text-600 text-sm block mb-2">Reste à payer</span>
<div class="font-bold text-xl" style="color: #{((factureView.entity.montantHT * 1.18) - factureView.entity.montantPaye) > 0 ? '#EF4444' : '#10B981'}">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{(factureView.entity.montantHT * 1.18) - factureView.entity.montantPaye}"/>
<ui:param name="size" value="large"/>
<ui:param name="bold" value="true"/>
</ui:include>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</p:panel>
<!-- SECTION 4: Informations de paiement -->
<p:panel header="Informations de paiement" toggleable="true" collapsed="true" class="mb-4">
<div class="formgrid grid">
<div class="field col-12 md:col-6">
<label for="modePaiement" class="font-bold">Mode de paiement</label>
<p:selectOneMenu id="modePaiement">
<f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true"/>
<f:selectItem itemLabel="Virement bancaire" itemValue="VIREMENT"/>
<f:selectItem itemLabel="Chèque" itemValue="CHEQUE"/>
<f:selectItem itemLabel="Espèces" itemValue="ESPECES"/>
<f:selectItem itemLabel="Carte bancaire" itemValue="CARTE"/>
<f:selectItem itemLabel="Mobile Money" itemValue="MOBILE_MONEY"/>
</p:selectOneMenu>
</div>
<div class="field col-12 md:col-6">
<label for="referencePaiement" class="font-bold">Référence de paiement</label>
<p:inputText id="referencePaiement"
placeholder="Ex: Virement du 15/01/2025"/>
</div>
<div class="field col-12">
<label for="conditionsPaiement" class="font-bold">Conditions de paiement</label>
<p:inputTextarea id="conditionsPaiement"
rows="3"
placeholder="Ex: Paiement à 30 jours fin de mois, escompte 2% si paiement sous 8 jours"
autoResize="false">
</p:inputTextarea>
</div>
</div>
</p:panel>
<!-- Boutons d'action -->
<div class="flex align-items-center justify-content-between pt-4 border-top-1 surface-border">
<div>
<span class="text-600 text-sm">Les champs marqués d'un </span>
<span class="text-red-500 font-bold">*</span>
<span class="text-600 text-sm"> sont obligatoires</span>
</div>
<div class="flex gap-2">
<p:commandButton value="Annuler"
icon="pi pi-times"
action="/factures?faces-redirect=true"
styleClass="ui-button-secondary"
immediate="true"/>
<p:commandButton value="Enregistrer comme brouillon"
icon="pi pi-save"
action="#{factureView.save}"
update="@form messages"
oncomplete="if (args &amp;&amp; !args.validationFailed) window.location.href='/factures.xhtml';"
styleClass="ui-button-secondary"/>
<p:commandButton value="Émettre la facture"
icon="pi pi-send"
action="#{factureView.save}"
update="@form messages"
oncomplete="if (args &amp;&amp; !args.validationFailed) window.location.href='/factures.xhtml';"
styleClass="ui-button-primary"/>
</div>
</div>
</h:form>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Nouvelle facture - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<!-- En-tête avec breadcrumb -->
<div class="flex align-items-center justify-content-between mb-4">
<div>
<h2 class="text-900 font-bold mb-2">Créer une nouvelle facture</h2>
<p class="text-600 mt-0">Émettez une facture pour un chantier ou prestation réalisée</p>
</div>
<p:commandButton value="Retour à la liste"
icon="pi pi-arrow-left"
outcome="/factures"
styleClass="ui-button-secondary ui-button-outlined"/>
</div>
<p:messages id="messages" showDetail="true" closable="true"/>
<h:form id="nouvelleFactureForm" styleClass="p-fluid">
<!-- SECTION 1: Informations générales -->
<p:panel header="Informations générales" toggleable="true" collapsed="false" class="mb-4">
<div class="formgrid grid">
<!-- Numéro (auto-généré) -->
<div class="field col-12 md:col-4">
<label for="numero" class="font-bold">Numéro de facture</label>
<div class="p-inputgroup">
<span class="p-inputgroup-addon bg-primary">
<i class="pi pi-hashtag text-white"></i>
</span>
<p:inputText id="numero"
value="#{factureView.entity.numero}"
disabled="true"
placeholder="Auto-généré"
styleClass="text-center font-bold text-primary"/>
</div>
<small class="text-600">Généré automatiquement selon la séquence configurée</small>
</div>
<!-- Statut -->
<div class="field col-12 md:col-4">
<label for="statut" class="font-bold">Statut <span class="text-red-500">*</span></label>
<p:selectOneMenu id="statut"
value="#{factureView.entity.statut}"
required="true">
<f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true"/>
<f:selectItem itemLabel="Brouillon" itemValue="BROUILLON"/>
<f:selectItem itemLabel="Émise" itemValue="EMISE"/>
<f:selectItem itemLabel="Payée" itemValue="PAYEE"/>
<f:selectItem itemLabel="Impayée" itemValue="IMPAYEE"/>
<f:selectItem itemLabel="En retard" itemValue="EN_RETARD"/>
<f:selectItem itemLabel="Annulée" itemValue="ANNULEE"/>
</p:selectOneMenu>
</div>
<!-- Type de facture -->
<div class="field col-12 md:col-4">
<label for="typeFacture" class="font-bold">Type de facture</label>
<p:selectOneMenu id="typeFacture">
<f:selectItem itemLabel="Standard" itemValue="STANDARD"/>
<f:selectItem itemLabel="Acompte" itemValue="ACOMPTE"/>
<f:selectItem itemLabel="Solde" itemValue="SOLDE"/>
<f:selectItem itemLabel="Avoir" itemValue="AVOIR"/>
</p:selectOneMenu>
<small class="text-600">Nature de la facturation</small>
</div>
<!-- Client -->
<div class="field col-12 md:col-8">
<label for="client" class="font-bold">Client <span class="text-red-500">*</span></label>
<p:inputText id="client"
value="#{factureView.entity.client}"
required="true"
requiredMessage="Le client est obligatoire"
placeholder="Ex: Entreprise ABC SARL">
<f:validateLength minimum="2" maximum="200"/>
</p:inputText>
<small class="text-600">Nom du client ou de l'entreprise</small>
</div>
<!-- Date d'émission -->
<div class="field col-12 md:col-4">
<label for="dateEmission" class="font-bold">Date d'émission <span class="text-red-500">*</span></label>
<p:calendar id="dateEmission"
value="#{factureView.entity.dateEmission}"
pattern="dd/MM/yyyy"
locale="fr"
required="true"
requiredMessage="La date d'émission est obligatoire"
showIcon="true"
showButtonBar="true"
monthNavigator="true"
yearNavigator="true"
yearRange="2020:2030"
placeholder="Sélectionner une date">
</p:calendar>
</div>
<!-- Objet -->
<div class="field col-12">
<label for="objet" class="font-bold">Objet de la facture <span class="text-red-500">*</span></label>
<p:inputTextarea id="objet"
value="#{factureView.entity.objet}"
required="true"
requiredMessage="L'objet de la facture est obligatoire"
rows="2"
placeholder="Ex: Travaux de construction immeuble R+3 - Phase gros œuvre"
autoResize="false">
<f:validateLength minimum="10" maximum="500"/>
</p:inputTextarea>
<small class="text-600">Description des prestations facturées</small>
</div>
<!-- Date d'échéance -->
<div class="field col-12 md:col-6">
<label for="dateEcheance" class="font-bold">Date d'échéance <span class="text-red-500">*</span></label>
<p:calendar id="dateEcheance"
value="#{factureView.entity.dateEcheance}"
pattern="dd/MM/yyyy"
locale="fr"
required="true"
requiredMessage="La date d'échéance est obligatoire"
showIcon="true"
showButtonBar="true"
monthNavigator="true"
yearNavigator="true"
yearRange="2020:2035"
mindate="#{factureView.entity.dateEmission}"
placeholder="Sélectionner une date">
</p:calendar>
<small class="text-600">Date limite de paiement (généralement 30 jours)</small>
</div>
<!-- Date de paiement (optionnel) -->
<div class="field col-12 md:col-6">
<label for="datePaiement" class="font-bold">Date de paiement</label>
<p:calendar id="datePaiement"
value="#{factureView.entity.datePaiement}"
pattern="dd/MM/yyyy"
locale="fr"
showIcon="true"
showButtonBar="true"
monthNavigator="true"
yearNavigator="true"
yearRange="2020:2030"
placeholder="À renseigner après paiement">
</p:calendar>
<small class="text-600">Date effective du paiement (optionnel)</small>
</div>
</div>
</p:panel>
<!-- SECTION 2: Lignes de facture -->
<p:panel header="Détail de la facture" toggleable="true" collapsed="false" class="mb-4">
<div class="mb-3">
<div class="surface-100 border-round p-3">
<div class="flex align-items-center gap-2 mb-2">
<i class="pi pi-info-circle text-blue-500"></i>
<span class="text-900 font-medium">Lignes de facturation</span>
</div>
<p class="text-600 text-sm mt-0 mb-0">
Ajoutez les différentes prestations, fournitures et quantités facturées.
Cette fonctionnalité sera disponible dans une prochaine version.
</p>
</div>
</div>
<!-- Placeholder pour table de lignes -->
<div class="surface-50 border-round p-4 text-center">
<i class="pi pi-list text-400 mb-3" style="font-size: 3rem;"></i>
<p class="text-600 mt-0 mb-3">Gestion des lignes de facture en cours de développement</p>
<p class="text-500 text-sm">
Bientôt disponible: ajout de lignes avec désignation, quantité, prix unitaire, remise, etc.
</p>
</div>
</p:panel>
<!-- SECTION 3: Montants -->
<p:panel header="Montants et totaux" toggleable="true" collapsed="false" class="mb-4">
<div class="formgrid grid">
<!-- Montant HT -->
<div class="field col-12 md:col-6">
<label for="montantHT" class="font-bold">Montant HT (FCFA) <span class="text-red-500">*</span></label>
<p:inputNumber id="montantHT"
value="#{factureView.entity.montantHT}"
required="true"
requiredMessage="Le montant HT est obligatoire"
minValue="0"
decimalPlaces="0"
thousandSeparator=" "
suffix=" FCFA"
placeholder="0">
</p:inputNumber>
<small class="text-600">Montant hors taxes</small>
</div>
<!-- TVA (calculée) -->
<div class="field col-12 md:col-6">
<label class="font-bold">TVA (18%)</label>
<div class="p-inputgroup">
<span class="p-inputgroup-addon bg-orange-100">
<i class="pi pi-percentage text-orange-600"></i>
</span>
<p:inputNumber value="#{factureView.entity.montantHT * 0.18}"
disabled="true"
decimalPlaces="0"
thousandSeparator=" "
suffix=" FCFA"
styleClass="text-center font-medium text-orange-600"/>
</div>
<small class="text-600">Calculé automatiquement (18% du montant HT)</small>
</div>
<!-- Montant TTC (calculé) -->
<div class="field col-12">
<label class="font-bold">Montant TTC (FCFA)</label>
<div class="p-inputgroup">
<span class="p-inputgroup-addon bg-primary">
<i class="pi pi-dollar text-white"></i>
</span>
<p:inputNumber value="#{factureView.entity.montantHT * 1.18}"
disabled="true"
decimalPlaces="0"
thousandSeparator=" "
suffix=" FCFA"
styleClass="text-center font-bold text-xl text-primary"/>
</div>
<small class="text-600">Montant toutes taxes comprises (HT + TVA)</small>
</div>
<!-- Montant payé -->
<div class="field col-12 md:col-6">
<label for="montantPaye" class="font-bold">Montant payé (FCFA)</label>
<p:inputNumber id="montantPaye"
value="#{factureView.entity.montantPaye}"
minValue="0"
decimalPlaces="0"
thousandSeparator=" "
suffix=" FCFA"
placeholder="0">
</p:inputNumber>
<small class="text-600">Montant déjà encaissé</small>
</div>
<!-- Montant restant (calculé) -->
<div class="field col-12 md:col-6">
<label class="font-bold">Montant restant (FCFA)</label>
<div class="p-inputgroup">
<span class="p-inputgroup-addon">
<i class="pi pi-exclamation-triangle"></i>
</span>
<p:inputNumber value="#{(factureView.entity.montantHT * 1.18) - factureView.entity.montantPaye}"
disabled="true"
decimalPlaces="0"
thousandSeparator=" "
suffix=" FCFA"
styleClass="text-center font-bold #{((factureView.entity.montantHT * 1.18) - factureView.entity.montantPaye) > 0 ? 'text-red-600' : 'text-green-600'}"/>
</div>
<small class="text-600">Reste à encaisser</small>
</div>
<!-- Récapitulatif visuel -->
<div class="field col-12">
<div class="surface-100 border-round p-3">
<div class="grid">
<div class="col-12 md:col-3">
<div class="text-center">
<span class="text-600 text-sm block mb-2">Montant HT</span>
<div class="text-900 font-bold text-xl">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{factureView.entity.montantHT}"/>
<ui:param name="size" value="normal"/>
</ui:include>
</div>
</div>
</div>
<div class="col-12 md:col-3">
<div class="text-center">
<span class="text-600 text-sm block mb-2">TVA (18%)</span>
<div class="text-orange-600 font-bold text-xl">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{factureView.entity.montantHT * 0.18}"/>
<ui:param name="size" value="normal"/>
</ui:include>
</div>
</div>
</div>
<div class="col-12 md:col-3">
<div class="text-center">
<span class="text-600 text-sm block mb-2">Total TTC</span>
<div class="text-primary font-bold text-2xl">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{factureView.entity.montantHT * 1.18}"/>
<ui:param name="size" value="large"/>
<ui:param name="bold" value="true"/>
</ui:include>
</div>
</div>
</div>
<div class="col-12 md:col-3">
<div class="text-center">
<span class="text-600 text-sm block mb-2">Reste à payer</span>
<div class="font-bold text-xl" style="color: #{((factureView.entity.montantHT * 1.18) - factureView.entity.montantPaye) > 0 ? '#EF4444' : '#10B981'}">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{(factureView.entity.montantHT * 1.18) - factureView.entity.montantPaye}"/>
<ui:param name="size" value="large"/>
<ui:param name="bold" value="true"/>
</ui:include>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</p:panel>
<!-- SECTION 4: Informations de paiement -->
<p:panel header="Informations de paiement" toggleable="true" collapsed="true" class="mb-4">
<div class="formgrid grid">
<div class="field col-12 md:col-6">
<label for="modePaiement" class="font-bold">Mode de paiement</label>
<p:selectOneMenu id="modePaiement">
<f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true"/>
<f:selectItem itemLabel="Virement bancaire" itemValue="VIREMENT"/>
<f:selectItem itemLabel="Chèque" itemValue="CHEQUE"/>
<f:selectItem itemLabel="Espèces" itemValue="ESPECES"/>
<f:selectItem itemLabel="Carte bancaire" itemValue="CARTE"/>
<f:selectItem itemLabel="Mobile Money" itemValue="MOBILE_MONEY"/>
</p:selectOneMenu>
</div>
<div class="field col-12 md:col-6">
<label for="referencePaiement" class="font-bold">Référence de paiement</label>
<p:inputText id="referencePaiement"
placeholder="Ex: Virement du 15/01/2025"/>
</div>
<div class="field col-12">
<label for="conditionsPaiement" class="font-bold">Conditions de paiement</label>
<p:inputTextarea id="conditionsPaiement"
rows="3"
placeholder="Ex: Paiement à 30 jours fin de mois, escompte 2% si paiement sous 8 jours"
autoResize="false">
</p:inputTextarea>
</div>
</div>
</p:panel>
<!-- Boutons d'action -->
<div class="flex align-items-center justify-content-between pt-4 border-top-1 surface-border">
<div>
<span class="text-600 text-sm">Les champs marqués d'un </span>
<span class="text-red-500 font-bold">*</span>
<span class="text-600 text-sm"> sont obligatoires</span>
</div>
<div class="flex gap-2">
<p:commandButton value="Annuler"
icon="pi pi-times"
action="/factures?faces-redirect=true"
styleClass="ui-button-secondary"
immediate="true"/>
<p:commandButton value="Enregistrer comme brouillon"
icon="pi pi-save"
action="#{factureView.save}"
update="@form messages"
oncomplete="if (args &amp;&amp; !args.validationFailed) window.location.href='/factures.xhtml';"
styleClass="ui-button-secondary"/>
<p:commandButton value="Émettre la facture"
icon="pi pi-send"
action="#{factureView.save}"
update="@form messages"
oncomplete="if (args &amp;&amp; !args.validationFailed) window.location.href='/factures.xhtml';"
styleClass="ui-button-primary"/>
</div>
</div>
</h:form>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1 +1 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Payees - FACTURES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Payees</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/factures" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Payees - FACTURES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Payees</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/factures" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>

View File

@@ -1 +1 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Retard - FACTURES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Retard</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/factures" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Retard - FACTURES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Retard</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/factures" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>

View File

@@ -1,28 +1,28 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Fournisseurs - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Fournisseurs</h6>
<p class="subtitle">Gestion des fournisseurs</p>
</div>
</div>
<p>Page en développement</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Fournisseurs - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Fournisseurs</h6>
<p class="subtitle">Gestion des fournisseurs</p>
</div>
</div>
<p>Page en développement</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

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

View File

@@ -1,85 +1,85 @@
<!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>Connexion - BTP Xpress</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 class="login-body">
<div class="login-wrapper">
<div class="login-container">
<div class="login-left">
<div class="login-content">
<h1>BTP Xpress</h1>
<p class="subtitle">Votre plateforme de gestion BTP</p>
<p>Gérez vos projets, clients, matériels et équipes en toute simplicité.</p>
</div>
</div>
<div class="login-right">
<div class="login-box">
<h2>Connexion</h2>
<p class="login-subtitle">Connectez-vous à votre compte</p>
<h:form id="loginForm">
<div class="login-input-group">
<label for="username">Nom d'utilisateur ou email</label>
<p:inputText id="username"
value="#{loginView.username}"
placeholder="Votre nom d'utilisateur"
required="true"
requiredMessage="Le nom d'utilisateur est requis"
styleClass="ui-input-filled"
style="width: 100%;"/>
</div>
<div class="login-input-group">
<label for="password">Mot de passe</label>
<p:password id="password"
value="#{loginView.password}"
placeholder="Votre mot de passe"
required="true"
requiredMessage="Le mot de passe est requis"
feedback="false"
toggleMask="true"
styleClass="ui-input-filled"
style="width: 100%;"/>
</div>
<div class="login-options">
<p:selectBooleanCheckbox id="rememberMe"
value="#{loginView.rememberMe}"
label="Se souvenir de moi"/>
<a href="#" style="text-decoration: none; color: var(--primary-color);">Mot de passe oublié ?</a>
</div>
<p:commandButton value="Se connecter"
icon="pi pi-sign-in"
action="#{loginView.login()}"
style="width: 100%; margin-top: 1rem;"
update="@form"
process="@form"/>
<p:messages id="messages" showDetail="true" closable="true"/>
</h:form>
</div>
</div>
</div>
</div>
</h:body>
</html>
<!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>Connexion - BTP Xpress</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 class="login-body">
<div class="login-wrapper">
<div class="login-container">
<div class="login-left">
<div class="login-content">
<h1>BTP Xpress</h1>
<p class="subtitle">Votre plateforme de gestion BTP</p>
<p>Gérez vos projets, clients, matériels et équipes en toute simplicité.</p>
</div>
</div>
<div class="login-right">
<div class="login-box">
<h2>Connexion</h2>
<p class="login-subtitle">Connectez-vous à votre compte</p>
<h:form id="loginForm">
<div class="login-input-group">
<label for="username">Nom d'utilisateur ou email</label>
<p:inputText id="username"
value="#{loginView.username}"
placeholder="Votre nom d'utilisateur"
required="true"
requiredMessage="Le nom d'utilisateur est requis"
styleClass="ui-input-filled"
style="width: 100%;"/>
</div>
<div class="login-input-group">
<label for="password">Mot de passe</label>
<p:password id="password"
value="#{loginView.password}"
placeholder="Votre mot de passe"
required="true"
requiredMessage="Le mot de passe est requis"
feedback="false"
toggleMask="true"
styleClass="ui-input-filled"
style="width: 100%;"/>
</div>
<div class="login-options">
<p:selectBooleanCheckbox id="rememberMe"
value="#{loginView.rememberMe}"
label="Se souvenir de moi"/>
<a href="#" style="text-decoration: none; color: var(--primary-color);">Mot de passe oublié ?</a>
</div>
<p:commandButton value="Se connecter"
icon="pi pi-sign-in"
action="#{loginView.login()}"
style="width: 100%; margin-top: 1rem;"
update="@form"
process="@form"/>
<p:messages id="messages" showDetail="true" closable="true"/>
</h:form>
</div>
</div>
</div>
</div>
</h:body>
</html>

View File

@@ -1,23 +1,23 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Maintenance - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<h1>Gestion de la Maintenance</h1>
<p>Module en cours de développement...</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Maintenance - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<h1>Gestion de la Maintenance</h1>
<p>Module en cours de développement...</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1 +1 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Corrective - MAINTENANCE - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Corrective</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/maintenance" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Corrective - MAINTENANCE - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Corrective</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/maintenance" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>

View File

@@ -1 +1 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">MAINTENANCE - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>MAINTENANCE</h1><p>Module en cours de développement...</p></div></div></div></div></ui:define></ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">MAINTENANCE - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>MAINTENANCE</h1><p>Module en cours de développement...</p></div></div></div></div></ui:define></ui:composition>

View File

@@ -1,27 +1,27 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Maintenance préventive - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Maintenance préventive</h6>
<p class="subtitle">Maintenances préventives planifiées</p>
</div>
</div>
<p>Page en développement</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Maintenance préventive - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Maintenance préventive</h6>
<p class="subtitle">Maintenances préventives planifiées</p>
</div>
</div>
<p>Page en développement</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1 +1 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Urgente - MAINTENANCE - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Urgente</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/maintenance" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Urgente - MAINTENANCE - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Urgente</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/maintenance" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>

View File

@@ -1,111 +1,111 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Matériels - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Gestion des Matériels</h1>
<p:commandButton value="Nouveau matériel" icon="pi pi-wrench"
action="#{materielView.createNew()}"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{materielView}"/>
<ui:param name="tableId" value="materielsTable"/>
<ui:define name="filter-fields">
<div class="grid">
<div class="col-12 md:col-4">
<h:outputLabel for="filtreNom" value="Nom"/>
<p:inputText id="filtreNom" value="#{materielView.filtreNom}"
placeholder="Rechercher par nom..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreType" value="Type"/>
<p:selectOneMenu id="filtreType" value="#{materielView.filtreType}" style="width: 100%;">
<f:selectItem itemLabel="Tous" itemValue="TOUS"/>
<f:selectItem itemLabel="Engin" itemValue="ENGIN"/>
<f:selectItem itemLabel="Outil" itemValue="OUTIL"/>
<f:selectItem itemLabel="Véhicule" itemValue="VEHICULE"/>
<f:selectItem itemLabel="Équipement" itemValue="EQUIPEMENT"/>
</p:selectOneMenu>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreStatut" value="Statut"/>
<p:selectOneMenu id="filtreStatut" value="#{materielView.filtreStatut}" style="width: 100%;">
<f:selectItem itemLabel="Tous" itemValue="TOUS"/>
<f:selectItem itemLabel="Disponible" itemValue="DISPONIBLE"/>
<f:selectItem itemLabel="En service" itemValue="EN_SERVICE"/>
<f:selectItem itemLabel="En maintenance" itemValue="EN_MAINTENANCE"/>
<f:selectItem itemLabel="Hors service" itemValue="HORS_SERVICE"/>
</p:selectOneMenu>
</div>
</div>
</ui:define>
</ui:include>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="materielsForm"/>
<ui:param name="tableId" value="materielsTable"/>
<ui:param name="viewBean" value="#{materielView}"/>
<ui:param name="var" value="materiel"/>
<ui:param name="title" value="Liste des matériels"/>
<ui:param name="createPath" value="/materiels/nouveau"/>
<ui:define name="columns">
<p:column headerText="Nom" sortBy="#{materiel.nom}">
<h:outputText value="#{materiel.nom}"/>
</p:column>
<p:column headerText="Type" sortBy="#{materiel.type}">
<p:tag value="#{materiel.type}" severity="info"/>
</p:column>
<p:column headerText="Marque" sortBy="#{materiel.marque}">
<h:outputText value="#{materiel.marque}"/>
</p:column>
<p:column headerText="Modèle">
<h:outputText value="#{materiel.modele}"/>
</p:column>
<p:column headerText="N° série">
<h:outputText value="#{materiel.numeroSerie}"/>
</p:column>
<p:column headerText="Valeur d'achat">
<h:outputText value="#{materiel.valeurAchat}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p:column>
<p:column headerText="Date achat" sortBy="#{materiel.dateAchat}">
<h:outputText value="#{materiel.dateAchat}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Statut" sortBy="#{materiel.statut}">
<p:tag value="#{materiel.statut}"
severity="#{materiel.statut == 'DISPONIBLE' ? 'success' : (materiel.statut == 'HORS_SERVICE' ? 'danger' : 'warning')}"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{materielView.viewDetails(materiel.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Matériels - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-3">
<h1>Gestion des Matériels</h1>
<p:commandButton value="Nouveau matériel" icon="pi pi-wrench"
action="#{materielView.createNew()}"
styleClass="ui-button-primary"/>
</div>
</div>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{materielView}"/>
<ui:param name="tableId" value="materielsTable"/>
<ui:define name="filter-fields">
<div class="grid">
<div class="col-12 md:col-4">
<h:outputLabel for="filtreNom" value="Nom"/>
<p:inputText id="filtreNom" value="#{materielView.filtreNom}"
placeholder="Rechercher par nom..." style="width: 100%;"/>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreType" value="Type"/>
<p:selectOneMenu id="filtreType" value="#{materielView.filtreType}" style="width: 100%;">
<f:selectItem itemLabel="Tous" itemValue="TOUS"/>
<f:selectItem itemLabel="Engin" itemValue="ENGIN"/>
<f:selectItem itemLabel="Outil" itemValue="OUTIL"/>
<f:selectItem itemLabel="Véhicule" itemValue="VEHICULE"/>
<f:selectItem itemLabel="Équipement" itemValue="EQUIPEMENT"/>
</p:selectOneMenu>
</div>
<div class="col-12 md:col-4">
<h:outputLabel for="filtreStatut" value="Statut"/>
<p:selectOneMenu id="filtreStatut" value="#{materielView.filtreStatut}" style="width: 100%;">
<f:selectItem itemLabel="Tous" itemValue="TOUS"/>
<f:selectItem itemLabel="Disponible" itemValue="DISPONIBLE"/>
<f:selectItem itemLabel="En service" itemValue="EN_SERVICE"/>
<f:selectItem itemLabel="En maintenance" itemValue="EN_MAINTENANCE"/>
<f:selectItem itemLabel="Hors service" itemValue="HORS_SERVICE"/>
</p:selectOneMenu>
</div>
</div>
</ui:define>
</ui:include>
</div>
<div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="materielsForm"/>
<ui:param name="tableId" value="materielsTable"/>
<ui:param name="viewBean" value="#{materielView}"/>
<ui:param name="var" value="materiel"/>
<ui:param name="title" value="Liste des matériels"/>
<ui:param name="createPath" value="/materiels/nouveau"/>
<ui:define name="columns">
<p:column headerText="Nom" sortBy="#{materiel.nom}">
<h:outputText value="#{materiel.nom}"/>
</p:column>
<p:column headerText="Type" sortBy="#{materiel.type}">
<p:tag value="#{materiel.type}" severity="info"/>
</p:column>
<p:column headerText="Marque" sortBy="#{materiel.marque}">
<h:outputText value="#{materiel.marque}"/>
</p:column>
<p:column headerText="Modèle">
<h:outputText value="#{materiel.modele}"/>
</p:column>
<p:column headerText="N° série">
<h:outputText value="#{materiel.numeroSerie}"/>
</p:column>
<p:column headerText="Valeur d'achat">
<h:outputText value="#{materiel.valeurAchat}">
<f:converter converterId="fcfaConverter"/>
</h:outputText>
<h:outputText value=" Fcfa"/>
</p:column>
<p:column headerText="Date achat" sortBy="#{materiel.dateAchat}">
<h:outputText value="#{materiel.dateAchat}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p:column>
<p:column headerText="Statut" sortBy="#{materiel.statut}">
<p:tag value="#{materiel.statut}"
severity="#{materiel.statut == 'DISPONIBLE' ? 'success' : (materiel.statut == 'HORS_SERVICE' ? 'danger' : 'warning')}"/>
</p:column>
<p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text"
action="#{materielView.viewDetails(materiel.id)}"/>
</p:column>
</ui:define>
</ui:include>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,23 +1,23 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Messages - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<h1>Messages</h1>
<p>Module en cours de développement...</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Messages - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<h1>Messages</h1>
<p>Module en cours de développement...</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1 +1 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Archives - MESSAGES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Archives</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/messages" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Archives - MESSAGES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Archives</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/messages" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>

View File

@@ -1 +1 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Envoyes - MESSAGES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Envoyes</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/messages" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Envoyes - MESSAGES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Envoyes</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/messages" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>

View File

@@ -1 +1 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">MESSAGES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>MESSAGES</h1><p>Module en cours de développement...</p></div></div></div></div></ui:define></ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">MESSAGES - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>MESSAGES</h1><p>Module en cours de développement...</p></div></div></div></div></ui:define></ui:composition>

View File

@@ -1,23 +1,23 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Notifications - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<h1>Notifications</h1>
<p>Module en cours de développement...</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Notifications - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<h1>Notifications</h1>
<p>Module en cours de développement...</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1 +1 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Non Lues - NOTIFICATIONS - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Non Lues</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/notifications" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Non Lues - NOTIFICATIONS - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Non Lues</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/notifications" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>

View File

@@ -1 +1 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">NOTIFICATIONS - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>NOTIFICATIONS</h1><p>Module en cours de développement...</p></div></div></div></div></ui:define></ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">NOTIFICATIONS - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>NOTIFICATIONS</h1><p>Module en cours de développement...</p></div></div></div></div></ui:define></ui:composition>

View File

@@ -1 +1 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Recentes - NOTIFICATIONS - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Recentes</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/notifications" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Recentes - NOTIFICATIONS - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Recentes</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/notifications" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>

View File

@@ -1 +1 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Statistiques - NOTIFICATIONS - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Statistiques</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/notifications" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" template="/WEB-INF/template.xhtml"><ui:define name="title">Statistiques - NOTIFICATIONS - BTP Xpress</ui:define><ui:define name="content"><div class="layout-dashboard"><div class="grid"><div class="col-12"><div class="card"><h1>Statistiques</h1><p>Module en cours de développement...</p><p:commandButton value="Retour" icon="pi pi-arrow-left" outcome="/notifications" styleClass="ui-button-secondary"/></div></div></div></div></ui:define></ui:composition>

View File

@@ -1,28 +1,28 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Paramètres - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Paramètres</h6>
<p class="subtitle">Configuration de l'application</p>
</div>
</div>
<p>Page en développement</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Paramètres - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-title">
<h6>Paramètres</h6>
<p class="subtitle">Configuration de l'application</p>
</div>
</div>
<p>Page en développement</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,23 +1,23 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Planning - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<h1>Planning</h1>
<p>Module en cours de développement...</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Planning - BTP Xpress</ui:define>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<div class="card">
<h1>Planning</h1>
<p>Module en cours de développement...</p>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

Some files were not shown because too many files have changed in this diff Show More