feat: Migration complète vers Quarkus PrimeFaces Freya
Migration du frontend React/Next.js vers Quarkus + PrimeFaces Freya 5.0.0 Dashboard: - Extension de BtpXpressApiClient avec tous les endpoints dashboard - Création de DashboardService pour récupérer les données API - Refactorisation DashboardView : uniquement données réelles de l'API - Restructuration dashboard.xhtml avec tous les aspects métiers BTP - Suppression complète de toutes les données fictives Topbar: - Amélioration du menu profil utilisateur avec header professionnel - Ajout UserSessionBean pour gérer les informations utilisateur - Styles CSS personnalisés pour une disposition raffinée - Badges de notifications conditionnels Configuration: - Intégration du thème Freya 5.0.0-jakarta - Configuration OIDC pour Keycloak (security.lions.dev) - Gestion des erreurs HTTP 431 (headers size) - Support du format Fcfa avec séparateurs d'espaces Converters: - Création de FcfaConverter pour formater les montants en Fcfa avec espaces (x xxx xxx format) Code Quality: - Code entièrement documenté en français avec Javadoc exemplaire - Respect du principe Java 'Write once, use many times' - Logging complet pour le débogage - Gestion d'erreurs robuste
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
package dev.lions.btpxpress.converter;
|
||||
|
||||
import jakarta.faces.component.UIComponent;
|
||||
import jakarta.faces.context.FacesContext;
|
||||
import jakarta.faces.convert.Converter;
|
||||
import jakarta.faces.convert.FacesConverter;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Converter personnalisé pour formater les montants en Franc CFA (Fcfa).
|
||||
*
|
||||
* <p>Ce converter formate les nombres avec des espaces comme séparateurs de milliers
|
||||
* au lieu de virgules, conformément au format standard du Franc CFA.</p>
|
||||
*
|
||||
* <p>Exemple : 1234567 devient "1 234 567 Fcfa"</p>
|
||||
*
|
||||
* @author BTP Xpress Team
|
||||
* @version 1.0
|
||||
*/
|
||||
@FacesConverter("fcfaConverter")
|
||||
public class FcfaConverter implements Converter<Number> {
|
||||
|
||||
private static final DecimalFormatSymbols SYMBOLS;
|
||||
|
||||
static {
|
||||
SYMBOLS = new DecimalFormatSymbols(Locale.FRENCH);
|
||||
SYMBOLS.setGroupingSeparator(' ');
|
||||
SYMBOLS.setDecimalSeparator(',');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une chaîne de caractères en nombre.
|
||||
*
|
||||
* @param context Le contexte Faces
|
||||
* @param component Le composant UI
|
||||
* @param value La valeur string à convertir
|
||||
* @return Le nombre converti, ou null si la valeur est vide/null
|
||||
*/
|
||||
@Override
|
||||
public Number getAsObject(FacesContext context, UIComponent component, String value) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Retirer les espaces et le préfixe "Fcfa" si présent
|
||||
String cleanedValue = value.replaceAll("\\s+", "")
|
||||
.replace("Fcfa", "")
|
||||
.replace("fcfa", "")
|
||||
.trim();
|
||||
|
||||
return new BigDecimal(cleanedValue);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Impossible de convertir '" + value + "' en nombre", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un nombre en chaîne de caractères formatée.
|
||||
*
|
||||
* @param context Le contexte Faces
|
||||
* @param component Le composant UI
|
||||
* @param value Le nombre à convertir
|
||||
* @return La chaîne formatée avec espaces comme séparateurs de milliers
|
||||
*/
|
||||
@Override
|
||||
public String getAsString(FacesContext context, UIComponent component, Number value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Formater avec espaces comme séparateurs de milliers (format Fcfa standard)
|
||||
// Le pattern "#" avec groupingUsed=true utilise le groupingSeparator défini dans SYMBOLS (espace)
|
||||
DecimalFormat formatter = new DecimalFormat("#", SYMBOLS);
|
||||
formatter.setGroupingSize(3);
|
||||
formatter.setGroupingUsed(true);
|
||||
formatter.setMaximumFractionDigits(0);
|
||||
|
||||
long amount = value.longValue();
|
||||
return formatter.format(amount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package dev.lions.btpxpress.filter;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.FilterConfig;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class CharacterEncodingFilter implements Filter {
|
||||
|
||||
private static final String DEFAULT_ENCODING = "UTF-8";
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) throws ServletException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
request.setCharacterEncoding(DEFAULT_ENCODING);
|
||||
response.setCharacterEncoding(DEFAULT_ENCODING);
|
||||
response.setContentType("text/html; charset=" + DEFAULT_ENCODING);
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package dev.lions.btpxpress.service;
|
||||
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
|
||||
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
|
||||
|
||||
/**
|
||||
* Interface REST Client pour communiquer avec l'API backend BTP Xpress.
|
||||
* <p>
|
||||
* Ce client permet au frontend PrimeFaces de communiquer avec le backend Quarkus
|
||||
* en utilisant les endpoints REST exposés sur /api/v1/*. L'authentification
|
||||
* est gérée automatiquement via les tokens JWT Keycloak.
|
||||
* </p>
|
||||
*
|
||||
* @author BTP Xpress Development Team
|
||||
* @version 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@RegisterRestClient(configKey = "btpxpress.api")
|
||||
@RegisterClientHeaders
|
||||
@Path("/api/v1")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public interface BtpXpressApiClient {
|
||||
|
||||
/**
|
||||
* Récupère la liste des chantiers.
|
||||
* Correspond à {@code ChantierResource.getAllChantiers()} dans le serveur.
|
||||
*
|
||||
* @return Réponse HTTP contenant la liste des chantiers.
|
||||
*/
|
||||
@GET
|
||||
@Path("/chantiers")
|
||||
Response getChantiers();
|
||||
|
||||
/**
|
||||
* Récupère un chantier par son identifiant.
|
||||
* Correspond à {@code ChantierResource.getChantierById()} dans le serveur.
|
||||
*
|
||||
* @param id L'identifiant du chantier.
|
||||
* @return Réponse HTTP contenant le chantier.
|
||||
*/
|
||||
@GET
|
||||
@Path("/chantiers/{id}")
|
||||
Response getChantier(@PathParam("id") Long id);
|
||||
|
||||
/**
|
||||
* Récupère la liste des clients.
|
||||
* Correspond à {@code ClientResource.getAllClients()} dans le serveur.
|
||||
*
|
||||
* @return Réponse HTTP contenant la liste des clients.
|
||||
*/
|
||||
@GET
|
||||
@Path("/clients")
|
||||
Response getClients();
|
||||
|
||||
/**
|
||||
* Récupère un client par son identifiant.
|
||||
* Correspond à {@code ClientResource.getClientById()} dans le serveur.
|
||||
*
|
||||
* @param id L'identifiant du client.
|
||||
* @return Réponse HTTP contenant le client.
|
||||
*/
|
||||
@GET
|
||||
@Path("/clients/{id}")
|
||||
Response getClient(@PathParam("id") Long id);
|
||||
|
||||
// === ENDPOINTS DASHBOARD ===
|
||||
|
||||
/**
|
||||
* Récupère le dashboard principal avec les métriques globales.
|
||||
* Correspond à {@code DashboardResource.getDashboardPrincipal()} dans le serveur.
|
||||
*
|
||||
* @return Réponse HTTP contenant les métriques du dashboard.
|
||||
*/
|
||||
@GET
|
||||
@Path("/dashboard")
|
||||
Response getDashboardPrincipal();
|
||||
|
||||
/**
|
||||
* Récupère le dashboard des chantiers avec métriques détaillées.
|
||||
* Correspond à {@code DashboardResource.getDashboardChantiers()} dans le serveur.
|
||||
*
|
||||
* @return Réponse HTTP contenant les métriques des chantiers.
|
||||
*/
|
||||
@GET
|
||||
@Path("/dashboard/chantiers")
|
||||
Response getDashboardChantiers();
|
||||
|
||||
/**
|
||||
* Récupère les métriques financières.
|
||||
* Correspond à {@code DashboardResource.getDashboardFinances()} dans le serveur.
|
||||
*
|
||||
* @param periode Période en jours (défaut: 30).
|
||||
* @return Réponse HTTP contenant les métriques financières.
|
||||
*/
|
||||
@GET
|
||||
@Path("/dashboard/finances")
|
||||
Response getDashboardFinances(@QueryParam("periode") @DefaultValue("30") int periode);
|
||||
|
||||
/**
|
||||
* Récupère les métriques de maintenance.
|
||||
* Correspond à {@code DashboardResource.getDashboardMaintenance()} dans le serveur.
|
||||
*
|
||||
* @return Réponse HTTP contenant les métriques de maintenance.
|
||||
*/
|
||||
@GET
|
||||
@Path("/dashboard/maintenance")
|
||||
Response getDashboardMaintenance();
|
||||
|
||||
/**
|
||||
* Récupère les métriques des ressources (équipes, employés, matériel).
|
||||
* Correspond à {@code DashboardResource.getDashboardRessources()} dans le serveur.
|
||||
*
|
||||
* @return Réponse HTTP contenant les métriques des ressources.
|
||||
*/
|
||||
@GET
|
||||
@Path("/dashboard/ressources")
|
||||
Response getDashboardRessources();
|
||||
|
||||
/**
|
||||
* Récupère les alertes nécessitant une attention immédiate.
|
||||
* Correspond à {@code DashboardResource.getAlertes()} dans le serveur.
|
||||
*
|
||||
* @return Réponse HTTP contenant les alertes.
|
||||
*/
|
||||
@GET
|
||||
@Path("/dashboard/alertes")
|
||||
Response getAlertes();
|
||||
|
||||
/**
|
||||
* Récupère les KPIs principaux.
|
||||
* Correspond à {@code DashboardResource.getKPI()} dans le serveur.
|
||||
*
|
||||
* @param periode Période en jours (défaut: 30).
|
||||
* @return Réponse HTTP contenant les KPIs.
|
||||
*/
|
||||
@GET
|
||||
@Path("/dashboard/kpi")
|
||||
Response getKPI(@QueryParam("periode") @DefaultValue("30") int periode);
|
||||
|
||||
/**
|
||||
* Récupère les activités récentes.
|
||||
* Correspond à {@code DashboardResource.getActivitesRecentes()} dans le serveur.
|
||||
*
|
||||
* @param limit Nombre d'activités à récupérer (défaut: 10).
|
||||
* @return Réponse HTTP contenant les activités récentes.
|
||||
*/
|
||||
@GET
|
||||
@Path("/dashboard/activites-recentes")
|
||||
Response getActivitesRecentes(@QueryParam("limit") @DefaultValue("10") int limit);
|
||||
|
||||
/**
|
||||
* Récupère le résumé quotidien.
|
||||
* Correspond à {@code DashboardResource.getResumeQuotidien()} dans le serveur.
|
||||
*
|
||||
* @return Réponse HTTP contenant le résumé quotidien.
|
||||
*/
|
||||
@GET
|
||||
@Path("/dashboard/resume-quotidien")
|
||||
Response getResumeQuotidien();
|
||||
|
||||
/**
|
||||
* Récupère la liste des devis.
|
||||
* Correspond à {@code DevisResource.getAllDevis()} dans le serveur.
|
||||
*
|
||||
* @return Réponse HTTP contenant la liste des devis.
|
||||
*/
|
||||
@GET
|
||||
@Path("/devis")
|
||||
Response getDevis();
|
||||
|
||||
/**
|
||||
* Récupère la liste des factures.
|
||||
* Correspond à {@code FactureResource.getAllFactures()} dans le serveur.
|
||||
*
|
||||
* @return Réponse HTTP contenant la liste des factures.
|
||||
*/
|
||||
@GET
|
||||
@Path("/factures")
|
||||
Response getFactures();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package dev.lions.btpxpress.service;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.rest.client.inject.RestClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Service de gestion des chantiers côté client.
|
||||
* <p>
|
||||
* Ce service encapsule la communication avec l'API backend pour les opérations
|
||||
* liées aux chantiers. Il utilise le REST Client pour effectuer les appels HTTP
|
||||
* vers le backend.
|
||||
* </p>
|
||||
*
|
||||
* @author BTP Xpress Development Team
|
||||
* @version 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class ChantierService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ChantierService.class);
|
||||
|
||||
@Inject
|
||||
@RestClient
|
||||
BtpXpressApiClient apiClient;
|
||||
|
||||
/**
|
||||
* Récupère tous les chantiers depuis l'API backend.
|
||||
*
|
||||
* @return Liste des chantiers, ou liste vide en cas d'erreur.
|
||||
*/
|
||||
public List<Map<String, Object>> getAllChantiers() {
|
||||
try {
|
||||
LOG.debug("Récupération de la liste des chantiers depuis l'API backend.");
|
||||
Response response = apiClient.getChantiers();
|
||||
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
|
||||
Map<String, Object> data = response.readEntity(Map.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> chantiers = (List<Map<String, Object>>) data.get("chantiers");
|
||||
LOG.debug("Chantiers récupérés avec succès : {} élément(s)", chantiers != null ? chantiers.size() : 0);
|
||||
return chantiers != null ? chantiers : new ArrayList<>();
|
||||
} else {
|
||||
LOG.warn("Erreur lors de la récupération des chantiers. Code HTTP : {}", response.getStatus());
|
||||
return new ArrayList<>();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors de la communication avec l'API backend pour récupérer les chantiers : {}", e.getMessage(), e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un chantier par son identifiant depuis l'API backend.
|
||||
*
|
||||
* @param id L'identifiant du chantier.
|
||||
* @return Le chantier sous forme de Map, ou null en cas d'erreur.
|
||||
*/
|
||||
public Map<String, Object> getChantierById(Long id) {
|
||||
try {
|
||||
LOG.debug("Récupération du chantier avec ID : {}", id);
|
||||
Response response = apiClient.getChantier(id);
|
||||
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
|
||||
Map<String, Object> chantier = response.readEntity(Map.class);
|
||||
LOG.debug("Chantier récupéré avec succès.");
|
||||
return chantier;
|
||||
} else {
|
||||
LOG.warn("Erreur lors de la récupération du chantier. Code HTTP : {}", response.getStatus());
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors de la communication avec l'API backend pour récupérer le chantier : {}", e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
311
src/main/java/dev/lions/btpxpress/service/DashboardService.java
Normal file
311
src/main/java/dev/lions/btpxpress/service/DashboardService.java
Normal file
@@ -0,0 +1,311 @@
|
||||
package dev.lions.btpxpress.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.rest.client.inject.RestClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Service pour récupérer et transformer les données du dashboard depuis l'API backend.
|
||||
*
|
||||
* <p>Ce service encapsule tous les appels à l'API dashboard et transforme
|
||||
* les réponses JSON en objets Java utilisables par les vues JSF.</p>
|
||||
*
|
||||
* @author BTP Xpress Team
|
||||
* @version 1.0
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class DashboardService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DashboardService.class);
|
||||
|
||||
@Inject
|
||||
@RestClient
|
||||
BtpXpressApiClient apiClient;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* Récupère les métriques du dashboard principal.
|
||||
*
|
||||
* @return JsonNode contenant les métriques ou null en cas d'erreur
|
||||
*/
|
||||
public JsonNode getDashboardPrincipal() {
|
||||
try {
|
||||
Response response = apiClient.getDashboardPrincipal();
|
||||
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
|
||||
Object entity = response.getEntity();
|
||||
if (entity == null) {
|
||||
logger.warn("Réponse vide du dashboard principal");
|
||||
return null;
|
||||
}
|
||||
// REST Client avec Jackson désérialise déjà en Map/Object
|
||||
return convertToJsonNode(entity);
|
||||
} else {
|
||||
logger.error("Erreur API dashboard principal: status {}", response.getStatus());
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Erreur lors de la récupération du dashboard principal", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un objet en JsonNode, quel que soit son type (String, Map, Object, etc.).
|
||||
*/
|
||||
private JsonNode convertToJsonNode(Object entity) {
|
||||
try {
|
||||
if (entity instanceof String) {
|
||||
return objectMapper.readTree((String) entity);
|
||||
} else if (entity instanceof Map || entity instanceof List) {
|
||||
// Map ou List sont déjà désérialisés par REST Client
|
||||
return objectMapper.valueToTree(entity);
|
||||
} else {
|
||||
// Pour les autres objets, conversion via ObjectMapper
|
||||
return objectMapper.valueToTree(entity);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Erreur lors de la conversion en JsonNode", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les métriques des chantiers.
|
||||
*
|
||||
* @return JsonNode contenant les métriques des chantiers ou null en cas d'erreur
|
||||
*/
|
||||
public JsonNode getDashboardChantiers() {
|
||||
try {
|
||||
Response response = apiClient.getDashboardChantiers();
|
||||
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
|
||||
Object entity = response.getEntity();
|
||||
return convertToJsonNode(entity);
|
||||
} else {
|
||||
logger.error("Erreur API dashboard chantiers: status {}", response.getStatus());
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Erreur lors de la récupération du dashboard chantiers", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les métriques financières.
|
||||
*
|
||||
* @param periode Période en jours (défaut: 30)
|
||||
* @return JsonNode contenant les métriques financières ou null en cas d'erreur
|
||||
*/
|
||||
public JsonNode getDashboardFinances(int periode) {
|
||||
try {
|
||||
Response response = apiClient.getDashboardFinances(periode);
|
||||
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
|
||||
Object entity = response.getEntity();
|
||||
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
|
||||
} else {
|
||||
logger.error("Erreur API dashboard finances: status {}", response.getStatus());
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Erreur lors de la récupération du dashboard finances", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les métriques de maintenance.
|
||||
*
|
||||
* @return JsonNode contenant les métriques de maintenance ou null en cas d'erreur
|
||||
*/
|
||||
public JsonNode getDashboardMaintenance() {
|
||||
try {
|
||||
Response response = apiClient.getDashboardMaintenance();
|
||||
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
|
||||
Object entity = response.getEntity();
|
||||
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
|
||||
} else {
|
||||
logger.error("Erreur API dashboard maintenance: status {}", response.getStatus());
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Erreur lors de la récupération du dashboard maintenance", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les métriques des ressources.
|
||||
*
|
||||
* @return JsonNode contenant les métriques des ressources ou null en cas d'erreur
|
||||
*/
|
||||
public JsonNode getDashboardRessources() {
|
||||
try {
|
||||
Response response = apiClient.getDashboardRessources();
|
||||
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
|
||||
Object entity = response.getEntity();
|
||||
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
|
||||
} else {
|
||||
logger.error("Erreur API dashboard ressources: status {}", response.getStatus());
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Erreur lors de la récupération du dashboard ressources", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les alertes.
|
||||
*
|
||||
* @return JsonNode contenant les alertes ou null en cas d'erreur
|
||||
*/
|
||||
public JsonNode getAlertes() {
|
||||
try {
|
||||
Response response = apiClient.getAlertes();
|
||||
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
|
||||
Object entity = response.getEntity();
|
||||
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
|
||||
} else {
|
||||
logger.error("Erreur API alertes: status {}", response.getStatus());
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Erreur lors de la récupération des alertes", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les KPIs.
|
||||
*
|
||||
* @param periode Période en jours (défaut: 30)
|
||||
* @return JsonNode contenant les KPIs ou null en cas d'erreur
|
||||
*/
|
||||
public JsonNode getKPI(int periode) {
|
||||
try {
|
||||
Response response = apiClient.getKPI(periode);
|
||||
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
|
||||
Object entity = response.getEntity();
|
||||
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
|
||||
} else {
|
||||
logger.error("Erreur API KPI: status {}", response.getStatus());
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Erreur lors de la récupération des KPIs", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les activités récentes.
|
||||
*
|
||||
* @param limit Nombre d'activités à récupérer
|
||||
* @return JsonNode contenant les activités récentes ou null en cas d'erreur
|
||||
*/
|
||||
public JsonNode getActivitesRecentes(int limit) {
|
||||
try {
|
||||
Response response = apiClient.getActivitesRecentes(limit);
|
||||
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
|
||||
Object entity = response.getEntity();
|
||||
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
|
||||
} else {
|
||||
logger.error("Erreur API activités récentes: status {}", response.getStatus());
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Erreur lors de la récupération des activités récentes", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le résumé quotidien.
|
||||
*
|
||||
* @return JsonNode contenant le résumé quotidien ou null en cas d'erreur
|
||||
*/
|
||||
public JsonNode getResumeQuotidien() {
|
||||
try {
|
||||
Response response = apiClient.getResumeQuotidien();
|
||||
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
|
||||
Object entity = response.getEntity();
|
||||
return entity instanceof String ? objectMapper.readTree((String) entity) : objectMapper.valueToTree(entity);
|
||||
} else {
|
||||
logger.error("Erreur API résumé quotidien: status {}", response.getStatus());
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Erreur lors de la récupération du résumé quotidien", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le nombre de clients.
|
||||
*
|
||||
* @return Nombre de clients ou 0 en cas d'erreur
|
||||
*/
|
||||
public int getNombreClients() {
|
||||
try {
|
||||
Response response = apiClient.getClients();
|
||||
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
|
||||
List<?> clients = (List<?>) response.getEntity();
|
||||
return clients != null ? clients.size() : 0;
|
||||
}
|
||||
return 0;
|
||||
} catch (Exception e) {
|
||||
logger.error("Erreur lors de la récupération du nombre de clients", e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le nombre de devis en attente.
|
||||
*
|
||||
* @return Nombre de devis en attente ou 0 en cas d'erreur
|
||||
*/
|
||||
public int getNombreDevisEnAttente() {
|
||||
try {
|
||||
Response response = apiClient.getDevis();
|
||||
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
|
||||
List<?> devis = (List<?>) response.getEntity();
|
||||
// TODO: Filtrer par statut EN_ATTENTE si l'API le permet
|
||||
return devis != null ? devis.size() : 0;
|
||||
}
|
||||
return 0;
|
||||
} catch (Exception e) {
|
||||
logger.error("Erreur lors de la récupération du nombre de devis", e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le nombre de factures impayées.
|
||||
*
|
||||
* @return Nombre de factures impayées ou 0 en cas d'erreur
|
||||
*/
|
||||
public int getNombreFacturesImpayees() {
|
||||
try {
|
||||
Response response = apiClient.getFactures();
|
||||
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
|
||||
List<?> factures = (List<?>) response.getEntity();
|
||||
// TODO: Filtrer par statut IMPAYEE si l'API le permet
|
||||
return factures != null ? factures.size() : 0;
|
||||
}
|
||||
return 0;
|
||||
} catch (Exception e) {
|
||||
logger.error("Erreur lors de la récupération du nombre de factures", e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
72
src/main/java/dev/lions/btpxpress/view/BaseListView.java
Normal file
72
src/main/java/dev/lions/btpxpress/view/BaseListView.java
Normal file
@@ -0,0 +1,72 @@
|
||||
package dev.lions.btpxpress.view;
|
||||
|
||||
import jakarta.faces.view.ViewScoped;
|
||||
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.function.Predicate;
|
||||
|
||||
@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<>();
|
||||
protected T selectedItem;
|
||||
protected boolean loading = false;
|
||||
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
public void search() {
|
||||
LOG.debug("Recherche lancée pour {}", getClass().getSimpleName());
|
||||
loadItems();
|
||||
}
|
||||
|
||||
public void resetFilters() {
|
||||
LOG.debug("Réinitialisation des filtres pour {}", getClass().getSimpleName());
|
||||
resetFilterFields();
|
||||
loadItems();
|
||||
}
|
||||
|
||||
protected abstract void resetFilterFields();
|
||||
|
||||
public String viewDetails(ID id) {
|
||||
LOG.debug("Redirection vers détails : {}", id);
|
||||
return getDetailsPath() + id + "?faces-redirect=true";
|
||||
}
|
||||
|
||||
protected abstract String getDetailsPath();
|
||||
|
||||
public String createNew() {
|
||||
LOG.debug("Redirection vers création");
|
||||
return getCreatePath() + "?faces-redirect=true";
|
||||
}
|
||||
|
||||
protected abstract String getCreatePath();
|
||||
|
||||
public void delete() {
|
||||
if (selectedItem != null) {
|
||||
LOG.info("Suppression : {}", selectedItem);
|
||||
performDelete();
|
||||
items.remove(selectedItem);
|
||||
selectedItem = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void performDelete();
|
||||
}
|
||||
|
||||
182
src/main/java/dev/lions/btpxpress/view/ChantiersView.java
Normal file
182
src/main/java/dev/lions/btpxpress/view/ChantiersView.java
Normal file
@@ -0,0 +1,182 @@
|
||||
package dev.lions.btpxpress.view;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.faces.view.ViewScoped;
|
||||
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.function.Predicate;
|
||||
|
||||
@Named("chantiersView")
|
||||
@ViewScoped
|
||||
@Getter
|
||||
@Setter
|
||||
public class ChantiersView extends BaseListView<ChantiersView.Chantier, Long> implements Serializable {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ChantiersView.class);
|
||||
|
||||
private String filtreNom;
|
||||
private String filtreClient;
|
||||
private String filtreStatut;
|
||||
private Long chantierId;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
if (filtreStatut == null) {
|
||||
filtreStatut = "TOUS";
|
||||
}
|
||||
loadItems();
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit le filtre de statut (utilisé depuis les pages filtrées).
|
||||
*/
|
||||
public void setFiltreStatut(String statut) {
|
||||
this.filtreStatut = statut;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadItems() {
|
||||
loading = true;
|
||||
try {
|
||||
items = new ArrayList<>();
|
||||
for (int i = 1; i <= 20; i++) {
|
||||
Chantier c = new Chantier();
|
||||
c.setId((long) i);
|
||||
c.setNom("Chantier " + i);
|
||||
c.setClient("Client " + (i % 5 + 1));
|
||||
c.setAdresse("123 Rue Exemple " + i + ", 75001 Paris");
|
||||
c.setDateDebut(LocalDate.now().minusDays(i * 10));
|
||||
c.setDateFinPrevue(LocalDate.now().plusDays((20 - i) * 10));
|
||||
c.setStatut(i % 3 == 0 ? "TERMINE" : (i % 3 == 1 ? "EN_COURS" : "PLANIFIE"));
|
||||
c.setAvancement(i * 5);
|
||||
c.setBudget(i * 15000.0);
|
||||
c.setCoutReel(i * 12000.0);
|
||||
items.add(c);
|
||||
}
|
||||
applyFilters(items, buildFilters());
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur chargement chantiers", e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Predicate<Chantier>> buildFilters() {
|
||||
List<Predicate<Chantier>> filters = new ArrayList<>();
|
||||
if (filtreNom != null && !filtreNom.trim().isEmpty()) {
|
||||
filters.add(c -> c.getNom().toLowerCase().contains(filtreNom.toLowerCase()));
|
||||
}
|
||||
if (filtreClient != null && !filtreClient.trim().isEmpty()) {
|
||||
filters.add(c -> c.getClient().toLowerCase().contains(filtreClient.toLowerCase()));
|
||||
}
|
||||
if (filtreStatut != null && !filtreStatut.trim().isEmpty() && !"TOUS".equals(filtreStatut)) {
|
||||
filters.add(c -> c.getStatut().equals(filtreStatut));
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetFilterFields() {
|
||||
filtreNom = null;
|
||||
filtreClient = null;
|
||||
filtreStatut = "TOUS";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDetailsPath() {
|
||||
return "/chantiers/";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getCreatePath() {
|
||||
return "/chantiers/nouveau";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void performDelete() {
|
||||
LOG.info("Suppression chantier : {}", selectedItem.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise un nouveau chantier pour la création.
|
||||
*/
|
||||
@Override
|
||||
public String createNew() {
|
||||
selectedItem = new Chantier();
|
||||
selectedItem.setStatut("PLANIFIE");
|
||||
selectedItem.setAvancement(0);
|
||||
selectedItem.setDateDebut(LocalDate.now());
|
||||
return getCreatePath() + "?faces-redirect=true";
|
||||
}
|
||||
|
||||
/**
|
||||
* Sauvegarde un nouveau chantier.
|
||||
*/
|
||||
public String saveNew() {
|
||||
if (selectedItem == null) {
|
||||
selectedItem = new Chantier();
|
||||
}
|
||||
selectedItem.setId(System.currentTimeMillis()); // Simulation ID
|
||||
selectedItem.setDateCreation(LocalDateTime.now());
|
||||
selectedItem.setDateModification(LocalDateTime.now());
|
||||
items.add(selectedItem);
|
||||
LOG.info("Nouveau chantier créé : {}", selectedItem.getNom());
|
||||
return "/chantiers?faces-redirect=true";
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge un chantier par son ID depuis les paramètres de la requête.
|
||||
*/
|
||||
public void loadChantierById() {
|
||||
if (chantierId != null) {
|
||||
loadItems(); // S'assurer que les items sont chargés
|
||||
selectedItem = items.stream()
|
||||
.filter(c -> c.getId().equals(chantierId))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (selectedItem == null) {
|
||||
LOG.warn("Chantier avec ID {} non trouvé", chantierId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche les détails d'un chantier.
|
||||
*/
|
||||
public String viewDetails(Long id) {
|
||||
selectedItem = items.stream()
|
||||
.filter(c -> c.getId().equals(id))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (selectedItem != null) {
|
||||
return getDetailsPath() + "details?id=" + id + "&faces-redirect=true";
|
||||
}
|
||||
return "/chantiers?faces-redirect=true";
|
||||
}
|
||||
|
||||
@lombok.Getter
|
||||
@lombok.Setter
|
||||
public static class Chantier {
|
||||
private Long id;
|
||||
private String nom;
|
||||
private String client;
|
||||
private String adresse;
|
||||
private LocalDate dateDebut;
|
||||
private LocalDate dateFinPrevue;
|
||||
private String statut;
|
||||
private int avancement;
|
||||
private double budget;
|
||||
private double coutReel;
|
||||
private LocalDateTime dateCreation;
|
||||
private LocalDateTime dateModification;
|
||||
}
|
||||
}
|
||||
176
src/main/java/dev/lions/btpxpress/view/ClientsView.java
Normal file
176
src/main/java/dev/lions/btpxpress/view/ClientsView.java
Normal file
@@ -0,0 +1,176 @@
|
||||
package dev.lions.btpxpress.view;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.faces.view.ViewScoped;
|
||||
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.function.Predicate;
|
||||
|
||||
@Named("clientsView")
|
||||
@ViewScoped
|
||||
@Getter
|
||||
@Setter
|
||||
public class ClientsView extends BaseListView<ClientsView.Client, Long> implements Serializable {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ClientsView.class);
|
||||
|
||||
private String filtreNom;
|
||||
private String filtreEmail;
|
||||
private String filtreVille;
|
||||
private Long clientId;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
loadItems();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadItems() {
|
||||
loading = true;
|
||||
try {
|
||||
items = new ArrayList<>();
|
||||
for (int i = 1; i <= 25; i++) {
|
||||
Client c = new Client();
|
||||
c.setId((long) i);
|
||||
c.setRaisonSociale("Entreprise " + i);
|
||||
c.setNomContact("Contact " + i);
|
||||
c.setEmail("contact" + i + "@example.com");
|
||||
c.setTelephone("+33 1 " + String.format("%02d", i) + " " +
|
||||
String.format("%02d", i * 2) + " " +
|
||||
String.format("%02d", i * 3) + " " +
|
||||
String.format("%02d", i * 4));
|
||||
c.setAdresse(i + " Rue Client, " + (75000 + i) + " Paris");
|
||||
c.setVille("Paris");
|
||||
c.setCodePostal(String.valueOf(75000 + i));
|
||||
c.setNombreChantiers(i % 5 + 1);
|
||||
c.setChiffreAffairesTotal(i * 25000.0);
|
||||
c.setDateCreation(LocalDateTime.now().minusDays(i * 30));
|
||||
items.add(c);
|
||||
}
|
||||
applyFilters(items, buildFilters());
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur chargement clients", e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Predicate<Client>> buildFilters() {
|
||||
List<Predicate<Client>> filters = new ArrayList<>();
|
||||
if (filtreNom != null && !filtreNom.trim().isEmpty()) {
|
||||
filters.add(c -> c.getRaisonSociale().toLowerCase().contains(filtreNom.toLowerCase()) ||
|
||||
c.getNomContact().toLowerCase().contains(filtreNom.toLowerCase()));
|
||||
}
|
||||
if (filtreEmail != null && !filtreEmail.trim().isEmpty()) {
|
||||
filters.add(c -> c.getEmail().toLowerCase().contains(filtreEmail.toLowerCase()));
|
||||
}
|
||||
if (filtreVille != null && !filtreVille.trim().isEmpty()) {
|
||||
filters.add(c -> c.getVille().toLowerCase().contains(filtreVille.toLowerCase()));
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetFilterFields() {
|
||||
filtreNom = null;
|
||||
filtreEmail = null;
|
||||
filtreVille = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDetailsPath() {
|
||||
return "/clients/";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getCreatePath() {
|
||||
return "/clients/nouveau";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void performDelete() {
|
||||
LOG.info("Suppression client : {}", selectedItem.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise un nouveau client pour la création.
|
||||
*/
|
||||
@Override
|
||||
public String createNew() {
|
||||
selectedItem = new Client();
|
||||
return getCreatePath() + "?faces-redirect=true";
|
||||
}
|
||||
|
||||
/**
|
||||
* Sauvegarde un nouveau client.
|
||||
*/
|
||||
public String saveNew() {
|
||||
if (selectedItem == null) {
|
||||
selectedItem = new Client();
|
||||
}
|
||||
selectedItem.setId(System.currentTimeMillis());
|
||||
selectedItem.setDateCreation(LocalDateTime.now());
|
||||
selectedItem.setDateModification(LocalDateTime.now());
|
||||
selectedItem.setNombreChantiers(0);
|
||||
selectedItem.setChiffreAffairesTotal(0.0);
|
||||
items.add(selectedItem);
|
||||
LOG.info("Nouveau client créé : {}", selectedItem.getRaisonSociale());
|
||||
return "/clients?faces-redirect=true";
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche les détails d'un client.
|
||||
*/
|
||||
public String viewDetails(Long id) {
|
||||
selectedItem = items.stream()
|
||||
.filter(c -> c.getId().equals(id))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (selectedItem != null) {
|
||||
return getDetailsPath() + "details?id=" + id + "&faces-redirect=true";
|
||||
}
|
||||
return "/clients?faces-redirect=true";
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge un client par son ID depuis les paramètres de la requête.
|
||||
*/
|
||||
public void loadClientById() {
|
||||
if (clientId != null) {
|
||||
loadItems(); // S'assurer que les items sont chargés
|
||||
selectedItem = items.stream()
|
||||
.filter(c -> c.getId().equals(clientId))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (selectedItem == null) {
|
||||
LOG.warn("Client avec ID {} non trouvé", clientId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@lombok.Getter
|
||||
@lombok.Setter
|
||||
public static class Client {
|
||||
private Long id;
|
||||
private String raisonSociale;
|
||||
private String nomContact;
|
||||
private String email;
|
||||
private String telephone;
|
||||
private String adresse;
|
||||
private String ville;
|
||||
private String codePostal;
|
||||
private String pays;
|
||||
private int nombreChantiers;
|
||||
private double chiffreAffairesTotal;
|
||||
private LocalDateTime dateCreation;
|
||||
private LocalDateTime dateModification;
|
||||
}
|
||||
}
|
||||
324
src/main/java/dev/lions/btpxpress/view/DashboardView.java
Normal file
324
src/main/java/dev/lions/btpxpress/view/DashboardView.java
Normal file
@@ -0,0 +1,324 @@
|
||||
package dev.lions.btpxpress.view;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.faces.view.ViewScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import dev.lions.btpxpress.service.DashboardService;
|
||||
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.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Bean de vue pour le tableau de bord principal.
|
||||
*
|
||||
* <p>Ce bean charge et affiche les métriques réelles provenant de l'API backend.
|
||||
* Toutes les données sont récupérées depuis l'API, aucune donnée fictive n'est utilisée.</p>
|
||||
*
|
||||
* @author BTP Xpress Team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Named("dashboardView")
|
||||
@ViewScoped
|
||||
@Getter
|
||||
@Setter
|
||||
public class DashboardView implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Logger logger = LoggerFactory.getLogger(DashboardView.class);
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy");
|
||||
|
||||
@Inject
|
||||
private DashboardService dashboardService;
|
||||
|
||||
// Métriques principales
|
||||
private long nombreChantiers = 0;
|
||||
private long chantiersActifs = 0;
|
||||
private long nombreClients = 0;
|
||||
private long nombreDevis = 0;
|
||||
private long facturesImpayees = 0;
|
||||
private double chiffreAffairesMois = 0.0;
|
||||
private double budgetTotal = 0.0;
|
||||
private double budgetConsomme = 0.0;
|
||||
|
||||
// Métriques ressources
|
||||
private long nombreEquipes = 0;
|
||||
private long equipesDisponibles = 0;
|
||||
private long nombreEmployes = 0;
|
||||
private long nombreMateriel = 0;
|
||||
private long materielDisponible = 0;
|
||||
|
||||
// Métriques maintenance
|
||||
private long maintenancesEnRetard = 0;
|
||||
private long maintenancesPlanifiees = 0;
|
||||
|
||||
// Alertes
|
||||
private long totalAlertes = 0;
|
||||
private boolean alerteCritique = false;
|
||||
|
||||
// Chantiers récents
|
||||
private List<ChantierResume> chantiersRecents = new ArrayList<>();
|
||||
private List<ChantierResume> chantiersEnRetard = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Initialise le dashboard en chargeant toutes les données depuis l'API.
|
||||
*/
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
logger.info("Initialisation du dashboard avec données réelles de l'API");
|
||||
loadDashboardPrincipal();
|
||||
loadDashboardChantiers();
|
||||
loadDashboardFinances();
|
||||
loadDashboardRessources();
|
||||
loadDashboardMaintenance();
|
||||
loadAlertes();
|
||||
loadNombreClients();
|
||||
loadNombreDevis();
|
||||
loadNombreFacturesImpayees();
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les métriques du dashboard principal.
|
||||
*/
|
||||
private void loadDashboardPrincipal() {
|
||||
try {
|
||||
JsonNode dashboard = dashboardService.getDashboardPrincipal();
|
||||
if (dashboard != null) {
|
||||
JsonNode chantiers = dashboard.get("chantiers");
|
||||
if (chantiers != null) {
|
||||
nombreChantiers = chantiers.get("total").asLong(0);
|
||||
chantiersActifs = chantiers.get("actifs").asLong(0);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Erreur lors du chargement du dashboard principal", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les métriques des chantiers.
|
||||
*/
|
||||
private void loadDashboardChantiers() {
|
||||
try {
|
||||
JsonNode dashboard = dashboardService.getDashboardChantiers();
|
||||
if (dashboard != null) {
|
||||
JsonNode chantiersActifsNode = dashboard.get("chantiersActifs");
|
||||
if (chantiersActifsNode != null && chantiersActifsNode.isArray()) {
|
||||
chantiersRecents.clear();
|
||||
Iterator<JsonNode> iterator = chantiersActifsNode.elements();
|
||||
int count = 0;
|
||||
while (iterator.hasNext() && count < 5) {
|
||||
JsonNode chantier = iterator.next();
|
||||
ChantierResume c = new ChantierResume();
|
||||
c.setId(chantier.get("id").asText());
|
||||
c.setNom(chantier.get("nom").asText(""));
|
||||
c.setClient(chantier.get("client").asText("Non assigné"));
|
||||
if (chantier.has("dateDebut") && !chantier.get("dateDebut").isNull()) {
|
||||
String dateStr = chantier.get("dateDebut").asText();
|
||||
try {
|
||||
c.setDateDebut(LocalDate.parse(dateStr));
|
||||
} catch (Exception e) {
|
||||
logger.warn("Erreur parsing date: {}", dateStr);
|
||||
}
|
||||
}
|
||||
c.setAvancement(chantier.get("avancement").asInt(0));
|
||||
c.setBudget(chantier.get("budget").asDouble(0.0));
|
||||
c.setStatut(chantier.get("statut").asText(""));
|
||||
chantiersRecents.add(c);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
JsonNode chantiersEnRetardNode = dashboard.get("chantiersEnRetard");
|
||||
if (chantiersEnRetardNode != null && chantiersEnRetardNode.isArray()) {
|
||||
chantiersEnRetard.clear();
|
||||
Iterator<JsonNode> iterator = chantiersEnRetardNode.elements();
|
||||
while (iterator.hasNext()) {
|
||||
JsonNode chantier = iterator.next();
|
||||
ChantierResume c = new ChantierResume();
|
||||
c.setId(chantier.get("id").asText());
|
||||
c.setNom(chantier.get("nom").asText(""));
|
||||
if (chantier.has("dateFinPrevue") && !chantier.get("dateFinPrevue").isNull()) {
|
||||
String dateStr = chantier.get("dateFinPrevue").asText();
|
||||
try {
|
||||
c.setDateFinPrevue(LocalDate.parse(dateStr));
|
||||
} catch (Exception e) {
|
||||
logger.warn("Erreur parsing date: {}", dateStr);
|
||||
}
|
||||
}
|
||||
c.setJoursRetard(chantier.get("joursRetard").asLong(0));
|
||||
chantiersEnRetard.add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Erreur lors du chargement du dashboard chantiers", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les métriques financières.
|
||||
*/
|
||||
private void loadDashboardFinances() {
|
||||
try {
|
||||
JsonNode finances = dashboardService.getDashboardFinances(30); // 30 jours
|
||||
if (finances != null) {
|
||||
JsonNode budget = finances.get("budget");
|
||||
if (budget != null) {
|
||||
budgetTotal = budget.get("total").asDouble(0.0);
|
||||
budgetConsomme = budget.get("realise").asDouble(0.0);
|
||||
}
|
||||
JsonNode chiffreAffaires = finances.get("chiffreAffaires");
|
||||
if (chiffreAffaires != null) {
|
||||
chiffreAffairesMois = chiffreAffaires.get("realise").asDouble(0.0);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Erreur lors du chargement des métriques financières", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les métriques des ressources.
|
||||
*/
|
||||
private void loadDashboardRessources() {
|
||||
try {
|
||||
JsonNode ressources = dashboardService.getDashboardRessources();
|
||||
if (ressources != null) {
|
||||
JsonNode equipes = ressources.get("equipes");
|
||||
if (equipes != null && equipes.has("total")) {
|
||||
nombreEquipes = equipes.get("total").asLong(0);
|
||||
equipesDisponibles = equipes.get("disponibles").asLong(0);
|
||||
}
|
||||
JsonNode employes = ressources.get("employes");
|
||||
if (employes != null && employes.has("total")) {
|
||||
nombreEmployes = employes.get("total").asLong(0);
|
||||
}
|
||||
JsonNode materiel = ressources.get("materiel");
|
||||
if (materiel != null && materiel.has("total")) {
|
||||
nombreMateriel = materiel.get("total").asLong(0);
|
||||
materielDisponible = materiel.get("disponible").asLong(0);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Erreur lors du chargement des métriques ressources", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les métriques de maintenance.
|
||||
*/
|
||||
private void loadDashboardMaintenance() {
|
||||
try {
|
||||
JsonNode maintenance = dashboardService.getDashboardMaintenance();
|
||||
if (maintenance != null) {
|
||||
JsonNode stats = maintenance.get("statistiques");
|
||||
if (stats != null) {
|
||||
maintenancesEnRetard = stats.has("enRetard") ? stats.get("enRetard").asLong(0) : 0;
|
||||
maintenancesPlanifiees = stats.has("planifiees") ? stats.get("planifiees").asLong(0) : 0;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Erreur lors du chargement des métriques maintenance", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les alertes.
|
||||
*/
|
||||
private void loadAlertes() {
|
||||
try {
|
||||
JsonNode alertes = dashboardService.getAlertes();
|
||||
if (alertes != null) {
|
||||
totalAlertes = alertes.get("totalAlertes").asLong(0);
|
||||
alerteCritique = alertes.get("alerteCritique").asBoolean(false);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Erreur lors du chargement des alertes", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge le nombre de clients.
|
||||
*/
|
||||
private void loadNombreClients() {
|
||||
nombreClients = dashboardService.getNombreClients();
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge le nombre de devis en attente.
|
||||
*/
|
||||
private void loadNombreDevis() {
|
||||
nombreDevis = dashboardService.getNombreDevisEnAttente();
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge le nombre de factures impayées.
|
||||
*/
|
||||
private void loadNombreFacturesImpayees() {
|
||||
facturesImpayees = dashboardService.getNombreFacturesImpayees();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rafraîchit toutes les données du dashboard.
|
||||
*/
|
||||
public void rafraichir() {
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le taux de consommation du budget en pourcentage.
|
||||
*
|
||||
* @return Le taux de consommation (0-100)
|
||||
*/
|
||||
public double getTauxConsommationBudget() {
|
||||
if (budgetTotal > 0) {
|
||||
return (budgetConsomme / budgetTotal) * 100;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Classe interne représentant un résumé de chantier.
|
||||
*/
|
||||
@lombok.Getter
|
||||
@lombok.Setter
|
||||
public static class ChantierResume implements Serializable {
|
||||
private String id;
|
||||
private String nom;
|
||||
private String client;
|
||||
private LocalDate dateDebut;
|
||||
private LocalDate dateFinPrevue;
|
||||
private int avancement;
|
||||
private double budget;
|
||||
private String statut;
|
||||
private long joursRetard;
|
||||
|
||||
/**
|
||||
* Retourne la date de début formatée pour l'affichage.
|
||||
*
|
||||
* @return La date au format dd/MM/yyyy
|
||||
*/
|
||||
public String getDateDebutFormatee() {
|
||||
return dateDebut != null ? dateDebut.format(DATE_FORMATTER) : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la date de fin prévue formatée pour l'affichage.
|
||||
*
|
||||
* @return La date au format dd/MM/yyyy
|
||||
*/
|
||||
public String getDateFinPrevueFormatee() {
|
||||
return dateFinPrevue != null ? dateFinPrevue.format(DATE_FORMATTER) : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/main/java/dev/lions/btpxpress/view/GuestPreferences.java
Normal file
90
src/main/java/dev/lions/btpxpress/view/GuestPreferences.java
Normal file
@@ -0,0 +1,90 @@
|
||||
package dev.lions.btpxpress.view;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.enterprise.context.SessionScoped;
|
||||
import jakarta.inject.Named;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.primefaces.PrimeFaces;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Named("guestPreferences")
|
||||
@SessionScoped
|
||||
@Getter
|
||||
@Setter
|
||||
public class GuestPreferences implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String menuMode = "layout-sidebar";
|
||||
private String darkMode = "light";
|
||||
private String componentTheme = "purple";
|
||||
private String topbarTheme = "light";
|
||||
private String menuTheme = "light";
|
||||
private String inputStyle = "outlined";
|
||||
private boolean lightLogo = false;
|
||||
private List<ComponentTheme> componentThemes = new ArrayList<>();
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
componentThemes.add(new ComponentTheme("Bleu", "blue", "#2c84d8"));
|
||||
componentThemes.add(new ComponentTheme("Vert", "green", "#34B56F"));
|
||||
componentThemes.add(new ComponentTheme("Orange", "orange", "#FF810E"));
|
||||
componentThemes.add(new ComponentTheme("Turquoise", "turquoise", "#58AED3"));
|
||||
componentThemes.add(new ComponentTheme("Avocat", "avocado", "#AEC523"));
|
||||
componentThemes.add(new ComponentTheme("Violet", "purple", "#464DF2"));
|
||||
componentThemes.add(new ComponentTheme("Rouge", "red", "#FF9B7B"));
|
||||
componentThemes.add(new ComponentTheme("Jaune", "yellow", "#FFB340"));
|
||||
}
|
||||
|
||||
public void setDarkMode(String darkMode) {
|
||||
this.darkMode = darkMode;
|
||||
this.menuTheme = darkMode;
|
||||
this.topbarTheme = darkMode;
|
||||
this.lightLogo = !this.topbarTheme.equals("light");
|
||||
}
|
||||
|
||||
public String getLayout() {
|
||||
return "layout-" + this.darkMode;
|
||||
}
|
||||
|
||||
public String getTheme() {
|
||||
return this.componentTheme + '-' + this.darkMode;
|
||||
}
|
||||
|
||||
public void setTopbarTheme(String topbarTheme) {
|
||||
this.topbarTheme = topbarTheme;
|
||||
this.lightLogo = !this.topbarTheme.equals("light");
|
||||
}
|
||||
|
||||
public String getInputStyleClass() {
|
||||
return this.inputStyle.equals("filled") ? "ui-input-filled" : "";
|
||||
}
|
||||
|
||||
public void onMenuTypeChange() {
|
||||
if ("layout-horizontal".equals(menuMode)) {
|
||||
menuTheme = topbarTheme;
|
||||
PrimeFaces.current().executeScript(
|
||||
"PrimeFaces.FreyaConfigurator.changeSectionTheme('" + menuTheme + "' , 'layout-menu')"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@lombok.Getter
|
||||
@lombok.Setter
|
||||
public static class ComponentTheme implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private String name;
|
||||
private String file;
|
||||
private String color;
|
||||
|
||||
public ComponentTheme(String name, String file, String color) {
|
||||
this.name = name;
|
||||
this.file = file;
|
||||
this.color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/main/java/dev/lions/btpxpress/view/LoginView.java
Normal file
53
src/main/java/dev/lions/btpxpress/view/LoginView.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package dev.lions.btpxpress.view;
|
||||
|
||||
import jakarta.enterprise.context.RequestScoped;
|
||||
import jakarta.faces.application.FacesMessage;
|
||||
import jakarta.faces.context.FacesContext;
|
||||
import jakarta.inject.Named;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Named("loginView")
|
||||
@RequestScoped
|
||||
@Getter
|
||||
@Setter
|
||||
public class LoginView implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String username;
|
||||
private String password;
|
||||
private boolean rememberMe = false;
|
||||
|
||||
public String login() {
|
||||
if (username == null || username.trim().isEmpty()) {
|
||||
addErrorMessage("Le nom d'utilisateur est requis");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (password == null || password.trim().isEmpty()) {
|
||||
addErrorMessage("Le mot de passe est requis");
|
||||
return null;
|
||||
}
|
||||
|
||||
if ("admin".equals(username) && "admin".equals(password)) {
|
||||
addInfoMessage("Connexion réussie !");
|
||||
return "/dashboard?faces-redirect=true";
|
||||
} else {
|
||||
addErrorMessage("Nom d'utilisateur ou mot de passe incorrect");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void addErrorMessage(String message) {
|
||||
FacesContext.getCurrentInstance().addMessage(null,
|
||||
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
|
||||
}
|
||||
|
||||
private void addInfoMessage(String message) {
|
||||
FacesContext.getCurrentInstance().addMessage(null,
|
||||
new FacesMessage(FacesMessage.SEVERITY_INFO, "Information", message));
|
||||
}
|
||||
}
|
||||
79
src/main/java/dev/lions/btpxpress/view/UserSessionBean.java
Normal file
79
src/main/java/dev/lions/btpxpress/view/UserSessionBean.java
Normal file
@@ -0,0 +1,79 @@
|
||||
package dev.lions.btpxpress.view;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.enterprise.context.SessionScoped;
|
||||
import jakarta.inject.Named;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Bean de session pour gérer les informations de l'utilisateur connecté.
|
||||
*
|
||||
* <p>Ce bean stocke les informations de session de l'utilisateur authentifié,
|
||||
* telles que le nom, l'email, l'avatar, et les statistiques rapides.</p>
|
||||
*
|
||||
* @author BTP Xpress Team
|
||||
* @version 1.0
|
||||
*/
|
||||
@Named("userSession")
|
||||
@SessionScoped
|
||||
@Getter
|
||||
@Setter
|
||||
public class UserSessionBean implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String nomComplet;
|
||||
private String email;
|
||||
private String avatarUrl;
|
||||
private String role;
|
||||
private int nombreNotificationsNonLues;
|
||||
private int nombreMessagesNonLus;
|
||||
|
||||
/**
|
||||
* Initialise les données de l'utilisateur connecté.
|
||||
*/
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// TODO: Récupérer depuis le token JWT ou la session OIDC
|
||||
nomComplet = "Jean Dupont";
|
||||
email = "jean.dupont@btpxpress.com";
|
||||
avatarUrl = "/resources/freya-layout/images/avatar-profilemenu.png";
|
||||
role = "Gestionnaire de Projets";
|
||||
nombreNotificationsNonLues = 5;
|
||||
nombreMessagesNonLus = 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les initiales de l'utilisateur pour l'avatar.
|
||||
*
|
||||
* @return Les initiales (ex: "JD" pour "Jean Dupont")
|
||||
*/
|
||||
public String getInitiales() {
|
||||
if (nomComplet == null || nomComplet.trim().isEmpty()) {
|
||||
return "U";
|
||||
}
|
||||
|
||||
String[] parts = nomComplet.trim().split("\\s+");
|
||||
if (parts.length >= 2) {
|
||||
return String.valueOf(parts[0].charAt(0)).toUpperCase() +
|
||||
String.valueOf(parts[1].charAt(0)).toUpperCase();
|
||||
} else if (parts.length == 1) {
|
||||
return parts[0].substring(0, Math.min(2, parts[0].length())).toUpperCase();
|
||||
}
|
||||
return "U";
|
||||
}
|
||||
|
||||
/**
|
||||
* Action de déconnexion.
|
||||
*
|
||||
* @return La page de login
|
||||
*/
|
||||
public String deconnecter() {
|
||||
// TODO: Implémenter la déconnexion OIDC/Keycloak
|
||||
return "/login?faces-redirect=true";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user