Initial commit
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user