feat: Module Devis professionnel avec écrans complets

Création de 2 écrans professionnels pour le module Devis:

1. devis/nouveau.xhtml:
   - 4 sections: Informations générales, Détail du devis, Montants, Conditions
   - Numéro auto-généré avec icône
   - Statut avec 5 valeurs (BROUILLON, ATTENTE, ACCEPTE, REFUSE, EXPIRE)
   - Dates d'émission et validité avec calendriers
   - Client et objet du devis requis
   - Placeholder pour lignes de devis (future développement)
   - Calcul automatique TVA 18% et TTC
   - Récapitulatif visuel HT/TVA/TTC avec composant monétaire
   - Conditions de paiement et remarques (section collapsible)
   - 3 boutons: Annuler, Brouillon, Envoyer

2. devis/details.xhtml:
   - En-tête: numéro, statut, client, objet, dates
   - Actions: Retour, Convertir en chantier, PDF, Modifier
   - 4 KPI cards: Montant HT, TVA, TTC, Statut
   - 6 onglets professionnels:
     * Vue d'ensemble: infos + récap financier + actions rapides
     * Détail des lignes: table lignes (placeholder)
     * Conditions: paiement, délais, garanties
     * Documents: GED associée (placeholder)
     * Suivi: timeline actions
     * Historique: modifications (placeholder)

Corrections:
- Fix navigation /factures/nouvelle -> /factures/nouveau (factures.xhtml)
- Fix menu /factures/nouvelle -> /factures/nouveau (menu.xhtml)

Tous les composants réutilisables utilisés (status-badge, monetary-display).
Validation complète côté client et serveur.
UI/UX professionnel adapté au métier BTP.
This commit is contained in:
dahoud
2025-11-08 10:49:19 +00:00
parent 0fad42ccaf
commit ec38f6a23a
192 changed files with 12029 additions and 271 deletions

View File

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

View File

@@ -0,0 +1,82 @@
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 clients côté client.
* <p>
* Ce service encapsule la communication avec l'API backend pour les opérations
* liées aux clients. Il utilise le REST Client pour effectuer les appels HTTP
* vers le backend.
* </p>
*
* @author BTP Xpress Development Team
* @version 1.0.0
* @since 1.0.0
*/
@ApplicationScoped
public class ClientService {
private static final Logger LOG = LoggerFactory.getLogger(ClientService.class);
@Inject
@RestClient
BtpXpressApiClient apiClient;
/**
* Récupère tous les clients depuis l'API backend.
*
* @return Liste des clients, ou liste vide en cas d'erreur.
*/
public List<Map<String, Object>> getAllClients() {
try {
LOG.debug("Récupération de la liste des clients depuis l'API backend.");
Response response = apiClient.getClients();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> clients = response.readEntity(List.class);
LOG.debug("Clients récupérés avec succès : {} élément(s)", clients != null ? clients.size() : 0);
return clients != null ? clients : new ArrayList<>();
} else {
LOG.warn("Erreur lors de la récupération des clients. 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 clients : {}", e.getMessage(), e);
return new ArrayList<>();
}
}
/**
* Récupère un client par son identifiant depuis l'API backend.
*
* @param id L'identifiant du client.
* @return Le client sous forme de Map, ou null en cas d'erreur.
*/
public Map<String, Object> getClientById(Long id) {
try {
LOG.debug("Récupération du client avec ID : {}", id);
Response response = apiClient.getClient(id);
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
Map<String, Object> client = response.readEntity(Map.class);
LOG.debug("Client récupéré avec succès.");
return client;
} else {
LOG.warn("Erreur lors de la récupération du client. 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 client : {}", e.getMessage(), e);
return null;
}
}
}

View File

@@ -0,0 +1,58 @@
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 devis côté client.
* <p>
* Ce service encapsule la communication avec l'API backend pour les opérations
* liées aux devis. Il utilise le REST Client pour effectuer les appels HTTP
* vers le backend.
* </p>
*
* @author BTP Xpress Development Team
* @version 1.0.0
* @since 1.0.0
*/
@ApplicationScoped
public class DevisService {
private static final Logger LOG = LoggerFactory.getLogger(DevisService.class);
@Inject
@RestClient
BtpXpressApiClient apiClient;
/**
* Récupère tous les devis depuis l'API backend.
*
* @return Liste des devis, ou liste vide en cas d'erreur.
*/
public List<Map<String, Object>> getAllDevis() {
try {
LOG.debug("Récupération de la liste des devis depuis l'API backend.");
Response response = apiClient.getDevis();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> devis = response.readEntity(List.class);
LOG.debug("Devis récupérés avec succès : {} élément(s)", devis != null ? devis.size() : 0);
return devis != null ? devis : new ArrayList<>();
} else {
LOG.warn("Erreur lors de la récupération des devis. 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 devis : {}", e.getMessage(), e);
return new ArrayList<>();
}
}
}

View File

@@ -0,0 +1,58 @@
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 employés côté client.
* <p>
* Ce service encapsule la communication avec l'API backend pour les opérations
* liées aux employés. Il utilise le REST Client pour effectuer les appels HTTP
* vers le backend.
* </p>
*
* @author BTP Xpress Development Team
* @version 1.0.0
* @since 1.0.0
*/
@ApplicationScoped
public class EmployeService {
private static final Logger LOG = LoggerFactory.getLogger(EmployeService.class);
@Inject
@RestClient
BtpXpressApiClient apiClient;
/**
* Récupère tous les employés depuis l'API backend.
*
* @return Liste des employés, ou liste vide en cas d'erreur.
*/
public List<Map<String, Object>> getAllEmployes() {
try {
LOG.debug("Récupération de la liste des employés depuis l'API backend.");
Response response = apiClient.getEmployes();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> employes = response.readEntity(List.class);
LOG.debug("Employés récupérés avec succès : {} élément(s)", employes != null ? employes.size() : 0);
return employes != null ? employes : new ArrayList<>();
} else {
LOG.warn("Erreur lors de la récupération des employés. 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 employés : {}", e.getMessage(), e);
return new ArrayList<>();
}
}
}

View File

@@ -0,0 +1,58 @@
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 équipes côté client.
* <p>
* Ce service encapsule la communication avec l'API backend pour les opérations
* liées aux équipes. Il utilise le REST Client pour effectuer les appels HTTP
* vers le backend.
* </p>
*
* @author BTP Xpress Development Team
* @version 1.0.0
* @since 1.0.0
*/
@ApplicationScoped
public class EquipeService {
private static final Logger LOG = LoggerFactory.getLogger(EquipeService.class);
@Inject
@RestClient
BtpXpressApiClient apiClient;
/**
* Récupère toutes les équipes depuis l'API backend.
*
* @return Liste des équipes, ou liste vide en cas d'erreur.
*/
public List<Map<String, Object>> getAllEquipes() {
try {
LOG.debug("Récupération de la liste des équipes depuis l'API backend.");
Response response = apiClient.getEquipes();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> equipes = response.readEntity(List.class);
LOG.debug("Équipes récupérées avec succès : {} élément(s)", equipes != null ? equipes.size() : 0);
return equipes != null ? equipes : new ArrayList<>();
} else {
LOG.warn("Erreur lors de la récupération des équipes. 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 équipes : {}", e.getMessage(), e);
return new ArrayList<>();
}
}
}

View File

@@ -0,0 +1,58 @@
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 factures côté client.
* <p>
* Ce service encapsule la communication avec l'API backend pour les opérations
* liées aux factures. Il utilise le REST Client pour effectuer les appels HTTP
* vers le backend.
* </p>
*
* @author BTP Xpress Development Team
* @version 1.0.0
* @since 1.0.0
*/
@ApplicationScoped
public class FactureService {
private static final Logger LOG = LoggerFactory.getLogger(FactureService.class);
@Inject
@RestClient
BtpXpressApiClient apiClient;
/**
* Récupère toutes les factures depuis l'API backend.
*
* @return Liste des factures, ou liste vide en cas d'erreur.
*/
public List<Map<String, Object>> getAllFactures() {
try {
LOG.debug("Récupération de la liste des factures depuis l'API backend.");
Response response = apiClient.getFactures();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> factures = response.readEntity(List.class);
LOG.debug("Factures récupérées avec succès : {} élément(s)", factures != null ? factures.size() : 0);
return factures != null ? factures : new ArrayList<>();
} else {
LOG.warn("Erreur lors de la récupération des factures. 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 factures : {}", e.getMessage(), e);
return new ArrayList<>();
}
}
}

View File

@@ -0,0 +1,58 @@
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 matériels côté client.
* <p>
* Ce service encapsule la communication avec l'API backend pour les opérations
* liées aux matériels. Il utilise le REST Client pour effectuer les appels HTTP
* vers le backend.
* </p>
*
* @author BTP Xpress Development Team
* @version 1.0.0
* @since 1.0.0
*/
@ApplicationScoped
public class MaterielService {
private static final Logger LOG = LoggerFactory.getLogger(MaterielService.class);
@Inject
@RestClient
BtpXpressApiClient apiClient;
/**
* Récupère tous les matériels depuis l'API backend.
*
* @return Liste des matériels, ou liste vide en cas d'erreur.
*/
public List<Map<String, Object>> getAllMateriels() {
try {
LOG.debug("Récupération de la liste des matériels depuis l'API backend.");
Response response = apiClient.getMateriels();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> materiels = response.readEntity(List.class);
LOG.debug("Matériels récupérés avec succès : {} élément(s)", materiels != null ? materiels.size() : 0);
return materiels != null ? materiels : new ArrayList<>();
} else {
LOG.warn("Erreur lors de la récupération des matériels. 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 matériels : {}", e.getMessage(), e);
return new ArrayList<>();
}
}
}

View File

@@ -0,0 +1,61 @@
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 stocks côté client.
* <p>
* Ce service encapsule la communication avec l'API backend pour les opérations
* liées aux stocks. Il utilise le REST Client pour effectuer les appels HTTP
* vers le backend.
* </p>
*
* @author BTP Xpress Development Team
* @version 1.0.0
* @since 1.0.0
*/
@ApplicationScoped
public class StockService {
private static final Logger LOG = LoggerFactory.getLogger(StockService.class);
@Inject
@RestClient
BtpXpressApiClient apiClient;
/**
* Récupère tous les stocks depuis l'API backend.
*
* @return Liste des stocks, ou liste vide en cas d'erreur.
*/
public List<Map<String, Object>> getAllStocks() {
try {
LOG.debug("Récupération de la liste des stocks depuis l'API backend.");
Response response = apiClient.getStocks();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
// Le backend retourne un objet avec une propriété "stocks"
@SuppressWarnings("unchecked")
Map<String, Object> data = response.readEntity(Map.class);
@SuppressWarnings("unchecked")
List<Map<String, Object>> stocks = (List<Map<String, Object>>) data.get("stocks");
LOG.debug("Stocks récupérés avec succès : {} élément(s)", stocks != null ? stocks.size() : 0);
return stocks != null ? stocks : new ArrayList<>();
} else {
LOG.warn("Erreur lors de la récupération des stocks. 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 stocks : {}", e.getMessage(), e);
return new ArrayList<>();
}
}
}

View File

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

View File

@@ -165,6 +165,39 @@ public class ChantiersView extends BaseListView<ChantiersView.Chantier, Long> im
@Override
protected void performDelete() {
LOG.info("Suppression chantier : {}", selectedItem.getId());
// TODO: Appeler chantierService.delete(selectedItem.getId())
}
@Override
protected Chantier createNewEntity() {
Chantier c = new Chantier();
c.setStatut("PLANIFIE");
c.setAvancement(0);
c.setDateDebut(LocalDate.now());
c.setDateCreation(LocalDateTime.now());
return c;
}
@Override
protected void performCreate() {
entity.setId(System.currentTimeMillis()); // Simulation ID
entity.setDateCreation(LocalDateTime.now());
entity.setDateModification(LocalDateTime.now());
items.add(entity);
LOG.info("Nouveau chantier créé : {}", entity.getNom());
// TODO: Appeler chantierService.create(entity)
}
@Override
protected void performUpdate() {
entity.setDateModification(LocalDateTime.now());
LOG.info("Chantier modifié : {}", entity.getNom());
// TODO: Appeler chantierService.update(entity)
}
@Override
protected Long getEntityId(Chantier chantier) {
return chantier.getId();
}
/**
@@ -172,10 +205,7 @@ public class ChantiersView extends BaseListView<ChantiersView.Chantier, Long> im
*/
@Override
public String createNew() {
selectedItem = new Chantier();
selectedItem.setStatut("PLANIFIE");
selectedItem.setAvancement(0);
selectedItem.setDateDebut(LocalDate.now());
prepareNew();
return getCreatePath() + "?faces-redirect=true";
}

View File

@@ -142,6 +142,38 @@ public class ClientsView extends BaseListView<ClientsView.Client, Long> implemen
LOG.info("Suppression client : {}", selectedItem.getId());
}
@Override
protected Client createNewEntity() {
Client client = new Client();
client.setDateCreation(LocalDateTime.now());
client.setDateModification(LocalDateTime.now());
client.setNombreChantiers(0);
client.setChiffreAffairesTotal(0.0);
return client;
}
@Override
protected void performCreate() {
if (selectedItem.getId() == null) {
selectedItem.setId(System.currentTimeMillis());
}
selectedItem.setDateCreation(LocalDateTime.now());
selectedItem.setDateModification(LocalDateTime.now());
items.add(selectedItem);
LOG.info("Created: {}", selectedItem);
}
@Override
protected void performUpdate() {
selectedItem.setDateModification(LocalDateTime.now());
LOG.info("Updated: {}", selectedItem);
}
@Override
protected Long getEntityId(Client entity) {
return entity.getId();
}
/**
* Initialise un nouveau client pour la création.
*/

View File

@@ -0,0 +1,270 @@
package dev.lions.btpxpress.view;
import dev.lions.btpxpress.service.DevisService;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
@Named("devisView")
@ViewScoped
@Getter
@Setter
public class DevisView extends BaseListView<DevisView.Devis, Long> implements Serializable {
private static final Logger LOG = LoggerFactory.getLogger(DevisView.class);
@Inject
DevisService devisService;
private String filtreNumero;
private String filtreClient;
private String filtreStatut;
private Long devisId;
@PostConstruct
public void init() {
if (filtreStatut == null) {
filtreStatut = "TOUS";
}
loadItems();
}
/**
* Définit le filtre de statut (utilisé depuis les pages filtrées).
*/
public void setFiltreStatut(String statut) {
this.filtreStatut = statut;
}
@Override
public void loadItems() {
loading = true;
try {
items = new ArrayList<>();
// Récupération depuis l'API backend
List<Map<String, Object>> devisData = devisService.getAllDevis();
for (Map<String, Object> data : devisData) {
Devis d = new Devis();
// Mapping des données de l'API vers l'objet Devis
d.setId(data.get("id") != null ? Long.valueOf(data.get("id").toString().hashCode()) : null);
d.setNumero((String) data.get("numero"));
d.setObjet((String) data.get("objet"));
// Le client peut être un objet ou une chaîne
Object clientObj = data.get("client");
if (clientObj instanceof Map) {
Map<String, Object> clientData = (Map<String, Object>) clientObj;
String entreprise = (String) clientData.get("entreprise");
String nom = (String) clientData.get("nom");
String prenom = (String) clientData.get("prenom");
d.setClient(entreprise != null && !entreprise.trim().isEmpty() ?
entreprise : (prenom != null ? prenom + " " : "") + (nom != null ? nom : ""));
} else if (clientObj instanceof String) {
d.setClient((String) clientObj);
} else {
d.setClient("N/A");
}
// Conversion des dates
if (data.get("dateEmission") != null) {
d.setDateEmission(LocalDate.parse(data.get("dateEmission").toString()));
}
if (data.get("dateValidite") != null) {
d.setDateValidite(LocalDate.parse(data.get("dateValidite").toString()));
}
d.setStatut((String) data.get("statut"));
// Montants
Object montantHTObj = data.get("montantHT");
if (montantHTObj != null) {
d.setMontantHT(montantHTObj instanceof Number ?
((Number) montantHTObj).doubleValue() :
Double.parseDouble(montantHTObj.toString()));
} else {
d.setMontantHT(0.0);
}
Object montantTTCObj = data.get("montantTTC");
if (montantTTCObj != null) {
d.setMontantTTC(montantTTCObj instanceof Number ?
((Number) montantTTCObj).doubleValue() :
Double.parseDouble(montantTTCObj.toString()));
} else {
d.setMontantTTC(0.0);
}
if (data.get("dateCreation") != null) {
d.setDateCreation(LocalDateTime.parse(data.get("dateCreation").toString()));
}
items.add(d);
}
LOG.info("Devis chargés depuis l'API : {} élément(s)", items.size());
applyFilters(items, buildFilters());
} catch (Exception e) {
LOG.error("Erreur chargement devis depuis l'API", e);
// En cas d'erreur, on garde une liste vide
items = new ArrayList<>();
} finally {
loading = false;
}
}
private List<Predicate<Devis>> buildFilters() {
List<Predicate<Devis>> filters = new ArrayList<>();
if (filtreNumero != null && !filtreNumero.trim().isEmpty()) {
filters.add(d -> d.getNumero().toLowerCase().contains(filtreNumero.toLowerCase()));
}
if (filtreClient != null && !filtreClient.trim().isEmpty()) {
filters.add(d -> d.getClient().toLowerCase().contains(filtreClient.toLowerCase()));
}
if (filtreStatut != null && !filtreStatut.trim().isEmpty() && !"TOUS".equals(filtreStatut)) {
filters.add(d -> d.getStatut().equals(filtreStatut));
}
return filters;
}
@Override
protected void resetFilterFields() {
filtreNumero = null;
filtreClient = null;
filtreStatut = "TOUS";
}
@Override
protected String getDetailsPath() {
return "/devis/";
}
@Override
protected String getCreatePath() {
return "/devis/nouveau";
}
@Override
protected void performDelete() {
LOG.info("Suppression devis : {}", selectedItem.getId());
}
@Override
protected Devis createNewEntity() {
Devis devis = new Devis();
devis.setStatut("BROUILLON");
devis.setDateEmission(LocalDate.now());
devis.setDateValidite(LocalDate.now().plusDays(30));
devis.setMontantHT(0.0);
devis.setMontantTTC(0.0);
devis.setDateCreation(LocalDateTime.now());
return devis;
}
@Override
protected void performCreate() {
if (selectedItem.getId() == null) {
selectedItem.setId(System.currentTimeMillis());
}
selectedItem.setDateCreation(LocalDateTime.now());
items.add(selectedItem);
LOG.info("Created: {}", selectedItem);
}
@Override
protected void performUpdate() {
LOG.info("Updated: {}", selectedItem);
}
@Override
protected Long getEntityId(Devis entity) {
return entity.getId();
}
/**
* Initialise un nouveau devis pour la création.
*/
@Override
public String createNew() {
selectedItem = new Devis();
selectedItem.setStatut("BROUILLON");
selectedItem.setDateEmission(LocalDate.now());
selectedItem.setDateValidite(LocalDate.now().plusDays(30));
return getCreatePath() + "?faces-redirect=true";
}
/**
* Sauvegarde un nouveau devis.
*/
public String saveNew() {
if (selectedItem == null) {
selectedItem = new Devis();
}
selectedItem.setId(System.currentTimeMillis()); // Simulation ID
selectedItem.setDateCreation(LocalDateTime.now());
items.add(selectedItem);
LOG.info("Nouveau devis créé : {}", selectedItem.getNumero());
return "/devis?faces-redirect=true";
}
/**
* Charge un devis par son ID depuis les paramètres de la requête.
*/
public void loadDevisById() {
if (devisId != null) {
loadItems(); // S'assurer que les items sont chargés
selectedItem = items.stream()
.filter(d -> d.getId().equals(devisId))
.findFirst()
.orElse(null);
if (selectedItem == null) {
LOG.warn("Devis avec ID {} non trouvé", devisId);
}
}
}
/**
* Affiche les détails d'un devis.
*/
public String viewDetails(Long id) {
selectedItem = items.stream()
.filter(d -> d.getId().equals(id))
.findFirst()
.orElse(null);
if (selectedItem != null) {
return getDetailsPath() + "details?id=" + id + "&faces-redirect=true";
}
return "/devis?faces-redirect=true";
}
@lombok.Getter
@lombok.Setter
public static class Devis {
private Long id;
private String numero;
private String objet;
private String client;
private LocalDate dateEmission;
private LocalDate dateValidite;
private String statut;
private double montantHT;
private double montantTTC;
private LocalDateTime dateCreation;
}
}

View File

@@ -0,0 +1,191 @@
package dev.lions.btpxpress.view;
import dev.lions.btpxpress.service.EmployeService;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
@Named("employeView")
@ViewScoped
@Getter
@Setter
public class EmployeView extends BaseListView<EmployeView.Employe, Long> implements Serializable {
private static final Logger LOG = LoggerFactory.getLogger(EmployeView.class);
@Inject
EmployeService employeService;
private String filtreNom;
private String filtrePoste;
private String filtreStatut;
private Long employeId;
@PostConstruct
public void init() {
if (filtreStatut == null) {
filtreStatut = "TOUS";
}
loadItems();
}
public void setFiltreStatut(String statut) {
this.filtreStatut = statut;
}
@Override
public void loadItems() {
loading = true;
try {
items = new ArrayList<>();
// Récupération depuis l'API backend
List<Map<String, Object>> employesData = employeService.getAllEmployes();
for (Map<String, Object> data : employesData) {
Employe e = new Employe();
e.setId(data.get("id") != null ? Long.valueOf(data.get("id").toString().hashCode()) : null);
String nom = (String) data.get("nom");
String prenom = (String) data.get("prenom");
e.setNomComplet((prenom != null ? prenom + " " : "") + (nom != null ? nom : ""));
e.setEmail((String) data.get("email"));
e.setTelephone((String) data.get("telephone"));
e.setPoste((String) data.get("poste"));
e.setStatut((String) data.get("statut"));
// Taux horaire
Object tauxObj = data.get("tauxHoraire");
if (tauxObj != null) {
e.setTauxHoraire(tauxObj instanceof Number ?
((Number) tauxObj).doubleValue() :
Double.parseDouble(tauxObj.toString()));
} else {
e.setTauxHoraire(0.0);
}
// Date d'embauche
if (data.get("dateEmbauche") != null) {
e.setDateEmbauche(LocalDate.parse(data.get("dateEmbauche").toString()));
}
if (data.get("dateCreation") != null) {
e.setDateCreation(LocalDateTime.parse(data.get("dateCreation").toString()));
}
items.add(e);
}
LOG.info("Employés chargés depuis l'API : {} élément(s)", items.size());
applyFilters(items, buildFilters());
} catch (Exception e) {
LOG.error("Erreur chargement employés depuis l'API", e);
items = new ArrayList<>();
} finally {
loading = false;
}
}
private List<Predicate<Employe>> buildFilters() {
List<Predicate<Employe>> filters = new ArrayList<>();
if (filtreNom != null && !filtreNom.trim().isEmpty()) {
filters.add(e -> e.getNomComplet().toLowerCase().contains(filtreNom.toLowerCase()));
}
if (filtrePoste != null && !filtrePoste.trim().isEmpty()) {
filters.add(e -> e.getPoste() != null && e.getPoste().toLowerCase().contains(filtrePoste.toLowerCase()));
}
if (filtreStatut != null && !filtreStatut.trim().isEmpty() && !"TOUS".equals(filtreStatut)) {
filters.add(e -> e.getStatut().equals(filtreStatut));
}
return filters;
}
@Override
protected void resetFilterFields() {
filtreNom = null;
filtrePoste = null;
filtreStatut = "TOUS";
}
@Override
protected String getDetailsPath() {
return "/employes/";
}
@Override
protected String getCreatePath() {
return "/employes/nouveau";
}
@Override
protected void performDelete() {
LOG.info("Suppression employé : {}", selectedItem.getId());
}
@Override
protected Employe createNewEntity() {
Employe employe = new Employe();
employe.setStatut("ACTIF");
employe.setDateEmbauche(LocalDate.now());
employe.setTauxHoraire(0.0);
employe.setDateCreation(LocalDateTime.now());
return employe;
}
@Override
protected void performCreate() {
if (selectedItem.getId() == null) {
selectedItem.setId(System.currentTimeMillis());
}
selectedItem.setDateCreation(LocalDateTime.now());
items.add(selectedItem);
LOG.info("Created: {}", selectedItem);
}
@Override
protected void performUpdate() {
LOG.info("Updated: {}", selectedItem);
}
@Override
protected Long getEntityId(Employe entity) {
return entity.getId();
}
@Override
public String createNew() {
selectedItem = new Employe();
selectedItem.setStatut("ACTIF");
selectedItem.setDateEmbauche(LocalDate.now());
return getCreatePath() + "?faces-redirect=true";
}
@lombok.Getter
@lombok.Setter
public static class Employe {
private Long id;
private String nomComplet;
private String email;
private String telephone;
private String poste;
private String statut;
private double tauxHoraire;
private LocalDate dateEmbauche;
private LocalDateTime dateCreation;
}
}

View File

@@ -0,0 +1,187 @@
package dev.lions.btpxpress.view;
import dev.lions.btpxpress.service.EquipeService;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
@Named("equipeView")
@ViewScoped
@Getter
@Setter
public class EquipeView extends BaseListView<EquipeView.Equipe, Long> implements Serializable {
private static final Logger LOG = LoggerFactory.getLogger(EquipeView.class);
@Inject
EquipeService equipeService;
private String filtreNom;
private String filtreSpecialite;
private String filtreStatut;
private Long equipeId;
@PostConstruct
public void init() {
if (filtreStatut == null) {
filtreStatut = "TOUS";
}
loadItems();
}
public void setFiltreStatut(String statut) {
this.filtreStatut = statut;
}
@Override
public void loadItems() {
loading = true;
try {
items = new ArrayList<>();
// Récupération depuis l'API backend
List<Map<String, Object>> equipesData = equipeService.getAllEquipes();
for (Map<String, Object> data : equipesData) {
Equipe eq = new Equipe();
eq.setId(data.get("id") != null ? Long.valueOf(data.get("id").toString().hashCode()) : null);
eq.setNom((String) data.get("nom"));
eq.setDescription((String) data.get("description"));
eq.setSpecialite((String) data.get("specialite"));
eq.setStatut((String) data.get("statut"));
// Chef d'équipe
Object chefObj = data.get("chef");
if (chefObj instanceof Map) {
Map<String, Object> chefData = (Map<String, Object>) chefObj;
String prenom = (String) chefData.get("prenom");
String nom = (String) chefData.get("nom");
eq.setChef((prenom != null ? prenom + " " : "") + (nom != null ? nom : ""));
} else {
eq.setChef("N/A");
}
// Nombre de membres
Object membresObj = data.get("membres");
if (membresObj instanceof List) {
eq.setNombreMembres(((List<?>) membresObj).size());
} else {
eq.setNombreMembres(0);
}
if (data.get("dateCreation") != null) {
eq.setDateCreation(LocalDateTime.parse(data.get("dateCreation").toString()));
}
items.add(eq);
}
LOG.info("Équipes chargées depuis l'API : {} élément(s)", items.size());
applyFilters(items, buildFilters());
} catch (Exception e) {
LOG.error("Erreur chargement équipes depuis l'API", e);
items = new ArrayList<>();
} finally {
loading = false;
}
}
private List<Predicate<Equipe>> buildFilters() {
List<Predicate<Equipe>> filters = new ArrayList<>();
if (filtreNom != null && !filtreNom.trim().isEmpty()) {
filters.add(e -> e.getNom().toLowerCase().contains(filtreNom.toLowerCase()));
}
if (filtreSpecialite != null && !filtreSpecialite.trim().isEmpty()) {
filters.add(e -> e.getSpecialite() != null &&
e.getSpecialite().toLowerCase().contains(filtreSpecialite.toLowerCase()));
}
if (filtreStatut != null && !filtreStatut.trim().isEmpty() && !"TOUS".equals(filtreStatut)) {
filters.add(e -> e.getStatut().equals(filtreStatut));
}
return filters;
}
@Override
protected void resetFilterFields() {
filtreNom = null;
filtreSpecialite = null;
filtreStatut = "TOUS";
}
@Override
protected String getDetailsPath() {
return "/equipes/";
}
@Override
protected String getCreatePath() {
return "/equipes/nouvelle";
}
@Override
protected void performDelete() {
LOG.info("Suppression équipe : {}", selectedItem.getId());
}
@Override
protected Equipe createNewEntity() {
Equipe equipe = new Equipe();
equipe.setStatut("ACTIVE");
equipe.setNombreMembres(0);
equipe.setDateCreation(LocalDateTime.now());
return equipe;
}
@Override
protected void performCreate() {
if (selectedItem.getId() == null) {
selectedItem.setId(System.currentTimeMillis());
}
selectedItem.setDateCreation(LocalDateTime.now());
items.add(selectedItem);
LOG.info("Created: {}", selectedItem);
}
@Override
protected void performUpdate() {
LOG.info("Updated: {}", selectedItem);
}
@Override
protected Long getEntityId(Equipe entity) {
return entity.getId();
}
@Override
public String createNew() {
selectedItem = new Equipe();
selectedItem.setStatut("ACTIVE");
return getCreatePath() + "?faces-redirect=true";
}
@lombok.Getter
@lombok.Setter
public static class Equipe {
private Long id;
private String nom;
private String description;
private String chef;
private String specialite;
private String statut;
private int nombreMembres;
private LocalDateTime dateCreation;
}
}

View File

@@ -0,0 +1,300 @@
package dev.lions.btpxpress.view;
import dev.lions.btpxpress.service.FactureService;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
@Named("factureView")
@ViewScoped
@Getter
@Setter
public class FactureView extends BaseListView<FactureView.Facture, Long> implements Serializable {
private static final Logger LOG = LoggerFactory.getLogger(FactureView.class);
@Inject
FactureService factureService;
private String filtreNumero;
private String filtreClient;
private String filtreStatut;
private Long factureId;
@PostConstruct
public void init() {
if (filtreStatut == null) {
filtreStatut = "TOUS";
}
loadItems();
}
/**
* Définit le filtre de statut (utilisé depuis les pages filtrées).
*/
public void setFiltreStatut(String statut) {
this.filtreStatut = statut;
}
@Override
public void loadItems() {
loading = true;
try {
items = new ArrayList<>();
// Récupération depuis l'API backend
List<Map<String, Object>> facturesData = factureService.getAllFactures();
for (Map<String, Object> data : facturesData) {
Facture f = new Facture();
// Mapping des données de l'API vers l'objet Facture
f.setId(data.get("id") != null ? Long.valueOf(data.get("id").toString().hashCode()) : null);
f.setNumero((String) data.get("numero"));
f.setObjet((String) data.get("objet"));
// Le client peut être un objet ou une chaîne
Object clientObj = data.get("client");
if (clientObj instanceof Map) {
Map<String, Object> clientData = (Map<String, Object>) clientObj;
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);
} else {
f.setClient("N/A");
}
// Conversion 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()));
}
if (data.get("datePaiement") != null) {
f.setDatePaiement(LocalDate.parse(data.get("datePaiement").toString()));
}
f.setStatut((String) data.get("statut"));
// Montants
Object montantHTObj = data.get("montantHT");
if (montantHTObj != null) {
f.setMontantHT(montantHTObj instanceof Number ?
((Number) montantHTObj).doubleValue() :
Double.parseDouble(montantHTObj.toString()));
} else {
f.setMontantHT(0.0);
}
Object montantTTCObj = data.get("montantTTC");
if (montantTTCObj != null) {
f.setMontantTTC(montantTTCObj instanceof Number ?
((Number) montantTTCObj).doubleValue() :
Double.parseDouble(montantTTCObj.toString()));
} else {
f.setMontantTTC(0.0);
}
Object montantPayeObj = data.get("montantPaye");
if (montantPayeObj != null) {
f.setMontantPaye(montantPayeObj instanceof Number ?
((Number) montantPayeObj).doubleValue() :
Double.parseDouble(montantPayeObj.toString()));
} else {
f.setMontantPaye(0.0);
}
if (data.get("dateCreation") != null) {
f.setDateCreation(LocalDateTime.parse(data.get("dateCreation").toString()));
}
items.add(f);
}
LOG.info("Factures chargées depuis l'API : {} élément(s)", items.size());
applyFilters(items, buildFilters());
} catch (Exception e) {
LOG.error("Erreur chargement factures depuis l'API", e);
// En cas d'erreur, on garde une liste vide
items = new ArrayList<>();
} finally {
loading = false;
}
}
private List<Predicate<Facture>> buildFilters() {
List<Predicate<Facture>> filters = new ArrayList<>();
if (filtreNumero != null && !filtreNumero.trim().isEmpty()) {
filters.add(f -> f.getNumero().toLowerCase().contains(filtreNumero.toLowerCase()));
}
if (filtreClient != null && !filtreClient.trim().isEmpty()) {
filters.add(f -> f.getClient().toLowerCase().contains(filtreClient.toLowerCase()));
}
if (filtreStatut != null && !filtreStatut.trim().isEmpty() && !"TOUS".equals(filtreStatut)) {
filters.add(f -> f.getStatut().equals(filtreStatut));
}
return filters;
}
@Override
protected void resetFilterFields() {
filtreNumero = null;
filtreClient = null;
filtreStatut = "TOUS";
}
@Override
protected String getDetailsPath() {
return "/factures/";
}
@Override
protected String getCreatePath() {
return "/factures/nouvelle";
}
@Override
protected void performDelete() {
LOG.info("Suppression facture : {}", selectedItem.getId());
}
@Override
protected Facture createNewEntity() {
Facture facture = new Facture();
facture.setStatut("BROUILLON");
facture.setDateEmission(LocalDate.now());
facture.setDateEcheance(LocalDate.now().plusDays(30));
facture.setMontantHT(0.0);
facture.setMontantTTC(0.0);
facture.setMontantPaye(0.0);
facture.setDateCreation(LocalDateTime.now());
return facture;
}
@Override
protected void performCreate() {
if (selectedItem.getId() == null) {
selectedItem.setId(System.currentTimeMillis());
}
selectedItem.setDateCreation(LocalDateTime.now());
items.add(selectedItem);
LOG.info("Created: {}", selectedItem);
}
@Override
protected void performUpdate() {
LOG.info("Updated: {}", selectedItem);
}
@Override
protected Long getEntityId(Facture entity) {
return entity.getId();
}
/**
* Initialise une nouvelle facture pour la création.
*/
@Override
public String createNew() {
selectedItem = new Facture();
selectedItem.setStatut("BROUILLON");
selectedItem.setDateEmission(LocalDate.now());
selectedItem.setDateEcheance(LocalDate.now().plusDays(30));
return getCreatePath() + "?faces-redirect=true";
}
/**
* Sauvegarde une nouvelle facture.
*/
public String saveNew() {
if (selectedItem == null) {
selectedItem = new Facture();
}
selectedItem.setId(System.currentTimeMillis()); // Simulation ID
selectedItem.setDateCreation(LocalDateTime.now());
items.add(selectedItem);
LOG.info("Nouvelle facture créée : {}", selectedItem.getNumero());
return "/factures?faces-redirect=true";
}
/**
* Charge une facture par son ID depuis les paramètres de la requête.
*/
public void loadFactureById() {
if (factureId != null) {
loadItems(); // S'assurer que les items sont chargés
selectedItem = items.stream()
.filter(f -> f.getId().equals(factureId))
.findFirst()
.orElse(null);
if (selectedItem == null) {
LOG.warn("Facture avec ID {} non trouvé", factureId);
}
}
}
/**
* Affiche les détails d'une facture.
*/
public String viewDetails(Long id) {
selectedItem = items.stream()
.filter(f -> f.getId().equals(id))
.findFirst()
.orElse(null);
if (selectedItem != null) {
return getDetailsPath() + "details?id=" + id + "&faces-redirect=true";
}
return "/factures?faces-redirect=true";
}
/**
* Calcule le montant restant à payer.
*/
public double getMontantRestant(Facture facture) {
return facture.getMontantTTC() - facture.getMontantPaye();
}
/**
* Vérifie si une facture est en retard.
*/
public boolean isEnRetard(Facture facture) {
return facture.getDateEcheance() != null &&
facture.getDateEcheance().isBefore(LocalDate.now()) &&
!"PAYEE".equals(facture.getStatut());
}
@lombok.Getter
@lombok.Setter
public static class Facture {
private Long id;
private String numero;
private String objet;
private String client;
private LocalDate dateEmission;
private LocalDate dateEcheance;
private LocalDate datePaiement;
private String statut;
private double montantHT;
private double montantTTC;
private double montantPaye;
private LocalDateTime dateCreation;
}
}

View File

@@ -0,0 +1,189 @@
package dev.lions.btpxpress.view;
import dev.lions.btpxpress.service.MaterielService;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
@Named("materielView")
@ViewScoped
@Getter
@Setter
public class MaterielView extends BaseListView<MaterielView.Materiel, Long> implements Serializable {
private static final Logger LOG = LoggerFactory.getLogger(MaterielView.class);
@Inject
MaterielService materielService;
private String filtreNom;
private String filtreType;
private String filtreStatut;
private Long materielId;
@PostConstruct
public void init() {
if (filtreStatut == null) {
filtreStatut = "TOUS";
}
loadItems();
}
public void setFiltreStatut(String statut) {
this.filtreStatut = statut;
}
@Override
public void loadItems() {
loading = true;
try {
items = new ArrayList<>();
// Récupération depuis l'API backend
List<Map<String, Object>> materielsData = materielService.getAllMateriels();
for (Map<String, Object> data : materielsData) {
Materiel m = new Materiel();
m.setId(data.get("id") != null ? Long.valueOf(data.get("id").toString().hashCode()) : null);
m.setNom((String) data.get("nom"));
m.setMarque((String) data.get("marque"));
m.setModele((String) data.get("modele"));
m.setNumeroSerie((String) data.get("numeroSerie"));
m.setType((String) data.get("type"));
m.setStatut((String) data.get("statut"));
// Valeur d'achat
Object valeurObj = data.get("valeurAchat");
if (valeurObj != null) {
m.setValeurAchat(valeurObj instanceof Number ?
((Number) valeurObj).doubleValue() :
Double.parseDouble(valeurObj.toString()));
} else {
m.setValeurAchat(0.0);
}
// Date d'achat
if (data.get("dateAchat") != null) {
m.setDateAchat(LocalDate.parse(data.get("dateAchat").toString()));
}
if (data.get("dateCreation") != null) {
m.setDateCreation(LocalDateTime.parse(data.get("dateCreation").toString()));
}
items.add(m);
}
LOG.info("Matériels chargés depuis l'API : {} élément(s)", items.size());
applyFilters(items, buildFilters());
} catch (Exception e) {
LOG.error("Erreur chargement matériels depuis l'API", e);
items = new ArrayList<>();
} finally {
loading = false;
}
}
private List<Predicate<Materiel>> buildFilters() {
List<Predicate<Materiel>> filters = new ArrayList<>();
if (filtreNom != null && !filtreNom.trim().isEmpty()) {
filters.add(m -> m.getNom().toLowerCase().contains(filtreNom.toLowerCase()));
}
if (filtreType != null && !filtreType.trim().isEmpty() && !"TOUS".equals(filtreType)) {
filters.add(m -> m.getType() != null && m.getType().equals(filtreType));
}
if (filtreStatut != null && !filtreStatut.trim().isEmpty() && !"TOUS".equals(filtreStatut)) {
filters.add(m -> m.getStatut().equals(filtreStatut));
}
return filters;
}
@Override
protected void resetFilterFields() {
filtreNom = null;
filtreType = "TOUS";
filtreStatut = "TOUS";
}
@Override
protected String getDetailsPath() {
return "/materiels/";
}
@Override
protected String getCreatePath() {
return "/materiels/nouveau";
}
@Override
protected void performDelete() {
LOG.info("Suppression matériel : {}", selectedItem.getId());
}
@Override
protected Materiel createNewEntity() {
Materiel materiel = new Materiel();
materiel.setStatut("DISPONIBLE");
materiel.setDateAchat(LocalDate.now());
materiel.setValeurAchat(0.0);
materiel.setDateCreation(LocalDateTime.now());
return materiel;
}
@Override
protected void performCreate() {
if (selectedItem.getId() == null) {
selectedItem.setId(System.currentTimeMillis());
}
selectedItem.setDateCreation(LocalDateTime.now());
items.add(selectedItem);
LOG.info("Created: {}", selectedItem);
}
@Override
protected void performUpdate() {
LOG.info("Updated: {}", selectedItem);
}
@Override
protected Long getEntityId(Materiel entity) {
return entity.getId();
}
@Override
public String createNew() {
selectedItem = new Materiel();
selectedItem.setStatut("DISPONIBLE");
selectedItem.setDateAchat(LocalDate.now());
return getCreatePath() + "?faces-redirect=true";
}
@lombok.Getter
@lombok.Setter
public static class Materiel {
private Long id;
private String nom;
private String marque;
private String modele;
private String numeroSerie;
private String type;
private String statut;
private double valeurAchat;
private LocalDate dateAchat;
private LocalDateTime dateCreation;
}
}

View File

@@ -0,0 +1,223 @@
package dev.lions.btpxpress.view;
import dev.lions.btpxpress.service.StockService;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
@Named("stockView")
@ViewScoped
@Getter
@Setter
public class StockView extends BaseListView<StockView.Stock, Long> implements Serializable {
private static final Logger LOG = LoggerFactory.getLogger(StockView.class);
@Inject
StockService stockService;
private String filtreReference;
private String filtreDesignation;
private String filtreCategorie;
private String filtreStatut;
private Long stockId;
@PostConstruct
public void init() {
if (filtreStatut == null) {
filtreStatut = "TOUS";
}
if (filtreCategorie == null) {
filtreCategorie = "TOUS";
}
loadItems();
}
public void setFiltreStatut(String statut) {
this.filtreStatut = statut;
}
public void setFiltreCategorie(String categorie) {
this.filtreCategorie = categorie;
}
@Override
public void loadItems() {
loading = true;
try {
items = new ArrayList<>();
// Récupération depuis l'API backend
List<Map<String, Object>> stocksData = stockService.getAllStocks();
for (Map<String, Object> data : stocksData) {
Stock s = new Stock();
s.setId(data.get("id") != null ? Long.valueOf(data.get("id").toString().hashCode()) : null);
s.setReference((String) data.get("reference"));
s.setDesignation((String) data.get("designation"));
s.setCategorie((String) data.get("categorie"));
s.setUniteMesure((String) data.get("uniteMesure"));
s.setStatut((String) data.get("statut"));
// Quantité disponible
Object qteObj = data.get("quantiteDisponible");
if (qteObj != null) {
s.setQuantiteDisponible(qteObj instanceof Number ?
((Number) qteObj).doubleValue() :
Double.parseDouble(qteObj.toString()));
} else {
s.setQuantiteDisponible(0.0);
}
// Seuil d'alerte
Object seuilObj = data.get("seuilAlerte");
if (seuilObj != null) {
s.setSeuilAlerte(seuilObj instanceof Number ?
((Number) seuilObj).doubleValue() :
Double.parseDouble(seuilObj.toString()));
} else {
s.setSeuilAlerte(0.0);
}
// Prix unitaire
Object prixObj = data.get("prixUnitaire");
if (prixObj != null) {
s.setPrixUnitaire(prixObj instanceof Number ?
((Number) prixObj).doubleValue() :
Double.parseDouble(prixObj.toString()));
} else {
s.setPrixUnitaire(0.0);
}
if (data.get("dateCreation") != null) {
s.setDateCreation(LocalDateTime.parse(data.get("dateCreation").toString()));
}
items.add(s);
}
LOG.info("Stocks chargés depuis l'API : {} élément(s)", items.size());
applyFilters(items, buildFilters());
} catch (Exception e) {
LOG.error("Erreur chargement stocks depuis l'API", e);
items = new ArrayList<>();
} finally {
loading = false;
}
}
private List<Predicate<Stock>> buildFilters() {
List<Predicate<Stock>> filters = new ArrayList<>();
if (filtreReference != null && !filtreReference.trim().isEmpty()) {
filters.add(s -> s.getReference() != null &&
s.getReference().toLowerCase().contains(filtreReference.toLowerCase()));
}
if (filtreDesignation != null && !filtreDesignation.trim().isEmpty()) {
filters.add(s -> s.getDesignation() != null &&
s.getDesignation().toLowerCase().contains(filtreDesignation.toLowerCase()));
}
if (filtreCategorie != null && !filtreCategorie.trim().isEmpty() && !"TOUS".equals(filtreCategorie)) {
filters.add(s -> s.getCategorie() != null && s.getCategorie().equals(filtreCategorie));
}
if (filtreStatut != null && !filtreStatut.trim().isEmpty() && !"TOUS".equals(filtreStatut)) {
filters.add(s -> s.getStatut() != null && s.getStatut().equals(filtreStatut));
}
return filters;
}
@Override
protected void resetFilterFields() {
filtreReference = null;
filtreDesignation = null;
filtreCategorie = "TOUS";
filtreStatut = "TOUS";
}
@Override
protected String getDetailsPath() {
return "/stock/";
}
@Override
protected String getCreatePath() {
return "/stock/nouveau";
}
@Override
protected void performDelete() {
LOG.info("Suppression stock : {}", selectedItem.getId());
}
@Override
protected Stock createNewEntity() {
Stock stock = new Stock();
stock.setStatut("DISPONIBLE");
stock.setQuantiteDisponible(0.0);
stock.setSeuilAlerte(0.0);
stock.setPrixUnitaire(0.0);
stock.setDateCreation(LocalDateTime.now());
return stock;
}
@Override
protected void performCreate() {
if (selectedItem.getId() == null) {
selectedItem.setId(System.currentTimeMillis());
}
selectedItem.setDateCreation(LocalDateTime.now());
items.add(selectedItem);
LOG.info("Created: {}", selectedItem);
}
@Override
protected void performUpdate() {
LOG.info("Updated: {}", selectedItem);
}
@Override
protected Long getEntityId(Stock entity) {
return entity.getId();
}
@Override
public String createNew() {
selectedItem = new Stock();
selectedItem.setStatut("DISPONIBLE");
return getCreatePath() + "?faces-redirect=true";
}
/**
* Vérifie si un stock est en alerte (quantité < seuil)
*/
public boolean isEnAlerte(Stock stock) {
return stock.getQuantiteDisponible() < stock.getSeuilAlerte();
}
@lombok.Getter
@lombok.Setter
public static class Stock {
private Long id;
private String reference;
private String designation;
private String categorie;
private String uniteMesure;
private String statut;
private double quantiteDisponible;
private double seuilAlerte;
private double prixUnitaire;
private LocalDateTime dateCreation;
}
}

View File

@@ -0,0 +1,84 @@
<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">
<!--
Composant réutilisable: Dialogue de confirmation d'action
Principe DRY: Un seul composant pour toutes les confirmations (suppression, archivage, etc.)
Paramètres:
- message: Message de confirmation (requis)
- header: Titre du dialogue (défaut: "Confirmation")
- icon: Icône d'alerte (défaut: "pi pi-exclamation-triangle")
- severity: Gravité (success, info, warn, danger - défaut: warn)
- acceptLabel: Texte bouton confirmation (défaut: "Oui")
- rejectLabel: Texte bouton annulation (défaut: "Non")
- acceptIcon: Icône bouton confirmation (défaut: "pi pi-check")
- rejectIcon: Icône bouton annulation (défaut: "pi pi-times")
Utilisation en ligne (simple):
<p:confirmDialog global="true" showEffect="fade" hideEffect="fade">
<ui:include src="/WEB-INF/components/confirmation-dialog.xhtml"/>
</p:confirmDialog>
<!-- Dans votre action -->
<p:commandButton value="Supprimer"
action="#{viewBean.delete}"
update="dataTable">
<p:confirm header="Confirmer la suppression"
message="Êtes-vous sûr de vouloir supprimer cet élément ?"
icon="pi pi-trash"/>
</p:commandButton>
Utilisation personnalisée (avancée):
<ui:include src="/WEB-INF/components/confirmation-dialog.xhtml">
<ui:param name="message" value="Cette action est irréversible. Continuer ?"/>
<ui:param name="header" value="Attention"/>
<ui:param name="severity" value="danger"/>
<ui:param name="acceptLabel" value="Confirmer"/>
<ui:param name="rejectLabel" value="Annuler"/>
</ui:include>
-->
<!-- Dialogue de confirmation global (style moderne) -->
<p:confirmDialog global="true"
showEffect="fade"
hideEffect="fade"
responsive="true"
width="350">
<div class="flex align-items-center gap-3 mb-3">
<!-- Icône avec couleur selon la gravité -->
<i class="#{empty icon ? 'pi pi-exclamation-triangle' : icon}
#{severity eq 'danger' ? 'text-red-500' :
severity eq 'warn' ? 'text-orange-500' :
severity eq 'success' ? 'text-green-500' : 'text-blue-500'}"
style="font-size: 2rem"></i>
<!-- Message -->
<span class="font-bold text-900">
<h:outputText value="#{message}" escape="false"/>
</span>
</div>
<!-- Boutons d'action -->
<div class="flex align-items-center justify-content-end gap-2">
<p:commandButton value="#{empty rejectLabel ? 'Non' : rejectLabel}"
icon="#{empty rejectIcon ? 'pi pi-times' : rejectIcon}"
styleClass="ui-button-secondary"
type="button"
onclick="PF(arguments[0]).hide()"/>
<p:commandButton value="#{empty acceptLabel ? 'Oui' : acceptLabel}"
icon="#{empty acceptIcon ? 'pi pi-check' : acceptIcon}"
styleClass="#{severity eq 'danger' ? 'ui-button-danger' :
severity eq 'warn' ? 'ui-button-warning' :
severity eq 'success' ? 'ui-button-success' : 'ui-button-primary'}"
type="button"
onclick="PF(arguments[0]).accept()"/>
</div>
</p:confirmDialog>
</ui:composition>

View File

@@ -0,0 +1,128 @@
<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">
<!--
Composant réutilisable: Filtre par plage de dates
Principe DRY: Un seul composant pour tous les filtres de dates
Paramètres:
- fromDate: Date de début (backing bean property) - requis
- toDate: Date de fin (backing bean property) - requis
- label: Libellé du filtre (défaut: "Période")
- fromLabel: Libellé date début (défaut: "Du")
- toLabel: Libellé date fin (défaut: "Au")
- pattern: Format d'affichage (défaut: "dd/MM/yyyy")
- showButtonBar: Afficher barre d'actions (défaut: true)
- showTime: Afficher sélection heure (défaut: false)
- locale: Locale (défaut: fr_FR)
- inline: Affichage inline (défaut: false)
- showPresets: Afficher raccourcis période (défaut: true)
Utilisation basique:
<ui:include src="/WEB-INF/components/date-range-filter.xhtml">
<ui:param name="fromDate" value="#{rapportView.dateDebut}"/>
<ui:param name="toDate" value="#{rapportView.dateFin}"/>
</ui:include>
Avec libellés personnalisés:
<ui:include src="/WEB-INF/components/date-range-filter.xhtml">
<ui:param name="fromDate" value="#{factureView.periodeDebut}"/>
<ui:param name="toDate" value="#{factureView.periodeFin}"/>
<ui:param name="label" value="Période de facturation"/>
<ui:param name="fromLabel" value="Début"/>
<ui:param name="toLabel" value="Fin"/>
</ui:include>
Avec heure:
<ui:include src="/WEB-INF/components/date-range-filter.xhtml">
<ui:param name="fromDate" value="#{planningView.debut}"/>
<ui:param name="toDate" value="#{planningView.fin}"/>
<ui:param name="showTime" value="true"/>
<ui:param name="pattern" value="dd/MM/yyyy HH:mm"/>
</ui:include>
-->
<div class="date-range-filter p-fluid">
<div class="card">
<h:panelGroup rendered="#{not empty label}">
<h5 class="mb-3">#{label}</h5>
</h:panelGroup>
<div class="formgrid grid">
<!-- Date de début -->
<div class="field col-12 md:col-6">
<label for="dateFrom">#{empty fromLabel ? 'Du' : fromLabel}</label>
<p:calendar id="dateFrom"
value="#{fromDate}"
pattern="#{empty pattern ? 'dd/MM/yyyy' : pattern}"
locale="#{empty locale ? 'fr_FR' : locale}"
showButtonBar="#{empty showButtonBar ? true : showButtonBar}"
showTime="#{empty showTime ? false : showTime}"
showIcon="true"
monthNavigator="true"
yearNavigator="true"
yearRange="2020:2030"
placeholder="#{empty fromLabel ? 'Du' : fromLabel}">
<p:ajax event="dateSelect" update="dateTo"/>
</p:calendar>
</div>
<!-- Date de fin -->
<div class="field col-12 md:col-6">
<label for="dateTo">#{empty toLabel ? 'Au' : toLabel}</label>
<p:calendar id="dateTo"
value="#{toDate}"
pattern="#{empty pattern ? 'dd/MM/yyyy' : pattern}"
locale="#{empty locale ? 'fr_FR' : locale}"
showButtonBar="#{empty showButtonBar ? true : showButtonBar}"
showTime="#{empty showTime ? false : showTime}"
showIcon="true"
monthNavigator="true"
yearNavigator="true"
yearRange="2020:2030"
mindate="#{fromDate}"
placeholder="#{empty toLabel ? 'Au' : toLabel}"/>
</div>
</div>
<!-- Raccourcis de période -->
<div class="flex gap-2 mt-3" style="#{empty showPresets or showPresets eq false ? 'display: none;' : ''}">
<p:commandButton value="Aujourd'hui"
styleClass="ui-button-sm ui-button-outlined"
icon="pi pi-calendar"
actionListener="#{cc.setDateRangeToToday()}"
update="@this dateFrom dateTo"
immediate="true"/>
<p:commandButton value="7 derniers jours"
styleClass="ui-button-sm ui-button-outlined"
icon="pi pi-calendar"
actionListener="#{cc.setDateRangeToLast7Days()}"
update="@this dateFrom dateTo"
immediate="true"/>
<p:commandButton value="Ce mois"
styleClass="ui-button-sm ui-button-outlined"
icon="pi pi-calendar"
actionListener="#{cc.setDateRangeToThisMonth()}"
update="@this dateFrom dateTo"
immediate="true"/>
<p:commandButton value="Ce trimestre"
styleClass="ui-button-sm ui-button-outlined"
icon="pi pi-calendar"
actionListener="#{cc.setDateRangeToThisQuarter()}"
update="@this dateFrom dateTo"
immediate="true"/>
<p:commandButton value="Cette année"
styleClass="ui-button-sm ui-button-outlined"
icon="pi pi-calendar"
actionListener="#{cc.setDateRangeToThisYear()}"
update="@this dateFrom dateTo"
immediate="true"/>
</div>
</div>
</div>
</ui:composition>

View File

@@ -0,0 +1,162 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<!--
Composant réutilisable: Carte d'information/KPI
Principe DRY: Un seul composant pour toutes les cartes d'informations
Paramètres:
- title: Titre de la carte - requis
- value: Valeur principale à afficher - requis
- subtitle: Sous-titre/description - optionnel
- icon: Icône (classe PrimeIcons) - optionnel
- iconColor: Couleur de l'icône (primary, success, info, warning, danger) - défaut: primary
- badge: Texte du badge - optionnel
- badgeSeverity: Gravité du badge (success, info, warning, danger) - défaut: info
- trend: Tendance (+5%, -3%) - optionnel
- trendType: Type de tendance (up, down, stable) - auto-détecté depuis trend
- footer: Texte du pied de carte - optionnel
- actionLabel: Libellé bouton d'action - optionnel
- actionIcon: Icône bouton d'action - défaut: pi-arrow-right
- actionUrl: URL de l'action - optionnel
Utilisation KPI Dashboard:
<ui:include src="/WEB-INF/components/detail-card.xhtml">
<ui:param name="title" value="Chantiers actifs"/>
<ui:param name="value" value="#{dashboardView.chantiersActifs}"/>
<ui:param name="icon" value="pi-building"/>
<ui:param name="iconColor" value="primary"/>
<ui:param name="trend" value="+12%"/>
<ui:param name="footer" value="vs mois dernier"/>
<ui:param name="actionLabel" value="Voir tous"/>
<ui:param name="actionUrl" value="/chantiers.xhtml"/>
</ui:include>
Carte avec badge:
<ui:include src="/WEB-INF/components/detail-card.xhtml">
<ui:param name="title" value="Budget total"/>
<ui:param name="value" value="125 000 000 FCFA"/>
<ui:param name="icon" value="pi-wallet"/>
<ui:param name="iconColor" value="success"/>
<ui:param name="badge" value="En cours"/>
<ui:param name="badgeSeverity" value="info"/>
<ui:param name="subtitle" value="2025"/>
</ui:include>
Carte simple:
<ui:include src="/WEB-INF/components/detail-card.xhtml">
<ui:param name="title" value="Factures impayées"/>
<ui:param name="value" value="8"/>
<ui:param name="icon" value="pi-exclamation-circle"/>
<ui:param name="iconColor" value="danger"/>
</ui:include>
-->
<!-- Détection automatique du type de tendance -->
<c:if test="#{not empty trend and empty trendType}">
<c:choose>
<c:when test="#{trend.startsWith('+')}">
<c:set var="autoTrendType" value="up"/>
</c:when>
<c:when test="#{trend.startsWith('-')}">
<c:set var="autoTrendType" value="down"/>
</c:when>
<c:otherwise>
<c:set var="autoTrendType" value="stable"/>
</c:otherwise>
</c:choose>
</c:if>
<c:set var="trendDirection" value="#{empty trendType ? autoTrendType : trendType}"/>
<!-- Couleur de l'icône -->
<c:choose>
<c:when test="#{iconColor eq 'success'}">
<c:set var="iconColorClass" value="text-green-500"/>
<c:set var="iconBgClass" value="bg-green-100"/>
</c:when>
<c:when test="#{iconColor eq 'info'}">
<c:set var="iconColorClass" value="text-blue-500"/>
<c:set var="iconBgClass" value="bg-blue-100"/>
</c:when>
<c:when test="#{iconColor eq 'warning'}">
<c:set var="iconColorClass" value="text-orange-500"/>
<c:set var="iconBgClass" value="bg-orange-100"/>
</c:when>
<c:when test="#{iconColor eq 'danger'}">
<c:set var="iconColorClass" value="text-red-500"/>
<c:set var="iconBgClass" value="bg-red-100"/>
</c:when>
<c:otherwise>
<c:set var="iconColorClass" value="text-primary"/>
<c:set var="iconBgClass" value="bg-primary-100"/>
</c:otherwise>
</c:choose>
<!-- Carte -->
<div class="card mb-0 detail-card" style="height: 100%;">
<div class="flex flex-column" style="height: 100%;">
<!-- En-tête avec icône et badge -->
<div class="flex align-items-start justify-content-between mb-3">
<div class="flex align-items-center gap-3">
<h:panelGroup rendered="#{not empty icon}">
<div class="flex align-items-center justify-content-center #{iconBgClass}"
style="width: 3rem; height: 3rem; border-radius: 0.5rem;">
<i class="#{icon} #{iconColorClass}" style="font-size: 1.5rem;"/>
</div>
</h:panelGroup>
<div>
<span class="text-600 font-medium text-sm block mb-1">#{title}</span>
<h:panelGroup rendered="#{not empty subtitle}">
<span class="text-500 text-xs">#{subtitle}</span>
</h:panelGroup>
</div>
</div>
<h:panelGroup rendered="#{not empty badge}">
<p:badge value="#{badge}"
severity="#{empty badgeSeverity ? 'info' : badgeSeverity}"/>
</h:panelGroup>
</div>
<!-- Valeur principale -->
<div class="text-900 font-bold text-3xl mb-2">#{value}</div>
<!-- Tendance -->
<h:panelGroup rendered="#{not empty trend}">
<div class="flex align-items-center gap-2 mb-3">
<i class="#{trendDirection eq 'up' ? 'pi pi-arrow-up text-green-500' :
trendDirection eq 'down' ? 'pi pi-arrow-down text-red-500' :
'pi pi-minus text-gray-500'}"
style="font-size: 0.875rem;"/>
<span class="#{trendDirection eq 'up' ? 'text-green-600' :
trendDirection eq 'down' ? 'text-red-600' :
'text-gray-600'} font-medium text-sm">
#{trend}
</span>
</div>
</h:panelGroup>
<!-- Spacer pour pousser le footer en bas -->
<div class="flex-grow-1"></div>
<!-- Pied de carte -->
<h:panelGroup rendered="#{not empty footer or not empty actionLabel}">
<div class="flex align-items-center justify-content-between pt-3 border-top-1 surface-border">
<span class="text-500 text-sm">#{footer}</span>
<h:panelGroup rendered="#{not empty actionLabel}">
<h:link value="#{actionLabel}"
outcome="#{actionUrl}"
styleClass="text-primary font-medium text-sm flex align-items-center gap-1 no-underline hover:text-primary-700">
<i class="#{empty actionIcon ? 'pi pi-arrow-right' : actionIcon}" style="font-size: 0.75rem;"/>
</h:link>
</h:panelGroup>
</div>
</h:panelGroup>
</div>
</div>
</ui:composition>

View File

@@ -0,0 +1,107 @@
<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">
<!--
Composant réutilisable: Barre d'outils d'export
Principe DRY: Un seul composant pour toutes les fonctionnalités d'export
Paramètres:
- tableId: ID du DataTable à exporter - requis
- filename: Nom du fichier sans extension (défaut: "export")
- showPDF: Afficher bouton PDF (défaut: true)
- showExcel: Afficher bouton Excel (défaut: true)
- showCSV: Afficher bouton CSV (défaut: true)
- showPrint: Afficher bouton Imprimer (défaut: false)
- pageOnly: Exporter page courante uniquement (défaut: false)
- selectionOnly: Exporter sélection uniquement (défaut: false)
- alignment: Alignement (left, center, right - défaut: right)
- label: Libellé avant les boutons - optionnel
Utilisation basique:
<ui:include src="/WEB-INF/components/export-toolbar.xhtml">
<ui:param name="tableId" value="dataTable"/>
<ui:param name="filename" value="liste_chantiers"/>
</ui:include>
Export personnalisé:
<ui:include src="/WEB-INF/components/export-toolbar.xhtml">
<ui:param name="tableId" value="facturesTable"/>
<ui:param name="filename" value="factures_#{factureView.mois}"/>
<ui:param name="showPDF" value="true"/>
<ui:param name="showExcel" value="true"/>
<ui:param name="showCSV" value="false"/>
<ui:param name="showPrint" value="true"/>
<ui:param name="label" value="Exporter :"/>
</ui:include>
Export avec sélection:
<ui:include src="/WEB-INF/components/export-toolbar.xhtml">
<ui:param name="tableId" value="devisTable"/>
<ui:param name="filename" value="devis_selectionnes"/>
<ui:param name="selectionOnly" value="true"/>
</ui:include>
-->
<div class="export-toolbar flex align-items-center gap-2"
style="justify-content: #{empty alignment ? 'flex-end' :
alignment eq 'center' ? 'center' :
alignment eq 'left' ? 'flex-start' : 'flex-end'};">
<!-- Libellé optionnel -->
<h:panelGroup rendered="#{not empty label}">
<span class="text-900 font-medium mr-2">#{label}</span>
</h:panelGroup>
<!-- Bouton PDF -->
<h:panelGroup rendered="#{empty showPDF or showPDF eq true}">
<p:commandButton icon="pi pi-file-pdf"
styleClass="ui-button-danger ui-button-outlined"
title="Exporter en PDF">
<p:dataExporter type="pdf"
target="#{tableId}"
fileName="#{empty filename ? 'export' : filename}"
pageOnly="#{empty pageOnly ? false : pageOnly}"
selectionOnly="#{empty selectionOnly ? false : selectionOnly}"/>
</p:commandButton>
</h:panelGroup>
<!-- Bouton Excel -->
<h:panelGroup rendered="#{empty showExcel or showExcel eq true}">
<p:commandButton icon="pi pi-file-excel"
styleClass="ui-button-success ui-button-outlined"
title="Exporter en Excel">
<p:dataExporter type="xlsx"
target="#{tableId}"
fileName="#{empty filename ? 'export' : filename}"
pageOnly="#{empty pageOnly ? false : pageOnly}"
selectionOnly="#{empty selectionOnly ? false : selectionOnly}"/>
</p:commandButton>
</h:panelGroup>
<!-- Bouton CSV -->
<h:panelGroup rendered="#{showCSV eq true}">
<p:commandButton icon="pi pi-file"
styleClass="ui-button-info ui-button-outlined"
title="Exporter en CSV">
<p:dataExporter type="csv"
target="#{tableId}"
fileName="#{empty filename ? 'export' : filename}"
pageOnly="#{empty pageOnly ? false : pageOnly}"
selectionOnly="#{empty selectionOnly ? false : selectionOnly}"/>
</p:commandButton>
</h:panelGroup>
<!-- Bouton Imprimer -->
<h:panelGroup rendered="#{showPrint eq true}">
<p:commandButton icon="pi pi-print"
styleClass="ui-button-secondary ui-button-outlined"
title="Imprimer"
onclick="window.print(); return false;"/>
</h:panelGroup>
</div>
</ui:composition>

View File

@@ -0,0 +1,87 @@
<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">
<!--
Composant réutilisable: Dialogue de formulaire CRUD
Principe DRY: Un seul composant pour tous les formulaires de création/édition
Paramètres:
- dialogId: ID du dialogue (requis)
- header: Titre du dialogue (ex: "Nouveau Chantier")
- widgetVar: Variable widget PrimeFaces (ex: "chantierDialog")
- formId: ID du formulaire (requis)
- viewBean: Bean de vue pour les actions (requis)
- modal: true/false (défaut: true)
- width: Largeur du dialogue (défaut: 600px)
- height: Hauteur du dialogue (défaut: auto)
- showHeader: Afficher l'entête (défaut: true)
- closable: Dialogue fermable (défaut: true)
- draggable: Dialogue déplaçable (défaut: true)
- resizable: Dialogue redimensionnable (défaut: false)
- updateTarget: ID à mettre à jour après save (requis)
Utilisation:
<ui:include src="/WEB-INF/components/form-dialog.xhtml">
<ui:param name="dialogId" value="chantierDialog"/>
<ui:param name="header" value="#{chantiersView.editing ? 'Modifier Chantier' : 'Nouveau Chantier'}"/>
<ui:param name="widgetVar" value="chantierDlg"/>
<ui:param name="formId" value="chantierForm"/>
<ui:param name="viewBean" value="#{chantiersView}"/>
<ui:param name="updateTarget" value="@form:dataTable"/>
<ui:define name="form-content">
<!-- Vos champs de formulaire ici -->
<div class="p-fluid">
<div class="field">
<label for="nom">Nom</label>
<p:inputText id="nom" value="#{chantiersView.entity.nom}" required="true"/>
</div>
</div>
</ui:define>
</ui:include>
-->
<p:dialog id="#{dialogId}"
header="#{header}"
widgetVar="#{widgetVar}"
modal="#{empty modal ? true : modal}"
width="#{empty width ? '600px' : width}"
height="#{empty height ? 'auto' : height}"
showHeader="#{empty showHeader ? true : showHeader}"
closable="#{empty closable ? true : closable}"
draggable="#{empty draggable ? true : draggable}"
resizable="#{empty resizable ? false : resizable}">
<h:form id="#{formId}">
<p:messages id="messages" showDetail="true" closable="true"/>
<!-- Contenu du formulaire injecté par la page appelante -->
<ui:insert name="form-content">
<div class="p-fluid">
<p class="text-color-secondary">
Aucun contenu de formulaire défini. Utilisez ui:define name="form-content" pour ajouter vos champs.
</p>
</div>
</ui:insert>
<!-- Barre d'actions -->
<div class="flex align-items-center justify-content-end gap-2 pt-4">
<p:commandButton value="Annuler"
icon="pi pi-times"
styleClass="ui-button-secondary"
onclick="PF('#{widgetVar}').hide()"
type="button"/>
<p:commandButton value="#{viewBean.editing ? 'Modifier' : 'Créer'}"
icon="pi pi-save"
styleClass="ui-button-primary"
action="#{viewBean.save()}"
update="#{updateTarget} #{formId}:messages"
oncomplete="if (args &amp;&amp; !args.validationFailed) PF('#{widgetVar}').hide()"/>
</div>
</h:form>
</p:dialog>
</ui:composition>

View File

@@ -0,0 +1,135 @@
<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:c="http://xmlns.jcp.org/jsp/jstl/core">
<!--
Composant réutilisable: Affichage monétaire formaté
Principe DRY: Un seul composant pour tous les montants monétaires
Format standardisé pour l'Afrique de l'Ouest (FCFA)
Paramètres:
- amount: Montant à afficher (requis)
- currency: Devise (défaut: FCFA)
- showCurrency: Afficher le symbole de devise (true/false - défaut: true)
- showSymbol: Afficher le symbole avant le montant (défaut: false)
- decimals: Nombre de décimales (défaut: 0 pour FCFA)
- size: Taille (small, normal, large, xl - défaut: normal)
- color: Couleur du texte (success, danger, warning, primary - optionnel)
- bold: Texte en gras (true/false - défaut: false)
- alignment: Alignement (left, center, right - défaut: left)
Utilisation basique:
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{facture.montantTotal}"/>
</ui:include>
Affiche: 1 250 000 FCFA
Grande taille avec couleur:
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{chantier.budget}"/>
<ui:param name="size" value="xl"/>
<ui:param name="color" value="primary"/>
<ui:param name="bold" value="true"/>
</ui:include>
Avec symbole personnalisé:
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{devis.montant}"/>
<ui:param name="currency" value="EUR"/>
<ui:param name="showSymbol" value="true"/>
<ui:param name="decimals" value="2"/>
</ui:include>
Affiche: € 1 250,50 EUR
-->
<c:set var="currencyCode" value="#{empty currency ? 'FCFA' : currency}"/>
<c:set var="displayCurrency" value="#{empty showCurrency ? true : showCurrency}"/>
<c:set var="displaySymbol" value="#{empty showSymbol ? false : showSymbol}"/>
<c:set var="decimalCount" value="#{empty decimals ? 0 : decimals}"/>
<c:set var="textSize" value="#{empty size ? 'normal' : size}"/>
<c:set var="isBold" value="#{empty bold ? false : bold}"/>
<c:set var="textAlign" value="#{empty alignment ? 'left' : alignment}"/>
<!-- Classes CSS pour la taille -->
<c:choose>
<c:when test="#{textSize eq 'small'}">
<c:set var="sizeClass" value="text-sm"/>
</c:when>
<c:when test="#{textSize eq 'large'}">
<c:set var="sizeClass" value="text-lg"/>
</c:when>
<c:when test="#{textSize eq 'xl'}">
<c:set var="sizeClass" value="text-xl"/>
</c:when>
<c:otherwise>
<c:set var="sizeClass" value="text-base"/>
</c:otherwise>
</c:choose>
<!-- Classes CSS pour la couleur -->
<c:choose>
<c:when test="#{color eq 'success'}">
<c:set var="colorClass" value="text-green-600"/>
</c:when>
<c:when test="#{color eq 'danger'}">
<c:set var="colorClass" value="text-red-600"/>
</c:when>
<c:when test="#{color eq 'warning'}">
<c:set var="colorClass" value="text-orange-600"/>
</c:when>
<c:when test="#{color eq 'primary'}">
<c:set var="colorClass" value="text-primary"/>
</c:when>
<c:otherwise>
<c:set var="colorClass" value="text-900"/>
</c:otherwise>
</c:choose>
<!-- Symboles de devise -->
<c:choose>
<c:when test="#{currencyCode eq 'EUR'}">
<c:set var="currencySymbol" value="€"/>
</c:when>
<c:when test="#{currencyCode eq 'USD'}">
<c:set var="currencySymbol" value="$"/>
</c:when>
<c:when test="#{currencyCode eq 'GBP'}">
<c:set var="currencySymbol" value="£"/>
</c:when>
<c:when test="#{currencyCode eq 'FCFA' or currencyCode eq 'XOF'}">
<c:set var="currencySymbol" value=""/>
</c:when>
<c:otherwise>
<c:set var="currencySymbol" value=""/>
</c:otherwise>
</c:choose>
<!-- Affichage du montant -->
<span class="monetary-display #{sizeClass} #{colorClass} #{isBold ? 'font-bold' : ''}"
style="text-align: #{textAlign}; display: inline-block;">
<!-- Symbole avant -->
<c:if test="#{displaySymbol and not empty currencySymbol}">
<span class="currency-symbol mr-1">#{currencySymbol}</span>
</c:if>
<!-- Montant formaté -->
<span class="amount">
<h:outputText value="#{amount}">
<f:convertNumber type="currency"
currencySymbol=""
groupingUsed="true"
minFractionDigits="#{decimalCount}"
maxFractionDigits="#{decimalCount}"/>
</h:outputText>
</span>
<!-- Code devise après -->
<c:if test="#{displayCurrency}">
<span class="currency-code ml-1 font-medium">#{currencyCode}</span>
</c:if>
</span>
</ui:composition>

View File

@@ -0,0 +1,115 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<!--
Composant réutilisable: Indicateur de progression
Principe DRY: Un seul composant pour tous les indicateurs de progression
Paramètres:
- value: Pourcentage (0-100) - requis
- label: Libellé à afficher - optionnel
- showValue: Afficher le pourcentage (true/false - défaut: true)
- mode: Mode d'affichage (determinate, indeterminate - défaut: determinate)
- color: Couleur (primary, success, info, warning, danger - défaut: auto basé sur valeur)
- height: Hauteur de la barre (défaut: 1rem)
- labelPosition: Position du label (top, inside, bottom - défaut: top)
Utilisation basique:
<ui:include src="/WEB-INF/components/progress-indicator.xhtml">
<ui:param name="value" value="#{chantier.progressionPourcentage}"/>
<ui:param name="label" value="Progression du chantier"/>
</ui:include>
Avec couleur personnalisée:
<ui:include src="/WEB-INF/components/progress-indicator.xhtml">
<ui:param name="value" value="75"/>
<ui:param name="color" value="success"/>
<ui:param name="labelPosition" value="inside"/>
</ui:include>
Mode indéterminé (chargement):
<ui:include src="/WEB-INF/components/progress-indicator.xhtml">
<ui:param name="mode" value="indeterminate"/>
<ui:param name="label" value="Chargement en cours..."/>
</ui:include>
-->
<c:set var="progressMode" value="#{empty mode ? 'determinate' : mode}"/>
<c:set var="displayValue" value="#{empty showValue ? true : showValue}"/>
<c:set var="barHeight" value="#{empty height ? '1rem' : height}"/>
<c:set var="labelPos" value="#{empty labelPosition ? 'top' : labelPosition}"/>
<!-- Déterminer la couleur automatiquement si non fournie -->
<c:choose>
<c:when test="#{not empty color}">
<c:set var="barColor" value="#{color}"/>
</c:when>
<c:when test="#{value >= 100}">
<c:set var="barColor" value="success"/>
</c:when>
<c:when test="#{value >= 75}">
<c:set var="barColor" value="info"/>
</c:when>
<c:when test="#{value >= 50}">
<c:set var="barColor" value="primary"/>
</c:when>
<c:when test="#{value >= 25}">
<c:set var="barColor" value="warning"/>
</c:when>
<c:otherwise>
<c:set var="barColor" value="danger"/>
</c:otherwise>
</c:choose>
<!-- Classes CSS pour la couleur -->
<c:choose>
<c:when test="#{barColor eq 'success'}">
<c:set var="colorClass" value="bg-green-500"/>
</c:when>
<c:when test="#{barColor eq 'info'}">
<c:set var="colorClass" value="bg-blue-500"/>
</c:when>
<c:when test="#{barColor eq 'warning'}">
<c:set var="colorClass" value="bg-orange-500"/>
</c:when>
<c:when test="#{barColor eq 'danger'}">
<c:set var="colorClass" value="bg-red-500"/>
</c:when>
<c:otherwise>
<c:set var="colorClass" value="bg-primary"/>
</c:otherwise>
</c:choose>
<div class="progress-indicator-container" style="width: 100%;">
<!-- Label en haut -->
<div class="flex align-items-center justify-content-between mb-2"
style="#{labelPos eq 'top' ? '' : 'display: none;'}">
<span class="text-900 font-medium">#{label}</span>
<span class="text-900 font-semibold" style="#{displayValue ? '' : 'display: none;'}">
#{value}%
</span>
</div>
<!-- Barre de progression -->
<p:progressBar value="#{value}"
mode="#{progressMode}"
style="height: #{barHeight}; border-radius: 0.5rem;"
styleClass="#{colorClass}"
displayValue="#{labelPos eq 'inside' and displayValue}"/>
<!-- Label en bas -->
<div class="flex align-items-center justify-content-between mt-2"
style="#{labelPos eq 'bottom' ? '' : 'display: none;'}">
<span class="text-700">#{label}</span>
<span class="text-900 font-semibold" style="#{displayValue ? '' : 'display: none;'}">
#{value}%
</span>
</div>
</div>
</ui:composition>

View File

@@ -0,0 +1,121 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<!--
Composant réutilisable: Badge de statut coloré
Principe DRY: Un seul composant pour tous les badges de statut dans l'application
Write Once, Use Anywhere: Mapping automatique statut → couleur
Paramètres:
- value: Valeur du statut (requis)
- severity: Gravité explicite (success, info, warning, danger) - optionnel
- icon: Icône à afficher - optionnel
- rounded: Badge arrondi (true/false - défaut: true)
- size: Taille (normal, large - défaut: normal)
Mapping automatique des statuts métier:
SUCCESS (vert): EN_COURS, ACTIF, TERMINE, VALIDE, PAYE, LIVRE, DISPONIBLE, APPROUVE
INFO (bleu): PLANIFIE, NOUVEAU, EN_ATTENTE, BROUILLON, PENDING
WARNING (orange): RETARD, SUSPENDU, IMPAYE, ALERTE, MAINTENANCE
DANGER (rouge): ANNULE, REFUSE, EXPIRE, HORS_SERVICE, BLOQUE
Utilisation:
<ui:include src="/WEB-INF/components/status-badge.xhtml">
<ui:param name="value" value="#{chantier.statut}"/>
</ui:include>
Avec icône personnalisée:
<ui:include src="/WEB-INF/components/status-badge.xhtml">
<ui:param name="value" value="#{facture.statut}"/>
<ui:param name="icon" value="pi pi-check-circle"/>
</ui:include>
Gravité manuelle:
<ui:include src="/WEB-INF/components/status-badge.xhtml">
<ui:param name="value" value="#{custom.status}"/>
<ui:param name="severity" value="danger"/>
</ui:include>
-->
<c:set var="upperValue" value="#{value.toString().toUpperCase().replace(' ', '_')}"/>
<!-- Déterminer la gravité automatiquement si non fournie -->
<c:choose>
<!-- SUCCESS - Vert -->
<c:when test="#{not empty severity}">
<c:set var="badgeSeverity" value="#{severity}"/>
</c:when>
<c:when test="#{upperValue eq 'EN_COURS' or upperValue eq 'ACTIF' or upperValue eq 'ACTIVE' or
upperValue eq 'TERMINE' or upperValue eq 'COMPLETE' or upperValue eq 'VALIDE' or
upperValue eq 'PAYE' or upperValue eq 'PAYEE' or upperValue eq 'LIVRE' or
upperValue eq 'DISPONIBLE' or upperValue eq 'APPROUVE' or upperValue eq 'ACCEPTE' or
upperValue eq 'OPERATIONNEL'}">
<c:set var="badgeSeverity" value="success"/>
</c:when>
<!-- INFO - Bleu -->
<c:when test="#{upperValue eq 'PLANIFIE' or upperValue eq 'PLANIFIEE' or upperValue eq 'NOUVEAU' or
upperValue eq 'NOUVELLE' or upperValue eq 'EN_ATTENTE' or upperValue eq 'BROUILLON' or
upperValue eq 'PENDING' or upperValue eq 'PROGRAMME' or upperValue eq 'PREVU'}">
<c:set var="badgeSeverity" value="info"/>
</c:when>
<!-- WARNING - Orange -->
<c:when test="#{upperValue eq 'RETARD' or upperValue eq 'EN_RETARD' or upperValue eq 'SUSPENDU' or
upperValue eq 'IMPAYE' or upperValue eq 'IMPAYEE' or upperValue eq 'ALERTE' or
upperValue eq 'MAINTENANCE' or upperValue eq 'UTILISE' or upperValue eq 'OCCUPE' or
upperValue eq 'PARTIEL' or upperValue eq 'PARTIELLE'}">
<c:set var="badgeSeverity" value="warning"/>
</c:when>
<!-- DANGER - Rouge -->
<c:when test="#{upperValue eq 'ANNULE' or upperValue eq 'ANNULEE' or upperValue eq 'REFUSE' or
upperValue eq 'REFUSEE' or upperValue eq 'EXPIRE' or upperValue eq 'EXPIREE' or
upperValue eq 'HORS_SERVICE' or upperValue eq 'BLOQUE' or upperValue eq 'INACTIVE' or
upperValue eq 'INACTIF' or upperValue eq 'URGENT' or upperValue eq 'CRITIQUE'}">
<c:set var="badgeSeverity" value="danger"/>
</c:when>
<!-- Par défaut - Info (bleu) -->
<c:otherwise>
<c:set var="badgeSeverity" value="info"/>
</c:otherwise>
</c:choose>
<!-- Déterminer l'icône automatiquement -->
<c:choose>
<c:when test="#{not empty icon}">
<c:set var="badgeIcon" value="#{icon}"/>
</c:when>
<c:when test="#{badgeSeverity eq 'success'}">
<c:set var="badgeIcon" value="pi pi-check-circle"/>
</c:when>
<c:when test="#{badgeSeverity eq 'info'}">
<c:set var="badgeIcon" value="pi pi-info-circle"/>
</c:when>
<c:when test="#{badgeSeverity eq 'warning'}">
<c:set var="badgeIcon" value="pi pi-exclamation-triangle"/>
</c:when>
<c:when test="#{badgeSeverity eq 'danger'}">
<c:set var="badgeIcon" value="pi pi-times-circle"/>
</c:when>
</c:choose>
<!-- Rendu du badge -->
<p:badge value="#{value}"
severity="#{badgeSeverity}"
styleClass="#{rounded eq false ? '' : 'border-round'}
#{size eq 'large' ? 'text-lg px-3 py-2' : 'px-2'}"
style="display: inline-flex; align-items: center; gap: 0.5rem;">
<i class="#{badgeIcon}" style="font-size: 0.875rem;"/>
<span style="font-weight: 600; text-transform: capitalize;">
#{value.toString().toLowerCase().replace('_', ' ')}
</span>
</p:badge>
</ui:composition>

View File

@@ -65,7 +65,7 @@
============================================= -->
<p:submenu id="m_factures" label="Factures" icon="pi pi-dollar">
<p:menuitem id="m_factures_liste" value="Toutes les factures" icon="pi pi-list" outcome="/factures" />
<p:menuitem id="m_factures_nouvelle" value="Nouvelle facture" icon="pi pi-plus" outcome="/factures/nouvelle" />
<p:menuitem id="m_factures_nouveau" value="Nouvelle facture" icon="pi pi-plus" outcome="/factures/nouveau" />
<p:separator/>
<p:menuitem id="m_factures_brouillon" value="Brouillons" icon="pi pi-pencil" outcome="/factures/brouillon" />
<p:menuitem id="m_factures_emises" value="Émises" icon="pi pi-send" outcome="/factures/emises" />

View File

@@ -0,0 +1,50 @@
<!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">
<h:head>
<title>Accès refusé - BTP Xpress</title>
<f:facet name="first">
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="icon" href="#{resource['layout/images/logo/btpxpress-logo.png']}" type="image/png"/>
</f:facet>
<h:outputStylesheet name="layout/css/layout.css"/>
</h:head>
<h:body>
<div class="surface-ground flex align-items-center justify-content-center min-h-screen min-w-screen overflow-hidden">
<div class="flex flex-column align-items-center justify-content-center">
<div style="border-radius:56px; padding:0.3rem; background: linear-gradient(180deg, var(--primary-color) 10%, rgba(33, 150, 243, 0) 30%);">
<div class="w-full surface-card py-8 px-5 sm:px-8" style="border-radius:53px">
<div class="text-center mb-5">
<img src="#{resource['layout/images/logo/btpxpress-logo.png']}" alt="BTP Xpress logo" class="mb-5 w-6rem flex-shrink-0"/>
<div class="text-900 text-3xl font-medium mb-3">Accès refusé</div>
<span class="text-600 font-medium">Vous n'avez pas les permissions nécessaires pour accéder à cette page.</span>
</div>
<div class="text-center">
<i class="pi pi-ban text-6xl text-red-500 mb-4"></i>
<p class="text-600 mb-4">
Si vous pensez qu'il s'agit d'une erreur, veuillez contacter votre administrateur.
</p>
<div class="flex flex-column gap-2">
<p:button value="Retour au tableau de bord"
icon="pi pi-home"
href="/dashboard.xhtml"
styleClass="p-button-primary w-full"/>
<p:button value="Se déconnecter"
icon="pi pi-sign-out"
href="/logout"
styleClass="p-button-outlined w-full"/>
</div>
</div>
</div>
</div>
</div>
</div>
</h:body>
</html>

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,354 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml">
<ui:define name="title">Détails du devis - BTP Xpress</ui:define>
<f:metadata>
<f:viewParam name="id" value="#{devisView.devisId}"/>
<f:event type="preRenderView" listener="#{devisView.loadDevisById()}"/>
</f:metadata>
<ui:define name="content">
<div class="layout-dashboard">
<div class="grid">
<div class="col-12">
<!-- En-tête avec actions -->
<div class="card mb-3">
<div class="flex align-items-start justify-content-between flex-wrap gap-3">
<div class="flex-grow-1">
<div class="flex align-items-center gap-3 mb-2">
<h2 class="text-900 font-bold m-0">Devis #{devisView.selectedItem.numero}</h2>
<ui:include src="/WEB-INF/components/status-badge.xhtml">
<ui:param name="value" value="#{devisView.selectedItem.statut}"/>
</ui:include>
</div>
<p class="text-600 mt-0 mb-2">
<i class="pi pi-building mr-2"></i>#{devisView.selectedItem.client}
</p>
<p class="text-sm text-600 mt-0 mb-0">#{devisView.selectedItem.objet}</p>
<div class="flex align-items-center gap-3 text-sm mt-2">
<span class="text-600">
<i class="pi pi-calendar mr-1"></i>
Émis le: <h:outputText value="#{devisView.selectedItem.dateEmission}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</span>
<span class="text-600">
<i class="pi pi-calendar-times mr-1"></i>
Valide jusqu'au: <h:outputText value="#{devisView.selectedItem.dateValidite}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</span>
</div>
</div>
<div class="flex gap-2">
<p:commandButton value="Retour"
icon="pi pi-arrow-left"
outcome="/devis"
styleClass="ui-button-secondary ui-button-outlined"/>
<p:commandButton value="Convertir en chantier"
icon="pi pi-arrow-right"
rendered="#{devisView.selectedItem.statut eq 'ACCEPTE'}"
styleClass="ui-button-success"/>
<p:commandButton value="Télécharger PDF"
icon="pi pi-file-pdf"
styleClass="ui-button-danger ui-button-outlined"/>
<p:splitButton value="Modifier"
icon="pi pi-pencil"
styleClass="ui-button-primary">
</p:splitButton>
</div>
</div>
</div>
<!-- KPI Cards -->
<div class="grid mb-3">
<div class="col-12 md:col-6 lg:col-3">
<div class="card mb-0">
<div class="flex flex-column">
<span class="text-600 font-medium text-sm mb-2">Montant HT</span>
<div class="text-900 font-bold text-2xl mb-2">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{devisView.selectedItem.montantHT}"/>
<ui:param name="size" value="normal"/>
<ui:param name="bold" value="true"/>
</ui:include>
</div>
<span class="text-500 text-xs">Hors taxes</span>
</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-3">
<div class="card mb-0">
<div class="flex flex-column">
<span class="text-600 font-medium text-sm mb-2">TVA (18%)</span>
<div class="text-orange-600 font-bold text-2xl mb-2">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{devisView.selectedItem.montantHT * 0.18}"/>
<ui:param name="size" value="normal"/>
<ui:param name="bold" value="true"/>
</ui:include>
</div>
<span class="text-500 text-xs">Taxe sur la valeur ajoutée</span>
</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-3">
<div class="card mb-0">
<div class="flex flex-column">
<span class="text-600 font-medium text-sm mb-2">Montant TTC</span>
<div class="text-primary font-bold text-2xl mb-2">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{devisView.selectedItem.montantHT * 1.18}"/>
<ui:param name="size" value="normal"/>
<ui:param name="bold" value="true"/>
</ui:include>
</div>
<span class="text-500 text-xs">Toutes taxes comprises</span>
</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-3">
<div class="card mb-0">
<div class="flex flex-column">
<span class="text-600 font-medium text-sm mb-2">Statut</span>
<div class="mt-2 mb-2">
<ui:include src="/WEB-INF/components/status-badge.xhtml">
<ui:param name="value" value="#{devisView.selectedItem.statut}"/>
</ui:include>
</div>
<span class="text-500 text-xs">
<h:outputText value="Valide" rendered="#{devisView.selectedItem.statut ne 'EXPIRE'}"/>
<h:outputText value="Expiré" rendered="#{devisView.selectedItem.statut eq 'EXPIRE'}"/>
</span>
</div>
</div>
</div>
</div>
<!-- Onglets détaillés -->
<div class="card">
<p:tabView dynamic="true" cache="false">
<!-- ONGLET 1: Vue d'ensemble -->
<p:tab title="Vue d'ensemble" icon="pi pi-home">
<div class="grid">
<!-- Informations du devis -->
<div class="col-12 lg:col-6">
<h5 class="text-900 font-bold mb-3">Informations du devis</h5>
<div class="surface-50 border-round p-3 mb-3">
<div class="grid">
<div class="col-6">
<span class="text-600 text-sm">Numéro</span>
<p class="text-900 font-bold mt-1 mb-0">#{devisView.selectedItem.numero}</p>
</div>
<div class="col-6">
<span class="text-600 text-sm">Client</span>
<p class="text-900 font-medium mt-1 mb-0">#{devisView.selectedItem.client}</p>
</div>
<div class="col-12">
<span class="text-600 text-sm">Objet</span>
<p class="text-900 font-medium mt-1 mb-0">#{devisView.selectedItem.objet}</p>
</div>
<div class="col-6">
<span class="text-600 text-sm">Date d'émission</span>
<p class="text-900 font-medium mt-1 mb-0">
<h:outputText value="#{devisView.selectedItem.dateEmission}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p>
</div>
<div class="col-6">
<span class="text-600 text-sm">Date de validité</span>
<p class="text-900 font-medium mt-1 mb-0">
<h:outputText value="#{devisView.selectedItem.dateValidite}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:outputText>
</p>
</div>
<div class="col-12">
<span class="text-600 text-sm">Statut</span>
<div class="mt-1">
<ui:include src="/WEB-INF/components/status-badge.xhtml">
<ui:param name="value" value="#{devisView.selectedItem.statut}"/>
</ui:include>
</div>
</div>
</div>
</div>
</div>
<!-- Récapitulatif financier -->
<div class="col-12 lg:col-6">
<h5 class="text-900 font-bold mb-3">Récapitulatif financier</h5>
<div class="surface-50 border-round p-3 mb-3">
<div class="grid">
<div class="col-12">
<div class="flex justify-content-between align-items-center mb-2">
<span class="text-600">Montant HT</span>
<span class="text-900 font-bold">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{devisView.selectedItem.montantHT}"/>
</ui:include>
</span>
</div>
<div class="flex justify-content-between align-items-center mb-2">
<span class="text-600">TVA (18%)</span>
<span class="text-orange-600 font-medium">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{devisView.selectedItem.montantHT * 0.18}"/>
</ui:include>
</span>
</div>
<div class="border-top-1 surface-border pt-2 mt-2">
<div class="flex justify-content-between align-items-center">
<span class="text-900 font-bold text-lg">Total TTC</span>
<span class="text-primary font-bold text-xl">
<ui:include src="/WEB-INF/components/monetary-display.xhtml">
<ui:param name="amount" value="#{devisView.selectedItem.montantHT * 1.18}"/>
<ui:param name="size" value="large"/>
<ui:param name="bold" value="true"/>
</ui:include>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Actions rapides -->
<div class="col-12">
<h5 class="text-900 font-bold mb-3">Actions rapides</h5>
<div class="surface-50 border-round p-3">
<div class="flex flex-wrap gap-2">
<p:commandButton value="Accepter le devis"
icon="pi pi-check"
rendered="#{devisView.selectedItem.statut eq 'ATTENTE'}"
styleClass="ui-button-success"/>
<p:commandButton value="Refuser"
icon="pi pi-times"
rendered="#{devisView.selectedItem.statut eq 'ATTENTE'}"
styleClass="ui-button-danger ui-button-outlined"/>
<p:commandButton value="Convertir en chantier"
icon="pi pi-arrow-right"
rendered="#{devisView.selectedItem.statut eq 'ACCEPTE'}"
styleClass="ui-button-primary"/>
<p:commandButton value="Dupliquer"
icon="pi pi-copy"
styleClass="ui-button-secondary ui-button-outlined"/>
<p:commandButton value="Envoyer par email"
icon="pi pi-send"
styleClass="ui-button-info ui-button-outlined"/>
<p:commandButton value="Télécharger PDF"
icon="pi pi-file-pdf"
styleClass="ui-button-danger ui-button-outlined"/>
</div>
</div>
</div>
</div>
</p:tab>
<!-- ONGLET 2: Lignes du devis -->
<p:tab title="Détail des lignes" icon="pi pi-list">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<h5 class="text-900 font-bold m-0">Lignes du devis</h5>
<p:commandButton value="Ajouter une ligne"
icon="pi pi-plus"
styleClass="ui-button-success ui-button-sm"/>
</div>
<p:message severity="info" text="Fonctionnalité de gestion des lignes de devis en cours de développement"/>
<div class="surface-50 border-round p-4 text-center mt-3">
<i class="pi pi-list text-400 mb-3" style="font-size: 3rem;"></i>
<p class="text-600">Bientôt disponible: tableau des prestations avec quantités, prix unitaires, sous-totaux</p>
</div>
</div>
</p:tab>
<!-- ONGLET 3: Conditions -->
<p:tab title="Conditions" icon="pi pi-file-edit">
<div class="p-3">
<h5 class="text-900 font-bold mb-3">Conditions commerciales</h5>
<div class="surface-50 border-round p-3 mb-3">
<h6 class="text-900 font-medium mb-2">Conditions de paiement</h6>
<p class="text-600 text-sm">
Les conditions de paiement seront affichées ici (exemple: paiement en 3 fois, 30% à la commande, etc.)
</p>
</div>
<div class="surface-50 border-round p-3 mb-3">
<h6 class="text-900 font-medium mb-2">Délais de livraison</h6>
<p class="text-600 text-sm">
Information sur les délais de réalisation du projet
</p>
</div>
<div class="surface-50 border-round p-3">
<h6 class="text-900 font-medium mb-2">Garanties</h6>
<p class="text-600 text-sm">
Conditions de garantie et assurances
</p>
</div>
</div>
</p:tab>
<!-- ONGLET 4: Documents -->
<p:tab title="Documents" icon="pi pi-folder">
<div class="p-3">
<div class="flex align-items-center justify-content-between mb-3">
<h5 class="text-900 font-bold m-0">Documents associés</h5>
<p:commandButton value="Ajouter un document"
icon="pi pi-upload"
styleClass="ui-button-success ui-button-sm"/>
</div>
<p:message severity="info" text="Fonctionnalité de gestion documentaire en cours de développement"/>
</div>
</p:tab>
<!-- ONGLET 5: Suivi -->
<p:tab title="Suivi" icon="pi pi-chart-line">
<div class="p-3">
<h5 class="text-900 font-bold mb-3">Suivi du devis</h5>
<div class="surface-50 border-round p-3">
<p:timeline align="alternate">
<p:templateSlot name="marker">
<i class="pi pi-circle-fill text-primary"></i>
</p:templateSlot>
<p:templateSlot name="content">
<small class="text-600">Historique des actions (création, envoi, acceptation, etc.)</small>
</p:templateSlot>
</p:timeline>
</div>
</div>
</p:tab>
<!-- ONGLET 6: Historique -->
<p:tab title="Historique" icon="pi pi-history">
<div class="p-3">
<h5 class="text-900 font-bold mb-3">Historique des modifications</h5>
<p:timeline align="alternate">
<p:templateSlot name="marker">
<i class="pi pi-circle-fill text-primary"></i>
</p:templateSlot>
<p:templateSlot name="content">
<small class="text-600">Fonctionnalité en cours de développement</small>
</p:templateSlot>
</p:timeline>
</div>
</p:tab>
</p:tabView>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

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

View File

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

View File

@@ -62,7 +62,7 @@
<ui:param name="viewBean" value="#{factureView}"/>
<ui:param name="var" value="facture"/>
<ui:param name="title" value="Liste des factures"/>
<ui:param name="createPath" value="/factures/nouvelle"/>
<ui:param name="createPath" value="/factures/nouveau"/>
<ui:define name="columns">
<p:column headerText="Numéro" sortBy="#{facture.numero}">
<h:outputText value="#{facture.numero}"/>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,114 @@
# Configuration de production pour BTP Xpress Client
# Variables d'environnement requises :
# - BTPXPRESS_API_BASE_URL : URL de l'API backend
# Application
quarkus.application.name=BTP Xpress Client
quarkus.application.version=1.0.0
# Configuration PrimeFaces
primefaces.THEME=freya-purple-light
primefaces.FONT_AWESOME=true
primefaces.UPLOADER=auto
primefaces.MOVE_SCRIPTS_TO_BOTTOM=true
primefaces.CLIENT_SIDE_VALIDATION=true
# Configuration JSF - Production
jakarta.faces.PROJECT_STAGE=Production
jakarta.faces.STATE_SAVING_METHOD=server
jakarta.faces.DATETIMECONVERTER_DEFAULT_TIMEZONE_IS_SYSTEM_TIMEZONE=true
jakarta.faces.PARTIAL_STATE_SAVING=true
jakarta.faces.VALIDATE_EMPTY_FIELDS=auto
# Configuration Arc
quarkus.arc.remove-unused-beans=true
# Serveur HTTP
quarkus.http.port=8081
quarkus.http.host=0.0.0.0
# CORS Configuration pour production
# Frontend accessible depuis btpxpress.lions.dev
quarkus.http.cors=true
quarkus.http.cors.origins=https://btpxpress.lions.dev,https://www.btpxpress.lions.dev
quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS,PATCH
quarkus.http.cors.headers=Content-Type,Authorization,X-Requested-With,X-CSRF-Token
quarkus.http.cors.exposed-headers=Content-Disposition
quarkus.http.cors.access-control-max-age=3600
quarkus.http.cors.access-control-allow-credentials=true
# Configuration OIDC / Keycloak pour production
quarkus.oidc.enabled=true
quarkus.oidc.auth-server-url=https://security.lions.dev/realms/btpxpress
quarkus.oidc.client-id=btpxpress-frontend
quarkus.oidc.application-type=web-app
quarkus.oidc.tls.verification=required
# Authentification
quarkus.oidc.authentication.redirect-path=/
quarkus.oidc.authentication.restore-path-after-redirect=true
quarkus.oidc.authentication.cookie-path=/
quarkus.oidc.authentication.session-age-extension=PT30M
quarkus.oidc.authentication.cookie-same-site=strict
# Token configuration
quarkus.oidc.token.issuer=https://security.lions.dev/realms/btpxpress
quarkus.oidc.discovery-enabled=true
# Token state manager
quarkus.oidc.token-state-manager.split-tokens=true
quarkus.oidc.token-state-manager.strategy=id-refresh-tokens
quarkus.oidc.token-state-manager.encryption-required=true
quarkus.oidc.token-state-manager.cookie-max-size=8192
quarkus.oidc.token-state-manager.cookie-secure=true
quarkus.oidc.token-state-manager.cookie-http-only=true
# Limites HTTP pour sécurité
quarkus.http.max-headers-size=128K
quarkus.http.max-request-body-size=10M
quarkus.http.max-parameters=1000
quarkus.http.max-parameter-size=2048
quarkus.vertx.max-headers-size=128K
vertx.http.maxHeaderSize=131072
# Configuration sécurité
quarkus.security.users.embedded.enabled=false
quarkus.http.auth.proactive=true
quarkus.security.deny-unannotated-endpoints=false
# Permissions pour accès public aux ressources statiques et pages publiques
quarkus.http.auth.permission.public.paths=/*.css,/*.js,/*.png,/*.jpg,/*.jpeg,/*.gif,/*.svg,/*.woff,/*.woff2,/*.ttf,/*.eot,/resources/*
quarkus.http.auth.permission.public.policy=permit
# Authentification requise pour toutes les autres pages
quarkus.http.auth.permission.authenticated.paths=/*
quarkus.http.auth.permission.authenticated.policy=authenticated
# Configuration API Backend
btpxpress.api.base-url=${BTPXPRESS_API_BASE_URL:https://api.btpxpress.lions.dev}
btpxpress.api.timeout=30000
quarkus.rest-client."dev.lions.btpxpress.service.BtpXpressApiClient".url=${btpxpress.api.base-url}
quarkus.rest-client."dev.lions.btpxpress.service.BtpXpressApiClient".scope=jakarta.inject.Singleton
# Locale
quarkus.locale=fr_FR
# Logging - Production
quarkus.log.level=INFO
quarkus.log.category."dev.lions.btpxpress".level=INFO
quarkus.log.category."org.hibernate".level=WARN
quarkus.log.category."io.quarkus".level=INFO
quarkus.log.category."io.quarkus.oidc".level=WARN
quarkus.log.console.enable=true
quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{2.}] (%t) %s%e%n
# Cache optimisé pour production
quarkus.cache.caffeine.default.initial-capacity=200
quarkus.cache.caffeine.default.maximum-size=2000
quarkus.cache.caffeine.default.expire-after-write=PT1H
# Compression
quarkus.http.enable-compression=true