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 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 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 findById(UUID id) { return planningRepository.findByIdOptional(id); } // === RECHERCHES SPÉCIALISÉES === /** Trouve les plannings pour un matériel */ public List findByMateriel(UUID materielId) { logger.debug("Recherche plannings pour matériel: {}", materielId); return planningRepository.findByMateriel(materielId); } /** Trouve les plannings sur une période */ public List 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 findByStatut(StatutPlanning statut) { return planningRepository.findByStatut(statut); } /** Trouve les plannings par type */ public List findByType(TypePlanning type) { return planningRepository.findByType(type); } /** Recherche textuelle dans les plannings */ public List 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 findAvecConflits() { return planningRepository.findAvecConflits(); } /** Trouve les plannings nécessitant attention */ public List findNecessitantAttention() { return planningRepository.findNecessitantAttention(); } /** Trouve les plannings en retard de validation */ public List findEnRetardValidation() { return planningRepository.findEnRetardValidation(); } /** Trouve les plannings prioritaires */ public List findPrioritaires() { return planningRepository.findPrioritaires(); } /** Trouve les plannings en cours */ public List 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 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 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 analyserDisponibilite( UUID materielId, LocalDate dateDebut, LocalDate dateFin) { logger.debug("Analyse disponibilité matériel: {} du {} au {}", materielId, dateDebut, dateFin); List 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> periodesOccupees = new ArrayList<>(); List> 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 optimiserPlannings() { logger.info("Démarrage optimisation automatique des plannings"); List candidats = planningRepository.findCandidatsOptimisation(); List 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 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 getStatistiques() { logger.debug("Génération statistiques plannings"); Map stats = planningRepository.calculerMetriques(); Map repartitionStatuts = planningRepository.compterParStatut(); List 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 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 analyserTauxUtilisation(LocalDate dateDebut, LocalDate dateFin) { List 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 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()); } }