451 lines
16 KiB
Java
451 lines
16 KiB
Java
package dev.lions.btpxpress.application.service;
|
|
|
|
import dev.lions.btpxpress.domain.core.entity.Chantier;
|
|
import dev.lions.btpxpress.domain.core.entity.Client;
|
|
import dev.lions.btpxpress.domain.core.entity.StatutChantier;
|
|
import dev.lions.btpxpress.domain.infrastructure.repository.ChantierRepository;
|
|
import dev.lions.btpxpress.domain.infrastructure.repository.ClientRepository;
|
|
import dev.lions.btpxpress.domain.shared.dto.ChantierCreateDTO;
|
|
import dev.lions.btpxpress.domain.shared.mapper.ChantierMapper;
|
|
import jakarta.enterprise.context.ApplicationScoped;
|
|
import jakarta.inject.Inject;
|
|
import jakarta.transaction.Transactional;
|
|
import jakarta.ws.rs.NotFoundException;
|
|
import java.math.BigDecimal;
|
|
import java.time.LocalDate;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.UUID;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
/**
|
|
* Service de gestion des chantiers - Architecture 2025 MIGRATION: Préservation exacte de toutes les
|
|
* fonctionnalités métier
|
|
*/
|
|
@ApplicationScoped
|
|
public class ChantierService {
|
|
|
|
private static final Logger logger = LoggerFactory.getLogger(ChantierService.class);
|
|
|
|
@Inject ChantierRepository chantierRepository;
|
|
|
|
@Inject ClientRepository clientRepository;
|
|
|
|
@Inject ChantierMapper chantierMapper;
|
|
|
|
// === MÉTHODES DE CONSULTATION - PRÉSERVÉES EXACTEMENT ===
|
|
|
|
public List<Chantier> findActifs() {
|
|
logger.debug("Recherche de tous les chantiers actifs");
|
|
return chantierRepository.findActifs();
|
|
}
|
|
|
|
public List<Chantier> findByChefChantier(UUID chefId) {
|
|
logger.debug("Recherche des chantiers par chef: {}", chefId);
|
|
return chantierRepository.findByChefChantier(chefId);
|
|
}
|
|
|
|
public List<Chantier> findChantiersEnRetard() {
|
|
logger.debug("Recherche des chantiers en retard");
|
|
return chantierRepository.findChantiersEnRetard();
|
|
}
|
|
|
|
public List<Chantier> findProchainsDemarrages(int jours) {
|
|
logger.debug("Recherche des prochains démarrages: {} jours", jours);
|
|
return chantierRepository.findProchainsDemarrages(jours);
|
|
}
|
|
|
|
public List<Chantier> findAll() {
|
|
logger.debug("Recherche de tous les chantiers (actifs et inactifs)");
|
|
return chantierRepository.listAll();
|
|
}
|
|
|
|
public List<Chantier> findAllActive() {
|
|
logger.debug("Recherche de tous les chantiers actifs uniquement");
|
|
return chantierRepository.listAll().stream()
|
|
.filter(c -> c.getActif() != null && c.getActif())
|
|
.collect(java.util.stream.Collectors.toList());
|
|
}
|
|
|
|
public long count() {
|
|
return chantierRepository.count();
|
|
}
|
|
|
|
public Optional<Chantier> findById(UUID id) {
|
|
logger.debug("Recherche du chantier par ID: {}", id);
|
|
return chantierRepository.findByIdOptional(id);
|
|
}
|
|
|
|
public Chantier findByIdRequired(UUID id) {
|
|
return findById(id)
|
|
.orElseThrow(() -> new NotFoundException("Chantier non trouvé avec l'ID: " + id));
|
|
}
|
|
|
|
public List<Chantier> findByClient(UUID clientId) {
|
|
logger.debug("Recherche des chantiers pour le client: {}", clientId);
|
|
return chantierRepository.findByClientId(clientId);
|
|
}
|
|
|
|
public List<Chantier> findByStatut(StatutChantier statut) {
|
|
logger.debug("Recherche des chantiers par statut: {}", statut);
|
|
return chantierRepository.findByStatut(statut);
|
|
}
|
|
|
|
public List<Chantier> findEnCours() {
|
|
logger.debug("Recherche des chantiers en cours");
|
|
return chantierRepository.findByStatut(StatutChantier.EN_COURS);
|
|
}
|
|
|
|
public List<Chantier> findPlanifies() {
|
|
logger.debug("Recherche des chantiers planifiés");
|
|
return chantierRepository.findByStatut(StatutChantier.PLANIFIE);
|
|
}
|
|
|
|
public List<Chantier> findTermines() {
|
|
logger.debug("Recherche des chantiers terminés");
|
|
return chantierRepository.findByStatut(StatutChantier.TERMINE);
|
|
}
|
|
|
|
public List<Chantier> findByVille(String ville) {
|
|
logger.debug("Recherche des chantiers par ville: {}", ville);
|
|
return chantierRepository.findByVille(ville);
|
|
}
|
|
|
|
@Transactional
|
|
public Chantier demarrerChantier(UUID id) {
|
|
logger.debug("Démarrage du chantier: {}", id);
|
|
Chantier chantier = findByIdRequired(id);
|
|
chantier.setStatut(StatutChantier.EN_COURS);
|
|
chantier.setDateDebutReelle(LocalDate.now());
|
|
return chantier;
|
|
}
|
|
|
|
@Transactional
|
|
public Chantier suspendreChantier(UUID id, String raison) {
|
|
logger.debug("Suspension du chantier: {} - Raison: {}", id, raison);
|
|
Chantier chantier = findByIdRequired(id);
|
|
chantier.setStatut(StatutChantier.SUSPENDU);
|
|
return chantier;
|
|
}
|
|
|
|
@Transactional
|
|
public Chantier terminerChantier(UUID id, LocalDate dateFin, String commentaire) {
|
|
logger.debug("Terminaison du chantier: {} - Date: {}", id, dateFin);
|
|
Chantier chantier = findByIdRequired(id);
|
|
chantier.setStatut(StatutChantier.TERMINE);
|
|
chantier.setDateFinReelle(dateFin);
|
|
return chantier;
|
|
}
|
|
|
|
@Transactional
|
|
public Chantier updateAvancementGlobal(UUID id, BigDecimal avancement) {
|
|
logger.debug("Mise à jour de l'avancement du chantier: {} - {}%", id, avancement);
|
|
Chantier chantier = findByIdRequired(id);
|
|
|
|
// Validation de l'avancement
|
|
if (avancement.compareTo(BigDecimal.ZERO) < 0
|
|
|| avancement.compareTo(BigDecimal.valueOf(100)) > 0) {
|
|
throw new IllegalArgumentException("L'avancement doit être entre 0 et 100%");
|
|
}
|
|
|
|
// Mise à jour de l'avancement
|
|
chantier.setPourcentageAvancement(avancement);
|
|
|
|
// Mise à jour automatique du statut si nécessaire
|
|
if (avancement.compareTo(BigDecimal.valueOf(100)) == 0
|
|
&& chantier.getStatut() != StatutChantier.TERMINE) {
|
|
chantier.setStatut(StatutChantier.TERMINE);
|
|
chantier.setDateFinReelle(LocalDate.now());
|
|
logger.info("Chantier automatiquement marqué comme terminé: {}", chantier.getNom());
|
|
} else if (avancement.compareTo(BigDecimal.ZERO) > 0
|
|
&& chantier.getStatut() == StatutChantier.PLANIFIE) {
|
|
chantier.setStatut(StatutChantier.EN_COURS);
|
|
chantier.setDateDebutReelle(LocalDate.now());
|
|
logger.info("Chantier automatiquement marqué comme en cours: {}", chantier.getNom());
|
|
}
|
|
|
|
chantierRepository.persist(chantier);
|
|
logger.info("Avancement du chantier {} mis à jour: {}%", chantier.getNom(), avancement);
|
|
|
|
return chantier;
|
|
}
|
|
|
|
public List<Chantier> searchChantiers(String query) {
|
|
logger.debug("Recherche de chantiers: {}", query);
|
|
if (query == null || query.trim().isEmpty()) {
|
|
return chantierRepository.findActifs();
|
|
}
|
|
return chantierRepository.searchByNomOrAdresse(query.trim());
|
|
}
|
|
|
|
public Map<String, Object> getStatistiques() {
|
|
logger.debug("Calcul des statistiques chantiers");
|
|
Map<String, Object> stats = new HashMap<>();
|
|
stats.put("total", count());
|
|
stats.put("enCours", findEnCours().size());
|
|
stats.put("planifies", findPlanifies().size());
|
|
stats.put("termines", findTermines().size());
|
|
stats.put("enRetard", findChantiersEnRetard().size());
|
|
return stats;
|
|
}
|
|
|
|
public Map<String, Object> calculerChiffreAffaires(Integer annee) {
|
|
logger.debug("Calcul du chiffre d'affaires pour l'année: {}", annee);
|
|
|
|
List<Chantier> chantiersAnnee =
|
|
chantierRepository.findByAnnee(annee != null ? annee : LocalDate.now().getYear());
|
|
|
|
BigDecimal totalEnCours =
|
|
chantiersAnnee.stream()
|
|
.filter(c -> c.getStatut() == StatutChantier.EN_COURS)
|
|
.map(c -> c.getMontantContrat() != null ? c.getMontantContrat() : BigDecimal.ZERO)
|
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
|
|
BigDecimal totalTermine =
|
|
chantiersAnnee.stream()
|
|
.filter(c -> c.getStatut() == StatutChantier.TERMINE)
|
|
.map(c -> c.getMontantContrat() != null ? c.getMontantContrat() : BigDecimal.ZERO)
|
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
|
|
BigDecimal totalGlobal = totalEnCours.add(totalTermine);
|
|
|
|
Map<String, Object> ca = new HashMap<>();
|
|
ca.put("annee", annee != null ? annee : LocalDate.now().getYear());
|
|
ca.put("total", totalGlobal);
|
|
ca.put("enCours", totalEnCours);
|
|
ca.put("termine", totalTermine);
|
|
ca.put("nombreChantiers", chantiersAnnee.size());
|
|
ca.put(
|
|
"montantMoyen",
|
|
chantiersAnnee.isEmpty()
|
|
? BigDecimal.ZERO
|
|
: totalGlobal.divide(
|
|
BigDecimal.valueOf(chantiersAnnee.size()), 2, java.math.RoundingMode.HALF_UP));
|
|
|
|
return ca;
|
|
}
|
|
|
|
public Map<String, Object> getDashboardChantier(UUID id) {
|
|
logger.debug("Dashboard du chantier: {}", id);
|
|
Chantier chantier = findByIdRequired(id);
|
|
Map<String, Object> dashboard = new HashMap<>();
|
|
dashboard.put("chantier", chantier);
|
|
dashboard.put("avancement", chantier.getPourcentageAvancement());
|
|
dashboard.put("enRetard", chantier.isEnRetard());
|
|
dashboard.put("montantContrat", chantier.getMontantContrat());
|
|
dashboard.put("coutReel", chantier.getCoutReel());
|
|
return dashboard;
|
|
}
|
|
|
|
// ===========================================
|
|
// MÉTHODES DE GESTION
|
|
// ===========================================
|
|
|
|
@Transactional
|
|
public Chantier create(ChantierCreateDTO dto) {
|
|
logger.debug("Création d'un nouveau chantier: {}", dto.getNom());
|
|
|
|
// Validation du client
|
|
Client client =
|
|
clientRepository
|
|
.findByIdOptional(dto.getClientId())
|
|
.orElseThrow(
|
|
() -> new IllegalArgumentException("Client non trouvé: " + dto.getClientId()));
|
|
|
|
// Validation des dates
|
|
if (dto.getDateDebut() != null && dto.getDateFinPrevue() != null) {
|
|
if (dto.getDateDebut().isAfter(dto.getDateFinPrevue())) {
|
|
throw new IllegalArgumentException(
|
|
"La date de début ne peut pas être après la date de fin prévue");
|
|
}
|
|
}
|
|
|
|
Chantier chantier = chantierMapper.toEntity(dto, client);
|
|
chantierRepository.persist(chantier);
|
|
|
|
logger.info(
|
|
"Chantier créé avec succès: {} pour le client: {}", chantier.getNom(), client.getNom());
|
|
|
|
return chantier;
|
|
}
|
|
|
|
@Transactional
|
|
public Chantier update(UUID id, ChantierCreateDTO dto) {
|
|
logger.debug("Mise à jour du chantier: {}", id);
|
|
|
|
Chantier chantier =
|
|
chantierRepository
|
|
.findByIdOptional(id)
|
|
.orElseThrow(() -> new IllegalArgumentException("Chantier non trouvé: " + id));
|
|
|
|
// Validation du client si changé
|
|
Client client =
|
|
clientRepository
|
|
.findByIdOptional(dto.getClientId())
|
|
.orElseThrow(
|
|
() -> new IllegalArgumentException("Client non trouvé: " + dto.getClientId()));
|
|
|
|
// Validation des dates
|
|
if (dto.getDateDebut() != null && dto.getDateFinPrevue() != null) {
|
|
if (dto.getDateDebut().isAfter(dto.getDateFinPrevue())) {
|
|
throw new IllegalArgumentException(
|
|
"La date de début ne peut pas être après la date de fin prévue");
|
|
}
|
|
}
|
|
|
|
chantierMapper.updateEntity(chantier, dto, client);
|
|
chantierRepository.persist(chantier);
|
|
|
|
logger.info("Chantier mis à jour avec succès: {}", chantier.getNom());
|
|
|
|
return chantier;
|
|
}
|
|
|
|
@Transactional
|
|
public Chantier updateStatut(UUID id, StatutChantier nouveauStatut) {
|
|
logger.debug("Mise à jour du statut du chantier {} vers {}", id, nouveauStatut);
|
|
|
|
Chantier chantier =
|
|
chantierRepository
|
|
.findByIdOptional(id)
|
|
.orElseThrow(() -> new IllegalArgumentException("Chantier non trouvé: " + id));
|
|
|
|
// Validation des transitions de statut
|
|
if (!isTransitionValide(chantier.getStatut(), nouveauStatut)) {
|
|
throw new IllegalArgumentException(
|
|
String.format(
|
|
"Transition de statut invalide: %s -> %s", chantier.getStatut(), nouveauStatut));
|
|
}
|
|
|
|
StatutChantier ancienStatut = chantier.getStatut();
|
|
chantier.setStatut(nouveauStatut);
|
|
|
|
// Mise à jour automatique de la date de fin réelle si terminé
|
|
if (nouveauStatut == StatutChantier.TERMINE && chantier.getDateFinReelle() == null) {
|
|
chantier.setDateFinReelle(LocalDate.now());
|
|
}
|
|
|
|
chantierRepository.persist(chantier);
|
|
|
|
logger.info(
|
|
"Statut du chantier {} changé de {} vers {}",
|
|
chantier.getNom(),
|
|
ancienStatut,
|
|
nouveauStatut);
|
|
|
|
return chantier;
|
|
}
|
|
|
|
@Transactional
|
|
public void delete(UUID id) {
|
|
logger.debug("Suppression logique du chantier: {}", id);
|
|
|
|
Chantier chantier =
|
|
chantierRepository
|
|
.findByIdOptional(id)
|
|
.orElseThrow(() -> new IllegalArgumentException("Chantier non trouvé: " + id));
|
|
|
|
// Vérification qu'on peut supprimer (pas de devis/factures en cours)
|
|
if (chantier.getStatut() == StatutChantier.EN_COURS) {
|
|
throw new IllegalStateException("Impossible de supprimer un chantier en cours");
|
|
}
|
|
|
|
chantierRepository.softDelete(id);
|
|
|
|
logger.info("Chantier supprimé logiquement: {}", chantier.getNom());
|
|
}
|
|
|
|
@Transactional
|
|
public void deletePhysically(UUID id) {
|
|
logger.debug("Suppression physique du chantier: {}", id);
|
|
|
|
Chantier chantier =
|
|
chantierRepository
|
|
.findByIdOptional(id)
|
|
.orElseThrow(() -> new IllegalArgumentException("Chantier non trouvé: " + id));
|
|
|
|
// Vérifications plus strictes pour suppression physique
|
|
if (chantier.getStatut() == StatutChantier.EN_COURS) {
|
|
throw new IllegalStateException("Impossible de supprimer physiquement un chantier en cours");
|
|
}
|
|
|
|
if (chantier.getStatut() == StatutChantier.TERMINE) {
|
|
throw new IllegalStateException("Impossible de supprimer physiquement un chantier terminé");
|
|
}
|
|
|
|
chantierRepository.physicalDelete(id);
|
|
|
|
logger.warn("Chantier supprimé physiquement (DÉFINITIVEMENT): {}", chantier.getNom());
|
|
}
|
|
|
|
// ===========================================
|
|
// MÉTHODES DE RECHERCHE ET FILTRAGE
|
|
// ===========================================
|
|
|
|
public List<Chantier> search(String searchTerm) {
|
|
if (searchTerm == null || searchTerm.trim().isEmpty()) {
|
|
return findAll();
|
|
}
|
|
|
|
logger.debug("Recherche de chantiers avec le terme: {}", searchTerm);
|
|
return chantierRepository.searchByNomOrAdresse(searchTerm.trim());
|
|
}
|
|
|
|
public List<Chantier> findByDateRange(LocalDate dateDebut, LocalDate dateFin) {
|
|
logger.debug("Recherche des chantiers entre {} et {}", dateDebut, dateFin);
|
|
return chantierRepository.findByDateRange(dateDebut, dateFin);
|
|
}
|
|
|
|
public List<Chantier> findRecents(int limit) {
|
|
logger.debug("Recherche des {} chantiers les plus récents", limit);
|
|
return chantierRepository.findRecents(limit);
|
|
}
|
|
|
|
// ===========================================
|
|
// MÉTHODES DE VALIDATION MÉTIER
|
|
// ===========================================
|
|
|
|
private boolean isTransitionValide(StatutChantier ancienStatut, StatutChantier nouveauStatut) {
|
|
if (ancienStatut == nouveauStatut) {
|
|
return true;
|
|
}
|
|
|
|
return switch (ancienStatut) {
|
|
case PLANIFIE ->
|
|
nouveauStatut == StatutChantier.EN_COURS || nouveauStatut == StatutChantier.ANNULE;
|
|
case EN_COURS ->
|
|
nouveauStatut == StatutChantier.TERMINE
|
|
|| nouveauStatut == StatutChantier.SUSPENDU
|
|
|| nouveauStatut == StatutChantier.ANNULE;
|
|
case SUSPENDU ->
|
|
nouveauStatut == StatutChantier.EN_COURS || nouveauStatut == StatutChantier.ANNULE;
|
|
case TERMINE -> false; // Un chantier terminé ne peut plus changer
|
|
case ANNULE -> false; // Un chantier annulé ne peut plus changer
|
|
};
|
|
}
|
|
|
|
// ===========================================
|
|
// MÉTHODES STATISTIQUES
|
|
// ===========================================
|
|
|
|
public long countByStatut(StatutChantier statut) {
|
|
return chantierRepository.countByStatut(statut);
|
|
}
|
|
|
|
public Object getStatistics() {
|
|
logger.debug("Génération des statistiques des chantiers");
|
|
|
|
return new Object() {
|
|
public final long total = count();
|
|
public final long planifies = countByStatut(StatutChantier.PLANIFIE);
|
|
public final long enCours = countByStatut(StatutChantier.EN_COURS);
|
|
public final long termines = countByStatut(StatutChantier.TERMINE);
|
|
public final long suspendus = countByStatut(StatutChantier.SUSPENDU);
|
|
public final long annules = countByStatut(StatutChantier.ANNULE);
|
|
};
|
|
}
|
|
}
|