Initial commit

This commit is contained in:
dahoud
2025-10-01 01:37:34 +00:00
commit f2bb633142
310 changed files with 86051 additions and 0 deletions

View File

@@ -0,0 +1,497 @@
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<String, Object> calculerPerformanceChantiers(LocalDate dateDebut, LocalDate dateFin) {
logger.info(
"Calcul des statistiques de performance des chantiers pour la période {} - {}",
dateDebut,
dateFin);
List<Chantier> chantiers = chantierRepository.findChantiersParPeriode(dateDebut, dateFin);
Map<String, Object> stats = new HashMap<>();
// Nombre de chantiers par statut
Map<StatutChantier, Long> 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<String, Long> 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<String, Object> calculerProductiviteEquipes() {
logger.info("Calcul des statistiques de productivité par équipe");
List<Equipe> equipes = equipeRepository.listAll();
Map<String, Object> stats = new HashMap<>();
List<Map<String, Object>> productiviteParEquipe = new ArrayList<>();
for (Equipe equipe : equipes) {
Map<String, Object> equipeStats = new HashMap<>();
equipeStats.put("equipe", equipe);
// Phases assignées à l'équipe
List<PhaseChantier> 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<Map<String, Object>> 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<String, Object> calculerRotationStocks() {
logger.info("Calcul des statistiques de rotation des stocks");
List<Stock> stocks = stockRepository.listAll();
Map<String, Object> stats = new HashMap<>();
// Articles les plus utilisés
List<Map<String, Object>> 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<Stock> 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<CategorieStock, Long> 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<String, Object> analyserTendancesAchat(LocalDate dateDebut, LocalDate dateFin) {
logger.info("Analyse des tendances d'achat pour la période {} - {}", dateDebut, dateFin);
List<BonCommande> commandes = bonCommandeRepository.findCommandesParPeriode(dateDebut, dateFin);
Map<String, Object> stats = new HashMap<>();
// Évolution mensuelle des achats
Map<String, BigDecimal> 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<String, BigDecimal> 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.<String, BigDecimal>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<String, Object> calculerQualiteFournisseurs() {
logger.info("Calcul des indicateurs de qualité des fournisseurs");
List<Fournisseur> fournisseurs = fournisseurRepository.listAll();
Map<String, Object> stats = new HashMap<>();
List<Map<String, Object>> qualiteParFournisseur = new ArrayList<>();
for (Fournisseur fournisseur : fournisseurs) {
Map<String, Object> 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<BonCommande> commandesEnCours =
bonCommandeRepository.findByFournisseurAndStatut(
fournisseur.getId(), StatutBonCommande.ENVOYEE);
fournisseurStats.put("commandesEnCours", commandesEnCours.size());
qualiteParFournisseur.add(fournisseurStats);
}
stats.put("qualiteParFournisseur", qualiteParFournisseur);
// Meilleurs fournisseurs (par note)
List<Map<String, Object>> 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<Map<String, Object>> 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<String, Object> genererKPIPilotage() {
logger.info("Génération des KPI de pilotage");
Map<String, Object> kpi = new HashMap<>();
LocalDate aujourd = LocalDate.now();
// KPI Chantiers
List<Chantier> chantiers = chantierRepository.listAll();
// Taux d'occupation des équipes
List<Equipe> equipes = equipeRepository.listAll();
List<Equipe> equipesActives =
equipes.stream()
.filter(e -> e.getStatut() == StatutEquipe.ACTIVE)
.collect(Collectors.toList());
long equipesOccupees =
equipesActives.stream()
.mapToLong(
e -> {
List<PhaseChantier> 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<Chantier> 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<PhaseChantier> phasesPrevisionnelles =
phaseChantierRepository.findPhasesPrevuesPeriode(aujourd, finPeriode);
kpi.put("chargePrevisionnelle", phasesPrevisionnelles.size());
// Taux de disponibilité matériel
List<Stock> 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<String, Object> calculerTendances(
LocalDate dateDebut, LocalDate dateFin, String granularite) {
logger.info(
"Calcul des tendances pour la période {} - {} avec granularité {}",
dateDebut,
dateFin,
granularite);
Map<String, Object> tendances = new HashMap<>();
// Tendances des chantiers
List<Chantier> chantiers = chantierRepository.findChantiersParPeriode(dateDebut, dateFin);
Map<String, Long> 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<BonCommande> commandes = bonCommandeRepository.findCommandesParPeriode(dateDebut, dateFin);
Map<String, BigDecimal> 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<String, Long> grouperParPeriode(Map<LocalDate, Long> 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();
}
}
}