Files
btpxpress-backend/src/main/java/dev/lions/btpxpress/application/service/PlanningService.java
2025-10-01 01:37:34 +00:00

640 lines
22 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 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<PlanningEvent> 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<LocalDate, List<PlanningEvent>> eventsByDay =
events.stream().collect(Collectors.groupingBy(event -> event.getDateDebut().toLocalDate()));
// Statistiques
long totalEvents = events.size();
Map<TypePlanningEvent, Long> eventsByType =
events.stream()
.collect(Collectors.groupingBy(PlanningEvent::getType, Collectors.counting()));
// Conflits détectés
List<Object> 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<LocalDate, List<PlanningEvent>> evenementsParJour = eventsByDay;
public final Map<TypePlanningEvent, Long> repartitionParType = eventsByType;
public final List<Object> 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<PlanningEvent> events = planningEventRepository.findByDateRange(debutSemaine, finSemaine);
// Organiser par jour de la semaine
Map<java.time.DayOfWeek, List<PlanningEvent>> 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<java.time.DayOfWeek, List<PlanningEvent>> 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<PlanningEvent> events = planningEventRepository.findByDateRange(debutMois, finMois);
// Organiser par semaine
Map<Integer, List<PlanningEvent>> 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<Integer, List<PlanningEvent>> evenementsParSemaine = eventsByWeek;
public final long totalEvenements = events.size();
};
}
// === MÉTHODES GESTION ÉVÉNEMENTS ===
public List<PlanningEvent> findAllEvents() {
logger.debug("Recherche de tous les événements de planning");
return planningEventRepository.findActifs();
}
public Optional<PlanningEvent> findEventById(UUID id) {
logger.debug("Recherche de l'événement par ID: {}", id);
return planningEventRepository.findByIdOptional(id);
}
public List<PlanningEvent> findEventsByDateRange(LocalDate dateDebut, LocalDate dateFin) {
logger.debug("Recherche des événements entre {} et {}", dateDebut, dateFin);
return planningEventRepository.findByDateRange(dateDebut, dateFin);
}
public List<PlanningEvent> findEventsByType(TypePlanningEvent type) {
logger.debug("Recherche des événements par type: {}", type);
return planningEventRepository.findByType(type);
}
public List<PlanningEvent> 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<UUID> employeIds,
List<UUID> 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<Employe> employes =
employeIds != null ? employeRepository.findByIds(employeIds) : new ArrayList<>();
List<Materiel> 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<UUID> employeIds,
List<UUID> 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<Employe> employes = employeRepository.findByIds(employeIds);
event.setEmployes(employes);
}
if (materielIds != null) {
List<Materiel> 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<Object> detectConflicts(LocalDate dateDebut, LocalDate dateFin, String resourceType) {
logger.debug("Détection des conflits du {} au {}", dateDebut, dateFin);
List<PlanningEvent> events = planningEventRepository.findByDateRange(dateDebut, dateFin);
List<Object> 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<UUID> employeIds,
List<UUID> materielIds,
UUID equipeId) {
return checkResourcesAvailabilityExcluding(
dateDebut, dateFin, employeIds, materielIds, equipeId, null);
}
public boolean checkResourcesAvailabilityExcluding(
LocalDateTime dateDebut,
LocalDateTime dateFin,
List<UUID> employeIds,
List<UUID> materielIds,
UUID equipeId,
UUID excludeEventId) {
logger.debug("Vérification de disponibilité des ressources du {} au {}", dateDebut, dateFin);
List<PlanningEvent> 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<UUID> employeIds,
List<UUID> materielIds,
UUID equipeId) {
logger.debug("Génération des détails de disponibilité");
List<PlanningEvent> conflictingEvents =
planningEventRepository.findConflictingEvents(dateDebut, dateFin, null);
// Analyser chaque ressource
Map<String, Object> employeDetails = new HashMap<>();
if (employeIds != null) {
for (UUID employeId : employeIds) {
employeDetails.put(
employeId.toString(),
isEmployeOccupied(employeId, conflictingEvents) ? "OCCUPÉ" : "DISPONIBLE");
}
}
Map<String, Object> 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<String, Object> employes = employeDetails;
public final Map<String, Object> materiels = materielDetails;
public final String equipe = equipeStatus;
public final List<PlanningEvent> evenementsConflictuels = conflictingEvents;
};
}
// === MÉTHODES STATISTIQUES ===
public Object getStatistics(LocalDate dateDebut, LocalDate dateFin) {
logger.debug("Génération des statistiques du planning");
List<PlanningEvent> events = planningEventRepository.findByDateRange(dateDebut, dateFin);
Map<TypePlanningEvent, Long> 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<TypePlanningEvent, Long> 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<Object> detectEmployeConflicts(List<PlanningEvent> events) {
List<Object> conflicts = new ArrayList<>();
Map<UUID, List<PlanningEvent>> 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<UUID, List<PlanningEvent>> entry : eventsByEmploye.entrySet()) {
List<PlanningEvent> 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<Object> detectMaterielConflicts(List<PlanningEvent> events) {
List<Object> conflicts = new ArrayList<>();
Map<UUID, List<PlanningEvent>> 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<UUID, List<PlanningEvent>> entry : eventsByMateriel.entrySet()) {
List<PlanningEvent> 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<Object> detectEquipeConflicts(List<PlanningEvent> events) {
List<Object> conflicts = new ArrayList<>();
Map<UUID, List<PlanningEvent>> 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<UUID, List<PlanningEvent>> entry : eventsByEquipe.entrySet()) {
List<PlanningEvent> 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<PlanningEvent> events) {
return events.stream()
.anyMatch(
event ->
event.getEmployes() != null
&& event.getEmployes().stream().anyMatch(e -> e.getId().equals(employeId)));
}
private boolean isMaterielOccupied(UUID materielId, List<PlanningEvent> events) {
return events.stream()
.anyMatch(
event ->
event.getMateriels() != null
&& event.getMateriels().stream().anyMatch(m -> m.getId().equals(materielId)));
}
private boolean isEquipeOccupied(UUID equipeId, List<PlanningEvent> events) {
return events.stream()
.anyMatch(event -> event.getEquipe() != null && event.getEquipe().getId().equals(equipeId));
}
// === MÉTHODES UTILITAIRES ===
// (Méthodes supprimées car redondantes)
}