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 # Docker ignore pour BTP Xpress Client
# Répertoires de build et target # Répertoires de build et target
target/ target/
.mvn/ .mvn/
.quarkus/ .quarkus/
# Fichiers IDE # Fichiers IDE
.idea/ .idea/
.vscode/ .vscode/
*.iml *.iml
*.ipr *.ipr
*.iws *.iws
.settings/ .settings/
.classpath .classpath
.project .project
# Documentation (non nécessaire dans l'image) # Documentation (non nécessaire dans l'image)
*.md *.md
!README.md !README.md
# Git # Git
.git/ .git/
.gitignore .gitignore
.gitattributes .gitattributes
# Fichiers de configuration locale # Fichiers de configuration locale
.env .env
.env.local .env.local
*.log *.log
# Tests # Tests
src/test/ src/test/
# Fichiers temporaires # Fichiers temporaires
*.tmp *.tmp
*.bak *.bak
*.swp *.swp
*~ *~
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Scripts de déploiement (non nécessaires dans l'image) # Scripts de déploiement (non nécessaires dans l'image)
scripts/ scripts/
*.sh *.sh
*.ps1 *.ps1
*.bat *.bat
# Kubernetes (géré séparément) # Kubernetes (géré séparément)
kubernetes/ kubernetes/
*.yaml *.yaml
*.yml *.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 # Audit de Configuration - BTP Xpress Client ↔ Serveur
## ✅ Résumé de l'audit effectué ## ✅ Résumé de l'audit effectué
Date : 2025-11-01 Date : 2025-11-01
Portée : Configuration complète du client PrimeFaces et mapping avec le serveur backend Portée : Configuration complète du client PrimeFaces et mapping avec le serveur backend
--- ---
## 1. Structure du Projet Client ## 1. Structure du Projet Client
### ✅ Structure des fichiers ### ✅ Structure des fichiers
- **XHTML** : `src/main/resources/META-INF/resources/` (structure Quarkus correcte) - **XHTML** : `src/main/resources/META-INF/resources/` (structure Quarkus correcte)
- **Configuration** : `src/main/resources/META-INF/web.xml` et `application.properties` - **Configuration** : `src/main/resources/META-INF/web.xml` et `application.properties`
- **Beans CDI** : `src/main/java/dev/lions/btpxpress/` - **Beans CDI** : `src/main/java/dev/lions/btpxpress/`
- **Services** : `src/main/java/dev/lions/btpxpress/service/` - **Services** : `src/main/java/dev/lions/btpxpress/service/`
### ✅ Fichiers créés/vérifiés ### ✅ Fichiers créés/vérifiés
-`BtpXpressApiClient.java` - Interface REST Client pour communication backend -`BtpXpressApiClient.java` - Interface REST Client pour communication backend
-`ChantierService.java` - Service encapsulant les appels API chantiers -`ChantierService.java` - Service encapsulant les appels API chantiers
-`application.properties` - Configuration complète OIDC + REST Client -`application.properties` - Configuration complète OIDC + REST Client
-`pom.xml` - Dépendances OIDC et JWT ajoutées -`pom.xml` - Dépendances OIDC et JWT ajoutées
--- ---
## 2. Configuration OIDC / Keycloak ## 2. Configuration OIDC / Keycloak
### ✅ Client (PrimeFaces) ### ✅ Client (PrimeFaces)
```properties ```properties
quarkus.oidc.enabled=true quarkus.oidc.enabled=true
quarkus.oidc.auth-server-url=https://security.lions.dev/realms/btpxpress quarkus.oidc.auth-server-url=https://security.lions.dev/realms/btpxpress
quarkus.oidc.client-id=btpxpress-frontend quarkus.oidc.client-id=btpxpress-frontend
quarkus.oidc.application-type=web-app quarkus.oidc.application-type=web-app
quarkus.oidc.token.issuer=https://security.lions.dev/realms/btpxpress quarkus.oidc.token.issuer=https://security.lions.dev/realms/btpxpress
``` ```
### ✅ Serveur (Backend) ### ✅ Serveur (Backend)
```properties ```properties
mp.jwt.verify.publickey.location=https://security.lions.dev/realms/btpxpress/protocol/openid-connect/certs 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 mp.jwt.verify.issuer=https://security.lions.dev/realms/btpxpress
quarkus.smallrye-jwt.enabled=true quarkus.smallrye-jwt.enabled=true
``` ```
### ✅ Vérifications ### ✅ Vérifications
-**Même realm** : `btpxpress` -**Même realm** : `btpxpress`
-**Même serveur Keycloak** : `https://security.lions.dev` -**Même serveur Keycloak** : `https://security.lions.dev`
-**Client ID frontend** : `btpxpress-frontend` (doit exister dans Keycloak) -**Client ID frontend** : `btpxpress-frontend` (doit exister dans Keycloak)
-**JWT Validation** : Backend valide les tokens via certificats publics -**JWT Validation** : Backend valide les tokens via certificats publics
--- ---
## 3. Communication Client ↔ Serveur ## 3. Communication Client ↔ Serveur
### ✅ Configuration REST Client ### ✅ Configuration REST Client
```properties ```properties
btpxpress.api.base-url=http://localhost:8080 btpxpress.api.base-url=http://localhost:8080
quarkus.rest-client."dev.lions.btpxpress.service.BtpXpressApiClient".url=${btpxpress.api.base-url} quarkus.rest-client."dev.lions.btpxpress.service.BtpXpressApiClient".url=${btpxpress.api.base-url}
``` ```
### ✅ Endpoints mappés ### ✅ Endpoints mappés
| Client (Interface) | Serveur (Resource) | Endpoint | Status | | Client (Interface) | Serveur (Resource) | Endpoint | Status |
|-------------------|-------------------|----------|--------| |-------------------|-------------------|----------|--------|
| `BtpXpressApiClient.getChantiers()` | `ChantierResource.getAllChantiers()` | `GET /api/v1/chantiers` | ✅ Existe | | `BtpXpressApiClient.getChantiers()` | `ChantierResource.getAllChantiers()` | `GET /api/v1/chantiers` | ✅ Existe |
| `BtpXpressApiClient.getChantier(id)` | `ChantierResource.getChantierById()` | `GET /api/v1/chantiers/{id}` | ✅ Existe | | `BtpXpressApiClient.getChantier(id)` | `ChantierResource.getChantierById()` | `GET /api/v1/chantiers/{id}` | ✅ Existe |
| `BtpXpressApiClient.getClients()` | `ClientResource.getAllClients()` | `GET /api/v1/clients` | ✅ Existe | | `BtpXpressApiClient.getClients()` | `ClientResource.getAllClients()` | `GET /api/v1/clients` | ✅ Existe |
| `BtpXpressApiClient.getClient(id)` | `ClientResource.getClientById()` | `GET /api/v1/clients/{id}` | ✅ Existe | | `BtpXpressApiClient.getClient(id)` | `ClientResource.getClientById()` | `GET /api/v1/clients/{id}` | ✅ Existe |
### ✅ CORS Configuration ### ✅ CORS Configuration
**Serveur** (`application.properties`) : **Serveur** (`application.properties`) :
```properties ```properties
quarkus.http.cors.origins=${CORS_ORIGINS:http://localhost:3000,http://localhost:5173,http://localhost:8081} quarkus.http.cors.origins=${CORS_ORIGINS:http://localhost:3000,http://localhost:5173,http://localhost:8081}
``` ```
**Port 8081 ajouté** aux origines autorisées **Port 8081 ajouté** aux origines autorisées
**Client** : **Client** :
```properties ```properties
quarkus.http.cors.origins=http://localhost:8080,https://security.lions.dev quarkus.http.cors.origins=http://localhost:8080,https://security.lions.dev
``` ```
--- ---
## 4. Ports et URLs ## 4. Ports et URLs
| Service | Port | URL | Description | | Service | Port | URL | Description |
|---------|------|-----|-------------| |---------|------|-----|-------------|
| Backend | 8080 | http://localhost:8080 | API REST backend | | Backend | 8080 | http://localhost:8080 | API REST backend |
| Client | 8081 | http://localhost:8081 | Application PrimeFaces | | Client | 8081 | http://localhost:8081 | Application PrimeFaces |
| Keycloak | - | https://security.lions.dev | Authentification OIDC | | Keycloak | - | https://security.lions.dev | Authentification OIDC |
--- ---
## 5. Dépendances Maven ## 5. Dépendances Maven
### ✅ Client (`pom.xml`) ### ✅ Client (`pom.xml`)
-`quarkus-oidc` - Authentification OIDC -`quarkus-oidc` - Authentification OIDC
-`quarkus-smallrye-jwt` - Support JWT -`quarkus-smallrye-jwt` - Support JWT
-`quarkus-rest-client` - REST Client -`quarkus-rest-client` - REST Client
-`quarkus-rest-jackson` - Sérialisation JSON -`quarkus-rest-jackson` - Sérialisation JSON
-`quarkus-primefaces` - PrimeFaces integration -`quarkus-primefaces` - PrimeFaces integration
-`freya-theme` - Thème PrimeFaces Freya -`freya-theme` - Thème PrimeFaces Freya
### ✅ Serveur (vérifié) ### ✅ Serveur (vérifié)
-`quarkus-smallrye-jwt` - Validation JWT -`quarkus-smallrye-jwt` - Validation JWT
- ✅ CORS activé avec origine `http://localhost:8081` - ✅ CORS activé avec origine `http://localhost:8081`
--- ---
## 6. Flux d'Authentification ## 6. Flux d'Authentification
``` ```
┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Client │ │ Keycloak │ │ Backend │ │ Client │ │ Keycloak │ │ Backend │
│ Port 8081 │ │security.lions│ │ Port 8080 │ │ Port 8081 │ │security.lions│ │ Port 8080 │
└─────────────┘ └──────────────┘ └─────────────┘ └─────────────┘ └──────────────┘ └─────────────┘
│ │ │ │ │ │
│ 1. Accès page protégée │ │ 1. Accès page protégée │
│────────────────────────────────────────────────►│ │────────────────────────────────────────────────►│
│ │ │ │ │ │
│ │ 2. Redirection OIDC │ │ │ 2. Redirection OIDC │
│◄────────────────────────────────────────────────│ │◄────────────────────────────────────────────────│
│ │ │ │ │ │
│ 3. Redirect Keycloak │ │ │ 3. Redirect Keycloak │ │
│────────────────────────►│ │ │────────────────────────►│ │
│ │ │ │ │ │
│ 4. Authentification │ │ │ 4. Authentification │ │
│ │ │ │ │ │
│ 5. Token JWT │ │ │ 5. Token JWT │ │
│◄────────────────────────│ │ │◄────────────────────────│ │
│ │ │ │ │ │
│ 6. Appel API + Token │ │ │ 6. Appel API + Token │ │
│────────────────────────────────────────────────►│ │────────────────────────────────────────────────►│
│ │ │ │ │ │
│ │ 7. Validation token │ │ │ 7. Validation token │
│ │◄───────────────────────│ │ │◄───────────────────────│
│ │ │ │ │ │
│ 8. Réponse API │ │ │ 8. Réponse API │ │
│◄────────────────────────────────────────────────│ │◄────────────────────────────────────────────────│
``` ```
--- ---
## 7. Points de Vérification Requis ## 7. Points de Vérification Requis
### ⚠️ À vérifier dans Keycloak ### ⚠️ À vérifier dans Keycloak
1. **Client `btpxpress-frontend` existe** dans le realm `btpxpress` 1. **Client `btpxpress-frontend` existe** dans le realm `btpxpress`
2. **Redirect URIs** incluent `http://localhost:8081/*` 2. **Redirect URIs** incluent `http://localhost:8081/*`
3. **Web Origins** incluent `http://localhost:8081` 3. **Web Origins** incluent `http://localhost:8081`
4. **Client Secret** configuré si nécessaire (pour confidential client) 4. **Client Secret** configuré si nécessaire (pour confidential client)
### ⚠️ À tester ### ⚠️ À tester
1. **Authentification OIDC** : Vérifier la redirection vers Keycloak 1. **Authentification OIDC** : Vérifier la redirection vers Keycloak
2. **Token JWT** : Vérifier l'envoi automatique dans les requêtes REST 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 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` 4. **Endpoints API** : Tester les appels `GET /api/v1/chantiers` et `/api/v1/clients`
--- ---
## 8. Configuration Complète Validée ## 8. Configuration Complète Validée
| Composant | Configuration | Status | | Composant | Configuration | Status |
|-----------|--------------|--------| |-----------|--------------|--------|
| Structure fichiers | Quarkus standard | ✅ | | Structure fichiers | Quarkus standard | ✅ |
| OIDC Client | `btpxpress-frontend` | ✅ | | OIDC Client | `btpxpress-frontend` | ✅ |
| OIDC Server | `security.lions.dev` | ✅ | | OIDC Server | `security.lions.dev` | ✅ |
| REST Client | `BtpXpressApiClient` | ✅ | | REST Client | `BtpXpressApiClient` | ✅ |
| Services | `ChantierService` | ✅ | | Services | `ChantierService` | ✅ |
| CORS Backend | Port 8081 autorisé | ✅ | | CORS Backend | Port 8081 autorisé | ✅ |
| CORS Client | Port 8080 autorisé | ✅ | | CORS Client | Port 8080 autorisé | ✅ |
| Endpoints mappés | Tous vérifiés | ✅ | | Endpoints mappés | Tous vérifiés | ✅ |
| Dépendances | Toutes présentes | ✅ | | Dépendances | Toutes présentes | ✅ |
--- ---
## 🎯 Conclusion ## 🎯 Conclusion
**✅ La configuration est complète et correcte** : **✅ La configuration est complète et correcte** :
- Le client PrimeFaces est correctement configuré pour communiquer avec le backend - Le client PrimeFaces est correctement configuré pour communiquer avec le backend
- L'authentification OIDC est configurée avec Keycloak sur `security.lions.dev` - L'authentification OIDC est configurée avec Keycloak sur `security.lions.dev`
- Les endpoints REST sont mappés correctement - Les endpoints REST sont mappés correctement
- Le CORS est configuré pour autoriser la communication bidirectionnelle - Le CORS est configuré pour autoriser la communication bidirectionnelle
- Toutes les dépendances nécessaires sont présentes - 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. **⚠️ 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 # Configuration BTP Xpress Client - PrimeFaces Freya
## ✅ Vérifications effectuées ## ✅ Vérifications effectuées
### 1. Structure du projet ### 1. Structure du projet
- ✅ Fichiers XHTML dans `src/main/resources/META-INF/resources/` - ✅ Fichiers XHTML dans `src/main/resources/META-INF/resources/`
- ✅ Configuration dans `src/main/resources/META-INF/web.xml` - ✅ Configuration dans `src/main/resources/META-INF/web.xml`
- ✅ Beans CDI dans `src/main/java/dev/lions/btpxpress/` - ✅ Beans CDI dans `src/main/java/dev/lions/btpxpress/`
### 2. Configuration OIDC / Keycloak ### 2. Configuration OIDC / Keycloak
-**Serveur Keycloak** : `https://security.lions.dev/realms/btpxpress` -**Serveur Keycloak** : `https://security.lions.dev/realms/btpxpress`
-**Client ID** : `btpxpress-frontend` -**Client ID** : `btpxpress-frontend`
-**Type d'application** : `web-app` -**Type d'application** : `web-app`
-**Redirection** : `/` (restauration du chemin après authentification) -**Redirection** : `/` (restauration du chemin après authentification)
-**Cookies** : Configurés pour la session -**Cookies** : Configurés pour la session
-**TLS** : `required` (production) -**TLS** : `required` (production)
### 3. Communication avec le backend ### 3. Communication avec le backend
-**URL Backend** : `http://localhost:8080` -**URL Backend** : `http://localhost:8080`
-**Endpoints API** : `/api/v1/*` -**Endpoints API** : `/api/v1/*`
-**REST Client** : `BtpXpressApiClient` configuré -**REST Client** : `BtpXpressApiClient` configuré
-**Service** : `ChantierService` créé pour encapsuler les appels API -**Service** : `ChantierService` créé pour encapsuler les appels API
-**CORS Backend** : `http://localhost:8081` ajouté aux origines autorisées -**CORS Backend** : `http://localhost:8081` ajouté aux origines autorisées
### 4. Configuration serveur backend ### 4. Configuration serveur backend
-**Port** : `8080` -**Port** : `8080`
-**CORS Origins** : `http://localhost:3000,http://localhost:5173,http://localhost:8081` -**CORS Origins** : `http://localhost:3000,http://localhost:5173,http://localhost:8081`
-**JWT Validation** : `https://security.lions.dev/realms/btpxpress/protocol/openid-connect/certs` -**JWT Validation** : `https://security.lions.dev/realms/btpxpress/protocol/openid-connect/certs`
-**Issuer** : `https://security.lions.dev/realms/btpxpress` -**Issuer** : `https://security.lions.dev/realms/btpxpress`
## 📋 Mapping Client ↔ Serveur ## 📋 Mapping Client ↔ Serveur
| Client (PrimeFaces) | Serveur (Quarkus) | Description | | Client (PrimeFaces) | Serveur (Quarkus) | Description |
|---------------------|-------------------|-------------| |---------------------|-------------------|-------------|
| `http://localhost:8081` | `http://localhost:8080` | Communication HTTP | | `http://localhost:8081` | `http://localhost:8080` | Communication HTTP |
| `BtpXpressApiClient` | `@Path("/api/v1/*")` | Interface REST Client | | `BtpXpressApiClient` | `@Path("/api/v1/*")` | Interface REST Client |
| OIDC Client `btpxpress-frontend` | JWT Validation | Authentification | | OIDC Client `btpxpress-frontend` | JWT Validation | Authentification |
| `ChantierService` | `ChantierResource` | Service métier chantiers | | `ChantierService` | `ChantierResource` | Service métier chantiers |
## 🔐 Authentification ## 🔐 Authentification
1. **Client accède à une page protégée** → Redirection vers Keycloak 1. **Client accède à une page protégée** → Redirection vers Keycloak
2. **Keycloak (security.lions.dev)** → Authentification utilisateur 2. **Keycloak (security.lions.dev)** → Authentification utilisateur
3. **Keycloak retourne le token** → Stocké dans la session du client 3. **Keycloak retourne le token** → Stocké dans la session du client
4. **Client fait appel API** → Token JWT envoyé dans header `Authorization` 4. **Client fait appel API** → Token JWT envoyé dans header `Authorization`
5. **Backend valide le token** → Via les certificats Keycloak publics 5. **Backend valide le token** → Via les certificats Keycloak publics
## 🚀 Démarrage ## 🚀 Démarrage
1. **Backend** : 1. **Backend** :
```bash ```bash
cd btpxpress-server cd btpxpress-server
mvn quarkus:dev mvn quarkus:dev
``` ```
→ Accessible sur http://localhost:8080 → Accessible sur http://localhost:8080
2. **Client** : 2. **Client** :
```bash ```bash
cd btpxpress-client cd btpxpress-client
mvn quarkus:dev mvn quarkus:dev
``` ```
→ Accessible sur http://localhost:8081 → Accessible sur http://localhost:8081
3. **Accès** : 3. **Accès** :
- Page d'accueil : http://localhost:8081/ - Page d'accueil : http://localhost:8081/
- Dashboard : http://localhost:8081/dashboard.xhtml - Dashboard : http://localhost:8081/dashboard.xhtml
- Login : http://localhost:8081/login.xhtml - Login : http://localhost:8081/login.xhtml
## ⚠️ Points d'attention ## ⚠️ Points d'attention
- Le client doit être configuré avec le **même realm Keycloak** que le serveur (`btpxpress`) - Le client doit être configuré avec le **même realm Keycloak** que le serveur (`btpxpress`)
- Le client ID `btpxpress-frontend` doit exister dans Keycloak - Le client ID `btpxpress-frontend` doit exister dans Keycloak
- Les tokens JWT doivent être envoyés automatiquement via le REST Client - Les tokens JWT doivent être envoyés automatiquement via le REST Client
- Le backend doit accepter les requêtes CORS depuis `http://localhost:8081` - 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 # Dockerfile pour BTP Xpress Client (Frontend) - Développement
# Utilisé pour les builds de développement local # Utilisé pour les builds de développement local
#### ####
## Stage 1 : Build avec Maven ## Stage 1 : Build avec Maven
FROM maven:3.9.6-eclipse-temurin-17 AS build FROM maven:3.9.6-eclipse-temurin-17 AS build
WORKDIR /build WORKDIR /build
# Copier pom.xml et télécharger les dépendances # Copier pom.xml et télécharger les dépendances
COPY pom.xml . COPY pom.xml .
RUN mvn dependency:go-offline -B RUN mvn dependency:go-offline -B
# Copier le code source # Copier le code source
COPY src ./src COPY src ./src
# Build de l'application (uber-jar pour compatibilité lionsctl) # Build de l'application (uber-jar pour compatibilité lionsctl)
RUN mvn clean package -DskipTests -B -Dquarkus.package.type=uber-jar RUN mvn clean package -DskipTests -B -Dquarkus.package.type=uber-jar
## Stage 2 : Runtime image ## Stage 2 : Runtime image
FROM eclipse-temurin:17-jre-alpine FROM eclipse-temurin:17-jre-alpine
ENV LANGUAGE='fr_FR:fr' ENV LANGUAGE='fr_FR:fr'
# Installer curl pour les health checks # Installer curl pour les health checks
RUN apk add --no-cache curl RUN apk add --no-cache curl
# Créer un utilisateur non-root pour la sécurité # Créer un utilisateur non-root pour la sécurité
RUN addgroup -g 185 -S appuser && adduser -u 185 -S appuser -G appuser RUN addgroup -g 185 -S appuser && adduser -u 185 -S appuser -G appuser
RUN mkdir -p /deployments && chown -R appuser:appuser /deployments RUN mkdir -p /deployments && chown -R appuser:appuser /deployments
# Copier le JAR depuis le build (lionsctl utilise uber-jar) # Copier le JAR depuis le build (lionsctl utilise uber-jar)
# Note: Le fichier sera btpxpress-client-1.0.0-runner.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 COPY --from=build --chown=appuser:appuser /build/target/*-runner.jar /deployments/app.jar
EXPOSE 8080 EXPOSE 8080
USER appuser USER appuser
# Variables d'environnement JVM optimisées # 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" 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 # Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/q/health/ready || exit 1 CMD curl -f http://localhost:8080/q/health/ready || exit 1
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /deployments/app.jar"] 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) # Dockerfile de production pour BTP Xpress Client (Frontend)
# Multi-stage build optimisé avec sécurité renforcée # Multi-stage build optimisé avec sécurité renforcée
#### ####
## Stage 1 : Build avec Maven ## Stage 1 : Build avec Maven
FROM maven:3.9.6-eclipse-temurin-17 AS builder FROM maven:3.9.6-eclipse-temurin-17 AS builder
WORKDIR /app WORKDIR /app
# Copier pom.xml et télécharger les dépendances (cache Docker) # Copier pom.xml et télécharger les dépendances (cache Docker)
COPY pom.xml . COPY pom.xml .
RUN mvn dependency:go-offline -B RUN mvn dependency:go-offline -B
# Copier le code source # Copier le code source
COPY src ./src COPY src ./src
# Build de l'application avec profil production (fast-jar par défaut) # Build de l'application avec profil production (fast-jar par défaut)
RUN mvn clean package -DskipTests -B RUN mvn clean package -DskipTests -B
## Stage 2 : Image de production optimisée et sécurisée ## Stage 2 : Image de production optimisée et sécurisée
FROM registry.access.redhat.com/ubi8/openjdk-17:1.18 FROM registry.access.redhat.com/ubi8/openjdk-17:1.18
ENV LANGUAGE='fr_FR:fr' ENV LANGUAGE='fr_FR:fr'
# Variables d'environnement de production # Variables d'environnement de production
# Ces valeurs peuvent être surchargées via docker-compose ou Kubernetes # Ces valeurs peuvent être surchargées via docker-compose ou Kubernetes
ENV QUARKUS_PROFILE=prod ENV QUARKUS_PROFILE=prod
ENV QUARKUS_HTTP_PORT=8080 ENV QUARKUS_HTTP_PORT=8080
ENV QUARKUS_HTTP_HOST=0.0.0.0 ENV QUARKUS_HTTP_HOST=0.0.0.0
# Configuration Keycloak/OIDC (production) # Configuration Keycloak/OIDC (production)
ENV QUARKUS_OIDC_AUTH_SERVER_URL=https://security.lions.dev/realms/btpxpress ENV QUARKUS_OIDC_AUTH_SERVER_URL=https://security.lions.dev/realms/btpxpress
ENV QUARKUS_OIDC_CLIENT_ID=btpxpress-frontend ENV QUARKUS_OIDC_CLIENT_ID=btpxpress-frontend
ENV QUARKUS_OIDC_ENABLED=true ENV QUARKUS_OIDC_ENABLED=true
ENV QUARKUS_OIDC_TLS_VERIFICATION=required ENV QUARKUS_OIDC_TLS_VERIFICATION=required
# Configuration API Backend # Configuration API Backend
ENV BTPXPRESS_API_BASE_URL=https://api.btpxpress.lions.dev ENV BTPXPRESS_API_BASE_URL=https://api.btpxpress.lions.dev
# Configuration CORS # Configuration CORS
ENV QUARKUS_HTTP_CORS_ORIGINS=https://btpxpress.lions.dev,https://www.btpxpress.lions.dev ENV QUARKUS_HTTP_CORS_ORIGINS=https://btpxpress.lions.dev,https://www.btpxpress.lions.dev
ENV QUARKUS_HTTP_CORS_ALLOW_CREDENTIALS=true ENV QUARKUS_HTTP_CORS_ALLOW_CREDENTIALS=true
# Installer curl pour les health checks # Installer curl pour les health checks
USER root USER root
RUN microdnf install -y curl && \ RUN microdnf install -y curl && \
microdnf clean all && \ microdnf clean all && \
rm -rf /var/cache/yum rm -rf /var/cache/yum
# Créer les répertoires et permissions pour utilisateur non-root # Créer les répertoires et permissions pour utilisateur non-root
RUN mkdir -p /deployments /app/logs && \ RUN mkdir -p /deployments /app/logs && \
chown -R 185:185 /deployments /app/logs chown -R 185:185 /deployments /app/logs
# Passer à l'utilisateur non-root pour la sécurité # Passer à l'utilisateur non-root pour la sécurité
USER 185 USER 185
# Copier l'application depuis le builder (format fast-jar Quarkus) # Copier l'application depuis le builder (format fast-jar Quarkus)
COPY --from=builder --chown=185 /app/target/quarkus-app/ /deployments/ COPY --from=builder --chown=185 /app/target/quarkus-app/ /deployments/
# Exposer le port # Exposer le port
EXPOSE 8080 EXPOSE 8080
# Variables JVM optimisées pour production avec sécurité # Variables JVM optimisées pour production avec sécurité
ENV JAVA_OPTS="-Xmx768m -Xms256m \ ENV JAVA_OPTS="-Xmx768m -Xms256m \
-XX:+UseG1GC \ -XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \ -XX:MaxGCPauseMillis=200 \
-XX:+UseStringDeduplication \ -XX:+UseStringDeduplication \
-XX:+ParallelRefProcEnabled \ -XX:+ParallelRefProcEnabled \
-XX:+HeapDumpOnOutOfMemoryError \ -XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/app/logs/heapdump.hprof \ -XX:HeapDumpPath=/app/logs/heapdump.hprof \
-Djava.security.egd=file:/dev/./urandom \ -Djava.security.egd=file:/dev/./urandom \
-Djava.awt.headless=true \ -Djava.awt.headless=true \
-Dfile.encoding=UTF-8 \ -Dfile.encoding=UTF-8 \
-Djava.util.logging.manager=org.jboss.logmanager.LogManager \ -Djava.util.logging.manager=org.jboss.logmanager.LogManager \
-Dquarkus.profile=${QUARKUS_PROFILE}" -Dquarkus.profile=${QUARKUS_PROFILE}"
# Health check avec endpoints Quarkus # Health check avec endpoints Quarkus
HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \
CMD curl -f http://localhost:8080/q/health/ready || exit 1 CMD curl -f http://localhost:8080/q/health/ready || exit 1
# Point d'entrée avec profil production (format fast-jar) # Point d'entrée avec profil production (format fast-jar)
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /deployments/quarkus-run.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" # Solution pour l'erreur HTTP 431 "Request Header Fields Too Large"
## Problème ## 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. 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 ## Solutions appliquées
### 1. Configuration Quarkus HTTP ### 1. Configuration Quarkus HTTP
```properties ```properties
quarkus.http.max-headers-size=64K quarkus.http.max-headers-size=64K
quarkus.vertx.max-headers-size=64K quarkus.vertx.max-headers-size=64K
``` ```
### 2. Optimisation OIDC Token Management ### 2. Optimisation OIDC Token Management
```properties ```properties
quarkus.oidc.token-state-manager.split-tokens=true quarkus.oidc.token-state-manager.split-tokens=true
quarkus.oidc.token-state-manager.strategy=id-refresh-tokens quarkus.oidc.token-state-manager.strategy=id-refresh-tokens
quarkus.oidc.token-state-manager.encryption-required=false quarkus.oidc.token-state-manager.encryption-required=false
quarkus.oidc.token-state-manager.cookie-max-size=8192 quarkus.oidc.token-state-manager.cookie-max-size=8192
``` ```
Ces configurations : Ces configurations :
- **split-tokens** : Divise les tokens en plusieurs cookies pour éviter qu'un seul cookie soit trop volumineux - **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 - **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) - **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 - **cookie-max-size=8192** : Limite la taille d'un cookie individuel à 8KB
## Actions à effectuer ## Actions à effectuer
### ⚠️ IMPORTANT : Supprimer les cookies du navigateur ### ⚠️ IMPORTANT : Supprimer les cookies du navigateur
Les cookies existants peuvent être trop volumineux. Vous devez : Les cookies existants peuvent être trop volumineux. Vous devez :
1. **Ouvrir les outils développeur** (F12) 1. **Ouvrir les outils développeur** (F12)
2. **Onglet Application > Cookies** 2. **Onglet Application > Cookies**
3. **Supprimer tous les cookies** pour `http://localhost:8081` 3. **Supprimer tous les cookies** pour `http://localhost:8081`
4. **Redémarrer l'application Quarkus** 4. **Redémarrer l'application Quarkus**
5. **Recharger la page** 5. **Recharger la page**
Ou via la console du navigateur : Ou via la console du navigateur :
```javascript ```javascript
// Supprimer tous les cookies pour localhost:8081 // Supprimer tous les cookies pour localhost:8081
document.cookie.split(";").forEach(c => { document.cookie.split(";").forEach(c => {
document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/"); document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/");
}); });
``` ```
### Redémarrer l'application ### Redémarrer l'application
Après modification de `application.properties`, vous **devez redémarrer** l'application Quarkus : Après modification de `application.properties`, vous **devez redémarrer** l'application Quarkus :
```bash ```bash
# Arrêter l'application (Ctrl+C) # Arrêter l'application (Ctrl+C)
# Puis relancer # Puis relancer
mvn quarkus:dev mvn quarkus:dev
``` ```
## Vérification ## Vérification
Une fois les cookies supprimés et l'application redémarrée : Une fois les cookies supprimés et l'application redémarrée :
1. Accédez à http://localhost:8081/dashboard.xhtml 1. Accédez à http://localhost:8081/dashboard.xhtml
2. Vous serez redirigé vers Keycloak pour l'authentification 2. Vous serez redirigé vers Keycloak pour l'authentification
3. Après authentification, les nouveaux cookies (optimisés) seront créés 3. Après authentification, les nouveaux cookies (optimisés) seront créés
## Si le problème persiste ## Si le problème persiste
1. **Augmenter encore la limite** : 1. **Augmenter encore la limite** :
```properties ```properties
quarkus.http.max-headers-size=128K quarkus.http.max-headers-size=128K
quarkus.vertx.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 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 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 # 🔒 Sécurisation Complète de l'Application Frontend BTP Xpress
**Date** : 2025-01-20 **Date** : 2025-01-20
**Version** : 1.0.0 **Version** : 1.0.0
**Statut** : ✅ **SÉCURISÉ POUR PRODUCTION** **Statut** : ✅ **SÉCURISÉ POUR PRODUCTION**
--- ---
## 📋 Vue d'ensemble ## 📋 Vue d'ensemble
L'application frontend BTP Xpress est maintenant complètement sécurisée pour la production avec : L'application frontend BTP Xpress est maintenant complètement sécurisée pour la production avec :
- ✅ Headers de sécurité HTTP complets - ✅ Headers de sécurité HTTP complets
- ✅ Configuration OIDC/Keycloak sécurisée - ✅ Configuration OIDC/Keycloak sécurisée
- ✅ CORS restreint aux domaines autorisés - ✅ CORS restreint aux domaines autorisés
- ✅ HTTPS/TLS forcé via Ingress - ✅ HTTPS/TLS forcé via Ingress
- ✅ Cookies sécurisés (HttpOnly, Secure, SameSite) - ✅ Cookies sécurisés (HttpOnly, Secure, SameSite)
- ✅ Content Security Policy (CSP) stricte - ✅ Content Security Policy (CSP) stricte
- ✅ Protection contre les attaques courantes - ✅ Protection contre les attaques courantes
--- ---
## 🔐 1. Headers de Sécurité HTTP ## 🔐 1. Headers de Sécurité HTTP
### Filtre de Sécurité (`SecurityHeadersFilter`) ### Filtre de Sécurité (`SecurityHeadersFilter`)
Le filtre `SecurityHeadersFilter` ajoute automatiquement les headers suivants à toutes les réponses : Le filtre `SecurityHeadersFilter` ajoute automatiquement les headers suivants à toutes les réponses :
| Header | Valeur | Protection | | Header | Valeur | Protection |
|--------|--------|------------| |--------|--------|------------|
| **Strict-Transport-Security** | `max-age=31536000; includeSubDomains; preload` | Force HTTPS pendant 1 an | | **Strict-Transport-Security** | `max-age=31536000; includeSubDomains; preload` | Force HTTPS pendant 1 an |
| **X-Frame-Options** | `DENY` | Empêche le clickjacking | | **X-Frame-Options** | `DENY` | Empêche le clickjacking |
| **X-Content-Type-Options** | `nosniff` | Empêche le MIME sniffing | | **X-Content-Type-Options** | `nosniff` | Empêche le MIME sniffing |
| **X-XSS-Protection** | `1; mode=block` | Active la protection XSS du navigateur | | **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 | | **Referrer-Policy** | `strict-origin-when-cross-origin` | Contrôle les informations de referrer |
| **Content-Security-Policy** | Voir ci-dessous | Politique de sécurité stricte | | **Content-Security-Policy** | Voir ci-dessous | Politique de sécurité stricte |
| **Permissions-Policy** | Désactive geolocation, microphone, etc. | Limite les fonctionnalités du navigateur | | **Permissions-Policy** | Désactive geolocation, microphone, etc. | Limite les fonctionnalités du navigateur |
| **X-Permitted-Cross-Domain-Policies** | `none` | Bloque les politiques Flash/Silverlight | | **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-Embedder-Policy** | `require-corp` | Protection contre les fuites de données |
| **Cross-Origin-Opener-Policy** | `same-origin` | Isolation des fenêtres | | **Cross-Origin-Opener-Policy** | `same-origin` | Isolation des fenêtres |
| **Cross-Origin-Resource-Policy** | `same-origin` | Contrôle des ressources cross-origin | | **Cross-Origin-Resource-Policy** | `same-origin` | Contrôle des ressources cross-origin |
### Content Security Policy (CSP) ### Content Security Policy (CSP)
```http ```http
default-src 'self'; default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://security.lions.dev; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://security.lions.dev;
style-src 'self' 'unsafe-inline' https://security.lions.dev; style-src 'self' 'unsafe-inline' https://security.lions.dev;
img-src 'self' data: https: blob:; img-src 'self' data: https: blob:;
font-src 'self' data: https://security.lions.dev; font-src 'self' data: https://security.lions.dev;
connect-src 'self' https://security.lions.dev https://api.btpxpress.lions.dev https://api.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; frame-src 'self' https://security.lions.dev;
object-src 'none'; object-src 'none';
base-uri 'self'; base-uri 'self';
form-action 'self' https://security.lions.dev; form-action 'self' https://security.lions.dev;
frame-ancestors 'none'; frame-ancestors 'none';
upgrade-insecure-requests; upgrade-insecure-requests;
``` ```
**Explication** : **Explication** :
- Autorise uniquement les ressources depuis `self` et `security.lions.dev` - Autorise uniquement les ressources depuis `self` et `security.lions.dev`
- Bloque les iframes externes (sauf Keycloak) - Bloque les iframes externes (sauf Keycloak)
- Force l'upgrade vers HTTPS - Force l'upgrade vers HTTPS
- Empêche l'injection de code malveillant - Empêche l'injection de code malveillant
--- ---
## 🌐 2. Configuration OIDC / Keycloak ## 🌐 2. Configuration OIDC / Keycloak
### Serveur d'authentification ### Serveur d'authentification
- **URL** : `https://security.lions.dev/realms/btpxpress` - **URL** : `https://security.lions.dev/realms/btpxpress`
- **Client ID** : `btpxpress-frontend` - **Client ID** : `btpxpress-frontend`
- **Type** : `web-app` (application publique) - **Type** : `web-app` (application publique)
- **TLS Verification** : `required` (obligatoire en production) - **TLS Verification** : `required` (obligatoire en production)
### Cookies de session sécurisés ### Cookies de session sécurisés
-**HttpOnly** : `true` (protection XSS) -**HttpOnly** : `true` (protection XSS)
-**Secure** : `true` (HTTPS uniquement) -**Secure** : `true` (HTTPS uniquement)
-**SameSite** : `strict` (protection CSRF) -**SameSite** : `strict` (protection CSRF)
-**Path** : `/` -**Path** : `/`
-**Encryption** : `required` (tokens chiffrés) -**Encryption** : `required` (tokens chiffrés)
-**Max Size** : `8192 bytes` -**Max Size** : `8192 bytes`
### Gestion des tokens ### Gestion des tokens
-**Split Tokens** : Activé (tokens divisés) -**Split Tokens** : Activé (tokens divisés)
-**Strategy** : `id-refresh-tokens` (refresh automatique) -**Strategy** : `id-refresh-tokens` (refresh automatique)
-**Session Age Extension** : `PT30M` (30 minutes) -**Session Age Extension** : `PT30M` (30 minutes)
-**Restore Path After Redirect** : `true` (navigation fluide) -**Restore Path After Redirect** : `true` (navigation fluide)
--- ---
## 🔒 3. Configuration CORS ## 🔒 3. Configuration CORS
### Origines autorisées (Production) ### Origines autorisées (Production)
```properties ```properties
quarkus.http.cors.origins=https://btpxpress.lions.dev,https://www.btpxpress.lions.dev quarkus.http.cors.origins=https://btpxpress.lions.dev,https://www.btpxpress.lions.dev
``` ```
### Méthodes HTTP autorisées ### Méthodes HTTP autorisées
- `GET`, `POST`, `PUT`, `DELETE`, `OPTIONS`, `PATCH` - `GET`, `POST`, `PUT`, `DELETE`, `OPTIONS`, `PATCH`
### Headers autorisés ### Headers autorisés
- `Content-Type` - `Content-Type`
- `Authorization` - `Authorization`
- `X-Requested-With` - `X-Requested-With`
- `X-CSRF-Token` - `X-CSRF-Token`
### Credentials ### Credentials
-**Access-Control-Allow-Credentials** : `true` -**Access-Control-Allow-Credentials** : `true`
-**Max Age** : `3600 seconds` (1 heure) -**Max Age** : `3600 seconds` (1 heure)
--- ---
## 🛡️ 4. Configuration Ingress Kubernetes ## 🛡️ 4. Configuration Ingress Kubernetes
### TLS/HTTPS ### TLS/HTTPS
-**SSL Redirect** : Forcé -**SSL Redirect** : Forcé
-**Force SSL Redirect** : Activé -**Force SSL Redirect** : Activé
-**Cert Manager** : Let's Encrypt (automatique) -**Cert Manager** : Let's Encrypt (automatique)
-**TLS Protocols** : `TLSv1.2`, `TLSv1.3` -**TLS Protocols** : `TLSv1.2`, `TLSv1.3`
### Headers ajoutés par Nginx Ingress ### Headers ajoutés par Nginx Ingress
Les headers suivants sont ajoutés au niveau de l'Ingress : Les headers suivants sont ajoutés au niveau de l'Ingress :
- `X-Frame-Options: DENY` - `X-Frame-Options: DENY`
- `X-Content-Type-Options: nosniff` - `X-Content-Type-Options: nosniff`
- `X-XSS-Protection: 1; mode=block` - `X-XSS-Protection: 1; mode=block`
- `Referrer-Policy: strict-origin-when-cross-origin` - `Referrer-Policy: strict-origin-when-cross-origin`
- `Permissions-Policy: geolocation=(), microphone=(), camera=()` - `Permissions-Policy: geolocation=(), microphone=(), camera=()`
- `X-Permitted-Cross-Domain-Policies: none` - `X-Permitted-Cross-Domain-Policies: none`
### Configuration HSTS ### 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`). 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 ## 🔐 5. Permissions et Accès
### Pages publiques (sans authentification) ### Pages publiques (sans authentification)
Les ressources statiques sont accessibles sans authentification : Les ressources statiques sont accessibles sans authentification :
- `/*.css`, `/*.js`, `/*.png`, `/*.jpg`, etc. - `/*.css`, `/*.js`, `/*.png`, `/*.jpg`, etc.
- `/resources/*` - `/resources/*`
### Pages protégées (authentification requise) ### Pages protégées (authentification requise)
Toutes les autres pages nécessitent une authentification OIDC : Toutes les autres pages nécessitent une authentification OIDC :
- ✅ Redirection automatique vers Keycloak si non authentifié - ✅ Redirection automatique vers Keycloak si non authentifié
- ✅ Restauration du chemin après authentification - ✅ Restauration du chemin après authentification
- ✅ Session maintenue pendant 30 minutes - ✅ Session maintenue pendant 30 minutes
--- ---
## 🔧 6. Configuration de Production ## 🔧 6. Configuration de Production
### Fichier de configuration ### Fichier de configuration
**Fichier** : `src/main/resources/application-prod.properties` **Fichier** : `src/main/resources/application-prod.properties`
### Variables d'environnement requises ### Variables d'environnement requises
- `BTPXPRESS_API_BASE_URL` : URL de l'API backend (défaut: `https://api.btpxpress.lions.dev`) - `BTPXPRESS_API_BASE_URL` : URL de l'API backend (défaut: `https://api.btpxpress.lions.dev`)
### Activation en production ### Activation en production
Pour activer la configuration de production : Pour activer la configuration de production :
```bash ```bash
export QUARKUS_PROFILE=prod export QUARKUS_PROFILE=prod
# ou # ou
java -Dquarkus.profile=prod -jar btpxpress-client.jar java -Dquarkus.profile=prod -jar btpxpress-client.jar
``` ```
--- ---
## ✅ 7. Checklist de Sécurisation ## ✅ 7. Checklist de Sécurisation
### Headers de sécurité ### Headers de sécurité
- [x] Strict-Transport-Security (HSTS) - [x] Strict-Transport-Security (HSTS)
- [x] X-Frame-Options - [x] X-Frame-Options
- [x] X-Content-Type-Options - [x] X-Content-Type-Options
- [x] X-XSS-Protection - [x] X-XSS-Protection
- [x] Referrer-Policy - [x] Referrer-Policy
- [x] Content-Security-Policy (CSP) - [x] Content-Security-Policy (CSP)
- [x] Permissions-Policy - [x] Permissions-Policy
- [x] Cross-Origin-Embedder-Policy - [x] Cross-Origin-Embedder-Policy
- [x] Cross-Origin-Opener-Policy - [x] Cross-Origin-Opener-Policy
- [x] Cross-Origin-Resource-Policy - [x] Cross-Origin-Resource-Policy
### Authentification ### Authentification
- [x] OIDC/Keycloak configuré - [x] OIDC/Keycloak configuré
- [x] TLS verification requis - [x] TLS verification requis
- [x] Cookies sécurisés (HttpOnly, Secure, SameSite) - [x] Cookies sécurisés (HttpOnly, Secure, SameSite)
- [x] Tokens chiffrés - [x] Tokens chiffrés
- [x] Refresh tokens automatique - [x] Refresh tokens automatique
### CORS ### CORS
- [x] Origines restreintes à `btpxpress.lions.dev` - [x] Origines restreintes à `btpxpress.lions.dev`
- [x] Credentials autorisés - [x] Credentials autorisés
- [x] Méthodes HTTP limitées - [x] Méthodes HTTP limitées
### Infrastructure ### Infrastructure
- [x] HTTPS forcé via Ingress - [x] HTTPS forcé via Ingress
- [x] Certificats TLS automatiques (Let's Encrypt) - [x] Certificats TLS automatiques (Let's Encrypt)
- [x] TLS 1.2+ uniquement - [x] TLS 1.2+ uniquement
- [x] Headers sécurité au niveau Ingress - [x] Headers sécurité au niveau Ingress
### Application ### Application
- [x] Filtre de sécurité activé - [x] Filtre de sécurité activé
- [x] Configuration production séparée - [x] Configuration production séparée
- [x] Logs sécurisés (pas de secrets) - [x] Logs sécurisés (pas de secrets)
- [x] Limites HTTP configurées - [x] Limites HTTP configurées
--- ---
## 🚀 8. Déploiement ## 🚀 8. Déploiement
### Prérequis ### Prérequis
1. ✅ Certificat TLS configuré pour `btpxpress.lions.dev` 1. ✅ Certificat TLS configuré pour `btpxpress.lions.dev`
2. ✅ Keycloak accessible sur `https://security.lions.dev` 2. ✅ Keycloak accessible sur `https://security.lions.dev`
3. ✅ Client OIDC `btpxpress-frontend` configuré dans Keycloak 3. ✅ Client OIDC `btpxpress-frontend` configuré dans Keycloak
4. ✅ Ingress Kubernetes configuré avec annotations de sécurité 4. ✅ Ingress Kubernetes configuré avec annotations de sécurité
### Vérification post-déploiement ### Vérification post-déploiement
#### 1. Vérifier les headers de sécurité #### 1. Vérifier les headers de sécurité
```bash ```bash
curl -I https://btpxpress.lions.dev curl -I https://btpxpress.lions.dev
# Vérifier la présence de : # Vérifier la présence de :
# - Strict-Transport-Security # - Strict-Transport-Security
# - X-Frame-Options # - X-Frame-Options
# - X-Content-Type-Options # - X-Content-Type-Options
# - Content-Security-Policy # - Content-Security-Policy
``` ```
#### 2. Tester l'authentification #### 2. Tester l'authentification
1. Accéder à `https://btpxpress.lions.dev` 1. Accéder à `https://btpxpress.lions.dev`
2. Vérifier la redirection vers Keycloak 2. Vérifier la redirection vers Keycloak
3. S'authentifier 3. S'authentifier
4. Vérifier le retour vers l'application 4. Vérifier le retour vers l'application
#### 3. Vérifier les cookies #### 3. Vérifier les cookies
Dans les DevTools du navigateur : Dans les DevTools du navigateur :
- ✅ Cookies avec `HttpOnly` - ✅ Cookies avec `HttpOnly`
- ✅ Cookies avec `Secure` - ✅ Cookies avec `Secure`
- ✅ Cookies avec `SameSite=Strict` - ✅ Cookies avec `SameSite=Strict`
#### 4. Tester HTTPS #### 4. Tester HTTPS
```bash ```bash
# Vérifier que HTTP redirige vers HTTPS # Vérifier que HTTP redirige vers HTTPS
curl -I http://btpxpress.lions.dev curl -I http://btpxpress.lions.dev
# Attendu : 301 ou 302 vers https:// # Attendu : 301 ou 302 vers https://
``` ```
#### 5. Vérifier CSP #### 5. Vérifier CSP
Dans la console du navigateur : Dans la console du navigateur :
- Aucune violation CSP - Aucune violation CSP
- Ressources chargées uniquement depuis les origines autorisées - Ressources chargées uniquement depuis les origines autorisées
--- ---
## 📊 9. Tests de Sécurité Recommandés ## 📊 9. Tests de Sécurité Recommandés
### Outils en ligne ### Outils en ligne
- [SSL Labs](https://www.ssllabs.com/ssltest/) : Test du certificat TLS - [SSL Labs](https://www.ssllabs.com/ssltest/) : Test du certificat TLS
- [Security Headers](https://securityheaders.com/) : Vérification des headers de sécurité - [Security Headers](https://securityheaders.com/) : Vérification des headers de sécurité
- [Mozilla Observatory](https://observatory.mozilla.org/) : Audit de sécurité complet - [Mozilla Observatory](https://observatory.mozilla.org/) : Audit de sécurité complet
### Commandes locales ### Commandes locales
```bash ```bash
# Test SSL # Test SSL
openssl s_client -connect btpxpress.lions.dev:443 openssl s_client -connect btpxpress.lions.dev:443
# Test headers # Test headers
curl -I https://btpxpress.lions.dev curl -I https://btpxpress.lions.dev
# Test CSP # Test CSP
curl -H "Content-Security-Policy-Report-Only: default-src 'self'" https://btpxpress.lions.dev curl -H "Content-Security-Policy-Report-Only: default-src 'self'" https://btpxpress.lions.dev
``` ```
--- ---
## 🔍 10. Monitoring et Alertes ## 🔍 10. Monitoring et Alertes
### Headers à surveiller ### Headers à surveiller
- Taux de violations CSP (si reporting configuré) - Taux de violations CSP (si reporting configuré)
- Échecs d'authentification OIDC - Échecs d'authentification OIDC
- Erreurs de certificat TLS - Erreurs de certificat TLS
- Redirections HTTP → HTTPS - Redirections HTTP → HTTPS
### Logs à surveiller ### Logs à surveiller
- Erreurs d'authentification OIDC - Erreurs d'authentification OIDC
- Violations de sécurité détectées - Violations de sécurité détectées
- Échecs de validation de token - Échecs de validation de token
- Tentatives d'accès non autorisées - Tentatives d'accès non autorisées
--- ---
## 📝 11. Maintenance ## 📝 11. Maintenance
### Mise à jour des certificats ### Mise à jour des certificats
Les certificats Let's Encrypt sont renouvelés automatiquement par cert-manager. Les certificats Let's Encrypt sont renouvelés automatiquement par cert-manager.
### Mise à jour de la CSP ### Mise à jour de la CSP
Si des violations CSP sont détectées en production, ajuster la CSP dans `SecurityHeadersFilter.java`. Si des violations CSP sont détectées en production, ajuster la CSP dans `SecurityHeadersFilter.java`.
### Mise à jour des dépendances ### Mise à jour des dépendances
Maintenir les dépendances à jour pour corriger les vulnérabilités : Maintenir les dépendances à jour pour corriger les vulnérabilités :
```bash ```bash
mvn versions:display-dependency-updates mvn versions:display-dependency-updates
mvn versions:display-plugin-updates mvn versions:display-plugin-updates
``` ```
--- ---
## 📚 12. Références ## 📚 12. Références
- [OWASP Top 10](https://owasp.org/www-project-top-ten/) - [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [MDN Web Security](https://developer.mozilla.org/en-US/docs/Web/Security) - [MDN Web Security](https://developer.mozilla.org/en-US/docs/Web/Security)
- [Quarkus Security Guide](https://quarkus.io/guides/security) - [Quarkus Security Guide](https://quarkus.io/guides/security)
- [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) - [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
--- ---
## ✅ Conclusion ## ✅ Conclusion
L'application frontend BTP Xpress est maintenant **complètement sécurisée** pour la production avec : L'application frontend BTP Xpress est maintenant **complètement sécurisée** pour la production avec :
-**Headers de sécurité complets** (10+ headers) -**Headers de sécurité complets** (10+ headers)
-**OIDC/Keycloak sécurisé** (TLS, cookies sécurisés) -**OIDC/Keycloak sécurisé** (TLS, cookies sécurisés)
-**CORS restreint** (btpxpress.lions.dev uniquement) -**CORS restreint** (btpxpress.lions.dev uniquement)
-**HTTPS forcé** (TLS 1.2+) -**HTTPS forcé** (TLS 1.2+)
-**CSP stricte** (protection injection) -**CSP stricte** (protection injection)
-**Infrastructure Kubernetes sécurisée** -**Infrastructure Kubernetes sécurisée**
**Statut** : ✅ **PRÊT POUR PRODUCTION** **Statut** : ✅ **PRÊT POUR PRODUCTION**
--- ---
**Auteur** : Équipe BTP Xpress **Auteur** : Équipe BTP Xpress
**Date de dernière mise à jour** : 2025-01-20 **Date de dernière mise à jour** : 2025-01-20
**Version** : 1.0.0 **Version** : 1.0.0

279
pom.xml
View File

@@ -1,153 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <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> <modelVersion>4.0.0</modelVersion>
<groupId>dev.lions</groupId> <groupId>dev.lions</groupId>
<artifactId>btpxpress-client</artifactId> <artifactId>btpxpress-client</artifactId>
<version>1.0.0</version> <version>1.0.0</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>BTP Xpress Client - PrimeFaces Freya</name> <name>BTP Xpress Client - PrimeFaces Freya</name>
<description>Application cliente BTP Xpress basée sur Quarkus et PrimeFaces Freya</description> <description>Application cliente BTP Xpress basée sur Quarkus et PrimeFaces Freya</description>
<properties> <properties>
<compiler-plugin.version>3.13.0</compiler-plugin.version> <compiler-plugin.version>3.13.0</compiler-plugin.version>
<maven.compiler.release>17</maven.compiler.release> <maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id> <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id> <quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>3.15.1</quarkus.platform.version> <quarkus.platform.version>3.27.3</quarkus.platform.version>
<skipTests>false</skipTests> <skipTests>false</skipTests>
<freya.theme.version>5.0.0-jakarta</freya.theme.version> </properties>
</properties>
<repositories>
<repositories> <repository>
<repository> <id>gitea-lionsdev</id>
<id>lions-maven-repo</id> <name>Lions Dev Gitea Maven</name>
<name>Lions Dev Maven Repository</name> <url>https://git.lions.dev/api/packages/lionsdev/maven</url>
<url>https://git.lions.dev/lionsdev/btpxpress-maven-repo/raw/branch/main</url> <releases><enabled>true</enabled></releases>
</repository> <snapshots><enabled>true</enabled></snapshots>
</repositories> </repository>
</repositories>
<dependencyManagement>
<dependencies> <dependencyManagement>
<dependency> <dependencies>
<groupId>${quarkus.platform.group-id}</groupId> <dependency>
<artifactId>${quarkus.platform.artifact-id}</artifactId> <groupId>${quarkus.platform.group-id}</groupId>
<version>${quarkus.platform.version}</version> <artifactId>${quarkus.platform.artifact-id}</artifactId>
<type>pom</type> <version>${quarkus.platform.version}</version>
<scope>import</scope> <type>pom</type>
</dependency> <scope>import</scope>
</dependencies> </dependency>
</dependencyManagement> </dependencies>
<dependencies> </dependencyManagement>
<dependency> <dependencies>
<groupId>io.quarkus</groupId> <dependency>
<artifactId>quarkus-arc</artifactId> <groupId>io.quarkus</groupId>
</dependency> <artifactId>quarkus-arc</artifactId>
<dependency> </dependency>
<groupId>io.quarkiverse.primefaces</groupId>
<artifactId>quarkus-primefaces</artifactId> <!-- ================================================================ -->
<version>3.15.0-RC2</version> <!-- lions-faces-layout : layout Freya + beans OIDC + assets Freya -->
</dependency> <!-- Remplace : primefaces-freya-extension (mort), freya-theme, freya -->
<dependency> <!-- Fournit transitivement : primefaces, freya-theme-jakarta, -->
<groupId>org.primefaces</groupId> <!-- quarkus-primefaces, quarkus-omnifaces, quarkus-oidc -->
<artifactId>freya-theme</artifactId> <!-- ================================================================ -->
<version>${freya.theme.version}</version> <dependency>
</dependency> <groupId>dev.lions</groupId>
<dependency> <artifactId>lions-faces-layout</artifactId>
<groupId>org.primefaces</groupId> <version>1.0.0</version>
<artifactId>freya</artifactId> </dependency>
<version>${freya.theme.version}</version>
</dependency> <dependency>
<dependency> <groupId>org.projectlombok</groupId>
<groupId>jakarta.faces</groupId> <artifactId>lombok</artifactId>
<artifactId>jakarta.faces-api</artifactId> <version>1.18.30</version>
<version>3.0.0</version> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>jakarta.servlet</groupId> <groupId>io.quarkus</groupId>
<artifactId>jakarta.servlet-api</artifactId> <artifactId>quarkus-logging-json</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>jakarta.enterprise</groupId> <groupId>io.quarkus</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId> <artifactId>quarkus-rest-client</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>jakarta.el</groupId> <groupId>io.quarkus</groupId>
<artifactId>jakarta.el-api</artifactId> <artifactId>quarkus-rest-jackson</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>io.quarkus</groupId>
<artifactId>lombok</artifactId> <artifactId>quarkus-smallrye-jwt</artifactId>
<version>1.18.30</version> </dependency>
<scope>provided</scope> <dependency>
</dependency> <groupId>io.quarkus</groupId>
<dependency> <artifactId>quarkus-junit5</artifactId>
<groupId>io.quarkus</groupId> <scope>test</scope>
<artifactId>quarkus-logging-json</artifactId> </dependency>
</dependency> <dependency>
<dependency> <groupId>io.rest-assured</groupId>
<groupId>io.quarkus</groupId> <artifactId>rest-assured</artifactId>
<artifactId>quarkus-rest-client</artifactId> <scope>test</scope>
</dependency> </dependency>
<dependency> </dependencies>
<groupId>io.quarkus</groupId> <build>
<artifactId>quarkus-rest-jackson</artifactId> <plugins>
</dependency> <plugin>
<groupId>${quarkus.platform.group-id}</groupId>
<dependency> <artifactId>quarkus-maven-plugin</artifactId>
<groupId>io.quarkus</groupId> <version>${quarkus.platform.version}</version>
<artifactId>quarkus-oidc</artifactId> <extensions>true</extensions>
</dependency> <executions>
<execution>
<dependency> <goals>
<groupId>io.quarkus</groupId> <goal>build</goal>
<artifactId>quarkus-smallrye-jwt</artifactId> <goal>generate-code</goal>
</dependency> <goal>generate-code-tests</goal>
</goals>
<dependency> </execution>
<groupId>io.quarkus</groupId> </executions>
<artifactId>quarkus-junit5</artifactId> </plugin>
<scope>test</scope> <plugin>
</dependency> <artifactId>maven-compiler-plugin</artifactId>
<dependency> <version>${compiler-plugin.version}</version>
<groupId>io.rest-assured</groupId> <configuration>
<artifactId>rest-assured</artifactId> <parameters>true</parameters>
<scope>test</scope> </configuration>
</dependency> </plugin>
</dependencies> <plugin>
<build> <artifactId>maven-surefire-plugin</artifactId>
<plugins> <version>3.5.0</version>
<plugin> <configuration>
<groupId>${quarkus.platform.group-id}</groupId> <systemPropertyVariables>
<artifactId>quarkus-maven-plugin</artifactId> <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<version>${quarkus.platform.version}</version> </systemPropertyVariables>
<extensions>true</extensions> </configuration>
<executions> </plugin>
<execution> </plugins>
<goals> </build>
<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> </project>

View File

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

View File

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

View File

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

View File

@@ -1,273 +1,336 @@
package dev.lions.btpxpress.service; package dev.lions.btpxpress.service;
import jakarta.ws.rs.*; import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
/** /**
* Interface REST Client pour communiquer avec l'API backend BTP Xpress. * Interface REST Client pour communiquer avec l'API backend BTP Xpress.
* <p> * <p>
* Ce client permet au frontend PrimeFaces de communiquer avec le backend Quarkus * Ce client permet au frontend PrimeFaces de communiquer avec le backend Quarkus
* en utilisant les endpoints REST exposés sur /api/v1/*. L'authentification * en utilisant les endpoints REST exposés sur /api/v1/*. L'authentification
* est gérée automatiquement via les tokens JWT Keycloak. * est gérée automatiquement via les tokens JWT Keycloak.
* </p> * </p>
* *
* @author BTP Xpress Development Team * @author BTP Xpress Development Team
* @version 1.0.0 * @version 1.0.0
* @since 1.0.0 * @since 1.0.0
*/ */
@RegisterRestClient(configKey = "btpxpress.api") @RegisterRestClient(configKey = "btpxpress.api")
@RegisterClientHeaders @RegisterClientHeaders
@Path("/api/v1") @Path("/api/v1")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
public interface BtpXpressApiClient { public interface BtpXpressApiClient {
/** /**
* Récupère la liste des chantiers. * Récupère la liste des chantiers.
* Correspond à {@code ChantierResource.getAllChantiers()} dans le serveur. * Correspond à {@code ChantierResource.getAllChantiers()} dans le serveur.
* *
* @return Réponse HTTP contenant la liste des chantiers. * @return Réponse HTTP contenant la liste des chantiers.
*/ */
@GET @GET
@Path("/chantiers") @Path("/chantiers")
Response getChantiers(); Response getChantiers();
/** /**
* Récupère un chantier par son identifiant. * Récupère un chantier par son identifiant.
* Correspond à {@code ChantierResource.getChantierById()} dans le serveur. * Correspond à {@code ChantierResource.getChantierById()} dans le serveur.
* *
* @param id L'identifiant du chantier. * @param id L'identifiant du chantier.
* @return Réponse HTTP contenant le chantier. * @return Réponse HTTP contenant le chantier.
*/ */
@GET @GET
@Path("/chantiers/{id}") @Path("/chantiers/{id}")
Response getChantier(@PathParam("id") Long id); Response getChantier(@PathParam("id") Long id);
/** /**
* Récupère la liste des clients. * Crée un nouveau chantier.
* Correspond à {@code ClientResource.getAllClients()} dans le serveur. * Correspond à {@code ChantierResource.createChantier()} dans le serveur.
* *
* @return Réponse HTTP contenant la liste des clients. * @param chantierDTO Les données du chantier à créer.
*/ * @return Réponse HTTP contenant le chantier créé.
@GET */
@Path("/clients") @POST
Response getClients(); @Path("/chantiers")
Response createChantier(Object chantierDTO);
/**
* Récupère un client par son identifiant. /**
* Correspond à {@code ClientResource.getClientById()} dans le serveur. * Met à jour un chantier existant.
* * Correspond à {@code ChantierResource.updateChantier()} dans le serveur.
* @param id L'identifiant du client. *
* @return Réponse HTTP contenant le client. * @param id L'identifiant du chantier.
*/ * @param chantierDTO Les nouvelles données du chantier.
@GET * @return Réponse HTTP contenant le chantier mis à jour.
@Path("/clients/{id}") */
Response getClient(@PathParam("id") Long id); @PUT
@Path("/chantiers/{id}")
// === ENDPOINTS DASHBOARD === Response updateChantier(@PathParam("id") String id, Object chantierDTO);
/** /**
* Récupère le dashboard principal avec les métriques globales. * Supprime un chantier.
* Correspond à {@code DashboardResource.getDashboardPrincipal()} dans le serveur. * Correspond à {@code ChantierResource.deleteChantier()} dans le serveur.
* *
* @return Réponse HTTP contenant les métriques du dashboard. * @param id L'identifiant du chantier.
*/ * @param permanent Si true, suppression définitive, sinon suppression logique.
@GET * @return Réponse HTTP (204 No Content en cas de succès).
@Path("/dashboard") */
Response getDashboardPrincipal(); @DELETE
@Path("/chantiers/{id}")
/** Response deleteChantier(@PathParam("id") String id, @QueryParam("permanent") @DefaultValue("false") boolean permanent);
* Récupère le dashboard des chantiers avec métriques détaillées.
* Correspond à {@code DashboardResource.getDashboardChantiers()} dans le serveur. /**
* * Récupère la liste des clients.
* @return Réponse HTTP contenant les métriques des chantiers. * Correspond à {@code ClientResource.getAllClients()} dans le serveur.
*/ *
@GET * @return Réponse HTTP contenant la liste des clients.
@Path("/dashboard/chantiers") */
Response getDashboardChantiers(); @GET
@Path("/clients")
/** Response getClients();
* Récupère les métriques financières.
* Correspond à {@code DashboardResource.getDashboardFinances()} dans le serveur. /**
* * Récupère un client par son identifiant.
* @param periode Période en jours (défaut: 30). * Correspond à {@code ClientResource.getClientById()} dans le serveur.
* @return Réponse HTTP contenant les métriques financières. *
*/ * @param id L'identifiant du client.
@GET * @return Réponse HTTP contenant le client.
@Path("/dashboard/finances") */
Response getDashboardFinances(@QueryParam("periode") @DefaultValue("30") int periode); @GET
@Path("/clients/{id}")
/** Response getClient(@PathParam("id") Long id);
* Récupère les métriques de maintenance.
* Correspond à {@code DashboardResource.getDashboardMaintenance()} dans le serveur. // === ENDPOINTS DASHBOARD ===
*
* @return Réponse HTTP contenant les métriques de maintenance. /**
*/ * Récupère le dashboard principal avec les métriques globales.
@GET * Correspond à {@code DashboardResource.getDashboardPrincipal()} dans le serveur.
@Path("/dashboard/maintenance") *
Response getDashboardMaintenance(); * @return Réponse HTTP contenant les métriques du dashboard.
*/
/** @GET
* Récupère les métriques des ressources (équipes, employés, matériel). @Path("/dashboard")
* Correspond à {@code DashboardResource.getDashboardRessources()} dans le serveur. Response getDashboardPrincipal();
*
* @return Réponse HTTP contenant les métriques des ressources. /**
*/ * Récupère le dashboard des chantiers avec métriques détaillées.
@GET * Correspond à {@code DashboardResource.getDashboardChantiers()} dans le serveur.
@Path("/dashboard/ressources") *
Response getDashboardRessources(); * @return Réponse HTTP contenant les métriques des chantiers.
*/
/** @GET
* Récupère les alertes nécessitant une attention immédiate. @Path("/dashboard/chantiers")
* Correspond à {@code DashboardResource.getAlertes()} dans le serveur. Response getDashboardChantiers();
*
* @return Réponse HTTP contenant les alertes. /**
*/ * Récupère les métriques financières.
@GET * Correspond à {@code DashboardResource.getDashboardFinances()} dans le serveur.
@Path("/dashboard/alertes") *
Response getAlertes(); * @param periode Période en jours (défaut: 30).
* @return Réponse HTTP contenant les métriques financières.
/** */
* Récupère les KPIs principaux. @GET
* Correspond à {@code DashboardResource.getKPI()} dans le serveur. @Path("/dashboard/finances")
* Response getDashboardFinances(@QueryParam("periode") @DefaultValue("30") int periode);
* @param periode Période en jours (défaut: 30).
* @return Réponse HTTP contenant les KPIs. /**
*/ * Récupère les métriques de maintenance.
@GET * Correspond à {@code DashboardResource.getDashboardMaintenance()} dans le serveur.
@Path("/dashboard/kpi") *
Response getKPI(@QueryParam("periode") @DefaultValue("30") int periode); * @return Réponse HTTP contenant les métriques de maintenance.
*/
/** @GET
* Récupère les activités récentes. @Path("/dashboard/maintenance")
* Correspond à {@code DashboardResource.getActivitesRecentes()} dans le serveur. Response getDashboardMaintenance();
*
* @param limit Nombre d'activités à récupérer (défaut: 10). /**
* @return Réponse HTTP contenant les activités récentes. * Récupère les métriques des ressources (équipes, employés, matériel).
*/ * Correspond à {@code DashboardResource.getDashboardRessources()} dans le serveur.
@GET *
@Path("/dashboard/activites-recentes") * @return Réponse HTTP contenant les métriques des ressources.
Response getActivitesRecentes(@QueryParam("limit") @DefaultValue("10") int limit); */
@GET
/** @Path("/dashboard/ressources")
* Récupère le résumé quotidien. Response getDashboardRessources();
* Correspond à {@code DashboardResource.getResumeQuotidien()} dans le serveur.
* /**
* @return Réponse HTTP contenant le résumé quotidien. * Récupère les alertes nécessitant une attention immédiate.
*/ * Correspond à {@code DashboardResource.getAlertes()} dans le serveur.
@GET *
@Path("/dashboard/resume-quotidien") * @return Réponse HTTP contenant les alertes.
Response getResumeQuotidien(); */
@GET
/** @Path("/dashboard/alertes")
* Récupère la liste des devis. Response getAlertes();
* Correspond à {@code DevisResource.getAllDevis()} dans le serveur.
* /**
* @return Réponse HTTP contenant la liste des devis. * Récupère les KPIs principaux.
*/ * Correspond à {@code DashboardResource.getKPI()} dans le serveur.
@GET *
@Path("/devis") * @param periode Période en jours (défaut: 30).
Response getDevis(); * @return Réponse HTTP contenant les KPIs.
*/
/** @GET
* Récupère la liste des factures. @Path("/dashboard/kpi")
* Correspond à {@code FactureResource.getAllFactures()} dans le serveur. Response getKPI(@QueryParam("periode") @DefaultValue("30") int periode);
*
* @return Réponse HTTP contenant la liste des factures. /**
*/ * Récupère les activités récentes.
@GET * Correspond à {@code DashboardResource.getActivitesRecentes()} dans le serveur.
@Path("/factures") *
Response getFactures(); * @param limit Nombre d'activités à récupérer (défaut: 10).
* @return Réponse HTTP contenant les activités récentes.
// === ENDPOINTS EMPLOYÉS === */
@GET
/** @Path("/dashboard/activites-recentes")
* Récupère la liste des employés. Response getActivitesRecentes(@QueryParam("limit") @DefaultValue("10") int limit);
* Correspond à {@code EmployeResource.getAllEmployes()} dans le serveur.
* /**
* @return Réponse HTTP contenant la liste des employés. * Récupère le résumé quotidien.
*/ * Correspond à {@code DashboardResource.getResumeQuotidien()} dans le serveur.
@GET *
@Path("/employes") * @return Réponse HTTP contenant le résumé quotidien.
Response getEmployes(); */
@GET
/** @Path("/dashboard/resume-quotidien")
* Récupère un employé par son identifiant. Response getResumeQuotidien();
*
* @param id L'identifiant de l'employé. /**
* @return Réponse HTTP contenant l'employé. * Récupère la liste des devis.
*/ * Correspond à {@code DevisResource.getAllDevis()} dans le serveur.
@GET *
@Path("/employes/{id}") * @return Réponse HTTP contenant la liste des devis.
Response getEmploye(@PathParam("id") String id); */
@GET
// === ENDPOINTS ÉQUIPES === @Path("/devis")
Response getDevis();
/**
* Récupère la liste des équipes. /**
* Correspond à {@code EquipeResource.getAllEquipes()} dans le serveur. * Récupère la liste des factures.
* * Correspond à {@code FactureResource.getAllFactures()} dans le serveur.
* @return Réponse HTTP contenant la liste des équipes. *
*/ * @return Réponse HTTP contenant la liste des factures.
@GET */
@Path("/equipes") @GET
Response getEquipes(); @Path("/factures")
Response getFactures();
/**
* Récupère une équipe par son identifiant. // === ENDPOINTS EMPLOYÉS ===
*
* @param id L'identifiant de l'équipe. /**
* @return Réponse HTTP contenant l'équipe. * Récupère la liste des employés.
*/ * Correspond à {@code EmployeResource.getAllEmployes()} dans le serveur.
@GET *
@Path("/equipes/{id}") * @return Réponse HTTP contenant la liste des employés.
Response getEquipe(@PathParam("id") String id); */
@GET
// === ENDPOINTS MATÉRIELS === @Path("/employes")
Response getEmployes();
/**
* Récupère la liste des matériels. /**
* Correspond à {@code MaterielResource.getAllMateriels()} dans le serveur. * Récupère un employé par son identifiant.
* *
* @return Réponse HTTP contenant la liste des matériels. * @param id L'identifiant de l'employé.
*/ * @return Réponse HTTP contenant l'employé.
@GET */
@Path("/materiels") @GET
Response getMateriels(); @Path("/employes/{id}")
Response getEmploye(@PathParam("id") String id);
/**
* Récupère un matériel par son identifiant. // === ENDPOINTS ÉQUIPES ===
*
* @param id L'identifiant du matériel. /**
* @return Réponse HTTP contenant le matériel. * Récupère la liste des équipes.
*/ * Correspond à {@code EquipeResource.getAllEquipes()} dans le serveur.
@GET *
@Path("/materiels/{id}") * @return Réponse HTTP contenant la liste des équipes.
Response getMateriel(@PathParam("id") String id); */
@GET
// === ENDPOINTS STOCKS === @Path("/equipes")
Response getEquipes();
/**
* Récupère la liste des stocks. /**
* Correspond à {@code StockResource.getAllStocks()} dans le serveur. * Récupère une équipe par son identifiant.
* *
* @return Réponse HTTP contenant la liste des stocks. * @param id L'identifiant de l'équipe.
*/ * @return Réponse HTTP contenant l'équipe.
@GET */
@Path("/stocks") @GET
Response getStocks(); @Path("/equipes/{id}")
Response getEquipe(@PathParam("id") String id);
/**
* Récupère un stock par son identifiant. // === ENDPOINTS MATÉRIELS ===
*
* @param id L'identifiant du stock. /**
* @return Réponse HTTP contenant le stock. * Récupère la liste des matériels.
*/ * Correspond à {@code MaterielResource.getAllMateriels()} dans le serveur.
@GET *
@Path("/stocks/{id}") * @return Réponse HTTP contenant la liste des matériels.
Response getStock(@PathParam("id") String id); */
} @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; package dev.lions.btpxpress.service;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.inject.RestClient; import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* Service de gestion des chantiers côté client. * Service de gestion des chantiers côté client.
* <p> * <p>
* Ce service encapsule la communication avec l'API backend pour les opérations * 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 * liées aux chantiers. Il utilise le REST Client pour effectuer les appels HTTP
* vers le backend. * vers le backend.
* </p> * </p>
* *
* @author BTP Xpress Development Team * @author BTP Xpress Development Team
* @version 1.0.0 * @version 1.0.0
* @since 1.0.0 * @since 1.0.0
*/ */
@ApplicationScoped @ApplicationScoped
public class ChantierService { public class ChantierService {
private static final Logger LOG = LoggerFactory.getLogger(ChantierService.class); private static final Logger LOG = LoggerFactory.getLogger(ChantierService.class);
@Inject @Inject
@RestClient @RestClient
BtpXpressApiClient apiClient; BtpXpressApiClient apiClient;
/** /**
* Récupère tous les chantiers depuis l'API backend. * Récupère tous les chantiers depuis l'API backend.
* *
* @return Liste des chantiers, ou liste vide en cas d'erreur. * @return Liste des chantiers, ou liste vide en cas d'erreur.
*/ */
public List<Map<String, Object>> getAllChantiers() { public List<Map<String, Object>> getAllChantiers() {
try { try {
LOG.debug("Récupération de la liste des chantiers depuis l'API backend."); LOG.debug("Récupération de la liste des chantiers depuis l'API backend.");
Response response = apiClient.getChantiers(); Response response = apiClient.getChantiers();
if (response.getStatus() == Response.Status.OK.getStatusCode()) { if (response.getStatus() == Response.Status.OK.getStatusCode()) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<Map<String, Object>> chantiers = response.readEntity(List.class); 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); LOG.debug("Chantiers récupérés avec succès : {} élément(s)", chantiers != null ? chantiers.size() : 0);
return chantiers != null ? chantiers : new ArrayList<>(); return chantiers != null ? chantiers : new ArrayList<>();
} else { } else {
LOG.warn("Erreur lors de la récupération des chantiers. Code HTTP : {}", response.getStatus()); LOG.warn("Erreur lors de la récupération des chantiers. Code HTTP : {}", response.getStatus());
return new ArrayList<>(); return new ArrayList<>();
} }
} catch (Exception e) { } catch (Exception e) {
LOG.error("Erreur lors de la communication avec l'API backend pour récupérer les chantiers : {}", e.getMessage(), e); LOG.error("Erreur lors de la communication avec l'API backend pour récupérer les chantiers : {}", e.getMessage(), e);
return new ArrayList<>(); return new ArrayList<>();
} }
} }
/** /**
* Récupère un chantier par son identifiant depuis l'API backend. * Récupère un chantier par son identifiant depuis l'API backend.
* *
* @param id L'identifiant du chantier. * @param id L'identifiant du chantier.
* @return Le chantier sous forme de Map, ou null en cas d'erreur. * @return Le chantier sous forme de Map, ou null en cas d'erreur.
*/ */
public Map<String, Object> getChantierById(Long id) { public Map<String, Object> getChantierById(Long id) {
try { try {
LOG.debug("Récupération du chantier avec ID : {}", id); LOG.debug("Récupération du chantier avec ID : {}", id);
Response response = apiClient.getChantier(id); Response response = apiClient.getChantier(id);
if (response.getStatus() == Response.Status.OK.getStatusCode()) { if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Map<String, Object> chantier = response.readEntity(Map.class); @SuppressWarnings("unchecked")
LOG.debug("Chantier récupéré avec succès."); Map<String, Object> chantier = response.readEntity(Map.class);
return chantier; LOG.debug("Chantier récupéré avec succès.");
} else { return chantier;
LOG.warn("Erreur lors de la récupération du chantier. Code HTTP : {}", response.getStatus()); } else {
return null; 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); } catch (Exception e) {
return null; 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); LOG.debug("Récupération du client avec ID : {}", id);
Response response = apiClient.getClient(id); Response response = apiClient.getClient(id);
if (response.getStatus() == Response.Status.OK.getStatusCode()) { if (response.getStatus() == Response.Status.OK.getStatusCode()) {
@SuppressWarnings("unchecked")
Map<String, Object> client = response.readEntity(Map.class); Map<String, Object> client = response.readEntity(Map.class);
LOG.debug("Client récupéré avec succès."); LOG.debug("Client récupéré avec succès.");
return client; return client;

View File

@@ -1,311 +1,341 @@
package dev.lions.btpxpress.service; package dev.lions.btpxpress.service;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.inject.RestClient; import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* Service pour récupérer et transformer les données du dashboard depuis l'API backend. * 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 * <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> * les réponses JSON en objets Java utilisables par les vues JSF.</p>
* *
* @author BTP Xpress Team * @author BTP Xpress Team
* @version 1.0 * @version 1.0
*/ */
@ApplicationScoped @ApplicationScoped
public class DashboardService { public class DashboardService {
private static final Logger logger = LoggerFactory.getLogger(DashboardService.class); private static final Logger logger = LoggerFactory.getLogger(DashboardService.class);
@Inject @Inject
@RestClient @RestClient
BtpXpressApiClient apiClient; BtpXpressApiClient apiClient;
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
/** /**
* Récupère les métriques du dashboard principal. * Récupère les métriques du dashboard principal.
* *
* @return JsonNode contenant les métriques ou null en cas d'erreur * @return JsonNode contenant les métriques ou null en cas d'erreur
*/ */
public JsonNode getDashboardPrincipal() { public JsonNode getDashboardPrincipal() {
try { try {
Response response = apiClient.getDashboardPrincipal(); Response response = apiClient.getDashboardPrincipal();
if (response.getStatus() == Response.Status.OK.getStatusCode()) { if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity(); Object entity = response.getEntity();
if (entity == null) { if (entity == null) {
logger.warn("Réponse vide du dashboard principal"); logger.warn("Réponse vide du dashboard principal");
return null; return null;
} }
// REST Client avec Jackson désérialise déjà en Map/Object // REST Client avec Jackson désérialise déjà en Map/Object
return convertToJsonNode(entity); return convertToJsonNode(entity);
} else { } else {
logger.error("Erreur API dashboard principal: status {}", response.getStatus()); logger.error("Erreur API dashboard principal: status {}", response.getStatus());
return null; return null;
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("Erreur lors de la récupération du dashboard principal", e); logger.error("Erreur lors de la récupération du dashboard principal", e);
return null; return null;
} }
} }
/** /**
* Convertit un objet en JsonNode, quel que soit son type (String, Map, Object, etc.). * Convertit un objet en JsonNode, quel que soit son type (String, Map, Object, etc.).
*/ */
private JsonNode convertToJsonNode(Object entity) { private JsonNode convertToJsonNode(Object entity) {
try { try {
if (entity instanceof String) { if (entity instanceof String) {
return objectMapper.readTree((String) entity); return objectMapper.readTree((String) entity);
} else if (entity instanceof Map || entity instanceof List) { } else if (entity instanceof Map || entity instanceof List) {
// Map ou List sont déjà désérialisés par REST Client // Map ou List sont déjà désérialisés par REST Client
return objectMapper.valueToTree(entity); return objectMapper.valueToTree(entity);
} else { } else {
// Pour les autres objets, conversion via ObjectMapper // Pour les autres objets, conversion via ObjectMapper
return objectMapper.valueToTree(entity); return objectMapper.valueToTree(entity);
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("Erreur lors de la conversion en JsonNode", e); logger.error("Erreur lors de la conversion en JsonNode", e);
return null; return null;
} }
} }
/** /**
* Récupère les métriques des chantiers. * Récupère les métriques des chantiers.
* *
* @return JsonNode contenant les métriques des chantiers ou null en cas d'erreur * @return JsonNode contenant les métriques des chantiers ou null en cas d'erreur
*/ */
public JsonNode getDashboardChantiers() { public JsonNode getDashboardChantiers() {
try { try {
Response response = apiClient.getDashboardChantiers(); Response response = apiClient.getDashboardChantiers();
if (response.getStatus() == Response.Status.OK.getStatusCode()) { if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity(); Object entity = response.getEntity();
return convertToJsonNode(entity); return convertToJsonNode(entity);
} else { } else {
logger.error("Erreur API dashboard chantiers: status {}", response.getStatus()); logger.error("Erreur API dashboard chantiers: status {}", response.getStatus());
return null; return null;
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("Erreur lors de la récupération du dashboard chantiers", e); logger.error("Erreur lors de la récupération du dashboard chantiers", e);
return null; return null;
} }
} }
/** /**
* Récupère les métriques financières. * Récupère les métriques financières.
* *
* @param periode Période en jours (défaut: 30) * @param periode Période en jours (défaut: 30)
* @return JsonNode contenant les métriques financières ou null en cas d'erreur * @return JsonNode contenant les métriques financières ou null en cas d'erreur
*/ */
public JsonNode getDashboardFinances(int periode) { public JsonNode getDashboardFinances(int periode) {
try { try {
Response response = apiClient.getDashboardFinances(periode); Response response = apiClient.getDashboardFinances(periode);
if (response.getStatus() == Response.Status.OK.getStatusCode()) { if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity(); Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity); return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else { } else {
logger.error("Erreur API dashboard finances: status {}", response.getStatus()); logger.error("Erreur API dashboard finances: status {}", response.getStatus());
return null; return null;
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("Erreur lors de la récupération du dashboard finances", e); logger.error("Erreur lors de la récupération du dashboard finances", e);
return null; return null;
} }
} }
/** /**
* Récupère les métriques de maintenance. * Récupère les métriques de maintenance.
* *
* @return JsonNode contenant les métriques de maintenance ou null en cas d'erreur * @return JsonNode contenant les métriques de maintenance ou null en cas d'erreur
*/ */
public JsonNode getDashboardMaintenance() { public JsonNode getDashboardMaintenance() {
try { try {
Response response = apiClient.getDashboardMaintenance(); Response response = apiClient.getDashboardMaintenance();
if (response.getStatus() == Response.Status.OK.getStatusCode()) { if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity(); Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity); return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else { } else {
logger.error("Erreur API dashboard maintenance: status {}", response.getStatus()); logger.error("Erreur API dashboard maintenance: status {}", response.getStatus());
return null; return null;
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("Erreur lors de la récupération du dashboard maintenance", e); logger.error("Erreur lors de la récupération du dashboard maintenance", e);
return null; return null;
} }
} }
/** /**
* Récupère les métriques des ressources. * Récupère les métriques des ressources.
* *
* @return JsonNode contenant les métriques des ressources ou null en cas d'erreur * @return JsonNode contenant les métriques des ressources ou null en cas d'erreur
*/ */
public JsonNode getDashboardRessources() { public JsonNode getDashboardRessources() {
try { try {
Response response = apiClient.getDashboardRessources(); Response response = apiClient.getDashboardRessources();
if (response.getStatus() == Response.Status.OK.getStatusCode()) { if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity(); Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity); return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else { } else {
logger.error("Erreur API dashboard ressources: status {}", response.getStatus()); logger.error("Erreur API dashboard ressources: status {}", response.getStatus());
return null; return null;
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("Erreur lors de la récupération du dashboard ressources", e); logger.error("Erreur lors de la récupération du dashboard ressources", e);
return null; return null;
} }
} }
/** /**
* Récupère les alertes. * Récupère les alertes.
* *
* @return JsonNode contenant les alertes ou null en cas d'erreur * @return JsonNode contenant les alertes ou null en cas d'erreur
*/ */
public JsonNode getAlertes() { public JsonNode getAlertes() {
try { try {
Response response = apiClient.getAlertes(); Response response = apiClient.getAlertes();
if (response.getStatus() == Response.Status.OK.getStatusCode()) { if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity(); Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity); return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else { } else {
logger.error("Erreur API alertes: status {}", response.getStatus()); logger.error("Erreur API alertes: status {}", response.getStatus());
return null; return null;
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("Erreur lors de la récupération des alertes", e); logger.error("Erreur lors de la récupération des alertes", e);
return null; return null;
} }
} }
/** /**
* Récupère les KPIs. * Récupère les KPIs.
* *
* @param periode Période en jours (défaut: 30) * @param periode Période en jours (défaut: 30)
* @return JsonNode contenant les KPIs ou null en cas d'erreur * @return JsonNode contenant les KPIs ou null en cas d'erreur
*/ */
public JsonNode getKPI(int periode) { public JsonNode getKPI(int periode) {
try { try {
Response response = apiClient.getKPI(periode); Response response = apiClient.getKPI(periode);
if (response.getStatus() == Response.Status.OK.getStatusCode()) { if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity(); Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity); return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else { } else {
logger.error("Erreur API KPI: status {}", response.getStatus()); logger.error("Erreur API KPI: status {}", response.getStatus());
return null; return null;
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("Erreur lors de la récupération des KPIs", e); logger.error("Erreur lors de la récupération des KPIs", e);
return null; return null;
} }
} }
/** /**
* Récupère les activités récentes. * Récupère les activités récentes.
* *
* @param limit Nombre d'activités à récupérer * @param limit Nombre d'activités à récupérer
* @return JsonNode contenant les activités récentes ou null en cas d'erreur * @return JsonNode contenant les activités récentes ou null en cas d'erreur
*/ */
public JsonNode getActivitesRecentes(int limit) { public JsonNode getActivitesRecentes(int limit) {
try { try {
Response response = apiClient.getActivitesRecentes(limit); Response response = apiClient.getActivitesRecentes(limit);
if (response.getStatus() == Response.Status.OK.getStatusCode()) { if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity(); Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity); return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else { } else {
logger.error("Erreur API activités récentes: status {}", response.getStatus()); logger.error("Erreur API activités récentes: status {}", response.getStatus());
return null; return null;
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("Erreur lors de la récupération des activités récentes", e); logger.error("Erreur lors de la récupération des activités récentes", e);
return null; return null;
} }
} }
/** /**
* Récupère le résumé quotidien. * Récupère le résumé quotidien.
* *
* @return JsonNode contenant le résumé quotidien ou null en cas d'erreur * @return JsonNode contenant le résumé quotidien ou null en cas d'erreur
*/ */
public JsonNode getResumeQuotidien() { public JsonNode getResumeQuotidien() {
try { try {
Response response = apiClient.getResumeQuotidien(); Response response = apiClient.getResumeQuotidien();
if (response.getStatus() == Response.Status.OK.getStatusCode()) { if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Object entity = response.getEntity(); Object entity = response.getEntity();
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity); return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
} else { } else {
logger.error("Erreur API résumé quotidien: status {}", response.getStatus()); logger.error("Erreur API résumé quotidien: status {}", response.getStatus());
return null; return null;
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("Erreur lors de la récupération du résumé quotidien", e); logger.error("Erreur lors de la récupération du résumé quotidien", e);
return null; return null;
} }
} }
/** /**
* Récupère le nombre de clients. * Récupère le nombre de clients.
* *
* @return Nombre de clients ou 0 en cas d'erreur * @return Nombre de clients ou 0 en cas d'erreur
*/ */
public int getNombreClients() { public int getNombreClients() {
try { try {
Response response = apiClient.getClients(); Response response = apiClient.getClients();
if (response.getStatus() == Response.Status.OK.getStatusCode()) { if (response.getStatus() == Response.Status.OK.getStatusCode()) {
List<?> clients = (List<?>) response.getEntity(); List<?> clients = (List<?>) response.getEntity();
return clients != null ? clients.size() : 0; return clients != null ? clients.size() : 0;
} }
return 0; return 0;
} catch (Exception e) { } catch (Exception e) {
logger.error("Erreur lors de la récupération du nombre de clients", e); logger.error("Erreur lors de la récupération du nombre de clients", e);
return 0; return 0;
} }
} }
/** /**
* Récupère le nombre de devis en attente. * 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 *
*/ * @return Nombre de devis en attente ou 0 en cas d'erreur
public int getNombreDevisEnAttente() { */
try { @SuppressWarnings("unchecked")
Response response = apiClient.getDevis(); public int getNombreDevisEnAttente() {
if (response.getStatus() == Response.Status.OK.getStatusCode()) { try {
List<?> devis = (List<?>) response.getEntity(); Response response = apiClient.getDevis();
// TODO: Filtrer par statut EN_ATTENTE si l'API le permet if (response.getStatus() == Response.Status.OK.getStatusCode()) {
return devis != null ? devis.size() : 0; List<Map<String, Object>> devis = response.readEntity(List.class);
} if (devis == null) {
return 0; return 0;
} catch (Exception e) { }
logger.error("Erreur lors de la récupération du nombre de devis", e); // Filtrer par statut EN_ATTENTE côté client
return 0; long count = devis.stream()
} .filter(d -> {
} Object statut = d.get("statut");
return statut != null &&
/** (statut.toString().equalsIgnoreCase("EN_ATTENTE") ||
* Récupère le nombre de factures impayées. statut.toString().equalsIgnoreCase("EN ATTENTE"));
* })
* @return Nombre de factures impayées ou 0 en cas d'erreur .count();
*/ return (int) count;
public int getNombreFacturesImpayees() { }
try { return 0;
Response response = apiClient.getFactures(); } catch (Exception e) {
if (response.getStatus() == Response.Status.OK.getStatusCode()) { logger.error("Erreur lors de la récupération du nombre de devis", e);
List<?> factures = (List<?>) response.getEntity(); return 0;
// TODO: Filtrer par statut IMPAYEE si l'API le permet }
return factures != null ? factures.size() : 0; }
}
return 0; /**
} catch (Exception e) { * Récupère le nombre de factures impayées.
logger.error("Erreur lors de la récupération du nombre de factures", e); * Filtre côté client les factures avec statut IMPAYEE ou EN_RETARD.
return 0; *
} * @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; package dev.lions.btpxpress.view;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage; import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext; import jakarta.faces.context.FacesContext;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.Serializable; import java.io.Serializable;
import java.util.*; import java.util.*;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* Classe de base pour les vues de type liste/CRUD. * Classe de base pour les vues de type liste/CRUD.
* *
* Fonctionnalités: * Fonctionnalités:
* - Chargement et affichage de listes * - Chargement et affichage de listes
* - Filtrage multi-critères * - Filtrage multi-critères
* - Tri (ascendant/descendant) * - Tri (ascendant/descendant)
* - Pagination * - Pagination
* - CRUD complet (Create, Read, Update, Delete) * - CRUD complet (Create, Read, Update, Delete)
* - Sélection simple/multiple * - Sélection simple/multiple
* - Messages utilisateur (succès, erreur, warning) * - Messages utilisateur (succès, erreur, warning)
* - Lazy loading pour grandes listes * - Lazy loading pour grandes listes
* *
* Principe DRY: Toute la logique commune des écrans de liste est centralisée ici. * Principe DRY: Toute la logique commune des écrans de liste est centralisée ici.
* *
* @param <T> Type d'entité * @param <T> Type d'entité
* @param <ID> Type de l'identifiant * @param <ID> Type de l'identifiant
*/ */
@Getter @Getter
@Setter @Setter
public abstract class BaseListView<T, ID> implements Serializable { public abstract class BaseListView<T, ID> implements Serializable {
protected static final Logger LOG = LoggerFactory.getLogger(BaseListView.class); protected static final Logger LOG = LoggerFactory.getLogger(BaseListView.class);
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
// ========== Données ========== // ========== Données ==========
protected List<T> items = new ArrayList<>(); protected List<T> items = new ArrayList<>();
protected List<T> filteredItems = new ArrayList<>(); protected List<T> filteredItems = new ArrayList<>();
protected T selectedItem; protected T selectedItem;
protected List<T> selectedItems = new ArrayList<>(); protected List<T> selectedItems = new ArrayList<>();
protected T entity; // Pour les formulaires create/edit protected T entity; // Pour les formulaires create/edit
// ========== États ========== // ========== États ==========
protected boolean loading = false; protected boolean loading = false;
protected boolean editing = false; // Mode édition vs création protected boolean editing = false; // Mode édition vs création
protected String globalFilter; // Recherche globale protected String globalFilter; // Recherche globale
// ========== Pagination ========== // ========== Pagination ==========
protected int first = 0; // Index de départ protected int first = 0; // Index de départ
protected int pageSize = 10; // Taille de page protected int pageSize = 10; // Taille de page
protected int totalRecords = 0; // Nombre total d'enregistrements protected int totalRecords = 0; // Nombre total d'enregistrements
// ========== Tri ========== // ========== Tri ==========
protected String sortField; // Champ de tri protected String sortField; // Champ de tri
protected boolean sortAscending = true; // Ordre de tri protected boolean sortAscending = true; // Ordre de tri
// ========== Sélection ========== // ========== Sélection ==========
protected String selectionMode = "single"; // single, multiple, checkbox protected String selectionMode = "single"; // single, multiple, checkbox
/** /**
* Initialisation du bean au chargement de la page. * Initialisation du bean au chargement de la page.
*/ */
@PostConstruct @PostConstruct
public void init() { public void init() {
LOG.debug("Initialisation de {}", getClass().getSimpleName()); LOG.debug("Initialisation de {}", getClass().getSimpleName());
try { try {
initializeFields(); initializeFields();
loadItems(); loadItems();
} catch (Exception e) { } catch (Exception e) {
LOG.error("Erreur lors de l'initialisation", e); LOG.error("Erreur lors de l'initialisation", e);
addErrorMessage("Erreur lors du chargement des données"); addErrorMessage("Erreur lors du chargement des données");
} }
} }
/** /**
* Initialiser les champs spécifiques de la vue. * Initialiser les champs spécifiques de la vue.
* Override si nécessaire. * Override si nécessaire.
*/ */
protected void initializeFields() { protected void initializeFields() {
// À surcharger dans les classes filles si besoin // À surcharger dans les classes filles si besoin
} }
/** /**
* Charger les items depuis la source de données. * Charger les items depuis la source de données.
* DOIT être implémenté par les classes filles. * DOIT être implémenté par les classes filles.
*/ */
public abstract void loadItems(); public abstract void loadItems();
/** /**
* Recharger les données (alias pour loadItems). * Recharger les données (alias pour loadItems).
*/ */
public void refresh() { public void refresh() {
LOG.debug("Rafraîchissement des données"); LOG.debug("Rafraîchissement des données");
loadItems(); loadItems();
} }
// ========== Filtrage ========== // ========== Filtrage ==========
/** /**
* Appliquer les filtres à la liste d'items. * Appliquer les filtres à la liste d'items.
*/ */
protected void applyFilters(List<T> sourceItems, List<Predicate<T>> filters) { protected void applyFilters(List<T> sourceItems, List<Predicate<T>> filters) {
if (filters == null || filters.isEmpty()) { if (filters == null || filters.isEmpty()) {
filteredItems = new ArrayList<>(sourceItems); filteredItems = new ArrayList<>(sourceItems);
return; return;
} }
filteredItems = sourceItems.stream() filteredItems = sourceItems.stream()
.filter(filters.stream().reduce(Predicate::and).orElse(x -> true)) .filter(filters.stream().reduce(Predicate::and).orElse(x -> true))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
/** /**
* Recherche avec les critères de filtrage actuels. * Recherche avec les critères de filtrage actuels.
*/ */
public void search() { public void search() {
LOG.debug("Recherche lancée pour {}", getClass().getSimpleName()); LOG.debug("Recherche lancée pour {}", getClass().getSimpleName());
first = 0; // Retour à la première page first = 0; // Retour à la première page
loadItems(); loadItems();
} }
/** /**
* Réinitialiser tous les filtres. * Réinitialiser tous les filtres.
*/ */
public void resetFilters() { public void resetFilters() {
LOG.debug("Réinitialisation des filtres pour {}", getClass().getSimpleName()); LOG.debug("Réinitialisation des filtres pour {}", getClass().getSimpleName());
globalFilter = null; globalFilter = null;
sortField = null; sortField = null;
sortAscending = true; sortAscending = true;
first = 0; first = 0;
resetFilterFields(); resetFilterFields();
loadItems(); loadItems();
} }
/** /**
* Réinitialiser les champs de filtre spécifiques. * Réinitialiser les champs de filtre spécifiques.
* DOIT être implémenté par les classes filles. * DOIT être implémenté par les classes filles.
*/ */
protected abstract void resetFilterFields(); protected abstract void resetFilterFields();
// ========== Tri ========== // ========== Tri ==========
/** /**
* Trier la liste par un champ donné. * Trier la liste par un champ donné.
*/ */
public void sort(String field) { public void sort(String field) {
if (field.equals(sortField)) { if (field.equals(sortField)) {
sortAscending = !sortAscending; sortAscending = !sortAscending;
} else { } else {
sortField = field; sortField = field;
sortAscending = true; sortAscending = true;
} }
LOG.debug("Tri par {} ({})", field, sortAscending ? "ASC" : "DESC"); LOG.debug("Tri par {} ({})", field, sortAscending ? "ASC" : "DESC");
loadItems(); loadItems();
} }
// ========== Navigation ========== // ========== Navigation ==========
/** /**
* Naviguer vers la page de détails d'un item. * Naviguer vers la page de détails d'un item.
*/ */
public String viewDetails(ID id) { public String viewDetails(ID id) {
LOG.debug("Redirection vers détails : {}", id); LOG.debug("Redirection vers détails : {}", id);
return getDetailsPath() + "?id=" + id + "&faces-redirect=true"; return getDetailsPath() + "?id=" + id + "&faces-redirect=true";
} }
/** /**
* Naviguer vers la page de détails de l'item sélectionné. * Naviguer vers la page de détails de l'item sélectionné.
*/ */
public String viewSelectedDetails() { public String viewSelectedDetails() {
if (selectedItem == null) { if (selectedItem == null) {
addWarningMessage("Aucun élément sélectionné"); addWarningMessage("Aucun élément sélectionné");
return null; return null;
} }
return viewDetails(getEntityId(selectedItem)); return viewDetails(getEntityId(selectedItem));
} }
/** /**
* Obtenir le chemin de la page de détails. * Obtenir le chemin de la page de détails.
*/ */
protected abstract String getDetailsPath(); protected abstract String getDetailsPath();
/** /**
* Naviguer vers la page de création. * Naviguer vers la page de création.
*/ */
public String createNew() { public String createNew() {
LOG.debug("Redirection vers création"); LOG.debug("Redirection vers création");
return getCreatePath() + "?faces-redirect=true"; return getCreatePath() + "?faces-redirect=true";
} }
/** /**
* Obtenir le chemin de la page de création. * Obtenir le chemin de la page de création.
*/ */
protected abstract String getCreatePath(); protected abstract String getCreatePath();
// ========== CRUD ========== // ========== CRUD ==========
/** /**
* Préparer un nouvel item pour création. * Préparer un nouvel item pour création.
*/ */
public void prepareNew() { public void prepareNew() {
LOG.debug("Préparation nouvelle entité"); LOG.debug("Préparation nouvelle entité");
entity = createNewEntity(); entity = createNewEntity();
editing = false; editing = false;
} }
/** /**
* Créer une nouvelle instance de l'entité. * Créer une nouvelle instance de l'entité.
* DOIT être implémenté par les classes filles. * DOIT être implémenté par les classes filles.
*/ */
protected abstract T createNewEntity(); protected abstract T createNewEntity();
/** /**
* Préparer un item pour édition. * Préparer un item pour édition.
*/ */
public void prepareEdit(T item) { public void prepareEdit(T item) {
LOG.debug("Préparation édition : {}", item); LOG.debug("Préparation édition : {}", item);
entity = item; entity = item;
editing = true; editing = true;
} }
/** /**
* Sauvegarder l'entité (création ou modification). * Sauvegarder l'entité (création ou modification).
*/ */
public void save() { public void save() {
try { try {
loading = true; loading = true;
if (editing) { if (editing) {
performUpdate(); performUpdate();
addSuccessMessage("Modification réussie"); addSuccessMessage("Modification réussie");
} else { } else {
performCreate(); performCreate();
addSuccessMessage("Création réussie"); addSuccessMessage("Création réussie");
} }
loadItems(); loadItems();
entity = null; entity = null;
editing = false; editing = false;
} catch (Exception e) { } catch (Exception e) {
LOG.error("Erreur lors de la sauvegarde", e); LOG.error("Erreur lors de la sauvegarde", e);
addErrorMessage("Erreur lors de la sauvegarde : " + e.getMessage()); addErrorMessage("Erreur lors de la sauvegarde : " + e.getMessage());
} finally { } finally {
loading = false; loading = false;
} }
} }
/** /**
* Créer une nouvelle entité. * Créer une nouvelle entité.
* DOIT être implémenté par les classes filles. * DOIT être implémenté par les classes filles.
*/ */
protected abstract void performCreate(); protected abstract void performCreate();
/** /**
* Mettre à jour une entité existante. * Mettre à jour une entité existante.
* DOIT être implémenté par les classes filles. * DOIT être implémenté par les classes filles.
*/ */
protected abstract void performUpdate(); protected abstract void performUpdate();
/** /**
* Supprimer l'item sélectionné. * Supprimer l'item sélectionné.
*/ */
public void delete() { public void delete() {
if (selectedItem == null) { if (selectedItem == null) {
addWarningMessage("Aucun élément sélectionné"); addWarningMessage("Aucun élément sélectionné");
return; return;
} }
try { try {
loading = true; loading = true;
LOG.info("Suppression : {}", selectedItem); LOG.info("Suppression : {}", selectedItem);
performDelete(); performDelete();
items.remove(selectedItem); items.remove(selectedItem);
selectedItem = null; selectedItem = null;
addSuccessMessage("Suppression réussie"); addSuccessMessage("Suppression réussie");
loadItems(); loadItems();
} catch (Exception e) { } catch (Exception e) {
LOG.error("Erreur lors de la suppression", e); LOG.error("Erreur lors de la suppression", e);
addErrorMessage("Erreur lors de la suppression : " + e.getMessage()); addErrorMessage("Erreur lors de la suppression : " + e.getMessage());
} finally { } finally {
loading = false; loading = false;
} }
} }
/** /**
* Supprimer les items sélectionnés (sélection multiple). * Supprimer les items sélectionnés (sélection multiple).
*/ */
public void deleteSelected() { public void deleteSelected() {
if (selectedItems == null || selectedItems.isEmpty()) { if (selectedItems == null || selectedItems.isEmpty()) {
addWarningMessage("Aucun élément sélectionné"); addWarningMessage("Aucun élément sélectionné");
return; return;
} }
try { try {
loading = true; loading = true;
int count = selectedItems.size(); int count = selectedItems.size();
for (T item : selectedItems) { for (T item : selectedItems) {
selectedItem = item; selectedItem = item;
performDelete(); performDelete();
} }
loadItems(); loadItems();
selectedItems.clear(); selectedItems.clear();
selectedItem = null; selectedItem = null;
addSuccessMessage(count + " élément(s) supprimé(s)"); addSuccessMessage(count + " élément(s) supprimé(s)");
} catch (Exception e) { } catch (Exception e) {
LOG.error("Erreur lors de la suppression multiple", e); LOG.error("Erreur lors de la suppression multiple", e);
addErrorMessage("Erreur lors de la suppression"); addErrorMessage("Erreur lors de la suppression");
} finally { } finally {
loading = false; loading = false;
} }
} }
/** /**
* Effectuer la suppression réelle. * Effectuer la suppression réelle.
* DOIT être implémenté par les classes filles. * DOIT être implémenté par les classes filles.
*/ */
protected abstract void performDelete(); protected abstract void performDelete();
/** /**
* Obtenir l'ID d'une entité. * Obtenir l'ID d'une entité.
* DOIT être implémenté par les classes filles. * DOIT être implémenté par les classes filles.
*/ */
protected abstract ID getEntityId(T entity); protected abstract ID getEntityId(T entity);
// ========== Messages utilisateur ========== // ========== Messages utilisateur ==========
protected void addSuccessMessage(String message) { protected void addSuccessMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null, FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message)); new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message));
} }
protected void addErrorMessage(String message) { protected void addErrorMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null, FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message)); new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
} }
protected void addWarningMessage(String message) { protected void addWarningMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null, FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_WARN, "Attention", message)); new FacesMessage(FacesMessage.SEVERITY_WARN, "Attention", message));
} }
protected void addInfoMessage(String message) { protected void addInfoMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null, FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Information", message)); new FacesMessage(FacesMessage.SEVERITY_INFO, "Information", message));
} }
// ========== Utilitaires ========== // ========== Utilitaires ==========
/** /**
* Vérifier si la liste est vide. * Vérifier si la liste est vide.
*/ */
public boolean isEmpty() { public boolean isEmpty() {
return items == null || items.isEmpty(); return items == null || items.isEmpty();
} }
/** /**
* Obtenir le nombre d'items. * Obtenir le nombre d'items.
*/ */
public int getItemCount() { public int getItemCount() {
return items == null ? 0 : items.size(); return items == null ? 0 : items.size();
} }
/** /**
* Vérifier si un item est sélectionné. * Vérifier si un item est sélectionné.
*/ */
public boolean hasSelection() { public boolean hasSelection() {
return selectedItem != null; return selectedItem != null;
} }
/** /**
* Vérifier si plusieurs items sont sélectionnés. * Vérifier si plusieurs items sont sélectionnés.
*/ */
public boolean hasMultipleSelection() { public boolean hasMultipleSelection() {
return selectedItems != null && !selectedItems.isEmpty(); return selectedItems != null && !selectedItems.isEmpty();
} }
} }

View File

@@ -1,273 +1,397 @@
package dev.lions.btpxpress.view; package dev.lions.btpxpress.view;
import dev.lions.btpxpress.service.ChantierService; import dev.lions.btpxpress.service.ChantierService;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped; import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.inject.Named; import jakarta.inject.Named;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.Serializable; import java.time.LocalDate;
import java.time.LocalDate; import java.time.LocalDateTime;
import java.time.LocalDateTime; import java.util.ArrayList;
import java.util.ArrayList; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Predicate; import java.util.function.Predicate;
@Named("chantiersView") @Named("chantiersView")
@ViewScoped @ViewScoped
@Getter @Getter
@Setter @Setter
public class ChantiersView extends BaseListView<ChantiersView.Chantier, Long> implements Serializable { public class ChantiersView extends BaseListView<ChantiersView.Chantier, Long> {
private static final Logger LOG = LoggerFactory.getLogger(ChantiersView.class); private static final Logger LOG = LoggerFactory.getLogger(ChantiersView.class);
@Inject @Inject
ChantierService chantierService; ChantierService chantierService;
private String filtreNom; private String filtreNom;
private String filtreClient; private String filtreClient;
private String filtreStatut; private String filtreStatut;
private Long chantierId; private Long chantierId;
@PostConstruct @PostConstruct
public void init() { public void init() {
if (filtreStatut == null) { if (filtreStatut == null) {
filtreStatut = "TOUS"; filtreStatut = "TOUS";
} }
loadItems(); loadItems();
} }
/** /**
* Définit le filtre de statut (utilisé depuis les pages filtrées). * Définit le filtre de statut (utilisé depuis les pages filtrées).
*/ */
public void setFiltreStatut(String statut) { public void setFiltreStatut(String statut) {
this.filtreStatut = statut; this.filtreStatut = statut;
} }
@Override @Override
public void loadItems() { public void loadItems() {
loading = true; loading = true;
try { try {
items = new ArrayList<>(); items = new ArrayList<>();
// Récupération depuis l'API backend // Récupération depuis l'API backend
List<Map<String, Object>> chantiersData = chantierService.getAllChantiers(); List<Map<String, Object>> chantiersData = chantierService.getAllChantiers();
for (Map<String, Object> data : chantiersData) { for (Map<String, Object> data : chantiersData) {
Chantier c = new Chantier(); Chantier c = new Chantier();
// Mapping des données de l'API vers l'objet Chantier // Mapping des données de l'API vers l'objet Chantier
c.setId(data.get("id") != null ? Long.valueOf(data.get("id").toString().hashCode()) : null); // Stocker l'UUID original comme String pour les opérations CRUD
c.setNom((String) data.get("nom")); Object idObj = data.get("id");
if (idObj != null) {
// Le client peut être un objet ou une chaîne String idString = idObj.toString();
Object clientObj = data.get("client"); // Stocker l'UUID comme String dans un champ caché, et utiliser hashCode pour l'affichage
if (clientObj instanceof Map) { c.setId(Long.valueOf(idString.hashCode())); // Pour compatibilité avec l'interface existante
Map<String, Object> clientData = (Map<String, Object>) clientObj; c.setUuidOriginal(idString); // Stocker l'UUID original
c.setClient((String) clientData.get("raisonSociale")); }
} else if (clientObj instanceof String) { c.setNom((String) data.get("nom"));
c.setClient((String) clientObj);
} else { // Le client peut être un objet ou une chaîne
c.setClient("N/A"); Object clientObj = data.get("client");
} if (clientObj instanceof Map) {
@SuppressWarnings("unchecked")
c.setAdresse((String) data.get("adresse")); Map<String, Object> clientData = (Map<String, Object>) clientObj;
c.setClient((String) clientData.get("raisonSociale"));
// Conversion des dates // Extraire l'ID du client si disponible
if (data.get("dateDebut") != null) { Object clientIdObj = clientData.get("id");
c.setDateDebut(LocalDate.parse(data.get("dateDebut").toString())); if (clientIdObj != null) {
} c.setClientId(clientIdObj.toString());
if (data.get("dateFinPrevue") != null) { }
c.setDateFinPrevue(LocalDate.parse(data.get("dateFinPrevue").toString())); } else if (clientObj instanceof String) {
} c.setClient((String) clientObj);
} else {
c.setStatut((String) data.get("statut")); c.setClient("N/A");
}
// Avancement en pourcentage
Object avancementObj = data.get("avancement"); // Vérifier aussi si clientId est directement dans les données
if (avancementObj != null) { Object clientIdDirect = data.get("clientId");
c.setAvancement(avancementObj instanceof Integer ? if (clientIdDirect != null && c.getClientId() == null) {
(Integer) avancementObj : c.setClientId(clientIdDirect.toString());
Integer.parseInt(avancementObj.toString())); }
} else {
c.setAvancement(0); c.setAdresse((String) data.get("adresse"));
}
// Conversion des dates
// Budget et coût réel if (data.get("dateDebut") != null) {
Object montantObj = data.get("montant"); c.setDateDebut(LocalDate.parse(data.get("dateDebut").toString()));
if (montantObj != null) { }
c.setBudget(montantObj instanceof Number ? if (data.get("dateFinPrevue") != null) {
((Number) montantObj).doubleValue() : c.setDateFinPrevue(LocalDate.parse(data.get("dateFinPrevue").toString()));
Double.parseDouble(montantObj.toString())); }
} else {
c.setBudget(0.0); c.setStatut((String) data.get("statut"));
}
// Avancement en pourcentage
Object coutReelObj = data.get("coutReel"); Object avancementObj = data.get("avancement");
if (coutReelObj != null) { if (avancementObj != null) {
c.setCoutReel(coutReelObj instanceof Number ? c.setAvancement(avancementObj instanceof Integer ?
((Number) coutReelObj).doubleValue() : (Integer) avancementObj :
Double.parseDouble(coutReelObj.toString())); Integer.parseInt(avancementObj.toString()));
} else { } else {
c.setCoutReel(0.0); c.setAvancement(0);
} }
items.add(c); // Budget et coût réel
} Object montantObj = data.get("montant");
if (montantObj != null) {
LOG.info("Chantiers chargés depuis l'API : {} élément(s)", items.size()); c.setBudget(montantObj instanceof Number ?
applyFilters(items, buildFilters()); ((Number) montantObj).doubleValue() :
} catch (Exception e) { Double.parseDouble(montantObj.toString()));
LOG.error("Erreur chargement chantiers depuis l'API", e); } else {
// En cas d'erreur, on garde une liste vide c.setBudget(0.0);
items = new ArrayList<>(); }
} finally {
loading = false; Object coutReelObj = data.get("coutReel");
} if (coutReelObj != null) {
} c.setCoutReel(coutReelObj instanceof Number ?
((Number) coutReelObj).doubleValue() :
private List<Predicate<Chantier>> buildFilters() { Double.parseDouble(coutReelObj.toString()));
List<Predicate<Chantier>> filters = new ArrayList<>(); } else {
if (filtreNom != null && !filtreNom.trim().isEmpty()) { c.setCoutReel(0.0);
filters.add(c -> c.getNom().toLowerCase().contains(filtreNom.toLowerCase())); }
}
if (filtreClient != null && !filtreClient.trim().isEmpty()) { items.add(c);
filters.add(c -> c.getClient().toLowerCase().contains(filtreClient.toLowerCase())); }
}
if (filtreStatut != null && !filtreStatut.trim().isEmpty() && !"TOUS".equals(filtreStatut)) { LOG.info("Chantiers chargés depuis l'API : {} élément(s)", items.size());
filters.add(c -> c.getStatut().equals(filtreStatut)); applyFilters(items, buildFilters());
} } catch (Exception e) {
return filters; LOG.error("Erreur chargement chantiers depuis l'API", e);
} // En cas d'erreur, on garde une liste vide
items = new ArrayList<>();
@Override } finally {
protected void resetFilterFields() { loading = false;
filtreNom = null; }
filtreClient = null; }
filtreStatut = "TOUS";
} private List<Predicate<Chantier>> buildFilters() {
List<Predicate<Chantier>> filters = new ArrayList<>();
@Override if (filtreNom != null && !filtreNom.trim().isEmpty()) {
protected String getDetailsPath() { filters.add(c -> c.getNom().toLowerCase().contains(filtreNom.toLowerCase()));
return "/chantiers/"; }
} if (filtreClient != null && !filtreClient.trim().isEmpty()) {
filters.add(c -> c.getClient().toLowerCase().contains(filtreClient.toLowerCase()));
@Override }
protected String getCreatePath() { if (filtreStatut != null && !filtreStatut.trim().isEmpty() && !"TOUS".equals(filtreStatut)) {
return "/chantiers/nouveau"; filters.add(c -> c.getStatut().equals(filtreStatut));
} }
return filters;
@Override }
protected void performDelete() {
LOG.info("Suppression chantier : {}", selectedItem.getId()); @Override
// TODO: Appeler chantierService.delete(selectedItem.getId()) protected void resetFilterFields() {
} filtreNom = null;
filtreClient = null;
@Override filtreStatut = "TOUS";
protected Chantier createNewEntity() { }
Chantier c = new Chantier();
c.setStatut("PLANIFIE"); @Override
c.setAvancement(0); protected String getDetailsPath() {
c.setDateDebut(LocalDate.now()); return "/chantiers/";
c.setDateCreation(LocalDateTime.now()); }
return c;
} @Override
protected String getCreatePath() {
@Override return "/chantiers/nouveau";
protected void performCreate() { }
entity.setId(System.currentTimeMillis()); // Simulation ID
entity.setDateCreation(LocalDateTime.now()); @Override
entity.setDateModification(LocalDateTime.now()); protected void performDelete() {
items.add(entity); if (selectedItem == null || selectedItem.getId() == null) {
LOG.info("Nouveau chantier créé : {}", entity.getNom()); LOG.warn("Aucun chantier sélectionné pour la suppression");
// TODO: Appeler chantierService.create(entity) return;
} }
@Override LOG.info("Suppression chantier : {}", selectedItem.getId());
protected void performUpdate() {
entity.setDateModification(LocalDateTime.now()); // Convertir l'ID Long en String UUID (le backend utilise UUID)
LOG.info("Chantier modifié : {}", entity.getNom()); String idString = convertIdToString(selectedItem);
// TODO: Appeler chantierService.update(entity)
} boolean success = chantierService.deleteChantier(idString, false);
if (success) {
@Override LOG.info("Chantier supprimé avec succès : {}", idString);
protected Long getEntityId(Chantier chantier) { // Recharger la liste après suppression
return chantier.getId(); loadItems();
} } else {
LOG.error("Échec de la suppression du chantier : {}", idString);
/** }
* Initialise un nouveau chantier pour la création. }
*/
@Override @Override
public String createNew() { protected Chantier createNewEntity() {
prepareNew(); Chantier c = new Chantier();
return getCreatePath() + "?faces-redirect=true"; c.setStatut("PLANIFIE");
} c.setAvancement(0);
c.setDateDebut(LocalDate.now());
/** c.setDateCreation(LocalDateTime.now());
* Sauvegarde un nouveau chantier. return c;
*/ }
public String saveNew() {
if (selectedItem == null) { @Override
selectedItem = new Chantier(); protected void performCreate() {
} if (entity == null) {
selectedItem.setId(System.currentTimeMillis()); // Simulation ID LOG.warn("Aucune entité à créer");
selectedItem.setDateCreation(LocalDateTime.now()); return;
selectedItem.setDateModification(LocalDateTime.now()); }
items.add(selectedItem);
LOG.info("Nouveau chantier créé : {}", selectedItem.getNom()); LOG.info("Création d'un nouveau chantier : {}", entity.getNom());
return "/chantiers?faces-redirect=true";
} // Convertir l'entité Chantier en Map pour l'API
Map<String, Object> chantierData = convertChantierToMap(entity);
/**
* Charge un chantier par son ID depuis les paramètres de la requête. Map<String, Object> createdChantier = chantierService.createChantier(chantierData);
*/ if (createdChantier != null) {
public void loadChantierById() { LOG.info("Chantier créé avec succès : {}", createdChantier.get("id"));
if (chantierId != null) { // Recharger la liste après création
loadItems(); // S'assurer que les items sont chargés loadItems();
selectedItem = items.stream() } else {
.filter(c -> c.getId().equals(chantierId)) LOG.error("Échec de la création du chantier : {}", entity.getNom());
.findFirst() }
.orElse(null); }
if (selectedItem == null) {
LOG.warn("Chantier avec ID {} non trouvé", chantierId); @Override
} protected void performUpdate() {
} if (entity == null || entity.getId() == null) {
} LOG.warn("Aucune entité à mettre à jour ou ID manquant");
return;
/** }
* Affiche les détails d'un chantier.
*/ LOG.info("Mise à jour du chantier : {} (ID: {})", entity.getNom(), entity.getId());
public String viewDetails(Long id) {
selectedItem = items.stream() // Convertir l'ID Long en String UUID
.filter(c -> c.getId().equals(id)) String idString = convertIdToString(entity);
.findFirst()
.orElse(null); // Convertir l'entité Chantier en Map pour l'API
if (selectedItem != null) { Map<String, Object> chantierData = convertChantierToMap(entity);
return getDetailsPath() + "details?id=" + id + "&faces-redirect=true";
} Map<String, Object> updatedChantier = chantierService.updateChantier(idString, chantierData);
return "/chantiers?faces-redirect=true"; if (updatedChantier != null) {
} LOG.info("Chantier mis à jour avec succès : {}", idString);
// Recharger la liste après mise à jour
@lombok.Getter loadItems();
@lombok.Setter } else {
public static class Chantier { LOG.error("Échec de la mise à jour du chantier : {}", idString);
private Long id; }
private String nom; }
private String client;
private String adresse; @Override
private LocalDate dateDebut; protected Long getEntityId(Chantier chantier) {
private LocalDate dateFinPrevue; return chantier.getId();
private String statut; }
private int avancement;
private double budget; /**
private double coutReel; * Initialise un nouveau chantier pour la création.
private LocalDateTime dateCreation; */
private LocalDateTime dateModification; @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; package dev.lions.btpxpress.view;
import dev.lions.btpxpress.service.ClientService; import dev.lions.btpxpress.service.ClientService;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped; import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.inject.Named; import jakarta.inject.Named;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.Serializable; import java.time.LocalDateTime;
import java.time.LocalDateTime; import java.util.ArrayList;
import java.util.ArrayList; import java.util.List;
import java.util.List; import java.util.Map;
import java.util.Map; import java.util.function.Predicate;
import java.util.function.Predicate;
@Named("clientsView")
@Named("clientsView") @ViewScoped
@ViewScoped @Getter
@Getter @Setter
@Setter public class ClientsView extends BaseListView<ClientsView.Client, Long> {
public class ClientsView extends BaseListView<ClientsView.Client, Long> implements Serializable {
private static final Logger LOG = LoggerFactory.getLogger(ClientsView.class);
private static final Logger LOG = LoggerFactory.getLogger(ClientsView.class);
@Inject
@Inject ClientService clientService;
ClientService clientService;
private String filtreNom;
private String filtreNom; private String filtreEmail;
private String filtreEmail; private String filtreVille;
private String filtreVille; private Long clientId;
private Long clientId;
@PostConstruct
@PostConstruct public void init() {
public void init() { loadItems();
loadItems(); }
}
@Override
@Override public void loadItems() {
public void loadItems() { loading = true;
loading = true; try {
try { items = new ArrayList<>();
items = new ArrayList<>();
// Récupération depuis l'API backend
// Récupération depuis l'API backend List<Map<String, Object>> clientsData = clientService.getAllClients();
List<Map<String, Object>> clientsData = clientService.getAllClients();
for (Map<String, Object> data : clientsData) {
for (Map<String, Object> data : clientsData) { Client c = new Client();
Client c = new Client();
// Mapping des données de l'API vers l'objet 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);
c.setId(data.get("id") != null ? Long.valueOf(data.get("id").toString().hashCode()) : null);
// Raison sociale : entreprise ou "Particulier" si vide
// Raison sociale : entreprise ou "Particulier" si vide String entreprise = (String) data.get("entreprise");
String entreprise = (String) data.get("entreprise"); c.setRaisonSociale(entreprise != null && !entreprise.trim().isEmpty() ?
c.setRaisonSociale(entreprise != null && !entreprise.trim().isEmpty() ? entreprise : "Particulier");
entreprise : "Particulier");
// Nom complet du contact : prénom + nom
// Nom complet du contact : prénom + nom String prenom = (String) data.get("prenom");
String prenom = (String) data.get("prenom"); String nom = (String) data.get("nom");
String nom = (String) data.get("nom"); c.setNomContact((prenom != null ? prenom + " " : "") + (nom != null ? nom : ""));
c.setNomContact((prenom != null ? prenom + " " : "") + (nom != null ? nom : ""));
c.setEmail((String) data.get("email"));
c.setEmail((String) data.get("email")); c.setTelephone((String) data.get("telephone"));
c.setTelephone((String) data.get("telephone")); c.setAdresse((String) data.get("adresse"));
c.setAdresse((String) data.get("adresse")); c.setVille((String) data.get("ville"));
c.setVille((String) data.get("ville")); c.setCodePostal((String) data.get("codePostal"));
c.setCodePostal((String) data.get("codePostal"));
// Nombre de chantiers (relation)
// Nombre de chantiers (relation) Object chantiersObj = data.get("chantiers");
Object chantiersObj = data.get("chantiers"); if (chantiersObj instanceof List) {
if (chantiersObj instanceof List) { c.setNombreChantiers(((List<?>) chantiersObj).size());
c.setNombreChantiers(((List<?>) chantiersObj).size()); } else {
} else { c.setNombreChantiers(0);
c.setNombreChantiers(0); }
}
// Chiffre d'affaires total (à calculer ou récupérer)
// 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
// Pour l'instant, on met 0 car cette donnée n'est pas dans l'API c.setChiffreAffairesTotal(0.0);
c.setChiffreAffairesTotal(0.0);
// Date de création
// Date de création if (data.get("dateCreation") != null) {
if (data.get("dateCreation") != null) { c.setDateCreation(LocalDateTime.parse(data.get("dateCreation").toString()));
c.setDateCreation(LocalDateTime.parse(data.get("dateCreation").toString())); }
}
// Date de modification
// Date de modification if (data.get("dateModification") != null) {
if (data.get("dateModification") != null) { c.setDateModification(LocalDateTime.parse(data.get("dateModification").toString()));
c.setDateModification(LocalDateTime.parse(data.get("dateModification").toString())); }
}
items.add(c);
items.add(c); }
}
LOG.info("Clients chargés depuis l'API : {} élément(s)", items.size());
LOG.info("Clients chargés depuis l'API : {} élément(s)", items.size()); applyFilters(items, buildFilters());
applyFilters(items, buildFilters()); } catch (Exception e) {
} catch (Exception e) { LOG.error("Erreur chargement clients depuis l'API", e);
LOG.error("Erreur chargement clients depuis l'API", e); // En cas d'erreur, on garde une liste vide
// En cas d'erreur, on garde une liste vide items = new ArrayList<>();
items = new ArrayList<>(); } finally {
} finally { loading = false;
loading = false; }
} }
}
private List<Predicate<Client>> buildFilters() {
private List<Predicate<Client>> buildFilters() { List<Predicate<Client>> filters = new ArrayList<>();
List<Predicate<Client>> filters = new ArrayList<>(); if (filtreNom != null && !filtreNom.trim().isEmpty()) {
if (filtreNom != null && !filtreNom.trim().isEmpty()) { filters.add(c -> c.getRaisonSociale().toLowerCase().contains(filtreNom.toLowerCase()) ||
filters.add(c -> c.getRaisonSociale().toLowerCase().contains(filtreNom.toLowerCase()) || c.getNomContact().toLowerCase().contains(filtreNom.toLowerCase()));
c.getNomContact().toLowerCase().contains(filtreNom.toLowerCase())); }
} if (filtreEmail != null && !filtreEmail.trim().isEmpty()) {
if (filtreEmail != null && !filtreEmail.trim().isEmpty()) { filters.add(c -> c.getEmail().toLowerCase().contains(filtreEmail.toLowerCase()));
filters.add(c -> c.getEmail().toLowerCase().contains(filtreEmail.toLowerCase())); }
} if (filtreVille != null && !filtreVille.trim().isEmpty()) {
if (filtreVille != null && !filtreVille.trim().isEmpty()) { filters.add(c -> c.getVille().toLowerCase().contains(filtreVille.toLowerCase()));
filters.add(c -> c.getVille().toLowerCase().contains(filtreVille.toLowerCase())); }
} return filters;
return filters; }
}
@Override
@Override protected void resetFilterFields() {
protected void resetFilterFields() { filtreNom = null;
filtreNom = null; filtreEmail = null;
filtreEmail = null; filtreVille = null;
filtreVille = null; }
}
@Override
@Override protected String getDetailsPath() {
protected String getDetailsPath() { return "/clients/";
return "/clients/"; }
}
@Override
@Override protected String getCreatePath() {
protected String getCreatePath() { return "/clients/nouveau";
return "/clients/nouveau"; }
}
@Override
@Override protected void performDelete() {
protected void performDelete() { LOG.info("Suppression client : {}", selectedItem.getId());
LOG.info("Suppression client : {}", selectedItem.getId()); }
}
@Override
@Override protected Client createNewEntity() {
protected Client createNewEntity() { Client client = new Client();
Client client = new Client(); client.setDateCreation(LocalDateTime.now());
client.setDateCreation(LocalDateTime.now()); client.setDateModification(LocalDateTime.now());
client.setDateModification(LocalDateTime.now()); client.setNombreChantiers(0);
client.setNombreChantiers(0); client.setChiffreAffairesTotal(0.0);
client.setChiffreAffairesTotal(0.0); return client;
return client; }
}
@Override
@Override protected void performCreate() {
protected void performCreate() { if (selectedItem.getId() == null) {
if (selectedItem.getId() == null) { selectedItem.setId(System.currentTimeMillis());
selectedItem.setId(System.currentTimeMillis()); }
} selectedItem.setDateCreation(LocalDateTime.now());
selectedItem.setDateCreation(LocalDateTime.now()); selectedItem.setDateModification(LocalDateTime.now());
selectedItem.setDateModification(LocalDateTime.now()); items.add(selectedItem);
items.add(selectedItem); LOG.info("Created: {}", selectedItem);
LOG.info("Created: {}", selectedItem); }
}
@Override
@Override protected void performUpdate() {
protected void performUpdate() { selectedItem.setDateModification(LocalDateTime.now());
selectedItem.setDateModification(LocalDateTime.now()); LOG.info("Updated: {}", selectedItem);
LOG.info("Updated: {}", selectedItem); }
}
@Override
@Override protected Long getEntityId(Client entity) {
protected Long getEntityId(Client entity) { return entity.getId();
return entity.getId(); }
}
/**
/** * Initialise un nouveau client pour la création.
* Initialise un nouveau client pour la création. */
*/ @Override
@Override public String createNew() {
public String createNew() { selectedItem = new Client();
selectedItem = new Client(); return getCreatePath() + "?faces-redirect=true";
return getCreatePath() + "?faces-redirect=true"; }
}
/**
/** * Sauvegarde un nouveau client.
* Sauvegarde un nouveau client. */
*/ public String saveNew() {
public String saveNew() { if (selectedItem == null) {
if (selectedItem == null) { selectedItem = new Client();
selectedItem = new Client(); }
} selectedItem.setId(System.currentTimeMillis());
selectedItem.setId(System.currentTimeMillis()); selectedItem.setDateCreation(LocalDateTime.now());
selectedItem.setDateCreation(LocalDateTime.now()); selectedItem.setDateModification(LocalDateTime.now());
selectedItem.setDateModification(LocalDateTime.now()); selectedItem.setNombreChantiers(0);
selectedItem.setNombreChantiers(0); selectedItem.setChiffreAffairesTotal(0.0);
selectedItem.setChiffreAffairesTotal(0.0); items.add(selectedItem);
items.add(selectedItem); LOG.info("Nouveau client créé : {}", selectedItem.getRaisonSociale());
LOG.info("Nouveau client créé : {}", selectedItem.getRaisonSociale()); return "/clients?faces-redirect=true";
return "/clients?faces-redirect=true"; }
}
/**
/** * Affiche les détails d'un client.
* Affiche les détails d'un client. */
*/ public String viewDetails(Long id) {
public String viewDetails(Long id) { selectedItem = items.stream()
selectedItem = items.stream() .filter(c -> c.getId().equals(id))
.filter(c -> c.getId().equals(id)) .findFirst()
.findFirst() .orElse(null);
.orElse(null); if (selectedItem != null) {
if (selectedItem != null) { return getDetailsPath() + "details?id=" + id + "&faces-redirect=true";
return getDetailsPath() + "details?id=" + id + "&faces-redirect=true"; }
} return "/clients?faces-redirect=true";
return "/clients?faces-redirect=true"; }
}
/**
/** * Charge un client par son ID depuis les paramètres de la requête.
* Charge un client par son ID depuis les paramètres de la requête. */
*/ public void loadClientById() {
public void loadClientById() { if (clientId != null) {
if (clientId != null) { loadItems(); // S'assurer que les items sont chargés
loadItems(); // S'assurer que les items sont chargés selectedItem = items.stream()
selectedItem = items.stream() .filter(c -> c.getId().equals(clientId))
.filter(c -> c.getId().equals(clientId)) .findFirst()
.findFirst() .orElse(null);
.orElse(null); if (selectedItem == null) {
if (selectedItem == null) { LOG.warn("Client avec ID {} non trouvé", clientId);
LOG.warn("Client avec ID {} non trouvé", clientId); }
} }
} }
}
@lombok.Getter
@lombok.Getter @lombok.Setter
@lombok.Setter public static class Client {
public static class Client { private Long id;
private Long id; private String raisonSociale;
private String raisonSociale; private String nomContact;
private String nomContact; private String email;
private String email; private String telephone;
private String telephone; private String adresse;
private String adresse; private String ville;
private String ville; private String codePostal;
private String codePostal; private String pays;
private String pays; private int nombreChantiers;
private int nombreChantiers; private double chiffreAffairesTotal;
private double chiffreAffairesTotal; private LocalDateTime dateCreation;
private LocalDateTime dateCreation; private LocalDateTime dateModification;
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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
@@ -23,7 +21,7 @@ import java.util.function.Predicate;
@ViewScoped @ViewScoped
@Getter @Getter
@Setter @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); 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 // Le client peut être un objet ou une chaîne
Object clientObj = data.get("client"); Object clientObj = data.get("client");
if (clientObj instanceof Map) { if (clientObj instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> clientData = (Map<String, Object>) clientObj; Map<String, Object> clientData = (Map<String, Object>) clientObj;
String entreprise = (String) clientData.get("entreprise"); String entreprise = (String) clientData.get("entreprise");
String nom = (String) clientData.get("nom"); String nom = (String) clientData.get("nom");

View File

@@ -10,7 +10,6 @@ import lombok.Setter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
@@ -22,7 +21,7 @@ import java.util.function.Predicate;
@ViewScoped @ViewScoped
@Getter @Getter
@Setter @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); 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -21,7 +20,7 @@ import java.util.function.Predicate;
@ViewScoped @ViewScoped
@Getter @Getter
@Setter @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); 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 // Chef d'équipe
Object chefObj = data.get("chef"); Object chefObj = data.get("chef");
if (chefObj instanceof Map) { if (chefObj instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> chefData = (Map<String, Object>) chefObj; Map<String, Object> chefData = (Map<String, Object>) chefObj;
String prenom = (String) chefData.get("prenom"); String prenom = (String) chefData.get("prenom");
String nom = (String) chefData.get("nom"); String nom = (String) chefData.get("nom");

View File

@@ -10,7 +10,6 @@ import lombok.Setter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
@@ -22,7 +21,7 @@ import java.util.function.Predicate;
@ViewScoped @ViewScoped
@Getter @Getter
@Setter @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); 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 // Le client peut être un objet ou une chaîne
Object clientObj = data.get("client"); Object clientObj = data.get("client");
if (clientObj instanceof Map) { if (clientObj instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> clientData = (Map<String, Object>) clientObj; Map<String, Object> clientData = (Map<String, Object>) clientObj;
String entreprise = (String) clientData.get("entreprise"); String entreprise = (String) clientData.get("entreprise");
String nom = (String) clientData.get("nom"); 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; package dev.lions.btpxpress.view;
import jakarta.enterprise.context.RequestScoped; import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.application.FacesMessage; import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext; import jakarta.faces.context.FacesContext;
import jakarta.inject.Named; import jakarta.inject.Named;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.io.Serializable; import java.io.Serializable;
@Named("loginView") @Named("loginView")
@RequestScoped @RequestScoped
@Getter @Getter
@Setter @Setter
public class LoginView implements Serializable { public class LoginView implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private String username; private String username;
private String password; private String password;
private boolean rememberMe = false; private boolean rememberMe = false;
public String login() { public String login() {
if (username == null || username.trim().isEmpty()) { if (username == null || username.trim().isEmpty()) {
addErrorMessage("Le nom d'utilisateur est requis"); addErrorMessage("Le nom d'utilisateur est requis");
return null; return null;
} }
if (password == null || password.trim().isEmpty()) { if (password == null || password.trim().isEmpty()) {
addErrorMessage("Le mot de passe est requis"); addErrorMessage("Le mot de passe est requis");
return null; return null;
} }
if ("admin".equals(username) && "admin".equals(password)) { if ("admin".equals(username) && "admin".equals(password)) {
addInfoMessage("Connexion réussie !"); addInfoMessage("Connexion réussie !");
return "/dashboard?faces-redirect=true"; return "/dashboard?faces-redirect=true";
} else { } else {
addErrorMessage("Nom d'utilisateur ou mot de passe incorrect"); addErrorMessage("Nom d'utilisateur ou mot de passe incorrect");
return null; return null;
} }
} }
private void addErrorMessage(String message) { private void addErrorMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null, FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message)); new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
} }
private void addInfoMessage(String message) { private void addInfoMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null, FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Information", message)); new FacesMessage(FacesMessage.SEVERITY_INFO, "Information", message));
} }
} }

View File

@@ -10,7 +10,6 @@ import lombok.Setter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
@@ -22,7 +21,7 @@ import java.util.function.Predicate;
@ViewScoped @ViewScoped
@Getter @Getter
@Setter @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); 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -21,7 +20,7 @@ import java.util.function.Predicate;
@ViewScoped @ViewScoped
@Getter @Getter
@Setter @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); 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"?> <?xml version="1.0" encoding="UTF-8"?>
<faces-config version="4.0" <faces-config version="4.0"
xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-facesconfig_4_0.xsd"> https://jakarta.ee/xml/ns/jakartaee/web-facesconfig_4_0.xsd">
<name>btpxpress_freya</name> <name>btpxpress_freya</name>
<application> <application>
<locale-config> <locale-config>
<default-locale>fr</default-locale> <default-locale>fr</default-locale>
<supported-locale>fr</supported-locale> <supported-locale>fr</supported-locale>
<supported-locale>en</supported-locale> <supported-locale>en</supported-locale>
</locale-config> </locale-config>
</application> </application>
<component> <component>
<component-type>org.primefaces.component.FreyaMenu</component-type> <component-type>org.primefaces.component.FreyaMenu</component-type>
<component-class>org.primefaces.freya.component.FreyaMenu</component-class> <component-class>org.primefaces.freya.component.FreyaMenu</component-class>
</component> </component>
<render-kit> <render-kit>
<renderer> <renderer>
<component-family>org.primefaces.component</component-family> <component-family>org.primefaces.component</component-family>
<renderer-type>org.primefaces.component.FreyaMenuRenderer</renderer-type> <renderer-type>org.primefaces.component.FreyaMenuRenderer</renderer-type>
<renderer-class>org.primefaces.freya.component.FreyaMenuRenderer</renderer-class> <renderer-class>org.primefaces.freya.component.FreyaMenuRenderer</renderer-class>
</renderer> </renderer>
</render-kit> </render-kit>
</faces-config> </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" <ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html" xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core" xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"> xmlns:p="http://primefaces.org/ui">
<ui:param name="formId" value="filtresForm"/> <ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value=""/> <ui:param name="viewBean" value=""/>
<ui:param name="tableId" value=""/> <ui:param name="tableId" value=""/>
<div class="card"> <div class="card">
<h5>Recherche et filtres</h5> <h5>Recherche et filtres</h5>
<h:form id="#{formId}"> <h:form id="#{formId}">
<ui:insert name="filter-fields"> <ui:insert name="filter-fields">
<!-- Les champs de filtre spécifiques à chaque page --> <!-- Les champs de filtre spécifiques à chaque page -->
</ui:insert> </ui:insert>
<div class="col-12"> <div class="col-12">
<p:commandButton value="Rechercher" <p:commandButton value="Rechercher"
icon="pi pi-search" icon="pi pi-search"
action="#{viewBean.search()}" action="#{viewBean.search()}"
update="#{tableId}" update="#{tableId}"
styleClass="ui-button-primary"/> styleClass="ui-button-primary"/>
<p:commandButton value="Réinitialiser" <p:commandButton value="Réinitialiser"
icon="pi pi-refresh" icon="pi pi-refresh"
action="#{viewBean.resetFilters()}" action="#{viewBean.resetFilters()}"
update="#{tableId} #{formId}" update="#{tableId} #{formId}"
styleClass="ui-button-secondary"/> styleClass="ui-button-secondary"/>
</div> </div>
</h:form> </h:form>
</div> </div>
</ui:composition> </ui:composition>

View File

@@ -1,44 +1,44 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" <ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html" xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core" xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"> xmlns:p="http://primefaces.org/ui">
<div class="card"> <div class="card">
<div class="flex align-items-center justify-content-between mb-3"> <div class="flex align-items-center justify-content-between mb-3">
<h5>#{title}</h5> <h5>#{title}</h5>
<p:commandButton value="Nouveau" <p:commandButton value="Nouveau"
icon="pi pi-plus" icon="pi pi-plus"
action="#{viewBean.createNew()}" action="#{viewBean.createNew()}"
styleClass="ui-button-primary" styleClass="ui-button-primary"
rendered="#{not empty createPath}"/> rendered="#{not empty createPath}"/>
</div> </div>
<h:form id="#{formId}"> <h:form id="#{formId}">
<p:dataTable id="#{tableId}" <p:dataTable id="#{tableId}"
value="#{viewBean.items}" value="#{viewBean.items}"
var="#{var}" var="#{var}"
rowKey="id" rowKey="id"
paginator="true" paginator="true"
rows="10" rows="10"
rowsPerPageTemplate="10,20,50" rowsPerPageTemplate="10,20,50"
emptyMessage="Aucun résultat trouvé" emptyMessage="Aucun résultat trouvé"
loading="#{viewBean.loading}" loading="#{viewBean.loading}"
selection="#{viewBean.selectedItem}" selection="#{viewBean.selectedItem}"
selectionMode="single"> selectionMode="single">
<f:facet name="header"> <f:facet name="header">
<div class="flex align-items-center justify-content-between"> <div class="flex align-items-center justify-content-between">
<span>Total : #{viewBean.items.size()} élément(s)</span> <span>Total : #{viewBean.items.size()} élément(s)</span>
<p:commandButton icon="pi pi-trash" <p:commandButton icon="pi pi-trash"
title="Supprimer" title="Supprimer"
disabled="#{empty viewBean.selectedItem}" disabled="#{empty viewBean.selectedItem}"
action="#{viewBean.delete()}" action="#{viewBean.delete()}"
update="#{tableId}" update="#{tableId}"
styleClass="ui-button-danger ui-button-text"/> styleClass="ui-button-danger ui-button-text"/>
</div> </div>
</f:facet> </f:facet>
<ui:insert name="columns"/> <ui:insert name="columns"/>
</p:dataTable> </p:dataTable>
</h:form> </h:form>
</div> </div>
</ui:composition> </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" <ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core" xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html" xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"> xmlns:p="http://primefaces.org/ui">
<a href="#" id="layout-config-button" class="layout-config-button"> <a href="#" id="layout-config-button" class="layout-config-button">
<i class="pi pi-cog"/> <i class="pi pi-cog"/>
</a> </a>
<div id="layout-config" class="layout-config"> <div id="layout-config" class="layout-config">
<h:form id="config-form" styleClass="layout-config-form"> <h:form id="config-form" styleClass="layout-config-form">
<h5 style="margin-top: 0">Type de Menu</h5> <h5 style="margin-top: 0">Type de Menu</h5>
<p:selectOneRadio value="#{guestPreferences.menuMode}" layout="pageDirection" <p:selectOneRadio value="#{guestPreferences.menuMode}" layout="pageDirection"
onchange="PrimeFaces.FreyaConfigurator.changeMenuMode(event.target.value)" > onchange="PrimeFaces.FreyaConfigurator.changeMenuMode(event.target.value)" >
<f:selectItem itemLabel="Horizontal" itemValue="layout-horizontal" /> <f:selectItem itemLabel="Horizontal" itemValue="layout-horizontal" />
<f:selectItem itemLabel="Sidebar" itemValue="layout-sidebar" /> <f:selectItem itemLabel="Sidebar" itemValue="layout-sidebar" />
<f:selectItem itemLabel="Slim" itemValue="layout-slim" /> <f:selectItem itemLabel="Slim" itemValue="layout-slim" />
<p:ajax listener="#{guestPreferences.onMenuTypeChange}" update="config-form" /> <p:ajax listener="#{guestPreferences.onMenuTypeChange}" update="config-form" />
</p:selectOneRadio> </p:selectOneRadio>
<hr/> <hr/>
<h5>Schéma de Couleurs</h5> <h5>Schéma de Couleurs</h5>
<p:selectOneRadio value="#{guestPreferences.darkMode}" layout="pageDirection" <p:selectOneRadio value="#{guestPreferences.darkMode}" layout="pageDirection"
onchange="PrimeFaces.FreyaConfigurator.changeLayout('#{guestPreferences.componentTheme}', event.target.value)" > onchange="PrimeFaces.FreyaConfigurator.changeLayout('#{guestPreferences.componentTheme}', event.target.value)" >
<f:selectItem itemLabel="Clair" itemValue="light" /> <f:selectItem itemLabel="Clair" itemValue="light" />
<f:selectItem itemLabel="Sombre" itemValue="dark" /> <f:selectItem itemLabel="Sombre" itemValue="dark" />
<p:ajax onstart="PrimeFaces.FreyaConfigurator.beforeResourceChange()" update="config-form logolink"/> <p:ajax onstart="PrimeFaces.FreyaConfigurator.beforeResourceChange()" update="config-form logolink"/>
</p:selectOneRadio> </p:selectOneRadio>
<p:outputPanel rendered="#{guestPreferences.menuMode eq 'layout-horizontal'}"> <p:outputPanel rendered="#{guestPreferences.menuMode eq 'layout-horizontal'}">
<hr/> <hr/>
<h5>Mode Topbar et Menu</h5> <h5>Mode Topbar et Menu</h5>
<p:selectOneRadio value="#{guestPreferences.topbarTheme}" layout="pageDirection" <p:selectOneRadio value="#{guestPreferences.topbarTheme}" layout="pageDirection"
onchange="PrimeFaces.FreyaConfigurator.changeSectionTheme(event.target.value , 'layout-topbar');PrimeFaces.FreyaConfigurator.changeSectionTheme(event.target.value , 'layout-menu')" > 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="Clair" itemValue="light" itemDisabled="#{guestPreferences.darkMode != 'light'}" />
<f:selectItem itemLabel="Sombre" itemValue="dark" itemDisabled="#{guestPreferences.darkMode != 'light'}"/> <f:selectItem itemLabel="Sombre" itemValue="dark" itemDisabled="#{guestPreferences.darkMode != 'light'}"/>
<p:ajax update="logolink config-form"/> <p:ajax update="logolink config-form"/>
</p:selectOneRadio> </p:selectOneRadio>
</p:outputPanel> </p:outputPanel>
<p:outputPanel rendered="#{guestPreferences.menuMode != 'layout-horizontal'}"> <p:outputPanel rendered="#{guestPreferences.menuMode != 'layout-horizontal'}">
<hr/> <hr/>
<h5>Mode Topbar</h5> <h5>Mode Topbar</h5>
<p:selectOneRadio value="#{guestPreferences.topbarTheme}" layout="pageDirection" <p:selectOneRadio value="#{guestPreferences.topbarTheme}" layout="pageDirection"
onchange="PrimeFaces.FreyaConfigurator.changeSectionTheme(event.target.value , 'layout-topbar')" > onchange="PrimeFaces.FreyaConfigurator.changeSectionTheme(event.target.value , 'layout-topbar')" >
<f:selectItem itemLabel="Clair" itemValue="light" itemDisabled="#{guestPreferences.darkMode != 'light'}" /> <f:selectItem itemLabel="Clair" itemValue="light" itemDisabled="#{guestPreferences.darkMode != 'light'}" />
<f:selectItem itemLabel="Sombre" itemValue="dark" itemDisabled="#{guestPreferences.darkMode != 'light'}"/> <f:selectItem itemLabel="Sombre" itemValue="dark" itemDisabled="#{guestPreferences.darkMode != 'light'}"/>
<p:ajax update="logolink config-form"/> <p:ajax update="logolink config-form"/>
</p:selectOneRadio> </p:selectOneRadio>
</p:outputPanel> </p:outputPanel>
<p:outputPanel rendered="#{guestPreferences.menuMode != 'layout-horizontal'}"> <p:outputPanel rendered="#{guestPreferences.menuMode != 'layout-horizontal'}">
<hr/> <hr/>
<h5>Mode Menu</h5> <h5>Mode Menu</h5>
<p:selectOneRadio value="#{guestPreferences.menuTheme}" layout="pageDirection" <p:selectOneRadio value="#{guestPreferences.menuTheme}" layout="pageDirection"
onchange="PrimeFaces.FreyaConfigurator.changeSectionTheme(event.target.value , 'layout-menu')" > onchange="PrimeFaces.FreyaConfigurator.changeSectionTheme(event.target.value , 'layout-menu')" >
<f:selectItem itemLabel="Clair" itemValue="light" itemDisabled="#{guestPreferences.darkMode != 'light'}" /> <f:selectItem itemLabel="Clair" itemValue="light" itemDisabled="#{guestPreferences.darkMode != 'light'}" />
<f:selectItem itemLabel="Sombre" itemValue="dark" itemDisabled="#{guestPreferences.darkMode != 'light'}"/> <f:selectItem itemLabel="Sombre" itemValue="dark" itemDisabled="#{guestPreferences.darkMode != 'light'}"/>
<p:ajax update="logolink config-form"/> <p:ajax update="logolink config-form"/>
</p:selectOneRadio> </p:selectOneRadio>
</p:outputPanel> </p:outputPanel>
<hr/> <hr/>
<h5>Style d'Input</h5> <h5>Style d'Input</h5>
<p:selectOneRadio value="#{guestPreferences.inputStyle}" layout="pageDirection" <p:selectOneRadio value="#{guestPreferences.inputStyle}" layout="pageDirection"
onchange="PrimeFaces.FreyaConfigurator.updateInputStyle(event.target.value)"> onchange="PrimeFaces.FreyaConfigurator.updateInputStyle(event.target.value)">
<f:selectItem itemLabel="Outlined" itemValue="outlined" /> <f:selectItem itemLabel="Outlined" itemValue="outlined" />
<f:selectItem itemLabel="Filled" itemValue="filled" /> <f:selectItem itemLabel="Filled" itemValue="filled" />
<p:ajax /> <p:ajax />
</p:selectOneRadio> </p:selectOneRadio>
<hr/> <hr/>
<h5>Couleurs du Thème</h5> <h5>Couleurs du Thème</h5>
<div class="layout-themes"> <div class="layout-themes">
<ui:repeat value="#{guestPreferences.componentThemes}" var="componentTheme"> <ui:repeat value="#{guestPreferences.componentThemes}" var="componentTheme">
<div> <div>
<p:commandLink actionListener="#{guestPreferences.setComponentTheme(componentTheme.file)}" <p:commandLink actionListener="#{guestPreferences.setComponentTheme(componentTheme.file)}"
style="background-color: #{componentTheme.color};" title="#{componentTheme.name}" style="background-color: #{componentTheme.color};" title="#{componentTheme.name}"
process="@this" process="@this"
update="config-form" update="config-form"
onstart="PrimeFaces.FreyaConfigurator.beforeResourceChange()" onstart="PrimeFaces.FreyaConfigurator.beforeResourceChange()"
oncomplete="PrimeFaces.FreyaConfigurator.changeComponentsTheme('#{componentTheme.file}', '#{guestPreferences.darkMode}')"> oncomplete="PrimeFaces.FreyaConfigurator.changeComponentsTheme('#{componentTheme.file}', '#{guestPreferences.darkMode}')">
</p:commandLink> </p:commandLink>
</div> </div>
</ui:repeat> </ui:repeat>
</div> </div>
</h:form> </h:form>
</div> </div>
</ui:composition> </ui:composition>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,102 +1,102 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" <ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html" xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core" xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml"> template="/WEB-INF/template.xhtml">
<ui:define name="title">Employés - BTP Xpress</ui:define> <ui:define name="title">Employés - BTP Xpress</ui:define>
<ui:define name="content"> <ui:define name="content">
<div class="layout-dashboard"> <div class="layout-dashboard">
<div class="grid"> <div class="grid">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="flex align-items-center justify-content-between mb-3"> <div class="flex align-items-center justify-content-between mb-3">
<h1>Gestion des Employés</h1> <h1>Gestion des Employés</h1>
<p:commandButton value="Nouvel employé" icon="pi pi-user-plus" <p:commandButton value="Nouvel employé" icon="pi pi-user-plus"
action="#{employeView.createNew()}" action="#{employeView.createNew()}"
styleClass="ui-button-primary"/> styleClass="ui-button-primary"/>
</div> </div>
</div> </div>
</div> </div>
<div class="col-12"> <div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml"> <ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/> <ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{employeView}"/> <ui:param name="viewBean" value="#{employeView}"/>
<ui:param name="tableId" value="employesTable"/> <ui:param name="tableId" value="employesTable"/>
<ui:define name="filter-fields"> <ui:define name="filter-fields">
<div class="grid"> <div class="grid">
<div class="col-12 md:col-4"> <div class="col-12 md:col-4">
<h:outputLabel for="filtreNom" value="Nom"/> <h:outputLabel for="filtreNom" value="Nom"/>
<p:inputText id="filtreNom" value="#{employeView.filtreNom}" <p:inputText id="filtreNom" value="#{employeView.filtreNom}"
placeholder="Rechercher par nom..." style="width: 100%;"/> placeholder="Rechercher par nom..." style="width: 100%;"/>
</div> </div>
<div class="col-12 md:col-4"> <div class="col-12 md:col-4">
<h:outputLabel for="filtrePoste" value="Poste"/> <h:outputLabel for="filtrePoste" value="Poste"/>
<p:inputText id="filtrePoste" value="#{employeView.filtrePoste}" <p:inputText id="filtrePoste" value="#{employeView.filtrePoste}"
placeholder="Rechercher par poste..." style="width: 100%;"/> placeholder="Rechercher par poste..." style="width: 100%;"/>
</div> </div>
<div class="col-12 md:col-4"> <div class="col-12 md:col-4">
<h:outputLabel for="filtreStatut" value="Statut"/> <h:outputLabel for="filtreStatut" value="Statut"/>
<p:selectOneMenu id="filtreStatut" value="#{employeView.filtreStatut}" style="width: 100%;"> <p:selectOneMenu id="filtreStatut" value="#{employeView.filtreStatut}" style="width: 100%;">
<f:selectItem itemLabel="Tous" itemValue="TOUS"/> <f:selectItem itemLabel="Tous" itemValue="TOUS"/>
<f:selectItem itemLabel="Actif" itemValue="ACTIF"/> <f:selectItem itemLabel="Actif" itemValue="ACTIF"/>
<f:selectItem itemLabel="Inactif" itemValue="INACTIF"/> <f:selectItem itemLabel="Inactif" itemValue="INACTIF"/>
<f:selectItem itemLabel="En congé" itemValue="EN_CONGE"/> <f:selectItem itemLabel="En congé" itemValue="EN_CONGE"/>
</p:selectOneMenu> </p:selectOneMenu>
</div> </div>
</div> </div>
</ui:define> </ui:define>
</ui:include> </ui:include>
</div> </div>
<div class="col-12"> <div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml"> <ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="employesForm"/> <ui:param name="formId" value="employesForm"/>
<ui:param name="tableId" value="employesTable"/> <ui:param name="tableId" value="employesTable"/>
<ui:param name="viewBean" value="#{employeView}"/> <ui:param name="viewBean" value="#{employeView}"/>
<ui:param name="var" value="employe"/> <ui:param name="var" value="employe"/>
<ui:param name="title" value="Liste des employés"/> <ui:param name="title" value="Liste des employés"/>
<ui:param name="createPath" value="/employes/nouveau"/> <ui:param name="createPath" value="/employes/nouveau"/>
<ui:define name="columns"> <ui:define name="columns">
<p:column headerText="Nom complet" sortBy="#{employe.nomComplet}"> <p:column headerText="Nom complet" sortBy="#{employe.nomComplet}">
<h:outputText value="#{employe.nomComplet}"/> <h:outputText value="#{employe.nomComplet}"/>
</p:column> </p:column>
<p:column headerText="Email" sortBy="#{employe.email}"> <p:column headerText="Email" sortBy="#{employe.email}">
<h:outputText value="#{employe.email}"/> <h:outputText value="#{employe.email}"/>
</p:column> </p:column>
<p:column headerText="Téléphone"> <p:column headerText="Téléphone">
<h:outputText value="#{employe.telephone}"/> <h:outputText value="#{employe.telephone}"/>
</p:column> </p:column>
<p:column headerText="Poste" sortBy="#{employe.poste}"> <p:column headerText="Poste" sortBy="#{employe.poste}">
<h:outputText value="#{employe.poste}"/> <h:outputText value="#{employe.poste}"/>
</p:column> </p:column>
<p:column headerText="Taux horaire"> <p:column headerText="Taux horaire">
<h:outputText value="#{employe.tauxHoraire}"> <h:outputText value="#{employe.tauxHoraire}">
<f:converter converterId="fcfaConverter"/> <f:converter converterId="fcfaConverter"/>
</h:outputText> </h:outputText>
<h:outputText value=" Fcfa/h"/> <h:outputText value=" Fcfa/h"/>
</p:column> </p:column>
<p:column headerText="Date embauche" sortBy="#{employe.dateEmbauche}"> <p:column headerText="Date embauche" sortBy="#{employe.dateEmbauche}">
<h:outputText value="#{employe.dateEmbauche}"> <h:outputText value="#{employe.dateEmbauche}">
<f:convertDateTime pattern="dd/MM/yyyy"/> <f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText> </h:outputText>
</p:column> </p:column>
<p:column headerText="Statut" sortBy="#{employe.statut}"> <p:column headerText="Statut" sortBy="#{employe.statut}">
<p:tag value="#{employe.statut}" <p:tag value="#{employe.statut}"
severity="#{employe.statut == 'ACTIF' ? 'success' : 'warning'}"/> severity="#{employe.statut == 'ACTIF' ? 'success' : 'warning'}"/>
</p:column> </p:column>
<p:column headerText="Actions" style="width: 150px;"> <p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails" <p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text" styleClass="ui-button-text"
action="#{employeView.viewDetails(employe.id)}"/> action="#{employeView.viewDetails(employe.id)}"/>
</p:column> </p:column>
</ui:define> </ui:define>
</ui:include> </ui:include>
</div> </div>
</div> </div>
</div> </div>
</ui:define> </ui:define>
</ui:composition> </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" <ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html" xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core" xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml"> template="/WEB-INF/template.xhtml">
<ui:define name="title">Équipes - BTP Xpress</ui:define> <ui:define name="title">Équipes - BTP Xpress</ui:define>
<ui:define name="content"> <ui:define name="content">
<div class="layout-dashboard"> <div class="layout-dashboard">
<div class="grid"> <div class="grid">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="flex align-items-center justify-content-between mb-3"> <div class="flex align-items-center justify-content-between mb-3">
<h1>Gestion des Équipes</h1> <h1>Gestion des Équipes</h1>
<p:commandButton value="Nouvelle équipe" icon="pi pi-users" <p:commandButton value="Nouvelle équipe" icon="pi pi-users"
action="#{equipeView.createNew()}" action="#{equipeView.createNew()}"
styleClass="ui-button-primary"/> styleClass="ui-button-primary"/>
</div> </div>
</div> </div>
</div> </div>
<div class="col-12"> <div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml"> <ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/> <ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{equipeView}"/> <ui:param name="viewBean" value="#{equipeView}"/>
<ui:param name="tableId" value="equipesTable"/> <ui:param name="tableId" value="equipesTable"/>
<ui:define name="filter-fields"> <ui:define name="filter-fields">
<div class="grid"> <div class="grid">
<div class="col-12 md:col-4"> <div class="col-12 md:col-4">
<h:outputLabel for="filtreNom" value="Nom de l'équipe"/> <h:outputLabel for="filtreNom" value="Nom de l'équipe"/>
<p:inputText id="filtreNom" value="#{equipeView.filtreNom}" <p:inputText id="filtreNom" value="#{equipeView.filtreNom}"
placeholder="Rechercher par nom..." style="width: 100%;"/> placeholder="Rechercher par nom..." style="width: 100%;"/>
</div> </div>
<div class="col-12 md:col-4"> <div class="col-12 md:col-4">
<h:outputLabel for="filtreSpecialite" value="Spécialité"/> <h:outputLabel for="filtreSpecialite" value="Spécialité"/>
<p:inputText id="filtreSpecialite" value="#{equipeView.filtreSpecialite}" <p:inputText id="filtreSpecialite" value="#{equipeView.filtreSpecialite}"
placeholder="Rechercher par spécialité..." style="width: 100%;"/> placeholder="Rechercher par spécialité..." style="width: 100%;"/>
</div> </div>
<div class="col-12 md:col-4"> <div class="col-12 md:col-4">
<h:outputLabel for="filtreStatut" value="Statut"/> <h:outputLabel for="filtreStatut" value="Statut"/>
<p:selectOneMenu id="filtreStatut" value="#{equipeView.filtreStatut}" style="width: 100%;"> <p:selectOneMenu id="filtreStatut" value="#{equipeView.filtreStatut}" style="width: 100%;">
<f:selectItem itemLabel="Tous" itemValue="TOUS"/> <f:selectItem itemLabel="Tous" itemValue="TOUS"/>
<f:selectItem itemLabel="Active" itemValue="ACTIVE"/> <f:selectItem itemLabel="Active" itemValue="ACTIVE"/>
<f:selectItem itemLabel="Inactive" itemValue="INACTIVE"/> <f:selectItem itemLabel="Inactive" itemValue="INACTIVE"/>
</p:selectOneMenu> </p:selectOneMenu>
</div> </div>
</div> </div>
</ui:define> </ui:define>
</ui:include> </ui:include>
</div> </div>
<div class="col-12"> <div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml"> <ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="equipesForm"/> <ui:param name="formId" value="equipesForm"/>
<ui:param name="tableId" value="equipesTable"/> <ui:param name="tableId" value="equipesTable"/>
<ui:param name="viewBean" value="#{equipeView}"/> <ui:param name="viewBean" value="#{equipeView}"/>
<ui:param name="var" value="equipe"/> <ui:param name="var" value="equipe"/>
<ui:param name="title" value="Liste des équipes"/> <ui:param name="title" value="Liste des équipes"/>
<ui:param name="createPath" value="/equipes/nouveau"/> <ui:param name="createPath" value="/equipes/nouveau"/>
<ui:define name="columns"> <ui:define name="columns">
<p:column headerText="Nom" sortBy="#{equipe.nom}"> <p:column headerText="Nom" sortBy="#{equipe.nom}">
<h:outputText value="#{equipe.nom}"/> <h:outputText value="#{equipe.nom}"/>
</p:column> </p:column>
<p:column headerText="Chef d'équipe" sortBy="#{equipe.chef}"> <p:column headerText="Chef d'équipe" sortBy="#{equipe.chef}">
<h:outputText value="#{equipe.chef}"/> <h:outputText value="#{equipe.chef}"/>
</p:column> </p:column>
<p:column headerText="Spécialité" sortBy="#{equipe.specialite}"> <p:column headerText="Spécialité" sortBy="#{equipe.specialite}">
<h:outputText value="#{equipe.specialite}"/> <h:outputText value="#{equipe.specialite}"/>
</p:column> </p:column>
<p:column headerText="Nombre de membres"> <p:column headerText="Nombre de membres">
<p:tag value="#{equipe.nombreMembres}" severity="info"/> <p:tag value="#{equipe.nombreMembres}" severity="info"/>
</p:column> </p:column>
<p:column headerText="Description"> <p:column headerText="Description">
<h:outputText value="#{equipe.description}"/> <h:outputText value="#{equipe.description}"/>
</p:column> </p:column>
<p:column headerText="Statut" sortBy="#{equipe.statut}"> <p:column headerText="Statut" sortBy="#{equipe.statut}">
<p:tag value="#{equipe.statut}" <p:tag value="#{equipe.statut}"
severity="#{equipe.statut == 'ACTIVE' ? 'success' : 'warning'}"/> severity="#{equipe.statut == 'ACTIVE' ? 'success' : 'warning'}"/>
</p:column> </p:column>
<p:column headerText="Actions" style="width: 150px;"> <p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails" <p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text" styleClass="ui-button-text"
action="#{equipeView.viewDetails(equipe.id)}"/> action="#{equipeView.viewDetails(equipe.id)}"/>
</p:column> </p:column>
</ui:define> </ui:define>
</ui:include> </ui:include>
</div> </div>
</div> </div>
</div> </div>
</ui:define> </ui:define>
</ui:composition> </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" <ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html" xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core" xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml"> template="/WEB-INF/template.xhtml">
<ui:define name="title">Factures - BTP Xpress</ui:define> <ui:define name="title">Factures - BTP Xpress</ui:define>
<ui:define name="content"> <ui:define name="content">
<div class="layout-dashboard"> <div class="layout-dashboard">
<div class="grid"> <div class="grid">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="flex align-items-center justify-content-between mb-3"> <div class="flex align-items-center justify-content-between mb-3">
<h1>Gestion des Factures</h1> <h1>Gestion des Factures</h1>
<p:commandButton value="Nouvelle facture" icon="pi pi-plus" <p:commandButton value="Nouvelle facture" icon="pi pi-plus"
action="#{factureView.createNew()}" action="#{factureView.createNew()}"
styleClass="ui-button-primary"/> styleClass="ui-button-primary"/>
</div> </div>
</div> </div>
</div> </div>
<div class="col-12"> <div class="col-12">
<ui:include src="/WEB-INF/components/liste-filters.xhtml"> <ui:include src="/WEB-INF/components/liste-filters.xhtml">
<ui:param name="formId" value="filtresForm"/> <ui:param name="formId" value="filtresForm"/>
<ui:param name="viewBean" value="#{factureView}"/> <ui:param name="viewBean" value="#{factureView}"/>
<ui:param name="tableId" value="facturesTable"/> <ui:param name="tableId" value="facturesTable"/>
<ui:define name="filter-fields"> <ui:define name="filter-fields">
<div class="grid"> <div class="grid">
<div class="col-12 md:col-4"> <div class="col-12 md:col-4">
<h:outputLabel for="filtreNumero" value="Numéro"/> <h:outputLabel for="filtreNumero" value="Numéro"/>
<p:inputText id="filtreNumero" value="#{factureView.filtreNumero}" <p:inputText id="filtreNumero" value="#{factureView.filtreNumero}"
placeholder="Rechercher par numéro..." style="width: 100%;"/> placeholder="Rechercher par numéro..." style="width: 100%;"/>
</div> </div>
<div class="col-12 md:col-4"> <div class="col-12 md:col-4">
<h:outputLabel for="filtreClient" value="Client"/> <h:outputLabel for="filtreClient" value="Client"/>
<p:inputText id="filtreClient" value="#{factureView.filtreClient}" <p:inputText id="filtreClient" value="#{factureView.filtreClient}"
placeholder="Rechercher par client..." style="width: 100%;"/> placeholder="Rechercher par client..." style="width: 100%;"/>
</div> </div>
<div class="col-12 md:col-4"> <div class="col-12 md:col-4">
<h:outputLabel for="filtreStatut" value="Statut"/> <h:outputLabel for="filtreStatut" value="Statut"/>
<p:selectOneMenu id="filtreStatut" value="#{factureView.filtreStatut}" style="width: 100%;"> <p:selectOneMenu id="filtreStatut" value="#{factureView.filtreStatut}" style="width: 100%;">
<f:selectItem itemLabel="Tous" itemValue="TOUS"/> <f:selectItem itemLabel="Tous" itemValue="TOUS"/>
<f:selectItem itemLabel="Brouillon" itemValue="BROUILLON"/> <f:selectItem itemLabel="Brouillon" itemValue="BROUILLON"/>
<f:selectItem itemLabel="Émise" itemValue="EMISE"/> <f:selectItem itemLabel="Émise" itemValue="EMISE"/>
<f:selectItem itemLabel="Envoyée" itemValue="ENVOYEE"/> <f:selectItem itemLabel="Envoyée" itemValue="ENVOYEE"/>
<f:selectItem itemLabel="Payée" itemValue="PAYEE"/> <f:selectItem itemLabel="Payée" itemValue="PAYEE"/>
<f:selectItem itemLabel="En retard" itemValue="EN_RETARD"/> <f:selectItem itemLabel="En retard" itemValue="EN_RETARD"/>
<f:selectItem itemLabel="Annulée" itemValue="ANNULEE"/> <f:selectItem itemLabel="Annulée" itemValue="ANNULEE"/>
</p:selectOneMenu> </p:selectOneMenu>
</div> </div>
</div> </div>
</ui:define> </ui:define>
</ui:include> </ui:include>
</div> </div>
<div class="col-12"> <div class="col-12">
<ui:include src="/WEB-INF/components/liste-table.xhtml"> <ui:include src="/WEB-INF/components/liste-table.xhtml">
<ui:param name="formId" value="facturesForm"/> <ui:param name="formId" value="facturesForm"/>
<ui:param name="tableId" value="facturesTable"/> <ui:param name="tableId" value="facturesTable"/>
<ui:param name="viewBean" value="#{factureView}"/> <ui:param name="viewBean" value="#{factureView}"/>
<ui:param name="var" value="facture"/> <ui:param name="var" value="facture"/>
<ui:param name="title" value="Liste des factures"/> <ui:param name="title" value="Liste des factures"/>
<ui:param name="createPath" value="/factures/nouveau"/> <ui:param name="createPath" value="/factures/nouveau"/>
<ui:define name="columns"> <ui:define name="columns">
<p:column headerText="Numéro" sortBy="#{facture.numero}"> <p:column headerText="Numéro" sortBy="#{facture.numero}">
<h:outputText value="#{facture.numero}"/> <h:outputText value="#{facture.numero}"/>
</p:column> </p:column>
<p:column headerText="Objet" sortBy="#{facture.objet}"> <p:column headerText="Objet" sortBy="#{facture.objet}">
<h:outputText value="#{facture.objet}"/> <h:outputText value="#{facture.objet}"/>
</p:column> </p:column>
<p:column headerText="Client" sortBy="#{facture.client}"> <p:column headerText="Client" sortBy="#{facture.client}">
<h:outputText value="#{facture.client}"/> <h:outputText value="#{facture.client}"/>
</p:column> </p:column>
<p:column headerText="Date émission" sortBy="#{facture.dateEmission}"> <p:column headerText="Date émission" sortBy="#{facture.dateEmission}">
<h:outputText value="#{facture.dateEmission}"> <h:outputText value="#{facture.dateEmission}">
<f:convertDateTime pattern="dd/MM/yyyy"/> <f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText> </h:outputText>
</p:column> </p:column>
<p:column headerText="Date échéance" sortBy="#{facture.dateEcheance}"> <p:column headerText="Date échéance" sortBy="#{facture.dateEcheance}">
<h:outputText value="#{facture.dateEcheance}"> <h:outputText value="#{facture.dateEcheance}">
<f:convertDateTime pattern="dd/MM/yyyy"/> <f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText> </h:outputText>
<h:outputText value=" ⚠️" rendered="#{factureView.isEnRetard(facture)}" <h:outputText value=" ⚠️" rendered="#{factureView.isEnRetard(facture)}"
title="Facture en retard" style="color: red;"/> title="Facture en retard" style="color: red;"/>
</p:column> </p:column>
<p:column headerText="Montant TTC"> <p:column headerText="Montant TTC">
<h:outputText value="#{facture.montantTTC}"> <h:outputText value="#{facture.montantTTC}">
<f:converter converterId="fcfaConverter"/> <f:converter converterId="fcfaConverter"/>
</h:outputText> </h:outputText>
<h:outputText value=" Fcfa"/> <h:outputText value=" Fcfa"/>
</p:column> </p:column>
<p:column headerText="Montant payé"> <p:column headerText="Montant payé">
<h:outputText value="#{facture.montantPaye}"> <h:outputText value="#{facture.montantPaye}">
<f:converter converterId="fcfaConverter"/> <f:converter converterId="fcfaConverter"/>
</h:outputText> </h:outputText>
<h:outputText value=" Fcfa"/> <h:outputText value=" Fcfa"/>
</p:column> </p:column>
<p:column headerText="Reste à payer"> <p:column headerText="Reste à payer">
<h:outputText value="#{factureView.getMontantRestant(facture)}"> <h:outputText value="#{factureView.getMontantRestant(facture)}">
<f:converter converterId="fcfaConverter"/> <f:converter converterId="fcfaConverter"/>
</h:outputText> </h:outputText>
<h:outputText value=" Fcfa" <h:outputText value=" Fcfa"
style="#{factureView.getMontantRestant(facture) > 0 ? 'color: red; font-weight: bold;' : ''}"/> style="#{factureView.getMontantRestant(facture) > 0 ? 'color: red; font-weight: bold;' : ''}"/>
</p:column> </p:column>
<p:column headerText="Statut" sortBy="#{facture.statut}"> <p:column headerText="Statut" sortBy="#{facture.statut}">
<p:tag value="#{facture.statut}" <p:tag value="#{facture.statut}"
severity="#{facture.statut == 'PAYEE' ? 'success' : (facture.statut == 'ANNULEE' ? 'danger' : (factureView.isEnRetard(facture) ? 'danger' : 'warning'))}"/> severity="#{facture.statut == 'PAYEE' ? 'success' : (facture.statut == 'ANNULEE' ? 'danger' : (factureView.isEnRetard(facture) ? 'danger' : 'warning'))}"/>
</p:column> </p:column>
<p:column headerText="Actions" style="width: 150px;"> <p:column headerText="Actions" style="width: 150px;">
<p:commandButton icon="pi pi-eye" title="Voir les détails" <p:commandButton icon="pi pi-eye" title="Voir les détails"
styleClass="ui-button-text" styleClass="ui-button-text"
action="#{factureView.viewDetails(facture.id)}"/> action="#{factureView.viewDetails(facture.id)}"/>
</p:column> </p:column>
</ui:define> </ui:define>
</ui:include> </ui:include>
</div> </div>
</div> </div>
</div> </div>
</ui:define> </ui:define>
</ui:composition> </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" <ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html" xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core" xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml"> template="/WEB-INF/template.xhtml">
<ui:define name="title">Nouvelle facture - BTP Xpress</ui:define> <ui:define name="title">Nouvelle facture - BTP Xpress</ui:define>
<ui:define name="content"> <ui:define name="content">
<div class="layout-dashboard"> <div class="layout-dashboard">
<div class="grid"> <div class="grid">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<!-- En-tête avec breadcrumb --> <!-- En-tête avec breadcrumb -->
<div class="flex align-items-center justify-content-between mb-4"> <div class="flex align-items-center justify-content-between mb-4">
<div> <div>
<h2 class="text-900 font-bold mb-2">Créer une nouvelle facture</h2> <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> <p class="text-600 mt-0">Émettez une facture pour un chantier ou prestation réalisée</p>
</div> </div>
<p:commandButton value="Retour à la liste" <p:commandButton value="Retour à la liste"
icon="pi pi-arrow-left" icon="pi pi-arrow-left"
outcome="/factures" outcome="/factures"
styleClass="ui-button-secondary ui-button-outlined"/> styleClass="ui-button-secondary ui-button-outlined"/>
</div> </div>
<p:messages id="messages" showDetail="true" closable="true"/> <p:messages id="messages" showDetail="true" closable="true"/>
<h:form id="nouvelleFactureForm" styleClass="p-fluid"> <h:form id="nouvelleFactureForm" styleClass="p-fluid">
<!-- SECTION 1: Informations générales --> <!-- SECTION 1: Informations générales -->
<p:panel header="Informations générales" toggleable="true" collapsed="false" class="mb-4"> <p:panel header="Informations générales" toggleable="true" collapsed="false" class="mb-4">
<div class="formgrid grid"> <div class="formgrid grid">
<!-- Numéro (auto-généré) --> <!-- Numéro (auto-généré) -->
<div class="field col-12 md:col-4"> <div class="field col-12 md:col-4">
<label for="numero" class="font-bold">Numéro de facture</label> <label for="numero" class="font-bold">Numéro de facture</label>
<div class="p-inputgroup"> <div class="p-inputgroup">
<span class="p-inputgroup-addon bg-primary"> <span class="p-inputgroup-addon bg-primary">
<i class="pi pi-hashtag text-white"></i> <i class="pi pi-hashtag text-white"></i>
</span> </span>
<p:inputText id="numero" <p:inputText id="numero"
value="#{factureView.entity.numero}" value="#{factureView.entity.numero}"
disabled="true" disabled="true"
placeholder="Auto-généré" placeholder="Auto-généré"
styleClass="text-center font-bold text-primary"/> styleClass="text-center font-bold text-primary"/>
</div> </div>
<small class="text-600">Généré automatiquement selon la séquence configurée</small> <small class="text-600">Généré automatiquement selon la séquence configurée</small>
</div> </div>
<!-- Statut --> <!-- Statut -->
<div class="field col-12 md:col-4"> <div class="field col-12 md:col-4">
<label for="statut" class="font-bold">Statut <span class="text-red-500">*</span></label> <label for="statut" class="font-bold">Statut <span class="text-red-500">*</span></label>
<p:selectOneMenu id="statut" <p:selectOneMenu id="statut"
value="#{factureView.entity.statut}" value="#{factureView.entity.statut}"
required="true"> required="true">
<f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true"/> <f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true"/>
<f:selectItem itemLabel="Brouillon" itemValue="BROUILLON"/> <f:selectItem itemLabel="Brouillon" itemValue="BROUILLON"/>
<f:selectItem itemLabel="Émise" itemValue="EMISE"/> <f:selectItem itemLabel="Émise" itemValue="EMISE"/>
<f:selectItem itemLabel="Payée" itemValue="PAYEE"/> <f:selectItem itemLabel="Payée" itemValue="PAYEE"/>
<f:selectItem itemLabel="Impayée" itemValue="IMPAYEE"/> <f:selectItem itemLabel="Impayée" itemValue="IMPAYEE"/>
<f:selectItem itemLabel="En retard" itemValue="EN_RETARD"/> <f:selectItem itemLabel="En retard" itemValue="EN_RETARD"/>
<f:selectItem itemLabel="Annulée" itemValue="ANNULEE"/> <f:selectItem itemLabel="Annulée" itemValue="ANNULEE"/>
</p:selectOneMenu> </p:selectOneMenu>
</div> </div>
<!-- Type de facture --> <!-- Type de facture -->
<div class="field col-12 md:col-4"> <div class="field col-12 md:col-4">
<label for="typeFacture" class="font-bold">Type de facture</label> <label for="typeFacture" class="font-bold">Type de facture</label>
<p:selectOneMenu id="typeFacture"> <p:selectOneMenu id="typeFacture">
<f:selectItem itemLabel="Standard" itemValue="STANDARD"/> <f:selectItem itemLabel="Standard" itemValue="STANDARD"/>
<f:selectItem itemLabel="Acompte" itemValue="ACOMPTE"/> <f:selectItem itemLabel="Acompte" itemValue="ACOMPTE"/>
<f:selectItem itemLabel="Solde" itemValue="SOLDE"/> <f:selectItem itemLabel="Solde" itemValue="SOLDE"/>
<f:selectItem itemLabel="Avoir" itemValue="AVOIR"/> <f:selectItem itemLabel="Avoir" itemValue="AVOIR"/>
</p:selectOneMenu> </p:selectOneMenu>
<small class="text-600">Nature de la facturation</small> <small class="text-600">Nature de la facturation</small>
</div> </div>
<!-- Client --> <!-- Client -->
<div class="field col-12 md:col-8"> <div class="field col-12 md:col-8">
<label for="client" class="font-bold">Client <span class="text-red-500">*</span></label> <label for="client" class="font-bold">Client <span class="text-red-500">*</span></label>
<p:inputText id="client" <p:inputText id="client"
value="#{factureView.entity.client}" value="#{factureView.entity.client}"
required="true" required="true"
requiredMessage="Le client est obligatoire" requiredMessage="Le client est obligatoire"
placeholder="Ex: Entreprise ABC SARL"> placeholder="Ex: Entreprise ABC SARL">
<f:validateLength minimum="2" maximum="200"/> <f:validateLength minimum="2" maximum="200"/>
</p:inputText> </p:inputText>
<small class="text-600">Nom du client ou de l'entreprise</small> <small class="text-600">Nom du client ou de l'entreprise</small>
</div> </div>
<!-- Date d'émission --> <!-- Date d'émission -->
<div class="field col-12 md:col-4"> <div class="field col-12 md:col-4">
<label for="dateEmission" class="font-bold">Date d'émission <span class="text-red-500">*</span></label> <label for="dateEmission" class="font-bold">Date d'émission <span class="text-red-500">*</span></label>
<p:calendar id="dateEmission" <p:calendar id="dateEmission"
value="#{factureView.entity.dateEmission}" value="#{factureView.entity.dateEmission}"
pattern="dd/MM/yyyy" pattern="dd/MM/yyyy"
locale="fr" locale="fr"
required="true" required="true"
requiredMessage="La date d'émission est obligatoire" requiredMessage="La date d'émission est obligatoire"
showIcon="true" showIcon="true"
showButtonBar="true" showButtonBar="true"
monthNavigator="true" monthNavigator="true"
yearNavigator="true" yearNavigator="true"
yearRange="2020:2030" yearRange="2020:2030"
placeholder="Sélectionner une date"> placeholder="Sélectionner une date">
</p:calendar> </p:calendar>
</div> </div>
<!-- Objet --> <!-- Objet -->
<div class="field col-12"> <div class="field col-12">
<label for="objet" class="font-bold">Objet de la facture <span class="text-red-500">*</span></label> <label for="objet" class="font-bold">Objet de la facture <span class="text-red-500">*</span></label>
<p:inputTextarea id="objet" <p:inputTextarea id="objet"
value="#{factureView.entity.objet}" value="#{factureView.entity.objet}"
required="true" required="true"
requiredMessage="L'objet de la facture est obligatoire" requiredMessage="L'objet de la facture est obligatoire"
rows="2" rows="2"
placeholder="Ex: Travaux de construction immeuble R+3 - Phase gros œuvre" placeholder="Ex: Travaux de construction immeuble R+3 - Phase gros œuvre"
autoResize="false"> autoResize="false">
<f:validateLength minimum="10" maximum="500"/> <f:validateLength minimum="10" maximum="500"/>
</p:inputTextarea> </p:inputTextarea>
<small class="text-600">Description des prestations facturées</small> <small class="text-600">Description des prestations facturées</small>
</div> </div>
<!-- Date d'échéance --> <!-- Date d'échéance -->
<div class="field col-12 md:col-6"> <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> <label for="dateEcheance" class="font-bold">Date d'échéance <span class="text-red-500">*</span></label>
<p:calendar id="dateEcheance" <p:calendar id="dateEcheance"
value="#{factureView.entity.dateEcheance}" value="#{factureView.entity.dateEcheance}"
pattern="dd/MM/yyyy" pattern="dd/MM/yyyy"
locale="fr" locale="fr"
required="true" required="true"
requiredMessage="La date d'échéance est obligatoire" requiredMessage="La date d'échéance est obligatoire"
showIcon="true" showIcon="true"
showButtonBar="true" showButtonBar="true"
monthNavigator="true" monthNavigator="true"
yearNavigator="true" yearNavigator="true"
yearRange="2020:2035" yearRange="2020:2035"
mindate="#{factureView.entity.dateEmission}" mindate="#{factureView.entity.dateEmission}"
placeholder="Sélectionner une date"> placeholder="Sélectionner une date">
</p:calendar> </p:calendar>
<small class="text-600">Date limite de paiement (généralement 30 jours)</small> <small class="text-600">Date limite de paiement (généralement 30 jours)</small>
</div> </div>
<!-- Date de paiement (optionnel) --> <!-- Date de paiement (optionnel) -->
<div class="field col-12 md:col-6"> <div class="field col-12 md:col-6">
<label for="datePaiement" class="font-bold">Date de paiement</label> <label for="datePaiement" class="font-bold">Date de paiement</label>
<p:calendar id="datePaiement" <p:calendar id="datePaiement"
value="#{factureView.entity.datePaiement}" value="#{factureView.entity.datePaiement}"
pattern="dd/MM/yyyy" pattern="dd/MM/yyyy"
locale="fr" locale="fr"
showIcon="true" showIcon="true"
showButtonBar="true" showButtonBar="true"
monthNavigator="true" monthNavigator="true"
yearNavigator="true" yearNavigator="true"
yearRange="2020:2030" yearRange="2020:2030"
placeholder="À renseigner après paiement"> placeholder="À renseigner après paiement">
</p:calendar> </p:calendar>
<small class="text-600">Date effective du paiement (optionnel)</small> <small class="text-600">Date effective du paiement (optionnel)</small>
</div> </div>
</div> </div>
</p:panel> </p:panel>
<!-- SECTION 2: Lignes de facture --> <!-- SECTION 2: Lignes de facture -->
<p:panel header="Détail de la facture" toggleable="true" collapsed="false" class="mb-4"> <p:panel header="Détail de la facture" toggleable="true" collapsed="false" class="mb-4">
<div class="mb-3"> <div class="mb-3">
<div class="surface-100 border-round p-3"> <div class="surface-100 border-round p-3">
<div class="flex align-items-center gap-2 mb-2"> <div class="flex align-items-center gap-2 mb-2">
<i class="pi pi-info-circle text-blue-500"></i> <i class="pi pi-info-circle text-blue-500"></i>
<span class="text-900 font-medium">Lignes de facturation</span> <span class="text-900 font-medium">Lignes de facturation</span>
</div> </div>
<p class="text-600 text-sm mt-0 mb-0"> <p class="text-600 text-sm mt-0 mb-0">
Ajoutez les différentes prestations, fournitures et quantités facturées. Ajoutez les différentes prestations, fournitures et quantités facturées.
Cette fonctionnalité sera disponible dans une prochaine version. Cette fonctionnalité sera disponible dans une prochaine version.
</p> </p>
</div> </div>
</div> </div>
<!-- Placeholder pour table de lignes --> <!-- Placeholder pour table de lignes -->
<div class="surface-50 border-round p-4 text-center"> <div class="surface-50 border-round p-4 text-center">
<i class="pi pi-list text-400 mb-3" style="font-size: 3rem;"></i> <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-600 mt-0 mb-3">Gestion des lignes de facture en cours de développement</p>
<p class="text-500 text-sm"> <p class="text-500 text-sm">
Bientôt disponible: ajout de lignes avec désignation, quantité, prix unitaire, remise, etc. Bientôt disponible: ajout de lignes avec désignation, quantité, prix unitaire, remise, etc.
</p> </p>
</div> </div>
</p:panel> </p:panel>
<!-- SECTION 3: Montants --> <!-- SECTION 3: Montants -->
<p:panel header="Montants et totaux" toggleable="true" collapsed="false" class="mb-4"> <p:panel header="Montants et totaux" toggleable="true" collapsed="false" class="mb-4">
<div class="formgrid grid"> <div class="formgrid grid">
<!-- Montant HT --> <!-- Montant HT -->
<div class="field col-12 md:col-6"> <div class="field col-12 md:col-6">
<label for="montantHT" class="font-bold">Montant HT (FCFA) <span class="text-red-500">*</span></label> <label for="montantHT" class="font-bold">Montant HT (FCFA) <span class="text-red-500">*</span></label>
<p:inputNumber id="montantHT" <p:inputNumber id="montantHT"
value="#{factureView.entity.montantHT}" value="#{factureView.entity.montantHT}"
required="true" required="true"
requiredMessage="Le montant HT est obligatoire" requiredMessage="Le montant HT est obligatoire"
minValue="0" minValue="0"
decimalPlaces="0" decimalPlaces="0"
thousandSeparator=" " thousandSeparator=" "
suffix=" FCFA" suffix=" FCFA"
placeholder="0"> placeholder="0">
</p:inputNumber> </p:inputNumber>
<small class="text-600">Montant hors taxes</small> <small class="text-600">Montant hors taxes</small>
</div> </div>
<!-- TVA (calculée) --> <!-- TVA (calculée) -->
<div class="field col-12 md:col-6"> <div class="field col-12 md:col-6">
<label class="font-bold">TVA (18%)</label> <label class="font-bold">TVA (18%)</label>
<div class="p-inputgroup"> <div class="p-inputgroup">
<span class="p-inputgroup-addon bg-orange-100"> <span class="p-inputgroup-addon bg-orange-100">
<i class="pi pi-percentage text-orange-600"></i> <i class="pi pi-percentage text-orange-600"></i>
</span> </span>
<p:inputNumber value="#{factureView.entity.montantHT * 0.18}" <p:inputNumber value="#{factureView.entity.montantHT * 0.18}"
disabled="true" disabled="true"
decimalPlaces="0" decimalPlaces="0"
thousandSeparator=" " thousandSeparator=" "
suffix=" FCFA" suffix=" FCFA"
styleClass="text-center font-medium text-orange-600"/> styleClass="text-center font-medium text-orange-600"/>
</div> </div>
<small class="text-600">Calculé automatiquement (18% du montant HT)</small> <small class="text-600">Calculé automatiquement (18% du montant HT)</small>
</div> </div>
<!-- Montant TTC (calculé) --> <!-- Montant TTC (calculé) -->
<div class="field col-12"> <div class="field col-12">
<label class="font-bold">Montant TTC (FCFA)</label> <label class="font-bold">Montant TTC (FCFA)</label>
<div class="p-inputgroup"> <div class="p-inputgroup">
<span class="p-inputgroup-addon bg-primary"> <span class="p-inputgroup-addon bg-primary">
<i class="pi pi-dollar text-white"></i> <i class="pi pi-dollar text-white"></i>
</span> </span>
<p:inputNumber value="#{factureView.entity.montantHT * 1.18}" <p:inputNumber value="#{factureView.entity.montantHT * 1.18}"
disabled="true" disabled="true"
decimalPlaces="0" decimalPlaces="0"
thousandSeparator=" " thousandSeparator=" "
suffix=" FCFA" suffix=" FCFA"
styleClass="text-center font-bold text-xl text-primary"/> styleClass="text-center font-bold text-xl text-primary"/>
</div> </div>
<small class="text-600">Montant toutes taxes comprises (HT + TVA)</small> <small class="text-600">Montant toutes taxes comprises (HT + TVA)</small>
</div> </div>
<!-- Montant payé --> <!-- Montant payé -->
<div class="field col-12 md:col-6"> <div class="field col-12 md:col-6">
<label for="montantPaye" class="font-bold">Montant payé (FCFA)</label> <label for="montantPaye" class="font-bold">Montant payé (FCFA)</label>
<p:inputNumber id="montantPaye" <p:inputNumber id="montantPaye"
value="#{factureView.entity.montantPaye}" value="#{factureView.entity.montantPaye}"
minValue="0" minValue="0"
decimalPlaces="0" decimalPlaces="0"
thousandSeparator=" " thousandSeparator=" "
suffix=" FCFA" suffix=" FCFA"
placeholder="0"> placeholder="0">
</p:inputNumber> </p:inputNumber>
<small class="text-600">Montant déjà encaissé</small> <small class="text-600">Montant déjà encaissé</small>
</div> </div>
<!-- Montant restant (calculé) --> <!-- Montant restant (calculé) -->
<div class="field col-12 md:col-6"> <div class="field col-12 md:col-6">
<label class="font-bold">Montant restant (FCFA)</label> <label class="font-bold">Montant restant (FCFA)</label>
<div class="p-inputgroup"> <div class="p-inputgroup">
<span class="p-inputgroup-addon"> <span class="p-inputgroup-addon">
<i class="pi pi-exclamation-triangle"></i> <i class="pi pi-exclamation-triangle"></i>
</span> </span>
<p:inputNumber value="#{(factureView.entity.montantHT * 1.18) - factureView.entity.montantPaye}" <p:inputNumber value="#{(factureView.entity.montantHT * 1.18) - factureView.entity.montantPaye}"
disabled="true" disabled="true"
decimalPlaces="0" decimalPlaces="0"
thousandSeparator=" " thousandSeparator=" "
suffix=" FCFA" suffix=" FCFA"
styleClass="text-center font-bold #{((factureView.entity.montantHT * 1.18) - factureView.entity.montantPaye) > 0 ? 'text-red-600' : 'text-green-600'}"/> styleClass="text-center font-bold #{((factureView.entity.montantHT * 1.18) - factureView.entity.montantPaye) > 0 ? 'text-red-600' : 'text-green-600'}"/>
</div> </div>
<small class="text-600">Reste à encaisser</small> <small class="text-600">Reste à encaisser</small>
</div> </div>
<!-- Récapitulatif visuel --> <!-- Récapitulatif visuel -->
<div class="field col-12"> <div class="field col-12">
<div class="surface-100 border-round p-3"> <div class="surface-100 border-round p-3">
<div class="grid"> <div class="grid">
<div class="col-12 md:col-3"> <div class="col-12 md:col-3">
<div class="text-center"> <div class="text-center">
<span class="text-600 text-sm block mb-2">Montant HT</span> <span class="text-600 text-sm block mb-2">Montant HT</span>
<div class="text-900 font-bold text-xl"> <div class="text-900 font-bold text-xl">
<ui:include src="/WEB-INF/components/monetary-display.xhtml"> <ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{factureView.entity.montantHT}"/> <ui:param name="amount" value="#{factureView.entity.montantHT}"/>
<ui:param name="size" value="normal"/> <ui:param name="size" value="normal"/>
</ui:include> </ui:include>
</div> </div>
</div> </div>
</div> </div>
<div class="col-12 md:col-3"> <div class="col-12 md:col-3">
<div class="text-center"> <div class="text-center">
<span class="text-600 text-sm block mb-2">TVA (18%)</span> <span class="text-600 text-sm block mb-2">TVA (18%)</span>
<div class="text-orange-600 font-bold text-xl"> <div class="text-orange-600 font-bold text-xl">
<ui:include src="/WEB-INF/components/monetary-display.xhtml"> <ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{factureView.entity.montantHT * 0.18}"/> <ui:param name="amount" value="#{factureView.entity.montantHT * 0.18}"/>
<ui:param name="size" value="normal"/> <ui:param name="size" value="normal"/>
</ui:include> </ui:include>
</div> </div>
</div> </div>
</div> </div>
<div class="col-12 md:col-3"> <div class="col-12 md:col-3">
<div class="text-center"> <div class="text-center">
<span class="text-600 text-sm block mb-2">Total TTC</span> <span class="text-600 text-sm block mb-2">Total TTC</span>
<div class="text-primary font-bold text-2xl"> <div class="text-primary font-bold text-2xl">
<ui:include src="/WEB-INF/components/monetary-display.xhtml"> <ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{factureView.entity.montantHT * 1.18}"/> <ui:param name="amount" value="#{factureView.entity.montantHT * 1.18}"/>
<ui:param name="size" value="large"/> <ui:param name="size" value="large"/>
<ui:param name="bold" value="true"/> <ui:param name="bold" value="true"/>
</ui:include> </ui:include>
</div> </div>
</div> </div>
</div> </div>
<div class="col-12 md:col-3"> <div class="col-12 md:col-3">
<div class="text-center"> <div class="text-center">
<span class="text-600 text-sm block mb-2">Reste à payer</span> <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'}"> <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:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{(factureView.entity.montantHT * 1.18) - factureView.entity.montantPaye}"/> <ui:param name="amount" value="#{(factureView.entity.montantHT * 1.18) - factureView.entity.montantPaye}"/>
<ui:param name="size" value="large"/> <ui:param name="size" value="large"/>
<ui:param name="bold" value="true"/> <ui:param name="bold" value="true"/>
</ui:include> </ui:include>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</p:panel> </p:panel>
<!-- SECTION 4: Informations de paiement --> <!-- SECTION 4: Informations de paiement -->
<p:panel header="Informations de paiement" toggleable="true" collapsed="true" class="mb-4"> <p:panel header="Informations de paiement" toggleable="true" collapsed="true" class="mb-4">
<div class="formgrid grid"> <div class="formgrid grid">
<div class="field col-12 md:col-6"> <div class="field col-12 md:col-6">
<label for="modePaiement" class="font-bold">Mode de paiement</label> <label for="modePaiement" class="font-bold">Mode de paiement</label>
<p:selectOneMenu id="modePaiement"> <p:selectOneMenu id="modePaiement">
<f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true"/> <f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true"/>
<f:selectItem itemLabel="Virement bancaire" itemValue="VIREMENT"/> <f:selectItem itemLabel="Virement bancaire" itemValue="VIREMENT"/>
<f:selectItem itemLabel="Chèque" itemValue="CHEQUE"/> <f:selectItem itemLabel="Chèque" itemValue="CHEQUE"/>
<f:selectItem itemLabel="Espèces" itemValue="ESPECES"/> <f:selectItem itemLabel="Espèces" itemValue="ESPECES"/>
<f:selectItem itemLabel="Carte bancaire" itemValue="CARTE"/> <f:selectItem itemLabel="Carte bancaire" itemValue="CARTE"/>
<f:selectItem itemLabel="Mobile Money" itemValue="MOBILE_MONEY"/> <f:selectItem itemLabel="Mobile Money" itemValue="MOBILE_MONEY"/>
</p:selectOneMenu> </p:selectOneMenu>
</div> </div>
<div class="field col-12 md:col-6"> <div class="field col-12 md:col-6">
<label for="referencePaiement" class="font-bold">Référence de paiement</label> <label for="referencePaiement" class="font-bold">Référence de paiement</label>
<p:inputText id="referencePaiement" <p:inputText id="referencePaiement"
placeholder="Ex: Virement du 15/01/2025"/> placeholder="Ex: Virement du 15/01/2025"/>
</div> </div>
<div class="field col-12"> <div class="field col-12">
<label for="conditionsPaiement" class="font-bold">Conditions de paiement</label> <label for="conditionsPaiement" class="font-bold">Conditions de paiement</label>
<p:inputTextarea id="conditionsPaiement" <p:inputTextarea id="conditionsPaiement"
rows="3" rows="3"
placeholder="Ex: Paiement à 30 jours fin de mois, escompte 2% si paiement sous 8 jours" placeholder="Ex: Paiement à 30 jours fin de mois, escompte 2% si paiement sous 8 jours"
autoResize="false"> autoResize="false">
</p:inputTextarea> </p:inputTextarea>
</div> </div>
</div> </div>
</p:panel> </p:panel>
<!-- Boutons d'action --> <!-- Boutons d'action -->
<div class="flex align-items-center justify-content-between pt-4 border-top-1 surface-border"> <div class="flex align-items-center justify-content-between pt-4 border-top-1 surface-border">
<div> <div>
<span class="text-600 text-sm">Les champs marqués d'un </span> <span class="text-600 text-sm">Les champs marqués d'un </span>
<span class="text-red-500 font-bold">*</span> <span class="text-red-500 font-bold">*</span>
<span class="text-600 text-sm"> sont obligatoires</span> <span class="text-600 text-sm"> sont obligatoires</span>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<p:commandButton value="Annuler" <p:commandButton value="Annuler"
icon="pi pi-times" icon="pi pi-times"
action="/factures?faces-redirect=true" action="/factures?faces-redirect=true"
styleClass="ui-button-secondary" styleClass="ui-button-secondary"
immediate="true"/> immediate="true"/>
<p:commandButton value="Enregistrer comme brouillon" <p:commandButton value="Enregistrer comme brouillon"
icon="pi pi-save" icon="pi pi-save"
action="#{factureView.save}" action="#{factureView.save}"
update="@form messages" update="@form messages"
oncomplete="if (args &amp;&amp; !args.validationFailed) window.location.href='/factures.xhtml';" oncomplete="if (args &amp;&amp; !args.validationFailed) window.location.href='/factures.xhtml';"
styleClass="ui-button-secondary"/> styleClass="ui-button-secondary"/>
<p:commandButton value="Émettre la facture" <p:commandButton value="Émettre la facture"
icon="pi pi-send" icon="pi pi-send"
action="#{factureView.save}" action="#{factureView.save}"
update="@form messages" update="@form messages"
oncomplete="if (args &amp;&amp; !args.validationFailed) window.location.href='/factures.xhtml';" oncomplete="if (args &amp;&amp; !args.validationFailed) window.location.href='/factures.xhtml';"
styleClass="ui-button-primary"/> styleClass="ui-button-primary"/>
</div> </div>
</div> </div>
</h:form> </h:form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</ui:define> </ui:define>
</ui:composition> </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" <ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html" xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core" xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml"> template="/WEB-INF/template.xhtml">
<ui:define name="title">Fournisseurs - BTP Xpress</ui:define> <ui:define name="title">Fournisseurs - BTP Xpress</ui:define>
<ui:define name="content"> <ui:define name="content">
<div class="layout-dashboard"> <div class="layout-dashboard">
<div class="grid"> <div class="grid">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<div class="card-title"> <div class="card-title">
<h6>Fournisseurs</h6> <h6>Fournisseurs</h6>
<p class="subtitle">Gestion des fournisseurs</p> <p class="subtitle">Gestion des fournisseurs</p>
</div> </div>
</div> </div>
<p>Page en développement</p> <p>Page en développement</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</ui:define> </ui:define>
</ui:composition> </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> <!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" <html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html" xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core" xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:p="http://primefaces.org/ui"
lang="fr"> lang="fr">
<h:head> <h:head>
<f:facet name="first"> <f:facet name="first">
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <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"/> <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"/> <link rel="icon" href="#{request.contextPath}/resources/freya-layout/images/favicon.ico" type="image/x-icon"/>
</f:facet> </f:facet>
<title>Connexion - BTP Xpress</title> <title>Connexion - BTP Xpress</title>
<h:outputStylesheet name="css/primeicons.css" library="freya-layout" /> <h:outputStylesheet name="css/primeicons.css" library="freya-layout" />
<h:outputStylesheet name="css/primeflex.min.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/layout-light.css" library="freya-layout" />
<h:outputStylesheet name="css/freya-purple-light.css" library="freya-layout" /> <h:outputStylesheet name="css/freya-purple-light.css" library="freya-layout" />
</h:head> </h:head>
<h:body class="login-body"> <h:body class="login-body">
<div class="login-wrapper"> <div class="login-wrapper">
<div class="login-container"> <div class="login-container">
<div class="login-left"> <div class="login-left">
<div class="login-content"> <div class="login-content">
<h1>BTP Xpress</h1> <h1>BTP Xpress</h1>
<p class="subtitle">Votre plateforme de gestion BTP</p> <p class="subtitle">Votre plateforme de gestion BTP</p>
<p>Gérez vos projets, clients, matériels et équipes en toute simplicité.</p> <p>Gérez vos projets, clients, matériels et équipes en toute simplicité.</p>
</div> </div>
</div> </div>
<div class="login-right"> <div class="login-right">
<div class="login-box"> <div class="login-box">
<h2>Connexion</h2> <h2>Connexion</h2>
<p class="login-subtitle">Connectez-vous à votre compte</p> <p class="login-subtitle">Connectez-vous à votre compte</p>
<h:form id="loginForm"> <h:form id="loginForm">
<div class="login-input-group"> <div class="login-input-group">
<label for="username">Nom d'utilisateur ou email</label> <label for="username">Nom d'utilisateur ou email</label>
<p:inputText id="username" <p:inputText id="username"
value="#{loginView.username}" value="#{loginView.username}"
placeholder="Votre nom d'utilisateur" placeholder="Votre nom d'utilisateur"
required="true" required="true"
requiredMessage="Le nom d'utilisateur est requis" requiredMessage="Le nom d'utilisateur est requis"
styleClass="ui-input-filled" styleClass="ui-input-filled"
style="width: 100%;"/> style="width: 100%;"/>
</div> </div>
<div class="login-input-group"> <div class="login-input-group">
<label for="password">Mot de passe</label> <label for="password">Mot de passe</label>
<p:password id="password" <p:password id="password"
value="#{loginView.password}" value="#{loginView.password}"
placeholder="Votre mot de passe" placeholder="Votre mot de passe"
required="true" required="true"
requiredMessage="Le mot de passe est requis" requiredMessage="Le mot de passe est requis"
feedback="false" feedback="false"
toggleMask="true" toggleMask="true"
styleClass="ui-input-filled" styleClass="ui-input-filled"
style="width: 100%;"/> style="width: 100%;"/>
</div> </div>
<div class="login-options"> <div class="login-options">
<p:selectBooleanCheckbox id="rememberMe" <p:selectBooleanCheckbox id="rememberMe"
value="#{loginView.rememberMe}" value="#{loginView.rememberMe}"
label="Se souvenir de moi"/> label="Se souvenir de moi"/>
<a href="#" style="text-decoration: none; color: var(--primary-color);">Mot de passe oublié ?</a> <a href="#" style="text-decoration: none; color: var(--primary-color);">Mot de passe oublié ?</a>
</div> </div>
<p:commandButton value="Se connecter" <p:commandButton value="Se connecter"
icon="pi pi-sign-in" icon="pi pi-sign-in"
action="#{loginView.login()}" action="#{loginView.login()}"
style="width: 100%; margin-top: 1rem;" style="width: 100%; margin-top: 1rem;"
update="@form" update="@form"
process="@form"/> process="@form"/>
<p:messages id="messages" showDetail="true" closable="true"/> <p:messages id="messages" showDetail="true" closable="true"/>
</h:form> </h:form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</h:body> </h:body>
</html> </html>

View File

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

View File

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

View File

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

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