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 jakarta.transaction.Transactional; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.temporal.TemporalAdjusters; import java.util.*; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Service de gestion du planning - Architecture 2025 MÉTIER: Logique complète planning BTP avec * détection conflits */ @ApplicationScoped public class PlanningService { private static final Logger logger = LoggerFactory.getLogger(PlanningService.class); @Inject PlanningEventRepository planningEventRepository; @Inject ChantierRepository chantierRepository; @Inject EquipeRepository equipeRepository; @Inject EmployeRepository employeRepository; @Inject MaterielRepository materielRepository; // === MÉTHODES VUE PLANNING GÉNÉRAL === public Object getPlanningGeneral( LocalDate dateDebut, LocalDate dateFin, UUID chantierId, UUID equipeId, TypePlanningEvent type) { logger.debug("Génération du planning général du {} au {}", dateDebut, dateFin); final LocalDate dateDebutFinal = dateDebut; final LocalDate dateFinFinal = dateFin; List events = planningEventRepository.findByDateRange(dateDebut, dateFin); // Filtrage selon les critères if (chantierId != null) { events = events.stream() .filter( event -> event.getChantier() != null && event.getChantier().getId().equals(chantierId)) .collect(Collectors.toList()); } if (equipeId != null) { events = events.stream() .filter( event -> event.getEquipe() != null && event.getEquipe().getId().equals(equipeId)) .collect(Collectors.toList()); } if (type != null) { events = events.stream().filter(event -> event.getType() == type).collect(Collectors.toList()); } // Organiser par jour Map> eventsByDay = events.stream().collect(Collectors.groupingBy(event -> event.getDateDebut().toLocalDate())); // Statistiques long totalEvents = events.size(); Map eventsByType = events.stream() .collect(Collectors.groupingBy(PlanningEvent::getType, Collectors.counting())); // Conflits détectés List conflicts = detectConflicts(dateDebut, dateFin, null); return new Object() { public final LocalDate dateDebut = dateDebutFinal; public final LocalDate dateFin = dateFinFinal; public final long totalEvenements = totalEvents; public final Map> evenementsParJour = eventsByDay; public final Map repartitionParType = eventsByType; public final List conflits = conflicts; public final int nombreConflits = conflicts.size(); }; } public Object getPlanningWeek(LocalDate dateRef) { logger.debug("Génération du planning hebdomadaire pour la semaine du {}", dateRef); LocalDate debutSemaine = dateRef.with(TemporalAdjusters.previousOrSame(java.time.DayOfWeek.MONDAY)); LocalDate finSemaine = debutSemaine.plusDays(6); List events = planningEventRepository.findByDateRange(debutSemaine, finSemaine); // Organiser par jour de la semaine Map> eventsByDayOfWeek = new LinkedHashMap<>(); for (java.time.DayOfWeek day : java.time.DayOfWeek.values()) { eventsByDayOfWeek.put(day, new ArrayList<>()); } events.forEach( event -> { java.time.DayOfWeek dayOfWeek = event.getDateDebut().getDayOfWeek(); eventsByDayOfWeek.get(dayOfWeek).add(event); }); final LocalDate debutSemaineFinal = debutSemaine; final LocalDate finSemaineFinal = finSemaine; return new Object() { public final LocalDate debutSemaine = debutSemaineFinal; public final LocalDate finSemaine = finSemaineFinal; public final Map> evenementsParJour = eventsByDayOfWeek; public final long totalEvenements = events.size(); }; } public Object getPlanningMonth(LocalDate dateRef) { logger.debug("Génération du planning mensuel pour {}", dateRef.getMonth()); LocalDate debutMois = dateRef.with(TemporalAdjusters.firstDayOfMonth()); LocalDate finMois = dateRef.with(TemporalAdjusters.lastDayOfMonth()); List events = planningEventRepository.findByDateRange(debutMois, finMois); // Organiser par semaine Map> eventsByWeek = events.stream() .collect( Collectors.groupingBy( event -> event .getDateDebut() .toLocalDate() .get(java.time.temporal.WeekFields.ISO.weekOfYear()))); final LocalDate debutMoisFinal = debutMois; final LocalDate finMoisFinal = finMois; return new Object() { public final int annee = dateRef.getYear(); public final String mois = dateRef.getMonth().name(); public final LocalDate debutMois = debutMoisFinal; public final LocalDate finMois = finMoisFinal; public final Map> evenementsParSemaine = eventsByWeek; public final long totalEvenements = events.size(); }; } // === MÉTHODES GESTION ÉVÉNEMENTS === public List findAllEvents() { logger.debug("Recherche de tous les événements de planning"); return planningEventRepository.findActifs(); } public Optional findEventById(UUID id) { logger.debug("Recherche de l'événement par ID: {}", id); return planningEventRepository.findByIdOptional(id); } public List findEventsByDateRange(LocalDate dateDebut, LocalDate dateFin) { logger.debug("Recherche des événements entre {} et {}", dateDebut, dateFin); return planningEventRepository.findByDateRange(dateDebut, dateFin); } public List findEventsByType(TypePlanningEvent type) { logger.debug("Recherche des événements par type: {}", type); return planningEventRepository.findByType(type); } public List findEventsByChantier(UUID chantierId) { logger.debug("Recherche des événements pour le chantier: {}", chantierId); return planningEventRepository.findByChantierId(chantierId); } @Transactional public PlanningEvent createEvent( String titre, String description, String typeStr, LocalDateTime dateDebut, LocalDateTime dateFin, UUID chantierId, UUID equipeId, List employeIds, List materielIds) { logger.debug("Création d'un nouvel événement: {}", titre); // Validation des données validateEventData(titre, dateDebut, dateFin); TypePlanningEvent type = TypePlanningEvent.valueOf(typeStr.toUpperCase()); // Récupération des entités Chantier chantier = chantierId != null ? chantierRepository .findByIdOptional(chantierId) .orElseThrow( () -> new IllegalArgumentException("Chantier non trouvé: " + chantierId)) : null; Equipe equipe = equipeId != null ? equipeRepository .findByIdOptional(equipeId) .orElseThrow(() -> new IllegalArgumentException("Équipe non trouvée: " + equipeId)) : null; List employes = employeIds != null ? employeRepository.findByIds(employeIds) : new ArrayList<>(); List materiels = materielIds != null ? materielRepository.findByIds(materielIds) : new ArrayList<>(); // Vérification des conflits de ressources if (!checkResourcesAvailability(dateDebut, dateFin, employeIds, materielIds, equipeId)) { throw new IllegalStateException("Conflit de ressources détecté pour cette période"); } // Création de l'événement PlanningEvent event = new PlanningEvent(); event.setTitre(titre); event.setDescription(description); event.setType(type); event.setDateDebut(dateDebut); event.setDateFin(dateFin); event.setChantier(chantier); event.setEquipe(equipe); event.setEmployes(employes); event.setMateriels(materiels); event.setActif(true); planningEventRepository.persist(event); logger.info("Événement créé avec succès: {} du {} au {}", titre, dateDebut, dateFin); return event; } @Transactional public PlanningEvent updateEvent( UUID id, String titre, String description, LocalDateTime dateDebut, LocalDateTime dateFin, UUID equipeId, List employeIds, List materielIds) { logger.debug("Mise à jour de l'événement: {}", id); PlanningEvent event = planningEventRepository .findByIdOptional(id) .orElseThrow(() -> new IllegalArgumentException("Événement non trouvé: " + id)); // Validation des nouvelles données if (dateDebut != null && dateFin != null) { validateEventData(titre, dateDebut, dateFin); // Vérifier les conflits (en excluant l'événement actuel) if (!checkResourcesAvailabilityExcluding( dateDebut, dateFin, employeIds, materielIds, equipeId, id)) { throw new IllegalStateException("Conflit de ressources détecté pour cette période"); } } // Mise à jour des champs if (titre != null) event.setTitre(titre); if (description != null) event.setDescription(description); if (dateDebut != null) event.setDateDebut(dateDebut); if (dateFin != null) event.setDateFin(dateFin); if (equipeId != null) { Equipe equipe = equipeRepository .findByIdOptional(equipeId) .orElseThrow(() -> new IllegalArgumentException("Équipe non trouvée: " + equipeId)); event.setEquipe(equipe); } if (employeIds != null) { List employes = employeRepository.findByIds(employeIds); event.setEmployes(employes); } if (materielIds != null) { List materiels = materielRepository.findByIds(materielIds); event.setMateriels(materiels); } planningEventRepository.persist(event); logger.info("Événement mis à jour avec succès: {}", event.getTitre()); return event; } @Transactional public void deleteEvent(UUID id) { logger.debug("Suppression de l'événement: {}", id); PlanningEvent event = planningEventRepository .findByIdOptional(id) .orElseThrow(() -> new IllegalArgumentException("Événement non trouvé: " + id)); planningEventRepository.softDelete(id); logger.info("Événement supprimé avec succès: {}", event.getTitre()); } // === MÉTHODES DÉTECTION CONFLITS === public List detectConflicts(LocalDate dateDebut, LocalDate dateFin, String resourceType) { logger.debug("Détection des conflits du {} au {}", dateDebut, dateFin); List events = planningEventRepository.findByDateRange(dateDebut, dateFin); List conflicts = new ArrayList<>(); // Détecter les conflits d'employés if (resourceType == null || "EMPLOYE".equals(resourceType)) { conflicts.addAll(detectEmployeConflicts(events)); } // Détecter les conflits de matériel if (resourceType == null || "MATERIEL".equals(resourceType)) { conflicts.addAll(detectMaterielConflicts(events)); } // Détecter les conflits d'équipes if (resourceType == null || "EQUIPE".equals(resourceType)) { conflicts.addAll(detectEquipeConflicts(events)); } logger.info("Détection terminée: {} conflits trouvés", conflicts.size()); return conflicts; } public boolean checkResourcesAvailability( LocalDateTime dateDebut, LocalDateTime dateFin, List employeIds, List materielIds, UUID equipeId) { return checkResourcesAvailabilityExcluding( dateDebut, dateFin, employeIds, materielIds, equipeId, null); } public boolean checkResourcesAvailabilityExcluding( LocalDateTime dateDebut, LocalDateTime dateFin, List employeIds, List materielIds, UUID equipeId, UUID excludeEventId) { logger.debug("Vérification de disponibilité des ressources du {} au {}", dateDebut, dateFin); List conflictingEvents = planningEventRepository.findConflictingEvents(dateDebut, dateFin, excludeEventId); // Vérifier les employés if (employeIds != null && !employeIds.isEmpty()) { for (UUID employeId : employeIds) { if (isEmployeOccupied(employeId, conflictingEvents)) { logger.warn("Employé {} occupé pendant cette période", employeId); return false; } } } // Vérifier le matériel if (materielIds != null && !materielIds.isEmpty()) { for (UUID materielId : materielIds) { if (isMaterielOccupied(materielId, conflictingEvents)) { logger.warn("Matériel {} occupé pendant cette période", materielId); return false; } } } // Vérifier l'équipe if (equipeId != null) { if (isEquipeOccupied(equipeId, conflictingEvents)) { logger.warn("Équipe {} occupée pendant cette période", equipeId); return false; } } return true; } public Object getAvailabilityDetails( LocalDateTime dateDebut, LocalDateTime dateFin, List employeIds, List materielIds, UUID equipeId) { logger.debug("Génération des détails de disponibilité"); List conflictingEvents = planningEventRepository.findConflictingEvents(dateDebut, dateFin, null); // Analyser chaque ressource Map employeDetails = new HashMap<>(); if (employeIds != null) { for (UUID employeId : employeIds) { employeDetails.put( employeId.toString(), isEmployeOccupied(employeId, conflictingEvents) ? "OCCUPÉ" : "DISPONIBLE"); } } Map materielDetails = new HashMap<>(); if (materielIds != null) { for (UUID materielId : materielIds) { materielDetails.put( materielId.toString(), isMaterielOccupied(materielId, conflictingEvents) ? "OCCUPÉ" : "DISPONIBLE"); } } final String equipeStatus; if (equipeId != null) { equipeStatus = isEquipeOccupied(equipeId, conflictingEvents) ? "OCCUPÉE" : "DISPONIBLE"; } else { equipeStatus = null; } return new Object() { public final Map employes = employeDetails; public final Map materiels = materielDetails; public final String equipe = equipeStatus; public final List evenementsConflictuels = conflictingEvents; }; } // === MÉTHODES STATISTIQUES === public Object getStatistics(LocalDate dateDebut, LocalDate dateFin) { logger.debug("Génération des statistiques du planning"); List events = planningEventRepository.findByDateRange(dateDebut, dateFin); Map eventsByType = events.stream() .collect(Collectors.groupingBy(PlanningEvent::getType, Collectors.counting())); long totalHeures = events.stream() .mapToLong( event -> java.time.Duration.between(event.getDateDebut(), event.getDateFin()).toHours()) .sum(); int conflitsDetectes = detectConflicts(dateDebut, dateFin, null).size(); return new Object() { public final long totalEvenements = events.size(); public final Map repartitionParType = eventsByType; public final long totalHeuresPlannifiees = totalHeures; public final int nombreConflits = conflitsDetectes; public final LocalDate periodeDebut = dateDebut; public final LocalDate periodeFin = dateFin; }; } // === MÉTHODES PRIVÉES DE VALIDATION === private void validateEventData(String titre, LocalDateTime dateDebut, LocalDateTime dateFin) { if (titre == null || titre.trim().isEmpty()) { throw new IllegalArgumentException("Le titre de l'événement est obligatoire"); } if (dateDebut == null || dateFin == null) { throw new IllegalArgumentException("Les dates de début et fin sont obligatoires"); } if (dateDebut.isAfter(dateFin)) { throw new IllegalArgumentException("La date de début ne peut pas être après la date de fin"); } if (dateDebut.isBefore(LocalDateTime.now().minusHours(1))) { throw new IllegalArgumentException("L'événement ne peut pas être planifié dans le passé"); } } // === MÉTHODES PRIVÉES DÉTECTION CONFLITS === private List detectEmployeConflicts(List events) { List conflicts = new ArrayList<>(); Map> eventsByEmploye = new HashMap<>(); // Grouper les événements par employé for (PlanningEvent event : events) { if (event.getEmployes() != null) { for (Employe employe : event.getEmployes()) { eventsByEmploye.computeIfAbsent(employe.getId(), k -> new ArrayList<>()).add(event); } } } // Détecter les chevauchements for (Map.Entry> entry : eventsByEmploye.entrySet()) { List employeEvents = entry.getValue(); for (int i = 0; i < employeEvents.size(); i++) { for (int j = i + 1; j < employeEvents.size(); j++) { PlanningEvent event1 = employeEvents.get(i); PlanningEvent event2 = employeEvents.get(j); if (eventsOverlap(event1, event2)) { conflicts.add(createConflictReport("EMPLOYE", entry.getKey(), event1, event2)); } } } } return conflicts; } private List detectMaterielConflicts(List events) { List conflicts = new ArrayList<>(); Map> eventsByMateriel = new HashMap<>(); // Grouper les événements par matériel for (PlanningEvent event : events) { if (event.getMateriels() != null) { for (Materiel materiel : event.getMateriels()) { eventsByMateriel.computeIfAbsent(materiel.getId(), k -> new ArrayList<>()).add(event); } } } // Détecter les chevauchements for (Map.Entry> entry : eventsByMateriel.entrySet()) { List materielEvents = entry.getValue(); for (int i = 0; i < materielEvents.size(); i++) { for (int j = i + 1; j < materielEvents.size(); j++) { PlanningEvent event1 = materielEvents.get(i); PlanningEvent event2 = materielEvents.get(j); if (eventsOverlap(event1, event2)) { conflicts.add(createConflictReport("MATERIEL", entry.getKey(), event1, event2)); } } } } return conflicts; } private List detectEquipeConflicts(List events) { List conflicts = new ArrayList<>(); Map> eventsByEquipe = new HashMap<>(); // Grouper les événements par équipe for (PlanningEvent event : events) { if (event.getEquipe() != null) { eventsByEquipe .computeIfAbsent(event.getEquipe().getId(), k -> new ArrayList<>()) .add(event); } } // Détecter les chevauchements for (Map.Entry> entry : eventsByEquipe.entrySet()) { List equipeEvents = entry.getValue(); for (int i = 0; i < equipeEvents.size(); i++) { for (int j = i + 1; j < equipeEvents.size(); j++) { PlanningEvent event1 = equipeEvents.get(i); PlanningEvent event2 = equipeEvents.get(j); if (eventsOverlap(event1, event2)) { conflicts.add(createConflictReport("EQUIPE", entry.getKey(), event1, event2)); } } } } return conflicts; } private boolean eventsOverlap(PlanningEvent event1, PlanningEvent event2) { return event1.getDateDebut().isBefore(event2.getDateFin()) && event2.getDateDebut().isBefore(event1.getDateFin()); } private Object createConflictReport( String resourceType, UUID resourceId, PlanningEvent event1, PlanningEvent event2) { return new Object() { public final String typeRessource = resourceType; public final UUID idRessource = resourceId; public final PlanningEvent evenement1 = event1; public final PlanningEvent evenement2 = event2; public final String description = String.format( "Conflit de %s: %s et %s se chevauchent", resourceType.toLowerCase(), event1.getTitre(), event2.getTitre()); }; } private boolean isEmployeOccupied(UUID employeId, List events) { return events.stream() .anyMatch( event -> event.getEmployes() != null && event.getEmployes().stream().anyMatch(e -> e.getId().equals(employeId))); } private boolean isMaterielOccupied(UUID materielId, List events) { return events.stream() .anyMatch( event -> event.getMateriels() != null && event.getMateriels().stream().anyMatch(m -> m.getId().equals(materielId))); } private boolean isEquipeOccupied(UUID equipeId, List events) { return events.stream() .anyMatch(event -> event.getEquipe() != null && event.getEquipe().getId().equals(equipeId)); } // === MÉTHODES UTILITAIRES === // (Méthodes supprimées car redondantes) }