498 lines
19 KiB
Java
498 lines
19 KiB
Java
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();
|
|
}
|
|
}
|
|
}
|