package dev.lions.btpxpress.application.service; import dev.lions.btpxpress.domain.core.entity.*; import dev.lions.btpxpress.domain.infrastructure.repository.*; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.*; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** Service de calcul de statistiques avancées */ @ApplicationScoped public class StatisticsService { private static final Logger logger = LoggerFactory.getLogger(StatisticsService.class); @Inject ChantierRepository chantierRepository; @Inject PhaseChantierRepository phaseChantierRepository; @Inject EmployeRepository employeRepository; @Inject EquipeRepository equipeRepository; @Inject FournisseurRepository fournisseurRepository; @Inject StockRepository stockRepository; @Inject BonCommandeRepository bonCommandeRepository; /** Calcule les statistiques de performance des chantiers */ public Map calculerPerformanceChantiers(LocalDate dateDebut, LocalDate dateFin) { logger.info( "Calcul des statistiques de performance des chantiers pour la période {} - {}", dateDebut, dateFin); List chantiers = chantierRepository.findChantiersParPeriode(dateDebut, dateFin); Map stats = new HashMap<>(); // Nombre de chantiers par statut Map chantiersParStatut = chantiers.stream() .collect(Collectors.groupingBy(Chantier::getStatut, Collectors.counting())); stats.put("chantiersParStatut", chantiersParStatut); // Taux de respect des délais long chantiersTermines = chantiers.stream().mapToLong(c -> c.getStatut() == StatutChantier.TERMINE ? 1 : 0).sum(); long chantiersEnRetard = chantiers.stream().mapToLong(c -> c.isEnRetard() ? 1 : 0).sum(); double tauxRespectDelais = chantiersTermines > 0 ? ((double) (chantiersTermines - chantiersEnRetard) / chantiersTermines) * 100 : 100.0; stats.put("tauxRespectDelais", Math.round(tauxRespectDelais * 100.0) / 100.0); // Durée moyenne des chantiers OptionalDouble dureeMoyenne = chantiers.stream() .filter(c -> c.getDateDebutReelle() != null && c.getDateFinReelle() != null) .mapToLong(c -> ChronoUnit.DAYS.between(c.getDateDebutReelle(), c.getDateFinReelle())) .average(); stats.put( "dureeMoyenneJours", dureeMoyenne.isPresent() ? Math.round(dureeMoyenne.getAsDouble()) : 0); // Rentabilité moyenne double rentabiliteMoyenne = chantiers.stream() .filter(c -> c.getMontantContrat() != null && c.getCoutReel() != null) .filter(c -> c.getMontantContrat().compareTo(BigDecimal.ZERO) > 0) .mapToDouble( c -> { BigDecimal marge = c.getMontantContrat().subtract(c.getCoutReel()); return marge .divide(c.getMontantContrat(), 4, RoundingMode.HALF_UP) .multiply(new BigDecimal("100")) .doubleValue(); }) .average() .orElse(0.0); stats.put("rentabiliteMoyenne", Math.round(rentabiliteMoyenne * 100.0) / 100.0); // Évolution mensuelle du nombre de chantiers Map evolutionMensuelle = chantiers.stream() .filter(c -> c.getDateDebutPrevue() != null) .collect( Collectors.groupingBy( c -> c.getDateDebutPrevue().getYear() + "-" + String.format("%02d", c.getDateDebutPrevue().getMonthValue()), Collectors.counting())); stats.put("evolutionMensuelle", evolutionMensuelle); return stats; } /** Calcule les statistiques de productivité par équipe */ public Map calculerProductiviteEquipes() { logger.info("Calcul des statistiques de productivité par équipe"); List equipes = equipeRepository.listAll(); Map stats = new HashMap<>(); List> productiviteParEquipe = new ArrayList<>(); for (Equipe equipe : equipes) { Map equipeStats = new HashMap<>(); equipeStats.put("equipe", equipe); // Phases assignées à l'équipe List phases = phaseChantierRepository.findPhasesByEquipe(equipe.getId()); equipeStats.put("nombrePhases", phases.size()); // Phases terminées long phasesTerminees = phases.stream() .mapToLong(p -> p.getStatut() == StatutPhaseChantier.TERMINEE ? 1 : 0) .sum(); equipeStats.put("phasesTerminees", phasesTerminees); // Taux de réalisation double tauxRealisation = phases.size() > 0 ? ((double) phasesTerminees / phases.size()) * 100 : 0.0; equipeStats.put("tauxRealisation", Math.round(tauxRealisation * 100.0) / 100.0); // Phases en retard long phasesEnRetard = phases.stream().mapToLong(p -> p.isEnRetard() ? 1 : 0).sum(); equipeStats.put("phasesEnRetard", phasesEnRetard); // Avancement moyen des phases en cours double avancementMoyen = phases.stream() .filter(p -> p.getStatut() == StatutPhaseChantier.EN_COURS) .filter(p -> p.getPourcentageAvancement() != null) .mapToDouble(p -> p.getPourcentageAvancement().doubleValue()) .average() .orElse(0.0); equipeStats.put("avancementMoyen", Math.round(avancementMoyen * 100.0) / 100.0); productiviteParEquipe.add(equipeStats); } stats.put("productiviteParEquipe", productiviteParEquipe); // Équipe la plus productive Optional> equipePlusProductive = productiviteParEquipe.stream() .max(Comparator.comparing(e -> (Double) e.get("tauxRealisation"))); stats.put("equipePlusProductive", equipePlusProductive.orElse(null)); return stats; } /** Calcule les statistiques de rotation des stocks */ public Map calculerRotationStocks() { logger.info("Calcul des statistiques de rotation des stocks"); List stocks = stockRepository.listAll(); Map stats = new HashMap<>(); // Articles les plus utilisés List> articlesActivite = stocks.stream() .filter(s -> s.getDateDerniereSortie() != null) .sorted((s1, s2) -> s2.getDateDerniereSortie().compareTo(s1.getDateDerniereSortie())) .limit(10) .map( s -> Map.of( "article", s, "derniereSortie", s.getDateDerniereSortie(), "valeurStock", s.getValeurStock())) .collect(Collectors.toList()); stats.put("articlesLesPlusActifs", articlesActivite); // Articles sans mouvement List articlesSansMouvement = stocks.stream() .filter( s -> s.getDateDerniereSortie() == null || ChronoUnit.DAYS.between(s.getDateDerniereSortie(), LocalDateTime.now()) > 90) .collect(Collectors.toList()); stats.put("articlesSansMouvement", articlesSansMouvement.size()); // Valeur des stocks dormants BigDecimal valeurStocksDormants = articlesSansMouvement.stream() .map(Stock::getValeurStock) .reduce(BigDecimal.ZERO, BigDecimal::add); stats.put("valeurStocksDormants", valeurStocksDormants); // Rotation par catégorie Map rotationParCategorie = stocks.stream() .filter(s -> s.getDateDerniereSortie() != null) .filter( s -> ChronoUnit.DAYS.between(s.getDateDerniereSortie(), LocalDateTime.now()) <= 30) .collect(Collectors.groupingBy(Stock::getCategorie, Collectors.counting())); stats.put("rotationParCategorie", rotationParCategorie); return stats; } /** Analyse des tendances d'achat */ public Map analyserTendancesAchat(LocalDate dateDebut, LocalDate dateFin) { logger.info("Analyse des tendances d'achat pour la période {} - {}", dateDebut, dateFin); List commandes = bonCommandeRepository.findCommandesParPeriode(dateDebut, dateFin); Map stats = new HashMap<>(); // Évolution mensuelle des achats Map evolutionMontants = commandes.stream() .filter(c -> c.getDateCommande() != null && c.getMontantTTC() != null) .collect( Collectors.groupingBy( c -> c.getDateCommande().getYear() + "-" + String.format("%02d", c.getDateCommande().getMonthValue()), Collectors.reducing( BigDecimal.ZERO, BonCommande::getMontantTTC, BigDecimal::add))); stats.put("evolutionMensuelleAchats", evolutionMontants); // Top fournisseurs par montant Map topFournisseurs = commandes.stream() .filter(c -> c.getMontantTTC() != null) .collect( Collectors.groupingBy( c -> c.getFournisseur().getNom(), Collectors.reducing( BigDecimal.ZERO, BonCommande::getMontantTTC, BigDecimal::add))) .entrySet() .stream() .sorted(Map.Entry.comparingByValue().reversed()) .limit(10) .collect( Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); stats.put("topFournisseurs", topFournisseurs); // Montant moyen par commande OptionalDouble montantMoyen = commandes.stream() .filter(c -> c.getMontantTTC() != null) .mapToDouble(c -> c.getMontantTTC().doubleValue()) .average(); stats.put( "montantMoyenCommande", montantMoyen.isPresent() ? BigDecimal.valueOf(montantMoyen.getAsDouble()).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO); // Délai moyen de livraison OptionalDouble delaiMoyen = commandes.stream() .filter(c -> c.getDateCommande() != null && c.getDateLivraisonReelle() != null) .mapToLong( c -> ChronoUnit.DAYS.between(c.getDateCommande(), c.getDateLivraisonReelle())) .average(); stats.put( "delaiMoyenLivraisonJours", delaiMoyen.isPresent() ? Math.round(delaiMoyen.getAsDouble()) : 0); return stats; } /** Calcule les indicateurs de qualité des fournisseurs */ public Map calculerQualiteFournisseurs() { logger.info("Calcul des indicateurs de qualité des fournisseurs"); List fournisseurs = fournisseurRepository.listAll(); Map stats = new HashMap<>(); List> qualiteParFournisseur = new ArrayList<>(); for (Fournisseur fournisseur : fournisseurs) { Map fournisseurStats = new HashMap<>(); fournisseurStats.put("fournisseur", fournisseur); // Note moyenne BigDecimal noteMoyenne = fournisseur.getNoteMoyenne(); fournisseurStats.put("noteMoyenne", noteMoyenne); // Nombre de commandes fournisseurStats.put("nombreCommandes", fournisseur.getNombreCommandesTotal()); // Montant total des achats fournisseurStats.put("montantTotalAchats", fournisseur.getMontantTotalAchats()); // Dernière commande fournisseurStats.put("derniereCommande", fournisseur.getDerniereCommande()); // Commandes en cours List commandesEnCours = bonCommandeRepository.findByFournisseurAndStatut( fournisseur.getId(), StatutBonCommande.ENVOYEE); fournisseurStats.put("commandesEnCours", commandesEnCours.size()); qualiteParFournisseur.add(fournisseurStats); } stats.put("qualiteParFournisseur", qualiteParFournisseur); // Meilleurs fournisseurs (par note) List> meilleursFournisseurs = qualiteParFournisseur.stream() .filter(f -> f.get("noteMoyenne") != null) .sorted( (f1, f2) -> { BigDecimal note1 = (BigDecimal) f1.get("noteMoyenne"); BigDecimal note2 = (BigDecimal) f2.get("noteMoyenne"); return note2.compareTo(note1); }) .limit(5) .collect(Collectors.toList()); stats.put("meilleursFournisseurs", meilleursFournisseurs); // Fournisseurs à surveiller (note faible ou pas de commande récente) List> fournisseursASurveiller = qualiteParFournisseur.stream() .filter( f -> { BigDecimal note = (BigDecimal) f.get("noteMoyenne"); LocalDateTime derniereCommande = (LocalDateTime) f.get("derniereCommande"); return (note != null && note.compareTo(new BigDecimal("3.0")) < 0) || (derniereCommande != null && ChronoUnit.DAYS.between(derniereCommande, LocalDateTime.now()) > 180); }) .collect(Collectors.toList()); stats.put("fournisseursASurveiller", fournisseursASurveiller); return stats; } /** Génère les KPI de pilotage */ public Map genererKPIPilotage() { logger.info("Génération des KPI de pilotage"); Map kpi = new HashMap<>(); LocalDate aujourd = LocalDate.now(); // KPI Chantiers List chantiers = chantierRepository.listAll(); // Taux d'occupation des équipes List equipes = equipeRepository.listAll(); List equipesActives = equipes.stream() .filter(e -> e.getStatut() == StatutEquipe.ACTIVE) .collect(Collectors.toList()); long equipesOccupees = equipesActives.stream() .mapToLong( e -> { List phasesEnCours = phaseChantierRepository.findPhasesByEquipe(e.getId()).stream() .filter(p -> p.getStatut() == StatutPhaseChantier.EN_COURS) .collect(Collectors.toList()); return phasesEnCours.isEmpty() ? 0 : 1; }) .sum(); double tauxOccupation = equipesActives.size() > 0 ? ((double) equipesOccupees / equipesActives.size()) * 100 : 0.0; kpi.put("tauxOccupationEquipes", Math.round(tauxOccupation * 100.0) / 100.0); // Prévisions de fin de chantier List chantiersEnCours = chantiers.stream() .filter(c -> c.getStatut() == StatutChantier.EN_COURS) .collect(Collectors.toList()); kpi.put("chantiersEnCours", chantiersEnCours.size()); // Chantiers à démarrer dans les 30 prochains jours long chantiersADemarrer = chantiers.stream() .filter(c -> c.getStatut() == StatutChantier.PLANIFIE) .filter(c -> c.getDateDebutPrevue() != null) .mapToLong( c -> { long joursAvantDebut = ChronoUnit.DAYS.between(aujourd, c.getDateDebutPrevue()); return (joursAvantDebut >= 0 && joursAvantDebut <= 30) ? 1 : 0; }) .sum(); kpi.put("chantiersADemarrer30Jours", chantiersADemarrer); // Alertes critiques long alertesCritiques = 0; // Chantiers en retard alertesCritiques += chantiers.stream().mapToLong(c -> c.isEnRetard() ? 1 : 0).sum(); // Stocks en rupture alertesCritiques += stockRepository.findStocksEnRupture().size(); // Commandes en retard alertesCritiques += bonCommandeRepository.findCommandesEnRetard().size(); kpi.put("alertesCritiques", alertesCritiques); // Charge de travail prévisionnelle (prochains 3 mois) LocalDate finPeriode = aujourd.plusMonths(3); List phasesPrevisionnelles = phaseChantierRepository.findPhasesPrevuesPeriode(aujourd, finPeriode); kpi.put("chargePrevisionnelle", phasesPrevisionnelles.size()); // Taux de disponibilité matériel List stocks = stockRepository.listAll(); long stocksDisponibles = stocks.stream() .mapToLong(s -> s.getStatut().isDisponible() && !s.isEnRupture() ? 1 : 0) .sum(); double tauxDisponibilite = stocks.size() > 0 ? ((double) stocksDisponibles / stocks.size()) * 100 : 100.0; kpi.put("tauxDisponibiliteMatériel", Math.round(tauxDisponibilite * 100.0) / 100.0); return kpi; } /** Calcule les tendances par période */ public Map calculerTendances( LocalDate dateDebut, LocalDate dateFin, String granularite) { logger.info( "Calcul des tendances pour la période {} - {} avec granularité {}", dateDebut, dateFin, granularite); Map tendances = new HashMap<>(); // Tendances des chantiers List chantiers = chantierRepository.findChantiersParPeriode(dateDebut, dateFin); Map tendancesChantiers = grouperParPeriode( chantiers.stream() .filter(c -> c.getDateDebutPrevue() != null) .collect(Collectors.toMap(c -> c.getDateDebutPrevue(), c -> 1L, Long::sum)), granularite); tendances.put("chantiers", tendancesChantiers); // Tendances des achats List commandes = bonCommandeRepository.findCommandesParPeriode(dateDebut, dateFin); Map tendancesAchats = commandes.stream() .filter(c -> c.getDateCommande() != null && c.getMontantTTC() != null) .collect( Collectors.groupingBy( c -> formatPeriode(c.getDateCommande(), granularite), Collectors.reducing( BigDecimal.ZERO, BonCommande::getMontantTTC, BigDecimal::add))); tendances.put("achats", tendancesAchats); return tendances; } /** Groupe les données par période selon la granularité */ private Map grouperParPeriode(Map donnees, String granularite) { return donnees.entrySet().stream() .collect( Collectors.groupingBy( entry -> formatPeriode(entry.getKey(), granularite), Collectors.summingLong(Map.Entry::getValue))); } /** Formate une date selon la granularité */ private String formatPeriode(LocalDate date, String granularite) { switch (granularite.toLowerCase()) { case "jour": return date.toString(); case "semaine": return date.getYear() + "-S" + date.getDayOfYear() / 7; case "mois": return date.getYear() + "-" + String.format("%02d", date.getMonthValue()); case "trimestre": return date.getYear() + "-T" + ((date.getMonthValue() - 1) / 3 + 1); case "année": return String.valueOf(date.getYear()); default: return date.toString(); } } }