611 lines
20 KiB
Java
611 lines
20 KiB
Java
package dev.lions.btpxpress.application.service;
|
|
|
|
import dev.lions.btpxpress.domain.core.entity.*;
|
|
import dev.lions.btpxpress.domain.infrastructure.repository.MaterielRepository;
|
|
import dev.lions.btpxpress.domain.infrastructure.repository.PlanningMaterielRepository;
|
|
import dev.lions.btpxpress.domain.infrastructure.repository.ReservationMaterielRepository;
|
|
import jakarta.enterprise.context.ApplicationScoped;
|
|
import jakarta.inject.Inject;
|
|
import jakarta.transaction.Transactional;
|
|
import jakarta.ws.rs.BadRequestException;
|
|
import jakarta.ws.rs.NotFoundException;
|
|
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 gestion des plannings matériel ORCHESTRATION: Logique métier planning, conflits et
|
|
* optimisation
|
|
*/
|
|
@ApplicationScoped
|
|
public class PlanningMaterielService {
|
|
|
|
private static final Logger logger = LoggerFactory.getLogger(PlanningMaterielService.class);
|
|
|
|
@Inject PlanningMaterielRepository planningRepository;
|
|
|
|
@Inject MaterielRepository materielRepository;
|
|
|
|
@Inject ReservationMaterielRepository reservationRepository;
|
|
|
|
// === OPÉRATIONS CRUD DE BASE ===
|
|
|
|
/** Récupère tous les plannings avec pagination */
|
|
public List<PlanningMateriel> findAll(int page, int size) {
|
|
logger.debug("Récupération des plannings - page: {}, size: {}", page, size);
|
|
return planningRepository.findAllActifs(page, size);
|
|
}
|
|
|
|
/** Récupère tous les plannings actifs */
|
|
public List<PlanningMateriel> findAll() {
|
|
return planningRepository.find("actif = true").list();
|
|
}
|
|
|
|
/** Trouve un planning par ID avec exception si non trouvé */
|
|
public PlanningMateriel findByIdRequired(UUID id) {
|
|
return planningRepository
|
|
.findByIdOptional(id)
|
|
.orElseThrow(() -> new NotFoundException("Planning non trouvé avec l'ID: " + id));
|
|
}
|
|
|
|
/** Trouve un planning par ID */
|
|
public Optional<PlanningMateriel> findById(UUID id) {
|
|
return planningRepository.findByIdOptional(id);
|
|
}
|
|
|
|
// === RECHERCHES SPÉCIALISÉES ===
|
|
|
|
/** Trouve les plannings pour un matériel */
|
|
public List<PlanningMateriel> findByMateriel(UUID materielId) {
|
|
logger.debug("Recherche plannings pour matériel: {}", materielId);
|
|
return planningRepository.findByMateriel(materielId);
|
|
}
|
|
|
|
/** Trouve les plannings sur une période */
|
|
public List<PlanningMateriel> findByPeriode(LocalDate dateDebut, LocalDate dateFin) {
|
|
if (dateDebut.isAfter(dateFin)) {
|
|
throw new BadRequestException("La date de début doit être antérieure à la date de fin");
|
|
}
|
|
return planningRepository.findByPeriode(dateDebut, dateFin);
|
|
}
|
|
|
|
/** Trouve les plannings par statut */
|
|
public List<PlanningMateriel> findByStatut(StatutPlanning statut) {
|
|
return planningRepository.findByStatut(statut);
|
|
}
|
|
|
|
/** Trouve les plannings par type */
|
|
public List<PlanningMateriel> findByType(TypePlanning type) {
|
|
return planningRepository.findByType(type);
|
|
}
|
|
|
|
/** Recherche textuelle dans les plannings */
|
|
public List<PlanningMateriel> search(String terme) {
|
|
if (terme == null || terme.trim().isEmpty()) {
|
|
return findAll();
|
|
}
|
|
return planningRepository.search(terme.trim());
|
|
}
|
|
|
|
// === REQUÊTES MÉTIER SPÉCIALISÉES ===
|
|
|
|
/** Trouve les plannings avec conflits */
|
|
public List<PlanningMateriel> findAvecConflits() {
|
|
return planningRepository.findAvecConflits();
|
|
}
|
|
|
|
/** Trouve les plannings nécessitant attention */
|
|
public List<PlanningMateriel> findNecessitantAttention() {
|
|
return planningRepository.findNecessitantAttention();
|
|
}
|
|
|
|
/** Trouve les plannings en retard de validation */
|
|
public List<PlanningMateriel> findEnRetardValidation() {
|
|
return planningRepository.findEnRetardValidation();
|
|
}
|
|
|
|
/** Trouve les plannings prioritaires */
|
|
public List<PlanningMateriel> findPrioritaires() {
|
|
return planningRepository.findPrioritaires();
|
|
}
|
|
|
|
/** Trouve les plannings en cours */
|
|
public List<PlanningMateriel> findEnCours() {
|
|
return planningRepository.findEnCours();
|
|
}
|
|
|
|
// === CRÉATION ET MODIFICATION ===
|
|
|
|
/** Crée un nouveau planning matériel */
|
|
@Transactional
|
|
public PlanningMateriel createPlanning(
|
|
UUID materielId,
|
|
String nomPlanning,
|
|
String description,
|
|
LocalDate dateDebut,
|
|
LocalDate dateFin,
|
|
TypePlanning type,
|
|
String planificateur) {
|
|
logger.info("Création planning matériel: {} pour matériel: {}", nomPlanning, materielId);
|
|
|
|
// Validation des données
|
|
if (dateDebut.isAfter(dateFin)) {
|
|
throw new BadRequestException("La date de début doit être antérieure à la date de fin");
|
|
}
|
|
|
|
Materiel materiel =
|
|
materielRepository
|
|
.findByIdOptional(materielId)
|
|
.orElseThrow(() -> new NotFoundException("Matériel non trouvé: " + materielId));
|
|
|
|
// Création du planning
|
|
PlanningMateriel planning =
|
|
PlanningMateriel.builder()
|
|
.materiel(materiel)
|
|
.nomPlanning(nomPlanning)
|
|
.descriptionPlanning(description)
|
|
.dateDebut(dateDebut)
|
|
.dateFin(dateFin)
|
|
.typePlanning(type)
|
|
.planificateur(planificateur)
|
|
.creePar(planificateur)
|
|
.build();
|
|
|
|
// Génération automatique du nom si nécessaire
|
|
planning.genererNomPlanning();
|
|
|
|
// Définition de la couleur par défaut selon le type
|
|
if (planning.getCouleurPlanning() == null) {
|
|
planning.setCouleurPlanning(type.getCouleurDefaut());
|
|
}
|
|
|
|
planningRepository.persist(planning);
|
|
|
|
// Vérification des conflits immédiate
|
|
verifierConflits(planning);
|
|
|
|
logger.info("Planning créé avec succès: {}", planning.getId());
|
|
return planning;
|
|
}
|
|
|
|
/** Met à jour un planning existant */
|
|
@Transactional
|
|
public PlanningMateriel updatePlanning(
|
|
UUID id,
|
|
String nomPlanning,
|
|
String description,
|
|
LocalDate dateDebut,
|
|
LocalDate dateFin,
|
|
String modifiePar) {
|
|
logger.info("Mise à jour planning: {}", id);
|
|
|
|
PlanningMateriel planning = findByIdRequired(id);
|
|
|
|
if (!planning.peutEtreModifie()) {
|
|
throw new BadRequestException(
|
|
"Ce planning ne peut pas être modifié dans son état actuel: "
|
|
+ planning.getStatutPlanning());
|
|
}
|
|
|
|
// Validation des nouvelles données
|
|
if (dateDebut != null && dateFin != null && dateDebut.isAfter(dateFin)) {
|
|
throw new BadRequestException("La date de début doit être antérieure à la date de fin");
|
|
}
|
|
|
|
// Mise à jour des champs
|
|
if (nomPlanning != null) planning.setNomPlanning(nomPlanning);
|
|
if (description != null) planning.setDescriptionPlanning(description);
|
|
if (dateDebut != null) planning.setDateDebut(dateDebut);
|
|
if (dateFin != null) planning.setDateFin(dateFin);
|
|
if (modifiePar != null) planning.setModifiePar(modifiePar);
|
|
|
|
// Revérification des conflits après modification
|
|
verifierConflits(planning);
|
|
|
|
return planning;
|
|
}
|
|
|
|
// === GESTION DU WORKFLOW ===
|
|
|
|
/** Valide un planning */
|
|
@Transactional
|
|
public PlanningMateriel validerPlanning(UUID id, String valideur, String commentaires) {
|
|
logger.info("Validation planning: {} par: {}", id, valideur);
|
|
|
|
PlanningMateriel planning = findByIdRequired(id);
|
|
|
|
if (planning.getStatutPlanning() != StatutPlanning.BROUILLON
|
|
&& planning.getStatutPlanning() != StatutPlanning.EN_REVISION) {
|
|
throw new BadRequestException("Ce planning ne peut pas être validé dans son état actuel");
|
|
}
|
|
|
|
// Vérification finale des conflits avant validation
|
|
verifierConflits(planning);
|
|
|
|
if (planning.getConflitsDetectes()) {
|
|
throw new BadRequestException(
|
|
"Impossible de valider un planning avec des conflits non résolus");
|
|
}
|
|
|
|
planning.valider(valideur, commentaires);
|
|
|
|
// Calcul du score d'optimisation initial
|
|
calculerScoreOptimisation(planning);
|
|
|
|
logger.info("Planning validé avec succès: {}", id);
|
|
return planning;
|
|
}
|
|
|
|
/** Met un planning en révision */
|
|
@Transactional
|
|
public PlanningMateriel mettreEnRevision(UUID id, String motif) {
|
|
logger.info("Mise en révision planning: {}", id);
|
|
|
|
PlanningMateriel planning = findByIdRequired(id);
|
|
planning.mettreEnRevision(motif);
|
|
|
|
return planning;
|
|
}
|
|
|
|
/** Archive un planning */
|
|
@Transactional
|
|
public PlanningMateriel archiverPlanning(UUID id) {
|
|
logger.info("Archivage planning: {}", id);
|
|
|
|
PlanningMateriel planning = findByIdRequired(id);
|
|
planning.archiver();
|
|
|
|
return planning;
|
|
}
|
|
|
|
/** Suspend un planning */
|
|
@Transactional
|
|
public PlanningMateriel suspendrePlanning(UUID id) {
|
|
logger.info("Suspension planning: {}", id);
|
|
|
|
PlanningMateriel planning = findByIdRequired(id);
|
|
|
|
if (planning.getStatutPlanning() != StatutPlanning.VALIDE) {
|
|
throw new BadRequestException("Seuls les plannings validés peuvent être suspendus");
|
|
}
|
|
|
|
planning.setStatutPlanning(StatutPlanning.SUSPENDU);
|
|
|
|
return planning;
|
|
}
|
|
|
|
/** Réactive un planning suspendu */
|
|
@Transactional
|
|
public PlanningMateriel reactiverPlanning(UUID id) {
|
|
logger.info("Réactivation planning: {}", id);
|
|
|
|
PlanningMateriel planning = findByIdRequired(id);
|
|
|
|
if (planning.getStatutPlanning() != StatutPlanning.SUSPENDU) {
|
|
throw new BadRequestException("Seuls les plannings suspendus peuvent être réactivés");
|
|
}
|
|
|
|
// Revérification des conflits avant réactivation
|
|
verifierConflits(planning);
|
|
|
|
planning.setStatutPlanning(StatutPlanning.VALIDE);
|
|
|
|
return planning;
|
|
}
|
|
|
|
// === GESTION DES CONFLITS ===
|
|
|
|
/** Vérifie et met à jour les conflits d'un planning */
|
|
@Transactional
|
|
public void verifierConflits(PlanningMateriel planning) {
|
|
logger.debug("Vérification conflits pour planning: {}", planning.getId());
|
|
|
|
List<PlanningMateriel> conflits =
|
|
planningRepository.findConflits(
|
|
planning.getMateriel().getId(),
|
|
planning.getDateDebut(),
|
|
planning.getDateFin(),
|
|
planning.getId());
|
|
|
|
planning.mettreAJourConflits(conflits.size());
|
|
|
|
if (!conflits.isEmpty()) {
|
|
logger.warn(
|
|
"Conflits détectés pour planning {}: {} conflit(s)", planning.getId(), conflits.size());
|
|
}
|
|
}
|
|
|
|
/** Trouve les conflits pour un matériel sur une période */
|
|
public List<PlanningMateriel> checkConflits(
|
|
UUID materielId, LocalDate dateDebut, LocalDate dateFin, UUID excludeId) {
|
|
return planningRepository.findConflits(materielId, dateDebut, dateFin, excludeId);
|
|
}
|
|
|
|
/** Analyse la disponibilité d'un matériel sur une période */
|
|
public Map<String, Object> analyserDisponibilite(
|
|
UUID materielId, LocalDate dateDebut, LocalDate dateFin) {
|
|
logger.debug("Analyse disponibilité matériel: {} du {} au {}", materielId, dateDebut, dateFin);
|
|
|
|
List<PlanningMateriel> plannings =
|
|
planningRepository.findByPeriode(dateDebut, dateFin).stream()
|
|
.filter(p -> p.getMateriel().getId().equals(materielId))
|
|
.filter(p -> p.getStatutPlanning() == StatutPlanning.VALIDE)
|
|
.sorted(Comparator.comparing(PlanningMateriel::getDateDebut))
|
|
.collect(Collectors.toList());
|
|
|
|
List<Map<String, Object>> periodesOccupees = new ArrayList<>();
|
|
List<Map<String, Object>> periodesLibres = new ArrayList<>();
|
|
|
|
LocalDate curseur = dateDebut;
|
|
|
|
for (PlanningMateriel planning : plannings) {
|
|
LocalDate debutPlanning =
|
|
planning.getDateDebut().isBefore(dateDebut) ? dateDebut : planning.getDateDebut();
|
|
LocalDate finPlanning =
|
|
planning.getDateFin().isAfter(dateFin) ? dateFin : planning.getDateFin();
|
|
|
|
// Période libre avant ce planning
|
|
if (curseur.isBefore(debutPlanning)) {
|
|
periodesLibres.add(
|
|
Map.of(
|
|
"debut", curseur,
|
|
"fin", debutPlanning.minusDays(1),
|
|
"duree", ChronoUnit.DAYS.between(curseur, debutPlanning)));
|
|
}
|
|
|
|
// Période occupée
|
|
periodesOccupees.add(
|
|
Map.of(
|
|
"debut",
|
|
debutPlanning,
|
|
"fin",
|
|
finPlanning,
|
|
"duree",
|
|
ChronoUnit.DAYS.between(debutPlanning, finPlanning) + 1,
|
|
"planning",
|
|
planning.getResume(),
|
|
"taux",
|
|
planning.getTauxUtilisationPrevu()));
|
|
|
|
curseur = finPlanning.plusDays(1);
|
|
}
|
|
|
|
// Période libre finale
|
|
if (curseur.isBefore(dateFin) || curseur.equals(dateFin)) {
|
|
periodesLibres.add(
|
|
Map.of(
|
|
"debut", curseur,
|
|
"fin", dateFin,
|
|
"duree", ChronoUnit.DAYS.between(curseur, dateFin) + 1));
|
|
}
|
|
|
|
long totalJours = ChronoUnit.DAYS.between(dateDebut, dateFin) + 1;
|
|
long joursOccupes = periodesOccupees.stream().mapToLong(p -> (Long) p.get("duree")).sum();
|
|
double tauxOccupation = totalJours > 0 ? (double) joursOccupes / totalJours * 100.0 : 0.0;
|
|
|
|
return Map.of(
|
|
"materielId", materielId,
|
|
"periode", Map.of("debut", dateDebut, "fin", dateFin),
|
|
"totalJours", totalJours,
|
|
"joursOccupes", joursOccupes,
|
|
"joursLibres", totalJours - joursOccupes,
|
|
"tauxOccupation", tauxOccupation,
|
|
"periodesOccupees", periodesOccupees,
|
|
"periodesLibres", periodesLibres,
|
|
"disponible", periodesLibres.size() > 0);
|
|
}
|
|
|
|
// === OPTIMISATION ===
|
|
|
|
/** Calcule le score d'optimisation d'un planning */
|
|
@Transactional
|
|
public void calculerScoreOptimisation(PlanningMateriel planning) {
|
|
logger.debug("Calcul score optimisation pour planning: {}", planning.getId());
|
|
|
|
double score = 100.0;
|
|
|
|
// Pénalité pour les conflits
|
|
if (planning.getConflitsDetectes()) {
|
|
score -= planning.getNombreConflits() * 15.0;
|
|
}
|
|
|
|
// Pénalité pour faible taux d'utilisation
|
|
if (planning.getTauxUtilisationPrevu() != null) {
|
|
if (planning.getTauxUtilisationPrevu() < 50.0) {
|
|
score -= (50.0 - planning.getTauxUtilisationPrevu()) * 0.5;
|
|
}
|
|
}
|
|
|
|
// Bonus pour planification anticipée
|
|
long joursAvance = ChronoUnit.DAYS.between(LocalDate.now(), planning.getDateDebut());
|
|
if (joursAvance > planning.getTypePlanning().getDelaiMinimumPreavis() / 24) {
|
|
score += Math.min(10.0, joursAvance * 0.1);
|
|
}
|
|
|
|
// Pénalité pour dépassement horizon recommandé
|
|
long duree = planning.getDureePlanningJours();
|
|
int horizonRecommande = planning.getTypePlanning().getHorizonPlanificationJours();
|
|
if (duree > horizonRecommande) {
|
|
score -= (duree - horizonRecommande) * 0.1;
|
|
}
|
|
|
|
// Normalisation du score
|
|
score = Math.max(0.0, Math.min(100.0, score));
|
|
|
|
planning.mettreAJourOptimisation(score);
|
|
|
|
logger.debug("Score d'optimisation calculé: {} pour planning: {}", score, planning.getId());
|
|
}
|
|
|
|
/** Optimise automatiquement les plannings éligibles */
|
|
@Transactional
|
|
public List<PlanningMateriel> optimiserPlannings() {
|
|
logger.info("Démarrage optimisation automatique des plannings");
|
|
|
|
List<PlanningMateriel> candidats = planningRepository.findCandidatsOptimisation();
|
|
List<PlanningMateriel> optimises = new ArrayList<>();
|
|
|
|
for (PlanningMateriel planning : candidats) {
|
|
try {
|
|
optimiserPlanning(planning);
|
|
optimises.add(planning);
|
|
} catch (Exception e) {
|
|
logger.error("Erreur lors de l'optimisation du planning: " + planning.getId(), e);
|
|
}
|
|
}
|
|
|
|
logger.info(
|
|
"Optimisation terminée: {} plannings optimisés sur {} candidats",
|
|
optimises.size(),
|
|
candidats.size());
|
|
|
|
return optimises;
|
|
}
|
|
|
|
/** Optimise un planning spécifique */
|
|
@Transactional
|
|
public void optimiserPlanning(PlanningMateriel planning) {
|
|
logger.debug("Optimisation planning: {}", planning.getId());
|
|
|
|
// Recalcul du score d'optimisation
|
|
calculerScoreOptimisation(planning);
|
|
|
|
// Vérification et résolution automatique des conflits si possible
|
|
if (planning.getResolutionConflitsAuto()) {
|
|
tenterResolutionConflits(planning);
|
|
}
|
|
|
|
// Optimisation du taux d'utilisation
|
|
optimiserTauxUtilisation(planning);
|
|
|
|
planning.setDerniereOptimisation(LocalDateTime.now());
|
|
}
|
|
|
|
/** Tente de résoudre automatiquement les conflits */
|
|
private void tenterResolutionConflits(PlanningMateriel planning) {
|
|
if (!planning.getConflitsDetectes()) {
|
|
return;
|
|
}
|
|
|
|
logger.debug("Tentative résolution conflits pour planning: {}", planning.getId());
|
|
|
|
List<PlanningMateriel> conflits =
|
|
checkConflits(
|
|
planning.getMateriel().getId(),
|
|
planning.getDateDebut(),
|
|
planning.getDateFin(),
|
|
planning.getId());
|
|
|
|
// Stratégie simple: décaler le planning si possible
|
|
for (PlanningMateriel conflit : conflits) {
|
|
if (conflit.getTypePlanning().estPrioritaireSur(planning.getTypePlanning())) {
|
|
// Le conflit est prioritaire, essayer de décaler notre planning
|
|
LocalDate nouvelleDate = conflit.getDateFin().plusDays(1);
|
|
long duree = planning.getDureePlanningJours();
|
|
|
|
// Vérifier si le décalage est dans les limites acceptables
|
|
if (ChronoUnit.DAYS.between(planning.getDateDebut(), nouvelleDate) <= 30) {
|
|
planning.setDateDebut(nouvelleDate);
|
|
planning.setDateFin(nouvelleDate.plusDays(duree - 1));
|
|
|
|
// Revérifier les conflits après décalage
|
|
verifierConflits(planning);
|
|
|
|
if (!planning.getConflitsDetectes()) {
|
|
logger.info("Conflit résolu par décalage pour planning: {}", planning.getId());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Optimise le taux d'utilisation d'un planning */
|
|
private void optimiserTauxUtilisation(PlanningMateriel planning) {
|
|
// Analyser les réservations associées pour calculer un taux optimal
|
|
if (planning.getReservations() != null && !planning.getReservations().isEmpty()) {
|
|
double tauxMoyen =
|
|
planning.getReservations().stream()
|
|
.filter(
|
|
r ->
|
|
r.getStatut() == StatutReservationMateriel.VALIDEE
|
|
|| r.getStatut() == StatutReservationMateriel.EN_COURS)
|
|
.mapToDouble(r -> 80.0) // Taux standard par réservation
|
|
.average()
|
|
.orElse(60.0);
|
|
|
|
planning.setTauxUtilisationPrevu(Math.min(100.0, tauxMoyen));
|
|
}
|
|
}
|
|
|
|
// === STATISTIQUES ET ANALYSES ===
|
|
|
|
/** Génère les statistiques des plannings */
|
|
public Map<String, Object> getStatistiques() {
|
|
logger.debug("Génération statistiques plannings");
|
|
|
|
Map<String, Object> stats = planningRepository.calculerMetriques();
|
|
Map<StatutPlanning, Long> repartitionStatuts = planningRepository.compterParStatut();
|
|
List<Object[]> conflitsParType = planningRepository.analyserConflitsParType();
|
|
|
|
return Map.of(
|
|
"metriques", stats,
|
|
"repartitionStatuts", repartitionStatuts,
|
|
"conflitsParType", conflitsParType,
|
|
"dateGeneration", LocalDateTime.now());
|
|
}
|
|
|
|
/** Génère le tableau de bord des plannings */
|
|
public Map<String, Object> getTableauBordPlannings() {
|
|
logger.debug("Génération tableau de bord plannings");
|
|
|
|
return Map.of(
|
|
"planningsEnCours", findEnCours(),
|
|
"planningsAvecConflits", findAvecConflits(),
|
|
"planningsEnRetard", findEnRetardValidation(),
|
|
"planningsPrioritaires", findPrioritaires(),
|
|
"planningsNecessitantAttention", findNecessitantAttention(),
|
|
"statistiques", getStatistiques());
|
|
}
|
|
|
|
/** Analyse les taux d'utilisation par matériel */
|
|
public List<Object> analyserTauxUtilisation(LocalDate dateDebut, LocalDate dateFin) {
|
|
List<Object[]> resultats =
|
|
planningRepository.calculerTauxUtilisationParMateriel(dateDebut, dateFin);
|
|
|
|
return resultats.stream()
|
|
.map(
|
|
row ->
|
|
Map.of(
|
|
"materiel", row[0],
|
|
"tauxMoyen", row[1],
|
|
"nombrePlannings", row[2]))
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
// === GESTION AUTOMATIQUE ===
|
|
|
|
/** Vérifie tous les plannings nécessitant une vérification des conflits */
|
|
@Transactional
|
|
public void verifierTousConflits() {
|
|
logger.info("Vérification automatique des conflits pour tous les plannings");
|
|
|
|
List<PlanningMateriel> plannings = planningRepository.findNecessitantVerificationConflits();
|
|
|
|
for (PlanningMateriel planning : plannings) {
|
|
try {
|
|
verifierConflits(planning);
|
|
} catch (Exception e) {
|
|
logger.error(
|
|
"Erreur lors de la vérification des conflits pour planning: " + planning.getId(), e);
|
|
}
|
|
}
|
|
|
|
logger.info("Vérification des conflits terminée pour {} plannings", plannings.size());
|
|
}
|
|
}
|