Initial commit

This commit is contained in:
dahoud
2025-10-01 01:37:34 +00:00
commit f2bb633142
310 changed files with 86051 additions and 0 deletions

View File

@@ -0,0 +1,258 @@
package dev.lions.btpxpress.adapter.http;
import jakarta.annotation.security.PermitAll;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;
import java.security.Principal;
import java.util.Map;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour l'authentification et les informations utilisateur
* Permet de récupérer les informations de l'utilisateur connecté depuis le token JWT Keycloak
*/
@Path("/api/v1/auth")
@Produces(MediaType.APPLICATION_JSON)
@Tag(name = "Authentification", description = "Gestion de l'authentification et informations utilisateur")
public class AuthResource {
private static final Logger logger = LoggerFactory.getLogger(AuthResource.class);
@Inject
JsonWebToken jwt;
/**
* Récupère les informations de l'utilisateur connecté depuis le token JWT
*/
@GET
@Path("/user")
@PermitAll // Accessible même sans authentification pour les tests
@Operation(
summary = "Informations utilisateur connecté",
description = "Récupère les informations de l'utilisateur connecté depuis le token JWT Keycloak")
@APIResponse(responseCode = "200", description = "Informations utilisateur récupérées")
@APIResponse(responseCode = "401", description = "Non authentifié")
public Response getCurrentUser(@Context SecurityContext securityContext) {
try {
logger.debug("Récupération des informations utilisateur connecté");
// Vérifier si l'utilisateur est authentifié
Principal principal = securityContext.getUserPrincipal();
if (principal == null || jwt == null) {
logger.warn("Aucun utilisateur authentifié trouvé");
// En mode développement, retourner un utilisateur de test
return Response.ok(createTestUser()).build();
}
// Extraire les informations du token JWT
String userId = jwt.getSubject();
String username = jwt.getClaim("preferred_username");
String email = jwt.getClaim("email");
String firstName = jwt.getClaim("given_name");
String lastName = jwt.getClaim("family_name");
String fullName = jwt.getClaim("name");
// Extraire les rôles
Object realmAccess = jwt.getClaim("realm_access");
Object resourceAccess = jwt.getClaim("resource_access");
// Construire la réponse avec les informations utilisateur
Map<String, Object> userInfo = new java.util.HashMap<>();
userInfo.put("id", userId != null ? userId : "unknown");
userInfo.put("username", username != null ? username : email);
userInfo.put("email", email != null ? email : "unknown@btpxpress.com");
userInfo.put("firstName", firstName != null ? firstName : "Utilisateur");
userInfo.put("lastName", lastName != null ? lastName : "Connecté");
userInfo.put("fullName", fullName != null ? fullName : (firstName + " " + lastName).trim());
userInfo.put("roles", extractRoles(realmAccess, resourceAccess));
userInfo.put("permissions", extractPermissions(realmAccess, resourceAccess));
userInfo.put("isAdmin", isAdmin(realmAccess, resourceAccess));
userInfo.put("isManager", isManager(realmAccess, resourceAccess));
userInfo.put("isEmployee", isEmployee(realmAccess, resourceAccess));
userInfo.put("isClient", isClient(realmAccess, resourceAccess));
logger.info("Informations utilisateur récupérées: {} ({})", username, email);
return Response.ok(userInfo).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des informations utilisateur", e);
// En cas d'erreur, retourner un utilisateur de test en mode développement
return Response.ok(createTestUser()).build();
}
}
/**
* Endpoint de test pour vérifier l'état de l'authentification
*/
@GET
@Path("/status")
@PermitAll
@Operation(
summary = "Statut d'authentification",
description = "Vérifie l'état de l'authentification de l'utilisateur")
@APIResponse(responseCode = "200", description = "Statut récupéré")
public Response getAuthStatus(@Context SecurityContext securityContext) {
try {
Principal principal = securityContext.getUserPrincipal();
boolean isAuthenticated = principal != null && jwt != null;
Map<String, Object> status = Map.of(
"authenticated", isAuthenticated,
"principal", principal != null ? principal.getName() : null,
"hasJWT", jwt != null,
"timestamp", System.currentTimeMillis()
);
return Response.ok(status).build();
} catch (Exception e) {
logger.error("Erreur lors de la vérification du statut d'authentification", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la vérification du statut"))
.build();
}
}
/**
* Crée un utilisateur de test pour le mode développement
*/
private Map<String, Object> createTestUser() {
Map<String, Object> testUser = new java.util.HashMap<>();
testUser.put("id", "dev-user-001");
testUser.put("username", "admin.btpxpress");
testUser.put("email", "admin@btpxpress.com");
testUser.put("firstName", "Jean-Michel");
testUser.put("lastName", "Martineau");
testUser.put("fullName", "Jean-Michel Martineau");
testUser.put("roles", java.util.List.of("SUPER_ADMIN", "ADMIN", "DIRECTEUR"));
testUser.put("permissions", java.util.List.of("ALL_PERMISSIONS"));
testUser.put("isAdmin", true);
testUser.put("isManager", true);
testUser.put("isEmployee", false);
testUser.put("isClient", false);
return testUser;
}
/**
* Extrait les rôles depuis les claims JWT
*/
private java.util.List<String> extractRoles(Object realmAccess, Object resourceAccess) {
java.util.List<String> roles = new java.util.ArrayList<>();
// Ajouter les rôles du realm
if (realmAccess instanceof Map) {
Object realmRoles = ((Map<?, ?>) realmAccess).get("roles");
if (realmRoles instanceof java.util.List) {
((java.util.List<?>) realmRoles).forEach(role -> {
if (role instanceof String) {
roles.add((String) role);
}
});
}
}
// Ajouter les rôles des ressources
if (resourceAccess instanceof Map) {
((Map<?, ?>) resourceAccess).values().forEach(resource -> {
if (resource instanceof Map) {
Object resourceRoles = ((Map<?, ?>) resource).get("roles");
if (resourceRoles instanceof java.util.List) {
((java.util.List<?>) resourceRoles).forEach(role -> {
if (role instanceof String) {
roles.add((String) role);
}
});
}
}
});
}
return roles.isEmpty() ? java.util.List.of("USER") : roles;
}
/**
* Extrait les permissions depuis les rôles
*/
private java.util.List<String> extractPermissions(Object realmAccess, Object resourceAccess) {
java.util.List<String> roles = extractRoles(realmAccess, resourceAccess);
java.util.List<String> permissions = new java.util.ArrayList<>();
// Mapper les rôles vers les permissions
for (String role : roles) {
switch (role.toUpperCase()) {
case "SUPER_ADMIN":
case "BTPXPRESS_SUPER_ADMIN":
permissions.add("ALL_PERMISSIONS");
break;
case "ADMIN":
case "BTPXPRESS_ADMIN":
permissions.addAll(java.util.List.of("MANAGE_USERS", "MANAGE_CHANTIERS", "MANAGE_CLIENTS"));
break;
case "DIRECTEUR":
case "MANAGER":
permissions.addAll(java.util.List.of("VIEW_REPORTS", "MANAGE_CHANTIERS"));
break;
case "CHEF_CHANTIER":
permissions.addAll(java.util.List.of("MANAGE_CHANTIER", "VIEW_PLANNING"));
break;
default:
permissions.add("VIEW_BASIC");
break;
}
}
return permissions.isEmpty() ? java.util.List.of("VIEW_BASIC") : permissions;
}
/**
* Vérifie si l'utilisateur est administrateur
*/
private boolean isAdmin(Object realmAccess, Object resourceAccess) {
java.util.List<String> roles = extractRoles(realmAccess, resourceAccess);
return roles.stream().anyMatch(role ->
role.toUpperCase().contains("ADMIN") || role.toUpperCase().contains("SUPER_ADMIN"));
}
/**
* Vérifie si l'utilisateur est manager
*/
private boolean isManager(Object realmAccess, Object resourceAccess) {
java.util.List<String> roles = extractRoles(realmAccess, resourceAccess);
return roles.stream().anyMatch(role ->
role.toUpperCase().contains("DIRECTEUR") ||
role.toUpperCase().contains("MANAGER") ||
role.toUpperCase().contains("CHEF"));
}
/**
* Vérifie si l'utilisateur est employé
*/
private boolean isEmployee(Object realmAccess, Object resourceAccess) {
java.util.List<String> roles = extractRoles(realmAccess, resourceAccess);
return roles.stream().anyMatch(role ->
role.toUpperCase().contains("EMPLOYE") ||
role.toUpperCase().contains("OUVRIER"));
}
/**
* Vérifie si l'utilisateur est client
*/
private boolean isClient(Object realmAccess, Object resourceAccess) {
java.util.List<String> roles = extractRoles(realmAccess, resourceAccess);
return roles.stream().anyMatch(role ->
role.toUpperCase().contains("CLIENT"));
}
}

View File

@@ -0,0 +1,304 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.BudgetService;
import dev.lions.btpxpress.domain.core.entity.Budget;
import dev.lions.btpxpress.domain.core.entity.Budget.StatutBudget;
import dev.lions.btpxpress.domain.core.entity.Budget.TendanceBudget;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour la gestion des budgets - Architecture 2025 Endpoints pour le suivi budgétaire
* des chantiers
*/
@Path("/api/v1/budgets")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Budgets", description = "Gestion du suivi budgétaire")
public class BudgetResource {
private static final Logger logger = LoggerFactory.getLogger(BudgetResource.class);
@Inject BudgetService budgetService;
// === ENDPOINTS DE CONSULTATION ===
@GET
@Operation(summary = "Récupérer tous les budgets")
@APIResponse(responseCode = "200", description = "Liste des budgets récupérée avec succès")
public Response getAllBudgets(
@Parameter(description = "Terme de recherche") @QueryParam("search") String search,
@Parameter(description = "Statut du budget") @QueryParam("statut") String statut,
@Parameter(description = "Tendance du budget") @QueryParam("tendance") String tendance) {
try {
List<Budget> budgets;
if (statut != null && !statut.isEmpty()) {
budgets = budgetService.findByStatut(StatutBudget.valueOf(statut.toUpperCase()));
} else if (tendance != null && !tendance.isEmpty()) {
budgets = budgetService.findByTendance(TendanceBudget.valueOf(tendance.toUpperCase()));
} else if (search != null && !search.isEmpty()) {
budgets = budgetService.search(search);
} else {
budgets = budgetService.findAll();
}
return Response.ok(budgets).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des budgets", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des budgets")
.build();
}
}
@GET
@Path("/{id}")
@Operation(summary = "Récupérer un budget par son ID")
@APIResponse(responseCode = "200", description = "Budget récupéré avec succès")
@APIResponse(responseCode = "404", description = "Budget non trouvé")
public Response getBudgetById(@PathParam("id") UUID id) {
try {
return budgetService
.findById(id)
.map(budget -> Response.ok(budget).build())
.orElse(Response.status(Response.Status.NOT_FOUND).build());
} catch (Exception e) {
logger.error("Erreur lors de la récupération du budget {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération du budget")
.build();
}
}
@GET
@Path("/chantier/{chantierId}")
@Operation(summary = "Récupérer le budget d'un chantier")
@APIResponse(responseCode = "200", description = "Budget du chantier récupéré avec succès")
@APIResponse(responseCode = "404", description = "Budget non trouvé pour ce chantier")
public Response getBudgetByChantier(@PathParam("chantierId") UUID chantierId) {
try {
return budgetService
.findByChantier(chantierId)
.map(budget -> Response.ok(budget).build())
.orElse(Response.status(Response.Status.NOT_FOUND).build());
} catch (Exception e) {
logger.error("Erreur lors de la récupération du budget pour le chantier {}", chantierId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération du budget")
.build();
}
}
@GET
@Path("/depassement")
@Operation(summary = "Récupérer les budgets en dépassement")
@APIResponse(responseCode = "200", description = "Budgets en dépassement récupérés avec succès")
public Response getBudgetsEnDepassement() {
try {
List<Budget> budgets = budgetService.findEnDepassement();
return Response.ok(budgets).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des budgets en dépassement", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des budgets")
.build();
}
}
@GET
@Path("/attention")
@Operation(summary = "Récupérer les budgets nécessitant une attention")
@APIResponse(
responseCode = "200",
description = "Budgets nécessitant attention récupérés avec succès")
public Response getBudgetsNecessitantAttention() {
try {
List<Budget> budgets = budgetService.findNecessitantAttention();
return Response.ok(budgets).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des budgets nécessitant attention", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des budgets")
.build();
}
}
@GET
@Path("/statistiques")
@Operation(summary = "Récupérer les statistiques globales des budgets")
@APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès")
public Response getStatistiques() {
try {
Map<String, Object> stats = budgetService.getStatistiquesGlobales();
return Response.ok(stats).build();
} catch (Exception e) {
logger.error("Erreur lors du calcul des statistiques", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du calcul des statistiques")
.build();
}
}
// === ENDPOINTS DE GESTION ===
@POST
@Operation(summary = "Créer un nouveau budget")
@APIResponse(responseCode = "201", description = "Budget créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response createBudget(Budget budget) {
try {
budgetService.validerBudget(budget);
Budget nouveauBudget = budgetService.create(budget);
return Response.status(Response.Status.CREATED).entity(nouveauBudget).build();
} catch (BadRequestException e) {
logger.warn(
"Tentative de création d'un budget avec des données invalides: {}", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la création du budget", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la création du budget")
.build();
}
}
@PUT
@Path("/{id}")
@Operation(summary = "Mettre à jour un budget")
@APIResponse(responseCode = "200", description = "Budget mis à jour avec succès")
@APIResponse(responseCode = "404", description = "Budget non trouvé")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response updateBudget(@PathParam("id") UUID id, Budget budget) {
try {
budgetService.validerBudget(budget);
Budget budgetMisAJour = budgetService.update(id, budget);
return Response.ok(budgetMisAJour).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
logger.warn(
"Tentative de mise à jour d'un budget avec des données invalides: {}", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la mise à jour du budget {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la mise à jour du budget")
.build();
}
}
@DELETE
@Path("/{id}")
@Operation(summary = "Supprimer un budget")
@APIResponse(responseCode = "204", description = "Budget supprimé avec succès")
@APIResponse(responseCode = "404", description = "Budget non trouvé")
public Response deleteBudget(@PathParam("id") UUID id) {
try {
budgetService.delete(id);
return Response.noContent().build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la suppression du budget {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la suppression du budget")
.build();
}
}
// === ENDPOINTS MÉTIER ===
@PUT
@Path("/{id}/depenses")
@Operation(summary = "Mettre à jour les dépenses d'un budget")
@APIResponse(responseCode = "200", description = "Dépenses mises à jour avec succès")
@APIResponse(responseCode = "404", description = "Budget non trouvé")
public Response updateDepenses(
@PathParam("id") UUID id, @Parameter(description = "Nouvelle dépense") BigDecimal depense) {
try {
Budget budget = budgetService.mettreAJourDepenses(id, depense);
return Response.ok(budget).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la mise à jour des dépenses pour le budget {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la mise à jour des dépenses")
.build();
}
}
@PUT
@Path("/{id}/avancement")
@Operation(summary = "Mettre à jour l'avancement d'un budget")
@APIResponse(responseCode = "200", description = "Avancement mis à jour avec succès")
@APIResponse(responseCode = "404", description = "Budget non trouvé")
public Response updateAvancement(
@PathParam("id") UUID id,
@Parameter(description = "Nouvel avancement") BigDecimal avancement) {
try {
Budget budget = budgetService.mettreAJourAvancement(id, avancement);
return Response.ok(budget).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la mise à jour de l'avancement pour le budget {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la mise à jour de l'avancement")
.build();
}
}
@POST
@Path("/{id}/alertes")
@Operation(summary = "Ajouter une alerte à un budget")
@APIResponse(responseCode = "200", description = "Alerte ajoutée avec succès")
@APIResponse(responseCode = "404", description = "Budget non trouvé")
public Response ajouterAlerte(
@PathParam("id") UUID id,
@Parameter(description = "Description de l'alerte") String description) {
try {
budgetService.ajouterAlerte(id, description);
return Response.ok().build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de l'ajout d'alerte pour le budget {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de l'ajout de l'alerte")
.build();
}
}
@DELETE
@Path("/{id}/alertes")
@Operation(summary = "Supprimer les alertes d'un budget")
@APIResponse(responseCode = "200", description = "Alertes supprimées avec succès")
@APIResponse(responseCode = "404", description = "Budget non trouvé")
public Response supprimerAlertes(@PathParam("id") UUID id) {
try {
budgetService.supprimerAlertes(id);
return Response.ok().build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la suppression des alertes pour le budget {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la suppression des alertes")
.build();
}
}
}

View File

@@ -0,0 +1,325 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.CalculateurTechniqueBTP;
import dev.lions.btpxpress.application.service.CalculateurTechniqueBTP.*;
import dev.lions.btpxpress.domain.core.entity.MaterielBTP;
import dev.lions.btpxpress.domain.core.entity.ZoneClimatique;
import dev.lions.btpxpress.domain.infrastructure.repository.MaterielBTPRepository;
import dev.lions.btpxpress.domain.infrastructure.repository.ZoneClimatiqueRepository;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* API REST pour les calculs techniques ultra-détaillés BTP Le plus ambitieux système de calculs BTP
* d'Afrique
*/
@Path("/api/v1/calculs-techniques")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(
name = "CalculsTechniques",
description = "Calculs techniques ultra-détaillés BTP - Système le plus avancé d'Afrique")
public class CalculsTechniquesResource {
private static final Logger logger = LoggerFactory.getLogger(CalculsTechniquesResource.class);
@Inject CalculateurTechniqueBTP calculateur;
@Inject MaterielBTPRepository materielRepository;
@Inject ZoneClimatiqueRepository zoneClimatiqueRepository;
// =================== CALCULS MAÇONNERIE ===================
@POST
@Path("/briques-mur")
@Operation(summary = "Calcul ultra-précis quantité briques pour mur")
@APIResponse(responseCode = "200", description = "Calcul réussi avec détails complets")
@APIResponse(responseCode = "400", description = "Paramètres invalides")
@APIResponse(responseCode = "404", description = "Matériau ou zone climatique non trouvée")
public Response calculerBriquesMur(
@Parameter(description = "Paramètres détaillés du calcul") @Valid @NotNull
ParametresCalculBriques params) {
try {
logger.info(
"🧮 Début calcul briques - Surface: {}m², Zone: {}",
params.surface,
params.zoneClimatique);
// Validation paramètres
if (params.surface == null || params.surface.compareTo(BigDecimal.ZERO) <= 0) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Surface doit être > 0"))
.build();
}
if (params.epaisseurMur == null || params.epaisseurMur.compareTo(BigDecimal.ZERO) <= 0) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Épaisseur mur doit être > 0"))
.build();
}
// Appel service calcul
ResultatCalculBriques resultat = calculateur.calculerBriquesMur(params);
logger.info("✅ Calcul briques terminé - {} briques nécessaires", resultat.nombreBriques);
return Response.ok(resultat).build();
} catch (IllegalArgumentException e) {
logger.error("❌ Erreur paramètres calcul briques: {}", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
logger.error("💥 Erreur inattendue calcul briques", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur interne lors du calcul"))
.build();
}
}
@POST
@Path("/mortier-maconnerie")
@Operation(summary = "Calcul mortier pour maçonnerie traditionnelle")
@APIResponse(responseCode = "200", description = "Quantités mortier calculées")
public Response calculerMortierMaconnerie(
@Parameter(description = "Paramètres calcul mortier") @Valid @NotNull
ParametresCalculMortier params) {
try {
// Validation
if (params.volumeMaconnerie == null
|| params.volumeMaconnerie.compareTo(BigDecimal.ZERO) <= 0) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Volume maçonnerie requis"))
.build();
}
// Calcul volume mortier (environ 20-25% du volume maçonnerie)
BigDecimal volumeMortier = params.volumeMaconnerie.multiply(new BigDecimal("0.23"));
// Dosage mortier selon usage
String dosage = params.typeMortier != null ? params.typeMortier : "STANDARD";
int cimentKgM3 =
switch (dosage) {
case "POSE_BRIQUES" -> 350;
case "JOINTOIEMENT" -> 450;
case "ENDUIT_BASE" -> 300;
case "ENDUIT_FINITION" -> 400;
default -> 350; // STANDARD
};
int cimentTotal = volumeMortier.multiply(new BigDecimal(cimentKgM3)).intValue();
int sableTotal = volumeMortier.multiply(new BigDecimal("800")).intValue(); // 800L/m³
int eauTotal = volumeMortier.multiply(new BigDecimal("175")).intValue(); // 175L/m³
ResultatCalculMortier resultat = new ResultatCalculMortier();
resultat.volumeTotal = volumeMortier;
resultat.cimentKg = cimentTotal;
resultat.sableLitres = sableTotal;
resultat.eauLitres = eauTotal;
resultat.sacs50kg = (int) Math.ceil(cimentTotal / 50.0);
return Response.ok(resultat).build();
} catch (Exception e) {
logger.error("Erreur calcul mortier", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur calcul mortier"))
.build();
}
}
// =================== CALCULS BÉTON ARMÉ ===================
@POST
@Path("/beton-arme")
@Operation(summary = "Calcul béton armé avec adaptation climatique africaine")
@APIResponse(responseCode = "200", description = "Calcul complet béton + armatures + adaptations")
public Response calculerBetonArme(
@Parameter(description = "Paramètres béton armé détaillés") @Valid @NotNull
ParametresCalculBetonArme params) {
try {
logger.info(
"🏗️ Calcul béton armé - Vol: {}m³, Classe: {}", params.volume, params.classeBeton);
// Validations
if (params.volume == null || params.volume.compareTo(BigDecimal.ZERO) <= 0) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Volume béton requis"))
.build();
}
if (params.classeBeton == null || params.classeBeton.trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Classe béton requise"))
.build();
}
// Appel service calcul
ResultatCalculBetonArme resultat = calculateur.calculerBetonArme(params);
logger.info(
"✅ Béton calculé - {} sacs ciment, {} kg acier",
resultat.cimentSacs50kg,
resultat.acierKgTotal);
return Response.ok(resultat).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
logger.error("Erreur calcul béton armé", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur calcul béton armé"))
.build();
}
}
@GET
@Path("/dosages-beton")
@Operation(summary = "Liste des dosages béton standard avec adaptations climatiques")
@APIResponse(responseCode = "200", description = "Dosages disponibles")
public Response getDosagesBeton() {
Map<String, Object> dosages =
Map.of(
"C20/25",
Map.of(
"usage", "Béton de propreté, fondations simples",
"ciment", "300 kg/m³",
"resistance", "20 MPa caractéristique",
"exposition", "XC1 - Intérieur sec"),
"C25/30",
Map.of(
"usage", "Dalles, poutres courantes, ouvrages courants",
"ciment", "350 kg/m³",
"resistance", "25 MPa caractéristique",
"exposition", "XC3 - Intérieur humide"),
"C30/37",
Map.of(
"usage", "Béton armé, précontraint, ouvrages d'art",
"ciment", "385 kg/m³",
"resistance", "30 MPa caractéristique",
"exposition", "XC4 - Extérieur avec gel/dégel"),
"C35/45",
Map.of(
"usage", "Béton haute performance, ouvrages spéciaux",
"ciment", "420 kg/m³",
"resistance", "35 MPa caractéristique",
"exposition", "XS1/XS3 - Environnement marin"));
return Response.ok(
Map.of(
"dosages",
dosages,
"notes",
List.of(
"Dosages adaptés climat tropical africain",
"Majoration ciment en zone très chaude (+25kg/m³)",
"Réduction E/C en zone marine (-10L/m³)",
"Cure renforcée obligatoire (7j minimum)")))
.build();
}
// =================== INFORMATIONS MATÉRIAUX ===================
@GET
@Path("/materiaux")
@Operation(summary = "Liste des matériaux BTP avec spécifications détaillées")
@APIResponse(responseCode = "200", description = "Catalogue matériaux ultra-détaillé")
public Response getMateriaux(
@QueryParam("categorie") String categorie, @QueryParam("zone") String zoneClimatique) {
try {
List<MaterielBTP> materiaux;
if (categorie != null && !categorie.trim().isEmpty()) {
MaterielBTP.CategorieMateriel cat = MaterielBTP.CategorieMateriel.valueOf(categorie);
materiaux = materielRepository.findByCategorie(cat);
} else {
materiaux = materielRepository.findAllActifs();
}
// Filtrage par zone climatique si spécifiée
if (zoneClimatique != null && !zoneClimatique.trim().isEmpty()) {
ZoneClimatique zone = zoneClimatiqueRepository.findByCode(zoneClimatique).orElse(null);
if (zone != null) {
materiaux = materiaux.stream().filter(m -> zone.isMaterielAdapte(m)).toList();
}
}
return Response.ok(
Map.of(
"materiaux", materiaux,
"total", materiaux.size(),
"filtres",
Map.of(
"categorie", categorie != null ? categorie : "TOUTES",
"zone", zoneClimatique != null ? zoneClimatique : "TOUTES")))
.build();
} catch (Exception e) {
logger.error("Erreur récupération matériaux", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur récupération matériaux"))
.build();
}
}
@GET
@Path("/zones-climatiques")
@Operation(summary = "Zones climatiques africaines avec contraintes construction")
@APIResponse(responseCode = "200", description = "Zones climatiques détaillées")
public Response getZonesClimatiques() {
try {
List<ZoneClimatique> zones = zoneClimatiqueRepository.findAllActives();
return Response.ok(
Map.of(
"zones",
zones,
"info",
"Zones climatiques spécialisées pour l'Afrique avec contraintes construction"
+ " détaillées"))
.build();
} catch (Exception e) {
logger.error("Erreur zones climatiques", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur zones climatiques"))
.build();
}
}
// =================== CLASSES DTO ===================
public static class ParametresCalculMortier {
public BigDecimal volumeMaconnerie;
public String typeMortier; // POSE_BRIQUES, JOINTOIEMENT, ENDUIT_BASE, ENDUIT_FINITION
public String zoneClimatique;
}
// [AUTRES CLASSES DTO DÉJÀ DÉFINIES DANS CalculateurTechniqueBTP...]
}

View File

@@ -0,0 +1,366 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.ChantierService;
import dev.lions.btpxpress.domain.core.entity.Chantier;
import dev.lions.btpxpress.domain.core.entity.StatutChantier;
import dev.lions.btpxpress.domain.shared.dto.ChantierCreateDTO;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour la gestion des chantiers - Architecture 2025 MIGRATION: Préservation exacte de
* tous les endpoints critiques
*/
@Path("/api/v1/chantiers")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Chantiers", description = "Gestion des chantiers BTP")
// @Authenticated - Désactivé pour les tests
public class ChantierResource {
private static final Logger logger = LoggerFactory.getLogger(ChantierResource.class);
@Inject ChantierService chantierService;
// === ENDPOINTS DE CONSULTATION - API CONTRACTS PRÉSERVÉS EXACTEMENT ===
@GET
@Operation(summary = "Récupérer tous les chantiers")
@APIResponse(responseCode = "200", description = "Liste des chantiers récupérée avec succès")
public Response getAllChantiers(
@Parameter(description = "Terme de recherche") @QueryParam("search") String search,
@Parameter(description = "Statut du chantier") @QueryParam("statut") String statut,
@Parameter(description = "ID du client") @QueryParam("clientId") String clientId) {
try {
List<Chantier> chantiers;
if (clientId != null && !clientId.isEmpty()) {
chantiers = chantierService.findByClient(UUID.fromString(clientId));
} else if (statut != null && !statut.isEmpty()) {
chantiers = chantierService.findByStatut(StatutChantier.valueOf(statut.toUpperCase()));
} else if (search != null && !search.isEmpty()) {
chantiers = chantierService.search(search);
} else {
chantiers = chantierService.findAll();
}
return Response.ok(chantiers).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des chantiers", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des chantiers: " + e.getMessage())
.build();
}
}
@GET
@Path("/actifs")
@Operation(summary = "Récupérer tous les chantiers actifs")
@APIResponse(
responseCode = "200",
description = "Liste des chantiers actifs récupérée avec succès")
public Response getAllActiveChantiers() {
try {
List<Chantier> chantiers = chantierService.findAllActive();
return Response.ok(chantiers).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des chantiers actifs", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des chantiers actifs: " + e.getMessage())
.build();
}
}
@GET
@Path("/{id}")
@Operation(summary = "Récupérer un chantier par ID")
@APIResponse(responseCode = "200", description = "Chantier récupéré avec succès")
@APIResponse(responseCode = "404", description = "Chantier non trouvé")
public Response getChantierById(
@Parameter(description = "ID du chantier") @PathParam("id") String id) {
try {
UUID chantierId = UUID.fromString(id);
return chantierService
.findById(chantierId)
.map(chantier -> Response.ok(chantier).build())
.orElse(
Response.status(Response.Status.NOT_FOUND)
.entity("Chantier non trouvé avec l'ID: " + id)
.build());
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID de chantier invalide: " + id)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération du chantier {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération du chantier: " + e.getMessage())
.build();
}
}
@GET
@Path("/count")
@Operation(summary = "Compter le nombre de chantiers")
@APIResponse(responseCode = "200", description = "Nombre de chantiers récupéré avec succès")
public Response countChantiers() {
try {
long count = chantierService.count();
return Response.ok(new CountResponse(count)).build();
} catch (Exception e) {
logger.error("Erreur lors du comptage des chantiers", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du comptage des chantiers: " + e.getMessage())
.build();
}
}
@GET
@Path("/stats")
@Operation(summary = "Obtenir les statistiques des chantiers")
@APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès")
public Response getStats() {
try {
Object stats = chantierService.getStatistics();
return Response.ok(stats).build();
} catch (Exception e) {
logger.error("Erreur lors de la génération des statistiques des chantiers", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la génération des statistiques: " + e.getMessage())
.build();
}
}
@GET
@Path("/statut/{statut}")
public Response getChantiersByStatut(@PathParam("statut") String statut) {
try {
StatutChantier statutEnum = StatutChantier.valueOf(statut.toUpperCase());
List<Chantier> chantiers = chantierService.findByStatut(statutEnum);
return Response.ok(chantiers).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(
"Statut invalide: "
+ statut
+ ". Valeurs possibles: PLANIFIE, EN_COURS, TERMINE, ANNULE, SUSPENDU")
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des chantiers par statut {}", statut, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des chantiers: " + e.getMessage())
.build();
}
}
@GET
@Path("/en-cours")
public Response getChantiersEnCours() {
try {
List<Chantier> chantiers = chantierService.findEnCours();
return Response.ok(chantiers).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des chantiers en cours", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des chantiers en cours: " + e.getMessage())
.build();
}
}
@GET
@Path("/planifies")
public Response getChantiersPlanifies() {
try {
List<Chantier> chantiers = chantierService.findPlanifies();
return Response.ok(chantiers).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des chantiers planifiés", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des chantiers planifiés: " + e.getMessage())
.build();
}
}
@GET
@Path("/termines")
public Response getChantiersTermines() {
try {
List<Chantier> chantiers = chantierService.findTermines();
return Response.ok(chantiers).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des chantiers terminés", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des chantiers terminés: " + e.getMessage())
.build();
}
}
// === ENDPOINTS DE GESTION - API CONTRACTS PRÉSERVÉS EXACTEMENT ===
@POST
@Operation(summary = "Créer un nouveau chantier")
@APIResponse(responseCode = "201", description = "Chantier créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response createChantier(
@Parameter(description = "Données du chantier à créer") @Valid @NotNull
ChantierCreateDTO chantierDTO) {
try {
Chantier chantier = chantierService.create(chantierDTO);
return Response.status(Response.Status.CREATED).entity(chantier).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Données invalides: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la création du chantier", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la création du chantier: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}")
@Operation(summary = "Mettre à jour un chantier")
@APIResponse(responseCode = "200", description = "Chantier mis à jour avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
@APIResponse(responseCode = "404", description = "Chantier non trouvé")
public Response updateChantier(
@Parameter(description = "ID du chantier") @PathParam("id") String id,
@Parameter(description = "Nouvelles données du chantier") @Valid @NotNull
ChantierCreateDTO chantierDTO) {
try {
UUID chantierId = UUID.fromString(id);
Chantier chantier = chantierService.update(chantierId, chantierDTO);
return Response.ok(chantier).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Données invalides: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la mise à jour du chantier {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la mise à jour du chantier: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/statut")
public Response updateChantierStatut(@PathParam("id") String id, UpdateStatutRequest request) {
try {
UUID chantierId = UUID.fromString(id);
StatutChantier nouveauStatut = StatutChantier.valueOf(request.statut.toUpperCase());
Chantier chantier = chantierService.updateStatut(chantierId, nouveauStatut);
return Response.ok(chantier).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Données invalides: " + e.getMessage())
.build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.CONFLICT)
.entity("Transition de statut non autorisée: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la mise à jour du statut du chantier {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la mise à jour du statut: " + e.getMessage())
.build();
}
}
@DELETE
@Path("/{id}")
@Operation(summary = "Supprimer un chantier")
@APIResponse(responseCode = "204", description = "Chantier supprimé avec succès")
@APIResponse(responseCode = "400", description = "ID invalide")
@APIResponse(responseCode = "404", description = "Chantier non trouvé")
@APIResponse(responseCode = "409", description = "Impossible de supprimer")
public Response deleteChantier(
@Parameter(description = "ID du chantier") @PathParam("id") String id,
@Parameter(description = "Suppression définitive (true) ou logique (false, défaut)")
@QueryParam("permanent")
@DefaultValue("false")
boolean permanent) {
try {
UUID chantierId = UUID.fromString(id);
if (permanent) {
chantierService.deletePhysically(chantierId);
} else {
chantierService.delete(chantierId);
}
return Response.noContent().build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID invalide: " + e.getMessage())
.build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.CONFLICT)
.entity("Impossible de supprimer: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la suppression du chantier {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la suppression du chantier: " + e.getMessage())
.build();
}
}
// ===========================================
// ENDPOINTS DE RECHERCHE AVANCÉE
// ===========================================
@GET
@Path("/date-range")
public Response getChantiersByDateRange(
@QueryParam("dateDebut") String dateDebut, @QueryParam("dateFin") String dateFin) {
try {
if (dateDebut == null || dateFin == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Les paramètres dateDebut et dateFin sont obligatoires")
.build();
}
LocalDate debut = LocalDate.parse(dateDebut);
LocalDate fin = LocalDate.parse(dateFin);
if (debut.isAfter(fin)) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("La date de début ne peut pas être après la date de fin")
.build();
}
List<Chantier> chantiers = chantierService.findByDateRange(debut, fin);
return Response.ok(chantiers).build();
} catch (Exception e) {
logger.error("Erreur lors de la recherche par plage de dates", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la recherche: " + e.getMessage())
.build();
}
}
// ===========================================
// CLASSES UTILITAIRES
// ===========================================
public static record CountResponse(long count) {}
public static record UpdateStatutRequest(String statut) {}
}

View File

@@ -0,0 +1,179 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.ClientService;
import dev.lions.btpxpress.domain.core.entity.Client;
import dev.lions.btpxpress.domain.core.entity.Permission;
import dev.lions.btpxpress.domain.shared.dto.ClientCreateDTO;
import dev.lions.btpxpress.infrastructure.security.RequirePermission;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour la gestion des clients - Architecture 2025 MIGRATION: Préservation exacte de
* toutes les API endpoints et contrats
*/
@Path("/api/v1/clients")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Clients", description = "Gestion des clients")
// @Authenticated - Désactivé pour les tests
public class ClientResource {
private static final Logger logger = LoggerFactory.getLogger(ClientResource.class);
@Inject ClientService clientService;
// === ENDPOINTS DE LECTURE - API CONTRACTS PRÉSERVÉS EXACTEMENT ===
@GET
@RequirePermission(Permission.CLIENTS_READ)
@Operation(summary = "Récupérer tous les clients")
@APIResponse(responseCode = "200", description = "Liste des clients récupérée avec succès")
public Response getAllClients(
@Parameter(description = "Numéro de page (0-based)") @QueryParam("page") @DefaultValue("0")
int page,
@Parameter(description = "Taille de la page") @QueryParam("size") @DefaultValue("20")
int size) {
logger.debug("GET /clients - page: {}, size: {}", page, size);
List<Client> clients;
if (page == 0 && size == 20) {
clients = clientService.findAll();
} else {
clients = clientService.findAll(page, size);
}
return Response.ok(clients).build();
}
@GET
@Path("/{id}")
@RequirePermission(Permission.CLIENTS_READ)
@Operation(summary = "Récupérer un client par ID")
@APIResponse(responseCode = "200", description = "Client trouvé")
@APIResponse(responseCode = "404", description = "Client non trouvé")
public Response getClientById(@Parameter(description = "ID du client") @PathParam("id") UUID id) {
logger.debug("GET /clients/{}", id);
Client client = clientService.findByIdRequired(id);
return Response.ok(client).build();
}
@GET
@Path("/search")
@Operation(summary = "Rechercher des clients")
@APIResponse(responseCode = "200", description = "Résultats de recherche")
public Response searchClients(
@Parameter(description = "Nom du client") @QueryParam("nom") String nom,
@Parameter(description = "Entreprise") @QueryParam("entreprise") String entreprise,
@Parameter(description = "Ville") @QueryParam("ville") String ville,
@Parameter(description = "Email") @QueryParam("email") String email) {
logger.debug(
"GET /clients/search - nom: {}, entreprise: {}, ville: {}, email: {}",
nom,
entreprise,
ville,
email);
List<Client> clients;
// Logique de recherche exacte préservée
if (email != null && !email.trim().isEmpty()) {
clients = clientService.findByEmail(email).map(List::of).orElse(List.of());
} else if (nom != null && !nom.trim().isEmpty()) {
clients = clientService.searchByNom(nom);
} else if (entreprise != null && !entreprise.trim().isEmpty()) {
clients = clientService.searchByEntreprise(entreprise);
} else if (ville != null && !ville.trim().isEmpty()) {
clients = clientService.searchByVille(ville);
} else {
clients = clientService.findAll();
}
return Response.ok(clients).build();
}
// === ENDPOINTS D'ÉCRITURE - API CONTRACTS PRÉSERVÉS EXACTEMENT ===
@POST
@RequirePermission(Permission.CLIENTS_CREATE)
@Operation(summary = "Créer un nouveau client")
@APIResponse(responseCode = "201", description = "Client créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response createClient(@Valid @NotNull ClientCreateDTO clientDTO) {
logger.debug("POST /clients");
logger.info(
"Données reçues: nom={}, prenom={}, email={}",
clientDTO.getNom(),
clientDTO.getPrenom(),
clientDTO.getEmail());
try {
Client createdClient = clientService.createFromDTO(clientDTO);
return Response.status(Response.Status.CREATED).entity(createdClient).build();
} catch (Exception e) {
logger.error("Erreur lors de la création du client: {}", e.getMessage(), e);
throw e;
}
}
@PUT
@Path("/{id}")
@RequirePermission(Permission.CLIENTS_UPDATE)
@Operation(summary = "Mettre à jour un client")
@APIResponse(responseCode = "200", description = "Client mis à jour avec succès")
@APIResponse(responseCode = "404", description = "Client non trouvé")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response updateClient(
@Parameter(description = "ID du client") @PathParam("id") UUID id,
@Valid @NotNull Client client) {
logger.debug("PUT /clients/{}", id);
Client updatedClient = clientService.update(id, client);
return Response.ok(updatedClient).build();
}
@DELETE
@Path("/{id}")
@RequirePermission(Permission.CLIENTS_DELETE)
@Operation(summary = "Supprimer un client")
@APIResponse(responseCode = "204", description = "Client supprimé avec succès")
@APIResponse(responseCode = "404", description = "Client non trouvé")
public Response deleteClient(@Parameter(description = "ID du client") @PathParam("id") UUID id) {
logger.debug("DELETE /clients/{}", id);
clientService.delete(id);
return Response.noContent().build();
}
// === ENDPOINTS STATISTIQUES - API CONTRACTS PRÉSERVÉS EXACTEMENT ===
@GET
@Path("/count")
@Operation(summary = "Compter le nombre de clients")
@APIResponse(responseCode = "200", description = "Nombre de clients")
public Response countClients() {
logger.debug("GET /clients/count");
long count = clientService.count();
return Response.ok(count).build();
}
}

View File

@@ -0,0 +1,725 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.*;
import dev.lions.btpxpress.domain.core.entity.*;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour le tableau de bord - Architecture 2025 DASHBOARD: API de métriques et
* indicateurs BTP
*/
@Path("/api/v1/dashboard")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Dashboard", description = "Tableau de bord et métriques BTP")
public class DashboardResource {
private static final Logger logger = LoggerFactory.getLogger(DashboardResource.class);
@Inject ChantierService chantierService;
@Inject EquipeService equipeService;
@Inject EmployeService employeService;
@Inject MaterielService materielService;
@Inject MaintenanceService maintenanceService;
@Inject DocumentService documentService;
@Inject DisponibiliteService disponibiliteService;
@Inject PlanningService planningService;
// === DASHBOARD PRINCIPAL ===
@GET
@Operation(
summary = "Tableau de bord principal",
description = "Récupère les métriques principales du système BTP")
@APIResponse(responseCode = "200", description = "Métriques du dashboard récupérées")
public Response getDashboardPrincipal() {
logger.debug("Génération du dashboard principal");
// Métriques globales
final long totalChantiers = chantierService.count();
final long chantiersEnCours = chantierService.countByStatut(StatutChantier.EN_COURS);
final long chantiersPlanifies = chantierService.countByStatut(StatutChantier.PLANIFIE);
final long chantiersActifs = chantiersEnCours + chantiersPlanifies;
final long totalEquipes = equipeService.count();
final long equipesDisponibles = equipeService.countByStatut(StatutEquipe.DISPONIBLE);
final long totalEmployes = employeService.count();
final long employesActifs = employeService.countActifs();
final long totalMateriel = materielService.count();
final long materielDisponible = materielService.countDisponible();
// Métriques de maintenance
final long maintenancesEnRetard = maintenanceService.findEnRetard().size();
final long maintenancesPlanifiees = maintenanceService.findPlanifiees().size();
// Métriques de planning
final List<PlanningEvent> evenementsAujourdhui =
planningService.findEventsByDateRange(LocalDate.now(), LocalDate.now());
final long evenementsAujourdhui_count = evenementsAujourdhui.size();
// Métriques de documents
final long totalDocuments = documentService.findAll().size();
final List<Document> documentsRecents = documentService.findRecents(5);
// Disponibilités en attente
final long disponibilitesEnAttenteCount = disponibiliteService.findEnAttente().size();
return Response.ok(
new Object() {
public final Object chantiers =
new Object() {
public final long total = totalChantiers;
public final long actifs = chantiersActifs;
public final double tauxActivite =
totalChantiers > 0 ? (double) chantiersActifs / totalChantiers * 100 : 0;
};
public final Object equipes =
new Object() {
public final long total = totalEquipes;
public final long disponibles = equipesDisponibles;
public final double tauxDisponibilite =
totalEquipes > 0 ? (double) equipesDisponibles / totalEquipes * 100 : 0;
};
public final Object employes =
new Object() {
public final long total = totalEmployes;
public final long actifs = employesActifs;
public final double tauxActivite =
totalEmployes > 0 ? (double) employesActifs / totalEmployes * 100 : 0;
};
public final Object materiel =
new Object() {
public final long total = totalMateriel;
public final long disponible = materielDisponible;
public final double tauxDisponibilite =
totalMateriel > 0 ? (double) materielDisponible / totalMateriel * 100 : 0;
};
public final Object maintenance =
new Object() {
public final long enRetard = maintenancesEnRetard;
public final long planifiees = maintenancesPlanifiees;
public final boolean alerteRetard = maintenancesEnRetard > 0;
};
public final Object planning =
new Object() {
public final long evenementsAujourdhui = evenementsAujourdhui_count;
public final long disponibilitesEnAttente = disponibilitesEnAttenteCount;
};
public final Object documents =
new Object() {
public final long total = totalDocuments;
public final List<Object> recents =
documentsRecents.stream()
.map(
doc ->
new Object() {
public final UUID id = doc.getId();
public final String nom = doc.getNom();
public final String type = doc.getTypeDocument().toString();
public final LocalDateTime dateCreation =
doc.getDateCreation();
})
.collect(Collectors.toList());
};
public final LocalDateTime derniereMAJ = LocalDateTime.now();
})
.build();
}
// === DASHBOARDS SPÉCIALISÉS ===
@GET
@Path("/chantiers")
@Operation(
summary = "Dashboard des chantiers",
description = "Métriques détaillées des chantiers")
@APIResponse(responseCode = "200", description = "Métriques des chantiers récupérées")
public Response getDashboardChantiers() {
logger.debug("Génération du dashboard chantiers");
final Object statistiquesChantiers = chantierService.getStatistics();
// Afficher tous les chantiers actifs (EN_COURS et PLANIFIE)
final List<Chantier> chantiersEnCours = chantierService.findByStatut(StatutChantier.EN_COURS);
final List<Chantier> chantiersPlanifies = chantierService.findByStatut(StatutChantier.PLANIFIE);
final List<Chantier> chantiersActivesListe = new java.util.ArrayList<>();
chantiersActivesListe.addAll(chantiersEnCours);
chantiersActivesListe.addAll(chantiersPlanifies);
// Chantiers en retard = chantiers dont la date de fin prévue est dépassée
final List<Chantier> chantiersEnRetardListe =
chantiersActivesListe.stream()
.filter(
c -> c.getDateFinPrevue() != null && c.getDateFinPrevue().isBefore(LocalDate.now()))
.collect(Collectors.toList());
return Response.ok(
new Object() {
public final Object statistiques = statistiquesChantiers;
public final List<Object> chantiersActifs =
chantiersActivesListe.stream()
.map(
chantier ->
new Object() {
public final UUID id = chantier.getId();
public final String nom = chantier.getNom();
public final String adresse = chantier.getAdresse();
public final LocalDate dateDebut = chantier.getDateDebut();
public final LocalDate dateFinPrevue = chantier.getDateFinPrevue();
public final String statut = chantier.getStatut().toString();
public final String client =
chantier.getClient() != null
? chantier.getClient().getPrenom()
+ " "
+ chantier.getClient().getNom()
: "Non assigné";
public final double budget =
chantier.getMontantContrat().doubleValue();
public final double coutReel = chantier.getCoutReel().doubleValue();
public final int avancement =
(int) chantier.getPourcentageAvancement();
})
.collect(Collectors.toList());
public final List<Object> chantiersEnRetard =
chantiersEnRetardListe.stream()
.map(
chantier ->
new Object() {
public final UUID id = chantier.getId();
public final String nom = chantier.getNom();
public final LocalDate dateFinPrevue = chantier.getDateFinPrevue();
public final long joursRetard =
LocalDate.now().toEpochDay()
- chantier.getDateFinPrevue().toEpochDay();
})
.collect(Collectors.toList());
})
.build();
}
@GET
@Path("/maintenance")
@Operation(
summary = "Dashboard de maintenance",
description = "Métriques de maintenance du matériel")
@APIResponse(responseCode = "200", description = "Métriques de maintenance récupérées")
public Response getDashboardMaintenance() {
logger.debug("Génération du dashboard maintenance");
final Object statistiquesMaintenance = maintenanceService.getStatistics();
final List<MaintenanceMateriel> maintenancesEnRetardListe = maintenanceService.findEnRetard();
final List<MaintenanceMateriel> prochainesMaintenancesListe =
maintenanceService.findProchainesMaintenances(30);
return Response.ok(
new Object() {
public final Object statistiques = statistiquesMaintenance;
public final List<Object> maintenancesEnRetard =
maintenancesEnRetardListe.stream()
.map(
maint ->
new Object() {
public final UUID id = maint.getId();
public final String materiel = maint.getMateriel().getNom();
public final String type = maint.getType().toString();
public final LocalDate datePrevue = maint.getDatePrevue();
public final String description = maint.getDescription();
public final long joursRetard =
LocalDate.now().toEpochDay()
- maint.getDatePrevue().toEpochDay();
})
.collect(Collectors.toList());
public final List<Object> prochainesMaintenances =
prochainesMaintenancesListe.stream()
.map(
maint ->
new Object() {
public final UUID id = maint.getId();
public final String materiel = maint.getMateriel().getNom();
public final String type = maint.getType().toString();
public final LocalDate datePrevue = maint.getDatePrevue();
public final String technicien = maint.getTechnicien();
})
.collect(Collectors.toList());
})
.build();
}
@GET
@Path("/ressources")
@Operation(
summary = "Dashboard des ressources",
description = "État des ressources humaines et matérielles")
@APIResponse(responseCode = "200", description = "État des ressources récupéré")
public Response getDashboardRessources() {
logger.debug("Génération du dashboard ressources");
final Object statsEquipes = equipeService.getStatistics();
final Object statsEmployes = employeService.getStatistics();
final Object statsMateriel = materielService.getStatistics();
// Disponibilités actuelles
final List<Disponibilite> disponibilitesActuelles = disponibiliteService.findActuelles();
final List<Disponibilite> disponibilitesEnAttente = disponibiliteService.findEnAttente();
return Response.ok(
new Object() {
public final Object equipes = statsEquipes;
public final Object employes = statsEmployes;
public final Object materiel = statsMateriel;
public final Object disponibilites =
new Object() {
public final long actuelles = disponibilitesActuelles.size();
public final long enAttente = disponibilitesEnAttente.size();
public final List<Object> enAttenteDetails =
disponibilitesEnAttente.stream()
.map(
dispo ->
new Object() {
public final UUID id = dispo.getId();
public final String employe =
dispo.getEmploye().getNom()
+ " "
+ dispo.getEmploye().getPrenom();
public final String type = dispo.getType().toString();
public final LocalDateTime dateDebut = dispo.getDateDebut();
public final LocalDateTime dateFin = dispo.getDateFin();
public final String motif = dispo.getMotif();
})
.collect(Collectors.toList());
};
})
.build();
}
@GET
@Path("/planning")
@Operation(summary = "Dashboard du planning", description = "Vue d'ensemble du planning")
@APIResponse(responseCode = "200", description = "Planning récupéré")
public Response getDashboardPlanning(
@Parameter(description = "Date de référence (yyyy-mm-dd)")
@QueryParam("date")
@DefaultValue("")
String dateStr) {
logger.debug("Génération du dashboard planning");
LocalDate dateRef = dateStr.isEmpty() ? LocalDate.now() : LocalDate.parse(dateStr);
final Object planningWeek = planningService.getPlanningWeek(dateRef);
final List<Object> conflits =
planningService.detectConflicts(dateRef, dateRef.plusDays(7), null);
return Response.ok(
new Object() {
public final LocalDate dateReference = dateRef;
public final Object planningSemaine = planningWeek;
public final List<Object> conflitsDetectes = conflits;
public final boolean alerteConflits = !conflits.isEmpty();
})
.build();
}
// === MÉTRIQUES TEMPS RÉEL ===
@GET
@Path("/alertes")
@Operation(
summary = "Alertes et notifications",
description = "Alertes nécessitant une attention immédiate")
@APIResponse(responseCode = "200", description = "Alertes récupérées")
public Response getAlertes() {
logger.debug("Récupération des alertes");
// Alertes critiques
final List<MaintenanceMateriel> maintenancesEnRetardAlertes = maintenanceService.findEnRetard();
final List<Chantier> chantiersEnRetardAlertes = chantierService.findChantiersEnRetard();
final List<Disponibilite> disponibilitesEnAttenteAlertes = disponibiliteService.findEnAttente();
final List<Object> conflitsPlanifiesAlertes =
planningService.detectConflicts(LocalDate.now(), LocalDate.now().plusDays(7), null);
final int totalAlertesCalcule =
maintenancesEnRetardAlertes.size()
+ chantiersEnRetardAlertes.size()
+ disponibilitesEnAttenteAlertes.size()
+ conflitsPlanifiesAlertes.size();
return Response.ok(
new Object() {
public final int totalAlertes = totalAlertesCalcule;
public final boolean alerteCritique = totalAlertesCalcule > 0;
public final Object maintenance =
new Object() {
public final int enRetard = maintenancesEnRetardAlertes.size();
public final List<String> details =
maintenancesEnRetardAlertes.stream()
.map(m -> m.getMateriel().getNom() + " - " + m.getType())
.collect(Collectors.toList());
};
public final Object chantiers =
new Object() {
public final int enRetard = chantiersEnRetardAlertes.size();
public final List<String> details =
chantiersEnRetardAlertes.stream()
.map(Chantier::getNom)
.collect(Collectors.toList());
};
public final Object disponibilites =
new Object() {
public final int enAttente = disponibilitesEnAttenteAlertes.size();
public final List<String> details =
disponibilitesEnAttenteAlertes.stream()
.map(d -> d.getEmploye().getNom() + " - " + d.getType())
.collect(Collectors.toList());
};
public final Object planning =
new Object() {
public final int conflits = conflitsPlanifiesAlertes.size();
public final boolean alerteConflits = !conflitsPlanifiesAlertes.isEmpty();
};
})
.build();
}
@GET
@Path("/kpi")
@Operation(
summary = "Indicateurs clés de performance",
description = "KPIs principaux du système BTP")
@APIResponse(responseCode = "200", description = "KPIs récupérés")
public Response getKPI(
@Parameter(description = "Période en jours", example = "30")
@QueryParam("periode")
@DefaultValue("30")
int periode) {
logger.debug("Calcul des KPIs sur {} jours", periode);
final LocalDate dateDebutRef = LocalDate.now().minusDays(periode);
final LocalDate dateFinRef = LocalDate.now();
// KPIs calculés
final long chantiersTerminesCount = chantierService.findByStatut(StatutChantier.TERMINE).size();
final long chantiersTotalCount = chantierService.count();
final double tauxReussiteCalc =
chantiersTotalCount > 0 ? (double) chantiersTerminesCount / chantiersTotalCount * 100 : 0;
final List<MaintenanceMateriel> maintenancesTermineesListe = maintenanceService.findTerminees();
final long maintenancesTotalCount = maintenanceService.findAll().size();
final double tauxMaintenanceRealiseeCalc =
maintenancesTotalCount > 0
? (double) maintenancesTermineesListe.size() / maintenancesTotalCount * 100
: 0;
final long equipesTotalCount = equipeService.count();
final long equipesOccupeesCount = equipeService.countByStatut(StatutEquipe.OCCUPEE);
final double tauxUtilisationEquipesCalc =
equipesTotalCount > 0 ? (double) equipesOccupeesCount / equipesTotalCount * 100 : 0;
return Response.ok(
new Object() {
public final int periodeJours = periode;
public final LocalDate dateDebut = dateDebutRef;
public final LocalDate dateFin = dateFinRef;
public final Object chantiers =
new Object() {
public final double tauxReussite = Math.round(tauxReussiteCalc * 100.0) / 100.0;
public final long termines = chantiersTerminesCount;
public final long total = chantiersTotalCount;
};
public final Object maintenance =
new Object() {
public final double tauxRealisation =
Math.round(tauxMaintenanceRealiseeCalc * 100.0) / 100.0;
public final long realisees = maintenancesTermineesListe.size();
public final long total = maintenancesTotalCount;
};
public final Object equipes =
new Object() {
public final double tauxUtilisation =
Math.round(tauxUtilisationEquipesCalc * 100.0) / 100.0;
public final long occupees = equipesOccupeesCount;
public final long total = equipesTotalCount;
};
public final LocalDateTime calculeLe = LocalDateTime.now();
})
.build();
}
// === EXPORTS ET RÉSUMÉS ===
@GET
@Path("/finances")
@Operation(
summary = "Métriques financières",
description = "Calculs financiers en temps réel basés sur les chantiers")
@APIResponse(responseCode = "200", description = "Métriques financières récupérées")
public Response getDashboardFinances(
@Parameter(description = "Période en jours", example = "30")
@QueryParam("periode")
@DefaultValue("30")
int periode) {
logger.debug("Calcul des métriques financières sur {} jours", periode);
final LocalDate dateDebutRef = LocalDate.now().minusDays(periode);
final LocalDate dateFinRef = LocalDate.now();
// Récupérer tous les chantiers pour calculs financiers
final List<Chantier> tousChantiers = chantierService.findAll();
final List<Chantier> chantiersActifs = chantierService.findActifs();
final List<Chantier> chantiersTermines = chantierService.findByStatut(StatutChantier.TERMINE);
// Calculs financiers réels
final double budgetTotalCalcule =
tousChantiers.stream().mapToDouble(c -> c.getMontantContrat().doubleValue()).sum();
final double coutReelCalcule =
tousChantiers.stream().mapToDouble(c -> c.getCoutReel().doubleValue()).sum();
final double chiffreAffairesRealise =
chantiersTermines.stream().mapToDouble(c -> c.getMontantContrat().doubleValue()).sum();
// Objectif CA = somme des contrats des chantiers actifs + terminés
final double objectifCACalcule =
chantiersActifs.stream().mapToDouble(c -> c.getMontantContrat().doubleValue()).sum()
+ chiffreAffairesRealise;
final double margeGlobaleCalculee =
chiffreAffairesRealise > 0
? ((chiffreAffairesRealise - coutReelCalcule) / chiffreAffairesRealise * 100)
: 0;
// Chantiers en retard financier (dépassement budget)
final long chantiersEnRetardFinancier =
tousChantiers.stream()
.mapToLong(
c -> c.getCoutReel().doubleValue() > c.getMontantContrat().doubleValue() ? 1 : 0)
.sum();
final double tauxRentabiliteCalcule =
budgetTotalCalcule > 0
? ((budgetTotalCalcule - coutReelCalcule) / budgetTotalCalcule * 100)
: 0;
return Response.ok(
new Object() {
public final int periodeJours = periode;
public final LocalDate dateDebut = dateDebutRef;
public final LocalDate dateFin = dateFinRef;
public final Object budget =
new Object() {
public final double total = Math.round(budgetTotalCalcule * 100.0) / 100.0;
public final double realise = Math.round(coutReelCalcule * 100.0) / 100.0;
public final double reste =
Math.round((budgetTotalCalcule - coutReelCalcule) * 100.0) / 100.0;
public final double tauxConsommation =
budgetTotalCalcule > 0
? Math.round((coutReelCalcule / budgetTotalCalcule * 100) * 100.0)
/ 100.0
: 0;
};
public final Object chiffreAffaires =
new Object() {
public final double realise =
Math.round(chiffreAffairesRealise * 100.0) / 100.0;
public final double objectif = Math.round(objectifCACalcule * 100.0) / 100.0;
public final double tauxRealisation =
objectifCACalcule > 0
? Math.round((chiffreAffairesRealise / objectifCACalcule * 100) * 100.0)
/ 100.0
: 0;
};
public final Object rentabilite =
new Object() {
public final double margeGlobale =
Math.round(margeGlobaleCalculee * 100.0) / 100.0;
public final double tauxRentabilite =
Math.round(tauxRentabiliteCalcule * 100.0) / 100.0;
public final long chantiersDeficitaires = chantiersEnRetardFinancier;
public final boolean alerteRentabilite =
tauxRentabiliteCalcule < 15.0; // Seuil d'alerte à 15%
};
public final Object effectifs =
new Object() {
public final long totalEmployes = employeService.count();
public final long effectifsSurSite =
chantiersActifs.size() > 0
? Math.round(employeService.count() * 0.8)
: 0; // Estimation 80% sur site
public final double coutMainOeuvre =
Math.round(coutReelCalcule * 0.6 * 100.0)
/ 100.0; // Estimation 60% main d'oeuvre
};
public final LocalDateTime calculeLe = LocalDateTime.now();
})
.build();
}
@GET
@Path("/activites-recentes")
@Operation(
summary = "Activités récentes",
description = "Liste des dernières activités du système")
@APIResponse(responseCode = "200", description = "Activités récentes récupérées")
public Response getActivitesRecentes(
@Parameter(description = "Nombre d'activités à récupérer")
@QueryParam("limit")
@DefaultValue("10")
int limit) {
logger.debug("Récupération des {} dernières activités", limit);
final List<Object> activites = new java.util.ArrayList<>();
// Chantiers récemment créés ou modifiés
final List<Chantier> chantiersRecents = chantierService.findRecents(limit / 2);
chantiersRecents.forEach(
chantier -> {
activites.add(
new Object() {
public final String id = chantier.getId().toString();
public final String type = "CHANTIER";
public final String titre = "Chantier " + chantier.getNom();
public final String description = "Statut: " + chantier.getStatut();
public final LocalDateTime date = chantier.getDateCreation();
public final String utilisateur = "Système";
public final String statut = "INFO";
});
});
// Maintenances récentes
final List<MaintenanceMateriel> maintenancesRecentes =
maintenanceService.findRecentes(limit / 2);
maintenancesRecentes.forEach(
maintenance -> {
activites.add(
new Object() {
public final String id = maintenance.getId().toString();
public final String type = "MAINTENANCE";
public final String titre = "Maintenance " + maintenance.getMateriel().getNom();
public final String description = maintenance.getDescription();
public final LocalDateTime date = maintenance.getDateCreation();
public final String utilisateur =
maintenance.getTechnicien() != null ? maintenance.getTechnicien() : "Système";
public final String statut =
maintenance.getStatut().toString().equals("EN_RETARD") ? "ERROR" : "SUCCESS";
});
});
// Trier par date décroissante et limiter
final List<Object> activitesTries =
activites.stream()
.sorted(
(a, b) -> {
try {
LocalDateTime dateA = (LocalDateTime) a.getClass().getField("date").get(a);
LocalDateTime dateB = (LocalDateTime) b.getClass().getField("date").get(b);
return dateB.compareTo(dateA);
} catch (Exception e) {
return 0;
}
})
.limit(limit)
.collect(Collectors.toList());
return Response.ok(
new Object() {
public final List<Object> activites = activitesTries;
public final int total = activitesTries.size();
public final LocalDateTime derniereMAJ = LocalDateTime.now();
})
.build();
}
@GET
@Path("/resume-quotidien")
@Operation(summary = "Résumé quotidien", description = "Résumé de l'activité quotidienne")
@APIResponse(responseCode = "200", description = "Résumé quotidien récupéré")
public Response getResumeQuotidien() {
logger.debug("Génération du résumé quotidien");
final LocalDate aujourdhui = LocalDate.now();
final List<PlanningEvent> evenementsAujourdhui =
planningService.findEventsByDateRange(aujourdhui, aujourdhui);
final List<Disponibilite> disponibilitesActuelles = disponibiliteService.findActuelles();
final List<MaintenanceMateriel> maintenancesDuJour =
maintenanceService.findByDateRange(aujourdhui, aujourdhui);
return Response.ok(
new Object() {
public final LocalDate date = aujourdhui;
public final String jourSemaine = aujourdhui.getDayOfWeek().name();
public final Object planning =
new Object() {
public final int evenements = evenementsAujourdhui.size();
public final List<String> resume =
evenementsAujourdhui.stream()
.map(PlanningEvent::getTitre)
.collect(Collectors.toList());
};
public final Object disponibilites =
new Object() {
public final int actuelles = disponibilitesActuelles.size();
public final List<String> types =
disponibilitesActuelles.stream()
.map(d -> d.getType().toString())
.distinct()
.collect(Collectors.toList());
};
public final Object maintenance =
new Object() {
public final int prevues = maintenancesDuJour.size();
public final List<String> materiels =
maintenancesDuJour.stream()
.map(m -> m.getMateriel().getNom())
.collect(Collectors.toList());
};
public final LocalDateTime genereA = LocalDateTime.now();
})
.build();
}
}

View File

@@ -0,0 +1,316 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.DevisService;
import dev.lions.btpxpress.application.service.PdfGeneratorService;
import dev.lions.btpxpress.domain.core.entity.Devis;
import dev.lions.btpxpress.domain.core.entity.StatutDevis;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour la gestion des devis - Architecture 2025 MIGRATION: Préservation exacte de
* toutes les API endpoints et contrats
*/
@Path("/api/v1/devis")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Devis", description = "Gestion des devis")
// @Authenticated - Désactivé pour les tests
public class DevisResource {
private static final Logger logger = LoggerFactory.getLogger(DevisResource.class);
@Inject DevisService devisService;
@Inject PdfGeneratorService pdfGeneratorService;
// === ENDPOINTS DE LECTURE - API CONTRACTS PRÉSERVÉS EXACTEMENT ===
@GET
@Operation(summary = "Récupérer tous les devis")
@APIResponse(responseCode = "200", description = "Liste des devis récupérée avec succès")
public Response getAllDevis(
@Parameter(description = "Numéro de page (0-based)") @QueryParam("page") @DefaultValue("0")
int page,
@Parameter(description = "Taille de la page") @QueryParam("size") @DefaultValue("20")
int size) {
logger.debug("GET /devis - page: {}, size: {}", page, size);
List<Devis> devis;
if (page == 0 && size == 20) {
devis = devisService.findAll();
} else {
devis = devisService.findAll(page, size);
}
return Response.ok(devis).build();
}
@GET
@Path("/{id}")
@Operation(summary = "Récupérer un devis par ID")
@APIResponse(responseCode = "200", description = "Devis trouvé")
@APIResponse(responseCode = "404", description = "Devis non trouvé")
public Response getDevisById(@Parameter(description = "ID du devis") @PathParam("id") UUID id) {
logger.debug("GET /devis/{}", id);
Devis devis = devisService.findByIdRequired(id);
return Response.ok(devis).build();
}
@GET
@Path("/numero/{numero}")
@Operation(summary = "Récupérer un devis par numéro")
@APIResponse(responseCode = "200", description = "Devis trouvé")
@APIResponse(responseCode = "404", description = "Devis non trouvé")
public Response getDevisByNumero(
@Parameter(description = "Numéro du devis") @PathParam("numero") String numero) {
logger.debug("GET /devis/numero/{}", numero);
return devisService
.findByNumero(numero)
.map(devis -> Response.ok(devis).build())
.orElse(Response.status(Response.Status.NOT_FOUND).build());
}
@GET
@Path("/client/{clientId}")
@Operation(summary = "Récupérer les devis d'un client")
@APIResponse(responseCode = "200", description = "Devis du client récupérés")
public Response getDevisByClient(
@Parameter(description = "ID du client") @PathParam("clientId") UUID clientId) {
logger.debug("GET /devis/client/{}", clientId);
List<Devis> devis = devisService.findByClient(clientId);
return Response.ok(devis).build();
}
@GET
@Path("/chantier/{chantierId}")
@Operation(summary = "Récupérer les devis d'un chantier")
@APIResponse(responseCode = "200", description = "Devis du chantier récupérés")
public Response getDevisByChantier(
@Parameter(description = "ID du chantier") @PathParam("chantierId") UUID chantierId) {
logger.debug("GET /devis/chantier/{}", chantierId);
List<Devis> devis = devisService.findByChantier(chantierId);
return Response.ok(devis).build();
}
@GET
@Path("/statut/{statut}")
@Operation(summary = "Récupérer les devis par statut")
@APIResponse(responseCode = "200", description = "Devis par statut récupérés")
public Response getDevisByStatut(
@Parameter(description = "Statut du devis") @PathParam("statut") StatutDevis statut) {
logger.debug("GET /devis/statut/{}", statut);
List<Devis> devis = devisService.findByStatut(statut);
return Response.ok(devis).build();
}
@GET
@Path("/en-attente")
@Operation(summary = "Récupérer les devis en attente")
@APIResponse(responseCode = "200", description = "Devis en attente récupérés")
public Response getDevisEnAttente() {
logger.debug("GET /devis/en-attente");
List<Devis> devis = devisService.findEnAttente();
return Response.ok(devis).build();
}
@GET
@Path("/acceptes")
@Operation(summary = "Récupérer les devis acceptés")
@APIResponse(responseCode = "200", description = "Devis acceptés récupérés")
public Response getDevisAcceptes() {
logger.debug("GET /devis/acceptes");
List<Devis> devis = devisService.findAcceptes();
return Response.ok(devis).build();
}
@GET
@Path("/expiring")
@Operation(summary = "Récupérer les devis expirant bientôt")
@APIResponse(responseCode = "200", description = "Devis expirant bientôt récupérés")
public Response getDevisExpiringBefore(
@Parameter(description = "Date limite (format: YYYY-MM-DD)") @QueryParam("before")
String before) {
logger.debug("GET /devis/expiring?before={}", before);
LocalDate dateLimit = before != null ? LocalDate.parse(before) : LocalDate.now().plusDays(7);
List<Devis> devis = devisService.findExpiringBefore(dateLimit);
return Response.ok(devis).build();
}
@GET
@Path("/search")
@Operation(summary = "Rechercher des devis")
@APIResponse(responseCode = "200", description = "Résultats de recherche")
public Response searchDevis(
@Parameter(description = "Date de début d'émission (format: YYYY-MM-DD)")
@QueryParam("dateDebut")
String dateDebut,
@Parameter(description = "Date de fin d'émission (format: YYYY-MM-DD)") @QueryParam("dateFin")
String dateFin) {
logger.debug("GET /devis/search - dateDebut: {}, dateFin: {}", dateDebut, dateFin);
List<Devis> devis;
if (dateDebut != null && dateFin != null) {
LocalDate debut = LocalDate.parse(dateDebut);
LocalDate fin = LocalDate.parse(dateFin);
devis = devisService.findByDateEmission(debut, fin);
} else {
devis = devisService.findAll();
}
return Response.ok(devis).build();
}
// === ENDPOINTS D'ÉCRITURE - API CONTRACTS PRÉSERVÉS EXACTEMENT ===
@POST
@Operation(summary = "Créer un nouveau devis")
@APIResponse(responseCode = "201", description = "Devis créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response createDevis(@Valid @NotNull Devis devis) {
logger.debug("POST /devis");
Devis createdDevis = devisService.create(devis);
return Response.status(Response.Status.CREATED).entity(createdDevis).build();
}
@PUT
@Path("/{id}")
@Operation(summary = "Mettre à jour un devis")
@APIResponse(responseCode = "200", description = "Devis mis à jour avec succès")
@APIResponse(responseCode = "404", description = "Devis non trouvé")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response updateDevis(
@Parameter(description = "ID du devis") @PathParam("id") UUID id,
@Valid @NotNull Devis devis) {
logger.debug("PUT /devis/{}", id);
Devis updatedDevis = devisService.update(id, devis);
return Response.ok(updatedDevis).build();
}
@PUT
@Path("/{id}/statut")
@Operation(summary = "Mettre à jour le statut d'un devis")
@APIResponse(responseCode = "200", description = "Statut mis à jour avec succès")
@APIResponse(responseCode = "404", description = "Devis non trouvé")
@APIResponse(responseCode = "400", description = "Transition de statut invalide")
public Response updateDevisStatut(
@Parameter(description = "ID du devis") @PathParam("id") UUID id,
@Parameter(description = "Nouveau statut") @QueryParam("statut") @NotNull
StatutDevis statut) {
logger.debug("PUT /devis/{}/statut - nouveau statut: {}", id, statut);
Devis updatedDevis = devisService.updateStatut(id, statut);
return Response.ok(updatedDevis).build();
}
@PUT
@Path("/{id}/envoyer")
@Operation(summary = "Envoyer un devis")
@APIResponse(responseCode = "200", description = "Devis envoyé avec succès")
@APIResponse(responseCode = "404", description = "Devis non trouvé")
@APIResponse(responseCode = "400", description = "Devis ne peut pas être envoyé")
public Response envoyerDevis(@Parameter(description = "ID du devis") @PathParam("id") UUID id) {
logger.debug("PUT /devis/{}/envoyer", id);
Devis devisEnvoye = devisService.envoyer(id);
return Response.ok(devisEnvoye).build();
}
@DELETE
@Path("/{id}")
@Operation(summary = "Supprimer un devis")
@APIResponse(responseCode = "204", description = "Devis supprimé avec succès")
@APIResponse(responseCode = "404", description = "Devis non trouvé")
@APIResponse(responseCode = "400", description = "Devis ne peut pas être supprimé")
public Response deleteDevis(@Parameter(description = "ID du devis") @PathParam("id") UUID id) {
logger.debug("DELETE /devis/{}", id);
devisService.delete(id);
return Response.noContent().build();
}
// === ENDPOINTS STATISTIQUES - API CONTRACTS PRÉSERVÉS EXACTEMENT ===
@GET
@Path("/count")
@Operation(summary = "Compter le nombre de devis")
@APIResponse(responseCode = "200", description = "Nombre de devis")
public Response countDevis() {
logger.debug("GET /devis/count");
long count = devisService.count();
return Response.ok(count).build();
}
@GET
@Path("/count/statut/{statut}")
@Operation(summary = "Compter le nombre de devis par statut")
@APIResponse(responseCode = "200", description = "Nombre de devis par statut")
public Response countDevisByStatut(
@Parameter(description = "Statut du devis") @PathParam("statut") StatutDevis statut) {
logger.debug("GET /devis/count/statut/{}", statut);
long count = devisService.countByStatut(statut);
return Response.ok(count).build();
}
// === ENDPOINTS PDF - GÉNÉRATION DE DOCUMENTS ===
@GET
@Path("/{id}/pdf")
@Operation(summary = "Générer le PDF d'un devis")
@APIResponse(responseCode = "200", description = "PDF généré avec succès")
@APIResponse(responseCode = "404", description = "Devis non trouvé")
public Response generateDevisPdf(
@Parameter(description = "ID du devis") @PathParam("id") UUID id) {
logger.debug("GET /devis/{}/pdf", id);
Devis devis = devisService.findByIdRequired(id);
byte[] pdfContent = pdfGeneratorService.generateDevisPdf(devis);
String fileName = pdfGeneratorService.generateFileName("devis", devis.getNumero());
return Response.ok(pdfContent)
.header("Content-Type", "application/pdf")
.header("Content-Disposition", "attachment; filename=\"" + fileName + "\"")
.build();
}
}

View File

@@ -0,0 +1,436 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.DisponibiliteService;
import dev.lions.btpxpress.domain.core.entity.Disponibilite;
import dev.lions.btpxpress.domain.core.entity.TypeDisponibilite;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour la gestion des disponibilités - Architecture 2025 RH: API complète de gestion
* des disponibilités employés
*/
@Path("/api/v1/disponibilites")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Disponibilités", description = "Gestion des disponibilités et absences des employés")
public class DisponibiliteResource {
private static final Logger logger = LoggerFactory.getLogger(DisponibiliteResource.class);
@Inject DisponibiliteService disponibiliteService;
// === ENDPOINTS DE CONSULTATION ===
@GET
@Operation(
summary = "Lister toutes les disponibilités",
description =
"Récupère la liste paginée de toutes les disponibilités avec filtres optionnels")
@APIResponse(
responseCode = "200",
description = "Liste des disponibilités récupérée avec succès",
content = @Content(schema = @Schema(implementation = Disponibilite.class)))
public Response getAllDisponibilites(
@Parameter(description = "Numéro de page (0-indexé)", example = "0")
@QueryParam("page")
@DefaultValue("0")
int page,
@Parameter(description = "Taille de page", example = "20")
@QueryParam("size")
@DefaultValue("20")
int size,
@Parameter(description = "Filtrer par employé (UUID)") @QueryParam("employeId")
UUID employeId,
@Parameter(description = "Filtrer par type de disponibilité") @QueryParam("type") String type,
@Parameter(description = "Filtrer par statut d'approbation") @QueryParam("approuvee")
Boolean approuvee) {
logger.debug("Récupération des disponibilités - page: {}, taille: {}", page, size);
List<Disponibilite> disponibilites;
if (employeId != null) {
disponibilites = disponibiliteService.findByEmployeId(employeId);
} else if (type != null) {
TypeDisponibilite typeEnum = TypeDisponibilite.valueOf(type.toUpperCase());
disponibilites = disponibiliteService.findByType(typeEnum);
} else if (approuvee != null && !approuvee) {
disponibilites = disponibiliteService.findEnAttente();
} else if (approuvee != null && approuvee) {
disponibilites = disponibiliteService.findApprouvees();
} else {
disponibilites = disponibiliteService.findAll(page, size);
}
return Response.ok(disponibilites).build();
}
@GET
@Path("/{id}")
@Operation(
summary = "Récupérer une disponibilité par ID",
description = "Récupère les détails d'une disponibilité spécifique")
@APIResponse(
responseCode = "200",
description = "Disponibilité trouvée",
content = @Content(schema = @Schema(implementation = Disponibilite.class)))
@APIResponse(responseCode = "404", description = "Disponibilité non trouvée")
public Response getDisponibiliteById(
@Parameter(description = "Identifiant unique de la disponibilité", required = true)
@PathParam("id")
UUID id) {
logger.debug("Récupération de la disponibilité avec l'ID: {}", id);
return disponibiliteService
.findById(id)
.map(disponibilite -> Response.ok(disponibilite).build())
.orElse(Response.status(Response.Status.NOT_FOUND).build());
}
@GET
@Path("/actuelles")
@Operation(
summary = "Lister les disponibilités actuelles",
description = "Récupère toutes les disponibilités actuellement actives")
@APIResponse(
responseCode = "200",
description = "Disponibilités actuelles récupérées",
content = @Content(schema = @Schema(implementation = Disponibilite.class)))
public Response getDisponibilitesActuelles() {
logger.debug("Récupération des disponibilités actuelles");
List<Disponibilite> disponibilites = disponibiliteService.findActuelles();
return Response.ok(disponibilites).build();
}
@GET
@Path("/futures")
@Operation(
summary = "Lister les disponibilités futures",
description = "Récupère toutes les disponibilités programmées pour le futur")
@APIResponse(
responseCode = "200",
description = "Disponibilités futures récupérées",
content = @Content(schema = @Schema(implementation = Disponibilite.class)))
public Response getDisponibilitesFutures() {
logger.debug("Récupération des disponibilités futures");
List<Disponibilite> disponibilites = disponibiliteService.findFutures();
return Response.ok(disponibilites).build();
}
@GET
@Path("/en-attente")
@Operation(
summary = "Lister les demandes en attente",
description = "Récupère toutes les demandes de disponibilité en attente d'approbation")
@APIResponse(
responseCode = "200",
description = "Demandes en attente récupérées",
content = @Content(schema = @Schema(implementation = Disponibilite.class)))
public Response getDemandesEnAttente() {
logger.debug("Récupération des demandes en attente");
List<Disponibilite> disponibilites = disponibiliteService.findEnAttente();
return Response.ok(disponibilites).build();
}
@GET
@Path("/periode")
@Operation(
summary = "Lister les disponibilités pour une période",
description = "Récupère toutes les disponibilités dans une période donnée")
@APIResponse(
responseCode = "200",
description = "Disponibilités de la période récupérées",
content = @Content(schema = @Schema(implementation = Disponibilite.class)))
public Response getDisponibilitesPourPeriode(
@Parameter(description = "Date de début (yyyy-mm-dd)", required = true)
@QueryParam("dateDebut")
@NotNull
LocalDate dateDebut,
@Parameter(description = "Date de fin (yyyy-mm-dd)", required = true)
@QueryParam("dateFin")
@NotNull
LocalDate dateFin) {
logger.debug("Récupération des disponibilités pour la période {} - {}", dateDebut, dateFin);
List<Disponibilite> disponibilites = disponibiliteService.findPourPeriode(dateDebut, dateFin);
return Response.ok(disponibilites).build();
}
// === ENDPOINTS DE GESTION CRUD ===
@POST
@Operation(
summary = "Créer une nouvelle disponibilité",
description = "Créé une nouvelle demande de disponibilité pour un employé")
@APIResponse(
responseCode = "201",
description = "Disponibilité créée avec succès",
content = @Content(schema = @Schema(implementation = Disponibilite.class)))
@APIResponse(responseCode = "400", description = "Données invalides ou conflit détecté")
public Response createDisponibilite(@Valid @NotNull CreateDisponibiliteRequest request) {
logger.info("Création d'une nouvelle disponibilité pour l'employé: {}", request.employeId);
Disponibilite disponibilite =
disponibiliteService.createDisponibilite(
request.employeId, request.dateDebut, request.dateFin, request.type, request.motif);
return Response.status(Response.Status.CREATED).entity(disponibilite).build();
}
@PUT
@Path("/{id}")
@Operation(
summary = "Mettre à jour une disponibilité",
description = "Met à jour les informations d'une disponibilité existante")
@APIResponse(
responseCode = "200",
description = "Disponibilité mise à jour avec succès",
content = @Content(schema = @Schema(implementation = Disponibilite.class)))
@APIResponse(responseCode = "404", description = "Disponibilité non trouvée")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response updateDisponibilite(
@Parameter(description = "Identifiant de la disponibilité", required = true) @PathParam("id")
UUID id,
@Valid @NotNull UpdateDisponibiliteRequest request) {
logger.info("Mise à jour de la disponibilité: {}", id);
Disponibilite disponibilite =
disponibiliteService.updateDisponibilite(
id, request.dateDebut, request.dateFin, request.motif);
return Response.ok(disponibilite).build();
}
@POST
@Path("/{id}/approuver")
@Operation(
summary = "Approuver une demande de disponibilité",
description = "Approuve une demande de disponibilité en attente")
@APIResponse(
responseCode = "200",
description = "Disponibilité approuvée avec succès",
content = @Content(schema = @Schema(implementation = Disponibilite.class)))
@APIResponse(responseCode = "404", description = "Disponibilité non trouvée")
@APIResponse(responseCode = "400", description = "Disponibilité déjà approuvée")
public Response approuverDisponibilite(
@Parameter(description = "Identifiant de la disponibilité", required = true) @PathParam("id")
UUID id) {
logger.info("Approbation de la disponibilité: {}", id);
Disponibilite disponibilite = disponibiliteService.approuverDisponibilite(id);
return Response.ok(disponibilite).build();
}
@POST
@Path("/{id}/rejeter")
@Operation(
summary = "Rejeter une demande de disponibilité",
description = "Rejette une demande de disponibilité avec une raison")
@APIResponse(
responseCode = "200",
description = "Disponibilité rejetée avec succès",
content = @Content(schema = @Schema(implementation = Disponibilite.class)))
@APIResponse(responseCode = "404", description = "Disponibilité non trouvée")
@APIResponse(responseCode = "400", description = "Impossible de rejeter")
public Response rejeterDisponibilite(
@Parameter(description = "Identifiant de la disponibilité", required = true) @PathParam("id")
UUID id,
@Valid @NotNull RejetDisponibiliteRequest request) {
logger.info("Rejet de la disponibilité: {}", id);
Disponibilite disponibilite =
disponibiliteService.rejeterDisponibilite(id, request.raisonRejet);
return Response.ok(disponibilite).build();
}
@DELETE
@Path("/{id}")
@Operation(
summary = "Supprimer une disponibilité",
description = "Supprime définitivement une disponibilité")
@APIResponse(responseCode = "204", description = "Disponibilité supprimée avec succès")
@APIResponse(responseCode = "404", description = "Disponibilité non trouvée")
@APIResponse(responseCode = "400", description = "Impossible de supprimer")
public Response deleteDisponibilite(
@Parameter(description = "Identifiant de la disponibilité", required = true) @PathParam("id")
UUID id) {
logger.info("Suppression de la disponibilité: {}", id);
disponibiliteService.deleteDisponibilite(id);
return Response.noContent().build();
}
// === ENDPOINTS DE VALIDATION ===
@GET
@Path("/employe/{employeId}/disponible")
@Operation(
summary = "Vérifier la disponibilité d'un employé",
description = "Vérifie si un employé est disponible pour une période donnée")
@APIResponse(responseCode = "200", description = "Statut de disponibilité retourné")
public Response checkEmployeDisponibilite(
@Parameter(description = "Identifiant de l'employé", required = true) @PathParam("employeId")
UUID employeId,
@Parameter(description = "Date/heure de début", required = true)
@QueryParam("dateDebut")
@NotNull
LocalDateTime dateDebut,
@Parameter(description = "Date/heure de fin", required = true) @QueryParam("dateFin") @NotNull
LocalDateTime dateFin) {
logger.debug("Vérification de disponibilité pour l'employé {}", employeId);
boolean estDisponible = disponibiliteService.isEmployeDisponible(employeId, dateDebut, dateFin);
return Response.ok(
new Object() {
public final boolean disponible = estDisponible;
public final String message =
estDisponible
? "Employé disponible pour cette période"
: "Employé indisponible pour cette période";
})
.build();
}
@GET
@Path("/employe/{employeId}/conflits")
@Operation(
summary = "Rechercher les conflits de disponibilité",
description = "Trouve les conflits de disponibilité pour un employé et une période")
@APIResponse(
responseCode = "200",
description = "Conflits trouvés",
content = @Content(schema = @Schema(implementation = Disponibilite.class)))
public Response getConflits(
@Parameter(description = "Identifiant de l'employé", required = true) @PathParam("employeId")
UUID employeId,
@Parameter(description = "Date/heure de début", required = true)
@QueryParam("dateDebut")
@NotNull
LocalDateTime dateDebut,
@Parameter(description = "Date/heure de fin", required = true) @QueryParam("dateFin") @NotNull
LocalDateTime dateFin,
@Parameter(description = "ID à exclure de la recherche") @QueryParam("excludeId")
UUID excludeId) {
logger.debug("Recherche de conflits pour l'employé {}", employeId);
List<Disponibilite> conflits =
disponibiliteService.getConflicts(employeId, dateDebut, dateFin, excludeId);
return Response.ok(conflits).build();
}
// === ENDPOINTS STATISTIQUES ===
@GET
@Path("/statistiques")
@Operation(
summary = "Obtenir les statistiques des disponibilités",
description = "Récupère les statistiques globales des disponibilités")
@APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès")
public Response getStatistiques() {
logger.debug("Récupération des statistiques des disponibilités");
Object statistiques = disponibiliteService.getStatistics();
return Response.ok(statistiques).build();
}
@GET
@Path("/statistiques/par-type")
@Operation(
summary = "Statistiques par type de disponibilité",
description = "Récupère les statistiques détaillées par type")
@APIResponse(responseCode = "200", description = "Statistiques par type récupérées")
public Response getStatistiquesParType() {
logger.debug("Récupération des statistiques par type");
List<Object[]> stats = disponibiliteService.getStatsByType();
return Response.ok(stats).build();
}
@GET
@Path("/statistiques/par-employe")
@Operation(
summary = "Statistiques par employé",
description = "Récupère les statistiques de disponibilité par employé")
@APIResponse(responseCode = "200", description = "Statistiques par employé récupérées")
public Response getStatistiquesParEmploye() {
logger.debug("Récupération des statistiques par employé");
List<Object[]> stats = disponibiliteService.getStatsByEmployee();
return Response.ok(stats).build();
}
// === CLASSES DE REQUÊTE ===
public static class CreateDisponibiliteRequest {
@Schema(description = "Identifiant unique de l'employé", required = true)
public UUID employeId;
@Schema(
description = "Date et heure de début de la disponibilité",
required = true,
example = "2024-03-15T08:00:00")
public LocalDateTime dateDebut;
@Schema(
description = "Date et heure de fin de la disponibilité",
required = true,
example = "2024-03-20T18:00:00")
public LocalDateTime dateFin;
@Schema(
description = "Type de disponibilité",
required = true,
enumeration = {
"CONGE_PAYE",
"CONGE_SANS_SOLDE",
"ARRET_MALADIE",
"FORMATION",
"ABSENCE",
"HORAIRE_REDUIT"
})
public String type;
@Schema(description = "Motif ou raison de la disponibilité", example = "Congés annuels")
public String motif;
}
public static class UpdateDisponibiliteRequest {
@Schema(description = "Nouvelle date de début")
public LocalDateTime dateDebut;
@Schema(description = "Nouvelle date de fin")
public LocalDateTime dateFin;
@Schema(description = "Nouveau motif")
public String motif;
}
public static class RejetDisponibiliteRequest {
@Schema(description = "Raison du rejet de la demande", required = true)
public String raisonRejet;
}
}

View File

@@ -0,0 +1,500 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.DocumentService;
import dev.lions.btpxpress.domain.core.entity.Document;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.StreamingOutput;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.resteasy.reactive.PartType;
import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.multipart.FileUpload;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour la gestion des documents - Architecture 2025 DOCUMENTS: API complète de
* gestion documentaire avec upload
*/
@Path("/api/v1/documents")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Documents", description = "Gestion des documents et fichiers BTP")
public class DocumentResource {
private static final Logger logger = LoggerFactory.getLogger(DocumentResource.class);
@Inject DocumentService documentService;
// === ENDPOINTS DE CONSULTATION ===
@GET
@Operation(
summary = "Lister tous les documents",
description = "Récupère la liste paginée de tous les documents avec filtres optionnels")
@APIResponse(
responseCode = "200",
description = "Liste des documents récupérée avec succès",
content = @Content(schema = @Schema(implementation = Document.class)))
public Response getAllDocuments(
@Parameter(description = "Numéro de page (0-indexé)", example = "0")
@QueryParam("page")
@DefaultValue("0")
int page,
@Parameter(description = "Taille de page", example = "20")
@QueryParam("size")
@DefaultValue("20")
int size,
@Parameter(description = "Filtrer par type de document") @QueryParam("type") String type,
@Parameter(description = "Filtrer par chantier (UUID)") @QueryParam("chantierId")
UUID chantierId,
@Parameter(description = "Filtrer par matériel (UUID)") @QueryParam("materielId")
UUID materielId,
@Parameter(description = "Filtrer par client (UUID)") @QueryParam("clientId") UUID clientId,
@Parameter(description = "Filtrer par employé (UUID)") @QueryParam("employeId")
UUID employeId,
@Parameter(description = "Afficher seulement les documents publics") @QueryParam("public")
Boolean estPublic,
@Parameter(description = "Terme de recherche") @QueryParam("search") String search) {
logger.debug("Récupération des documents - page: {}, taille: {}", page, size);
List<Document> documents;
if (search != null || type != null || chantierId != null || materielId != null) {
documents = documentService.search(search, type, chantierId, materielId, estPublic);
} else if (chantierId != null) {
documents = documentService.findByChantier(chantierId);
} else if (materielId != null) {
documents = documentService.findByMateriel(materielId);
} else if (clientId != null) {
documents = documentService.findByClient(clientId);
} else if (employeId != null) {
documents = documentService.findByEmploye(employeId);
} else if (estPublic != null && estPublic) {
documents = documentService.findPublics();
} else {
documents = documentService.findAll(page, size);
}
return Response.ok(documents).build();
}
@GET
@Path("/{id}")
@Operation(
summary = "Récupérer un document par ID",
description = "Récupère les métadonnées d'un document spécifique")
@APIResponse(
responseCode = "200",
description = "Document trouvé",
content = @Content(schema = @Schema(implementation = Document.class)))
@APIResponse(responseCode = "404", description = "Document non trouvé")
public Response getDocumentById(
@Parameter(description = "Identifiant unique du document", required = true) @PathParam("id")
UUID id) {
logger.debug("Récupération du document avec l'ID: {}", id);
return documentService
.findById(id)
.map(document -> Response.ok(document).build())
.orElse(Response.status(Response.Status.NOT_FOUND).build());
}
@GET
@Path("/images")
@Operation(
summary = "Lister les documents images",
description = "Récupère tous les documents de type image")
@APIResponse(
responseCode = "200",
description = "Images récupérées",
content = @Content(schema = @Schema(implementation = Document.class)))
public Response getImages() {
logger.debug("Récupération des documents images");
List<Document> images = documentService.findImages();
return Response.ok(images).build();
}
@GET
@Path("/pdfs")
@Operation(summary = "Lister les documents PDF", description = "Récupère tous les documents PDF")
@APIResponse(
responseCode = "200",
description = "PDFs récupérés",
content = @Content(schema = @Schema(implementation = Document.class)))
public Response getPdfs() {
logger.debug("Récupération des documents PDF");
List<Document> pdfs = documentService.findPdfs();
return Response.ok(pdfs).build();
}
@GET
@Path("/publics")
@Operation(
summary = "Lister les documents publics",
description = "Récupère tous les documents marqués comme publics")
@APIResponse(
responseCode = "200",
description = "Documents publics récupérés",
content = @Content(schema = @Schema(implementation = Document.class)))
public Response getDocumentsPublics() {
logger.debug("Récupération des documents publics");
List<Document> documents = documentService.findPublics();
return Response.ok(documents).build();
}
@GET
@Path("/recents")
@Operation(
summary = "Lister les documents récents",
description = "Récupère les documents les plus récemment ajoutés")
@APIResponse(
responseCode = "200",
description = "Documents récents récupérés",
content = @Content(schema = @Schema(implementation = Document.class)))
public Response getDocumentsRecents(
@Parameter(description = "Nombre de documents à retourner", example = "10")
@QueryParam("limite")
@DefaultValue("10")
int limite) {
logger.debug("Récupération des {} documents les plus récents", limite);
List<Document> documents = documentService.findRecents(limite);
return Response.ok(documents).build();
}
@GET
@Path("/orphelins")
@Operation(
summary = "Lister les documents orphelins",
description = "Récupère les documents non liés à une entité spécifique")
@APIResponse(
responseCode = "200",
description = "Documents orphelins récupérés",
content = @Content(schema = @Schema(implementation = Document.class)))
public Response getDocumentsOrphelins() {
logger.debug("Récupération des documents orphelins");
List<Document> documents = documentService.findDocumentsOrphelins();
return Response.ok(documents).build();
}
// === ENDPOINTS D'UPLOAD ===
@POST
@Path("/upload")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Operation(
summary = "Uploader un nouveau document",
description = "Upload un fichier avec ses métadonnées")
@APIResponse(
responseCode = "201",
description = "Document uploadé avec succès",
content = @Content(schema = @Schema(implementation = Document.class)))
@APIResponse(responseCode = "400", description = "Données invalides ou fichier non supporté")
public Response uploadDocument(
@RestForm("nom") String nom,
@RestForm("description") String description,
@RestForm("type") String type,
@RestForm("file") FileUpload file,
@RestForm("fileName") String fileName,
@RestForm("contentType") String contentType,
@RestForm("fileSize") Long fileSize,
@RestForm("chantierId") UUID chantierId,
@RestForm("materielId") UUID materielId,
@RestForm("equipeId") UUID equipeId,
@RestForm("employeId") UUID employeId) {
logger.info("Upload de document: {}", nom);
Document document =
documentService.uploadDocument(
nom,
description,
type,
file,
fileName,
contentType,
fileSize != null ? fileSize : 0L,
chantierId,
materielId,
equipeId,
employeId,
null, // clientId - ajouté si besoin
null, // tags - ajouté si besoin
false, // estPublic - défaut
null); // userId - ajouté si besoin
return Response.status(Response.Status.CREATED).entity(document).build();
}
// === ENDPOINTS DE TÉLÉCHARGEMENT ===
@GET
@Path("/{id}/download")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
@Operation(
summary = "Télécharger un document",
description = "Télécharge le fichier physique d'un document")
@APIResponse(responseCode = "200", description = "Fichier téléchargé avec succès")
@APIResponse(responseCode = "404", description = "Document ou fichier non trouvé")
public Response downloadDocument(
@Parameter(description = "Identifiant du document", required = true) @PathParam("id")
UUID id) {
logger.debug("Téléchargement du document: {}", id);
Document document = documentService.findByIdRequired(id);
InputStream inputStream = documentService.downloadDocument(id);
StreamingOutput streamingOutput =
output -> {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
inputStream.close();
};
return Response.ok(streamingOutput)
.header("Content-Disposition", "attachment; filename=\"" + document.getNomFichier() + "\"")
.header("Content-Type", document.getTypeMime())
.build();
}
@GET
@Path("/{id}/preview")
@Operation(
summary = "Prévisualiser un document",
description = "Affiche le document dans le navigateur (pour images et PDFs)")
@APIResponse(responseCode = "200", description = "Prévisualisation disponible")
@APIResponse(responseCode = "404", description = "Document non trouvé")
public Response previewDocument(
@Parameter(description = "Identifiant du document", required = true) @PathParam("id")
UUID id) {
logger.debug("Prévisualisation du document: {}", id);
Document document = documentService.findByIdRequired(id);
// Vérifier si le document peut être prévisualisé
if (!document.isImage() && !document.isPdf()) {
return Response.status(Response.Status.NOT_ACCEPTABLE)
.entity("Ce type de document ne peut pas être prévisualisé")
.build();
}
InputStream inputStream = documentService.downloadDocument(id);
StreamingOutput streamingOutput =
output -> {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
inputStream.close();
};
return Response.ok(streamingOutput)
.header("Content-Type", document.getTypeMime())
.header("Content-Disposition", "inline; filename=\"" + document.getNomFichier() + "\"")
.build();
}
// === ENDPOINTS DE GESTION ===
@PUT
@Path("/{id}")
@Operation(
summary = "Mettre à jour un document",
description = "Met à jour les métadonnées d'un document")
@APIResponse(
responseCode = "200",
description = "Document mis à jour avec succès",
content = @Content(schema = @Schema(implementation = Document.class)))
@APIResponse(responseCode = "404", description = "Document non trouvé")
public Response updateDocument(
@Parameter(description = "Identifiant du document", required = true) @PathParam("id") UUID id,
@Valid @NotNull UpdateDocumentRequest request) {
logger.info("Mise à jour du document: {}", id);
Document document =
documentService.updateDocument(
id, request.nom, request.description, request.tags, request.estPublic);
return Response.ok(document).build();
}
@DELETE
@Path("/{id}")
@Operation(
summary = "Supprimer un document",
description = "Supprime définitivement un document et son fichier")
@APIResponse(responseCode = "204", description = "Document supprimé avec succès")
@APIResponse(responseCode = "404", description = "Document non trouvé")
public Response deleteDocument(
@Parameter(description = "Identifiant du document", required = true) @PathParam("id")
UUID id) {
logger.info("Suppression du document: {}", id);
documentService.deleteDocument(id);
return Response.noContent().build();
}
// === ENDPOINTS STATISTIQUES ===
@GET
@Path("/statistiques")
@Operation(
summary = "Obtenir les statistiques des documents",
description = "Récupère les statistiques globales des documents")
@APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès")
public Response getStatistiques() {
logger.debug("Récupération des statistiques des documents");
Object statistiques = documentService.getStatistics();
return Response.ok(statistiques).build();
}
@GET
@Path("/statistiques/par-type")
@Operation(
summary = "Statistiques par type de document",
description = "Récupère les statistiques détaillées par type")
@APIResponse(responseCode = "200", description = "Statistiques par type récupérées")
public Response getStatistiquesParType() {
logger.debug("Récupération des statistiques par type");
List<Object> stats = documentService.getStatsByType();
return Response.ok(stats).build();
}
@GET
@Path("/statistiques/par-extension")
@Operation(
summary = "Statistiques par extension de fichier",
description = "Récupère les statistiques par extension de fichier")
@APIResponse(responseCode = "200", description = "Statistiques par extension récupérées")
public Response getStatistiquesParExtension() {
logger.debug("Récupération des statistiques par extension");
List<Object> stats = documentService.getStatsByExtension();
return Response.ok(stats).build();
}
@GET
@Path("/statistiques/tendances-upload")
@Operation(
summary = "Tendances des uploads",
description = "Récupère les tendances d'upload sur plusieurs mois")
@APIResponse(responseCode = "200", description = "Tendances d'upload récupérées")
public Response getTendancesUpload(
@Parameter(description = "Nombre de mois", example = "12")
@QueryParam("mois")
@DefaultValue("12")
int mois) {
logger.debug("Récupération des tendances d'upload sur {} mois", mois);
List<Object> tendances = documentService.getUploadTrends(mois);
return Response.ok(tendances).build();
}
// === CLASSES DE REQUÊTE ===
public static class UploadDocumentForm {
@RestForm("nom")
@Schema(description = "Nom du document", required = true)
public String nom;
@RestForm("description")
@Schema(description = "Description du document")
public String description;
@RestForm("type")
@Schema(
description = "Type de document",
required = true,
enumeration = {
"PLAN",
"PERMIS_CONSTRUIRE",
"PHOTO_CHANTIER",
"CONTRAT",
"FACTURE",
"AUTRE"
})
public String type;
@RestForm("file")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
@Schema(description = "Fichier à uploader", required = true)
public InputStream file;
@RestForm("fileName")
@Schema(description = "Nom du fichier", required = true)
public String fileName;
@RestForm("contentType")
@Schema(description = "Type MIME du fichier")
public String contentType;
@RestForm("fileSize")
@Schema(description = "Taille du fichier en bytes")
public long fileSize;
@RestForm("chantierId")
@Schema(description = "ID du chantier associé")
public UUID chantierId;
@RestForm("materielId")
@Schema(description = "ID du matériel associé")
public UUID materielId;
@RestForm("employeId")
@Schema(description = "ID de l'employé associé")
public UUID employeId;
@RestForm("clientId")
@Schema(description = "ID du client associé")
public UUID clientId;
@RestForm("tags")
@Schema(description = "Tags séparés par des virgules")
public String tags;
@RestForm("estPublic")
@Schema(description = "Document public ou privé")
public Boolean estPublic;
@RestForm("userId")
@Schema(description = "ID de l'utilisateur qui upload")
public UUID userId;
}
public static class UpdateDocumentRequest {
@Schema(description = "Nouveau nom du document")
public String nom;
@Schema(description = "Nouvelle description")
public String description;
@Schema(description = "Nouveaux tags")
public String tags;
@Schema(description = "Visibilité publique")
public Boolean estPublic;
}
}

View File

@@ -0,0 +1,171 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.EmployeService;
import dev.lions.btpxpress.domain.core.entity.Employe;
import dev.lions.btpxpress.domain.core.entity.StatutEmploye;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour la gestion des employés - Architecture 2025 MIGRATION: Préservation exacte de
* tous les endpoints critiques
*/
@Path("/api/v1/employes")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Employés", description = "Gestion des employés")
public class EmployeResource {
private static final Logger logger = LoggerFactory.getLogger(EmployeResource.class);
@Inject EmployeService employeService;
// === ENDPOINTS DE CONSULTATION - API CONTRACTS PRÉSERVÉS EXACTEMENT ===
@GET
@Operation(summary = "Récupérer tous les employés")
@APIResponse(responseCode = "200", description = "Liste des employés récupérée avec succès")
public Response getAllEmployes(
@Parameter(description = "Terme de recherche") @QueryParam("search") String search,
@Parameter(description = "Statut de l'employé") @QueryParam("statut") String statut) {
try {
List<Employe> employes;
if (statut != null && !statut.isEmpty()) {
employes = employeService.findByStatut(StatutEmploye.valueOf(statut.toUpperCase()));
} else if (search != null && !search.isEmpty()) {
employes = employeService.search(search, null, null, null);
} else {
employes = employeService.findAll();
}
return Response.ok(employes).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des employés", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des employés: " + e.getMessage())
.build();
}
}
@GET
@Path("/{id}")
@Operation(summary = "Récupérer un employé par ID")
@APIResponse(responseCode = "200", description = "Employé récupéré avec succès")
@APIResponse(responseCode = "404", description = "Employé non trouvé")
public Response getEmployeById(
@Parameter(description = "ID de l'employé") @PathParam("id") String id) {
try {
UUID employeId = UUID.fromString(id);
return employeService
.findById(employeId)
.map(employe -> Response.ok(employe).build())
.orElse(
Response.status(Response.Status.NOT_FOUND)
.entity("Employé non trouvé avec l'ID: " + id)
.build());
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID d'employé invalide: " + id)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération de l'employé {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération de l'employé: " + e.getMessage())
.build();
}
}
@GET
@Path("/count")
@Operation(summary = "Compter le nombre d'employés")
@APIResponse(responseCode = "200", description = "Nombre d'employés retourné avec succès")
public Response countEmployes() {
try {
long count = employeService.count();
return Response.ok(new CountResponse(count)).build();
} catch (Exception e) {
logger.error("Erreur lors du comptage des employés", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du comptage des employés: " + e.getMessage())
.build();
}
}
@GET
@Path("/stats")
@Operation(summary = "Obtenir les statistiques des employés")
@APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès")
public Response getStats() {
try {
Object stats = employeService.getStatistics();
return Response.ok(stats).build();
} catch (Exception e) {
logger.error("Erreur lors de la génération des statistiques des employés", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la génération des statistiques: " + e.getMessage())
.build();
}
}
@GET
@Path("/disponibles")
@Operation(summary = "Récupérer les employés disponibles")
@APIResponse(
responseCode = "200",
description = "Liste des employés disponibles récupérée avec succès")
@APIResponse(responseCode = "400", description = "Paramètres de date manquants")
public Response getEmployesDisponibles(
@Parameter(description = "Date de début (YYYY-MM-DD)") @QueryParam("dateDebut")
String dateDebut,
@Parameter(description = "Date de fin (YYYY-MM-DD)") @QueryParam("dateFin") String dateFin) {
try {
if (dateDebut == null || dateFin == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Les paramètres dateDebut et dateFin sont obligatoires")
.build();
}
List<Employe> employes = employeService.findDisponibles(dateDebut, dateFin);
return Response.ok(employes).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des employés disponibles", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des employés disponibles: " + e.getMessage())
.build();
}
}
@GET
@Path("/actifs")
@Operation(summary = "Récupérer les employés actifs")
@APIResponse(
responseCode = "200",
description = "Liste des employés actifs récupérée avec succès")
public Response getEmployesActifs() {
try {
List<Employe> employes = employeService.findByStatut(StatutEmploye.ACTIF);
return Response.ok(employes).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des employés actifs", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des employés actifs: " + e.getMessage())
.build();
}
}
// ============================================
// CLASSES UTILITAIRES
// ============================================
public static record CountResponse(long count) {}
}

View File

@@ -0,0 +1,486 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.EquipeService;
import dev.lions.btpxpress.domain.core.entity.Equipe;
import dev.lions.btpxpress.domain.core.entity.StatutEquipe;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour la gestion des équipes - Architecture 2025 MÉTIER: Gestion complète des
* équipes BTP avec membres et disponibilités
*/
@Path("/api/v1/equipes")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Équipes", description = "Gestion des équipes de travail BTP")
public class EquipeResource {
private static final Logger logger = LoggerFactory.getLogger(EquipeResource.class);
@Inject EquipeService equipeService;
// === ENDPOINTS DE CONSULTATION ===
@GET
@Operation(summary = "Récupérer toutes les équipes")
@APIResponse(responseCode = "200", description = "Liste des équipes récupérée avec succès")
public Response getAllEquipes(
@Parameter(description = "Terme de recherche") @QueryParam("search") String search,
@Parameter(description = "Filtrer par statut") @QueryParam("statut") String statut,
@Parameter(description = "Filtrer par spécialité") @QueryParam("specialite")
String specialite,
@Parameter(description = "Nombre minimum de membres") @QueryParam("minMembers")
Integer minMembers,
@Parameter(description = "Nombre maximum de membres") @QueryParam("maxMembers")
Integer maxMembers,
@Parameter(description = "Numéro de page (0-indexed)") @QueryParam("page") @DefaultValue("0")
int page,
@Parameter(description = "Taille de la page") @QueryParam("size") @DefaultValue("20")
int size) {
try {
List<Equipe> equipes;
if (search != null && !search.isEmpty()) {
equipes = equipeService.search(search);
} else if (statut != null || specialite != null || minMembers != null || maxMembers != null) {
StatutEquipe statutEquipe =
statut != null ? StatutEquipe.valueOf(statut.toUpperCase()) : null;
equipes =
equipeService.findByMultipleCriteria(statutEquipe, specialite, minMembers, maxMembers);
} else {
equipes = equipeService.findAll(page, size);
}
return Response.ok(equipes).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Paramètres invalides: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des équipes", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des équipes: " + e.getMessage())
.build();
}
}
@GET
@Path("/{id}")
@Operation(summary = "Récupérer une équipe par ID")
@APIResponse(responseCode = "200", description = "Équipe récupérée avec succès")
@APIResponse(responseCode = "404", description = "Équipe non trouvée")
public Response getEquipeById(
@Parameter(description = "ID de l'équipe") @PathParam("id") String id) {
try {
UUID equipeId = UUID.fromString(id);
return equipeService
.findById(equipeId)
.map(equipe -> Response.ok(equipe).build())
.orElse(
Response.status(Response.Status.NOT_FOUND)
.entity("Équipe non trouvée avec l'ID: " + id)
.build());
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID d'équipe invalide: " + id)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération de l'équipe {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération de l'équipe: " + e.getMessage())
.build();
}
}
@GET
@Path("/count")
@Operation(summary = "Compter le nombre d'équipes")
@APIResponse(responseCode = "200", description = "Nombre d'équipes retourné avec succès")
public Response countEquipes(
@Parameter(description = "Filtrer par statut") @QueryParam("statut") String statut) {
try {
long count;
if (statut != null && !statut.isEmpty()) {
StatutEquipe statutEquipe = StatutEquipe.valueOf(statut.toUpperCase());
count = equipeService.countByStatut(statutEquipe);
} else {
count = equipeService.count();
}
return Response.ok(new CountResponse(count)).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Statut invalide: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors du comptage des équipes", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du comptage des équipes: " + e.getMessage())
.build();
}
}
@GET
@Path("/stats")
@Operation(summary = "Obtenir les statistiques des équipes")
@APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès")
public Response getStats() {
try {
Object stats = equipeService.getStatistics();
return Response.ok(stats).build();
} catch (Exception e) {
logger.error("Erreur lors de la génération des statistiques des équipes", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la génération des statistiques: " + e.getMessage())
.build();
}
}
// === ENDPOINTS DISPONIBILITÉ ===
@GET
@Path("/disponibles")
@Operation(summary = "Récupérer les équipes disponibles")
@APIResponse(
responseCode = "200",
description = "Liste des équipes disponibles récupérée avec succès")
@APIResponse(responseCode = "400", description = "Paramètres de date manquants")
public Response getEquipesDisponibles(
@Parameter(description = "Date de début (YYYY-MM-DD)") @QueryParam("dateDebut")
String dateDebut,
@Parameter(description = "Date de fin (YYYY-MM-DD)") @QueryParam("dateFin") String dateFin,
@Parameter(description = "Spécialité requise") @QueryParam("specialite") String specialite) {
try {
if (dateDebut == null || dateFin == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Les paramètres dateDebut et dateFin sont obligatoires")
.build();
}
LocalDate debut = LocalDate.parse(dateDebut);
LocalDate fin = LocalDate.parse(dateFin);
if (debut.isAfter(fin)) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("La date de début ne peut pas être après la date de fin")
.build();
}
List<Equipe> equipes = equipeService.findDisponibles(debut, fin, specialite);
return Response.ok(equipes).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Format de date invalide: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des équipes disponibles", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des équipes disponibles: " + e.getMessage())
.build();
}
}
@GET
@Path("/specialites")
@Operation(summary = "Récupérer toutes les spécialités disponibles")
@APIResponse(responseCode = "200", description = "Liste des spécialités récupérée avec succès")
public Response getAllSpecialites() {
try {
List<String> specialites = equipeService.findAllSpecialites();
return Response.ok(new SpecialitesResponse(specialites)).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des spécialités", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des spécialités: " + e.getMessage())
.build();
}
}
// === ENDPOINTS GESTION ÉQUIPES ===
@POST
@Operation(summary = "Créer une nouvelle équipe")
@APIResponse(responseCode = "201", description = "Équipe créée avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response createEquipe(
@Parameter(description = "Données de la nouvelle équipe") @Valid @NotNull
CreateEquipeRequest request) {
try {
Equipe equipe =
equipeService.createEquipe(
request.nom,
request.specialite,
request.description,
request.chefEquipeId,
request.membresIds);
return Response.status(Response.Status.CREATED).entity(equipe).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Données invalides: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la création de l'équipe", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la création de l'équipe: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}")
@Operation(summary = "Modifier une équipe")
@APIResponse(responseCode = "200", description = "Équipe modifiée avec succès")
@APIResponse(responseCode = "404", description = "Équipe non trouvée")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response updateEquipe(
@Parameter(description = "ID de l'équipe") @PathParam("id") String id,
@Parameter(description = "Nouvelles données de l'équipe") @Valid @NotNull
UpdateEquipeRequest request) {
try {
UUID equipeId = UUID.fromString(id);
Equipe equipe =
equipeService.updateEquipe(
equipeId, request.nom, request.specialite, request.description, request.chefEquipeId);
return Response.ok(equipe).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Données invalides: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la modification de l'équipe {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la modification de l'équipe: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/statut")
@Operation(summary = "Modifier le statut d'une équipe")
@APIResponse(responseCode = "200", description = "Statut modifié avec succès")
@APIResponse(responseCode = "404", description = "Équipe non trouvée")
@APIResponse(responseCode = "400", description = "Statut invalide")
public Response updateStatut(
@Parameter(description = "ID de l'équipe") @PathParam("id") String id,
@Parameter(description = "Nouveau statut") @Valid @NotNull UpdateStatutRequest request) {
try {
UUID equipeId = UUID.fromString(id);
StatutEquipe statut = StatutEquipe.valueOf(request.statut.toUpperCase());
Equipe equipe = equipeService.updateStatut(equipeId, statut);
return Response.ok(equipe).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Statut invalide: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la modification du statut de l'équipe {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la modification du statut: " + e.getMessage())
.build();
}
}
@DELETE
@Path("/{id}")
@Operation(summary = "Supprimer une équipe")
@APIResponse(responseCode = "204", description = "Équipe supprimée avec succès")
@APIResponse(responseCode = "404", description = "Équipe non trouvée")
@APIResponse(responseCode = "409", description = "Équipe en cours d'utilisation")
public Response deleteEquipe(
@Parameter(description = "ID de l'équipe") @PathParam("id") String id) {
try {
UUID equipeId = UUID.fromString(id);
equipeService.deleteEquipe(equipeId);
return Response.noContent().build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID invalide: " + e.getMessage())
.build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.CONFLICT)
.entity("Impossible de supprimer: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la suppression de l'équipe {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la suppression de l'équipe: " + e.getMessage())
.build();
}
}
// === ENDPOINTS GESTION MEMBRES ===
@GET
@Path("/{id}/members")
@Operation(summary = "Récupérer les membres d'une équipe")
@APIResponse(responseCode = "200", description = "Liste des membres récupérée avec succès")
@APIResponse(responseCode = "404", description = "Équipe non trouvée")
public Response getEquipeMembers(
@Parameter(description = "ID de l'équipe") @PathParam("id") String id) {
try {
UUID equipeId = UUID.fromString(id);
List<Object> membres = equipeService.getMembers(equipeId);
return Response.ok(new MembersResponse(membres)).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID d'équipe invalide: " + id)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des membres de l'équipe {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des membres: " + e.getMessage())
.build();
}
}
@POST
@Path("/{id}/members")
@Operation(summary = "Ajouter un membre à l'équipe")
@APIResponse(responseCode = "200", description = "Membre ajouté avec succès")
@APIResponse(responseCode = "404", description = "Équipe ou employé non trouvé")
@APIResponse(responseCode = "409", description = "Employé déjà membre d'une autre équipe")
public Response addMember(
@Parameter(description = "ID de l'équipe") @PathParam("id") String id,
@Parameter(description = "ID de l'employé à ajouter") @Valid @NotNull
AddMemberRequest request) {
try {
UUID equipeId = UUID.fromString(id);
Equipe equipe = equipeService.addMember(equipeId, request.employeId);
return Response.ok(equipe).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Données invalides: " + e.getMessage())
.build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.CONFLICT).entity("Conflit: " + e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de l'ajout du membre à l'équipe {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de l'ajout du membre: " + e.getMessage())
.build();
}
}
@DELETE
@Path("/{id}/members/{employeId}")
@Operation(summary = "Retirer un membre de l'équipe")
@APIResponse(responseCode = "200", description = "Membre retiré avec succès")
@APIResponse(responseCode = "404", description = "Équipe ou employé non trouvé")
@APIResponse(responseCode = "409", description = "Impossible de retirer le chef d'équipe")
public Response removeMember(
@Parameter(description = "ID de l'équipe") @PathParam("id") String id,
@Parameter(description = "ID de l'employé à retirer") @PathParam("employeId")
String employeId) {
try {
UUID equipeId = UUID.fromString(id);
UUID employeUUID = UUID.fromString(employeId);
Equipe equipe = equipeService.removeMember(equipeId, employeUUID);
return Response.ok(equipe).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("IDs invalides: " + e.getMessage())
.build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.CONFLICT)
.entity("Impossible de retirer: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors du retrait du membre {} de l'équipe {}", employeId, id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du retrait du membre: " + e.getMessage())
.build();
}
}
// === ENDPOINTS RECHERCHE OPTIMISÉE ===
@GET
@Path("/optimal")
@Operation(summary = "Trouver l'équipe optimale pour un chantier")
@APIResponse(responseCode = "200", description = "Équipes optimales trouvées avec succès")
@APIResponse(responseCode = "400", description = "Critères invalides")
public Response findOptimalEquipe(
@Parameter(description = "Spécialité requise") @QueryParam("specialite") String specialite,
@Parameter(description = "Nombre minimum de membres")
@QueryParam("minMembers")
@DefaultValue("1")
int minMembers,
@Parameter(description = "Date de début (YYYY-MM-DD)") @QueryParam("dateDebut")
String dateDebut,
@Parameter(description = "Date de fin (YYYY-MM-DD)") @QueryParam("dateFin") String dateFin) {
try {
if (specialite == null || dateDebut == null || dateFin == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Les paramètres spécialité, dateDebut et dateFin sont obligatoires")
.build();
}
LocalDate debut = LocalDate.parse(dateDebut);
LocalDate fin = LocalDate.parse(dateFin);
List<Equipe> equipesOptimales =
equipeService.findOptimalForChantier(specialite, minMembers, debut, fin);
return Response.ok(equipesOptimales).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Paramètres invalides: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la recherche d'équipes optimales", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la recherche: " + e.getMessage())
.build();
}
}
// === CLASSES UTILITAIRES ===
public static record CountResponse(long count) {}
public static record SpecialitesResponse(List<String> specialites) {}
public static record MembersResponse(List<Object> membres) {}
public static record CreateEquipeRequest(
@Parameter(description = "Nom de l'équipe") String nom,
@Parameter(description = "Spécialité de l'équipe") String specialite,
@Parameter(description = "Description de l'équipe") String description,
@Parameter(description = "ID du chef d'équipe") UUID chefEquipeId,
@Parameter(description = "Liste des IDs des membres") List<UUID> membresIds) {}
public static record UpdateEquipeRequest(
@Parameter(description = "Nouveau nom") String nom,
@Parameter(description = "Nouvelle spécialité") String specialite,
@Parameter(description = "Nouvelle description") String description,
@Parameter(description = "Nouvel ID du chef d'équipe") UUID chefEquipeId) {}
public static record UpdateStatutRequest(
@Parameter(description = "Nouveau statut") String statut) {}
public static record AddMemberRequest(
@Parameter(description = "ID de l'employé à ajouter") UUID employeId) {}
}

View File

@@ -0,0 +1,493 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.FactureService;
import dev.lions.btpxpress.application.service.PdfGeneratorService;
import dev.lions.btpxpress.domain.core.entity.Facture;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour la gestion des factures - Architecture 2025 MIGRATION: Préservation exacte de
* tous les endpoints critiques
*/
@Path("/api/v1/factures")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Factures", description = "Gestion des factures BTP")
// @Authenticated - Désactivé pour les tests
public class FactureResource {
private static final Logger logger = LoggerFactory.getLogger(FactureResource.class);
@Inject FactureService factureService;
@Inject PdfGeneratorService pdfGeneratorService;
// === ENDPOINTS DE CONSULTATION - API CONTRACTS PRÉSERVÉS EXACTEMENT ===
@GET
@Operation(summary = "Récupérer toutes les factures")
@APIResponse(responseCode = "200", description = "Liste des factures récupérée avec succès")
public Response getAllFactures(
@Parameter(description = "Terme de recherche") @QueryParam("search") String search,
@Parameter(description = "ID du client") @QueryParam("clientId") String clientId,
@Parameter(description = "ID du chantier") @QueryParam("chantierId") String chantierId) {
try {
List<Facture> factures;
if (clientId != null && !clientId.isEmpty()) {
factures = factureService.findByClient(UUID.fromString(clientId));
} else if (chantierId != null && !chantierId.isEmpty()) {
factures = factureService.findByChantier(UUID.fromString(chantierId));
} else if (search != null && !search.isEmpty()) {
factures = factureService.search(search);
} else {
factures = factureService.findAll();
}
return Response.ok(factures).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des factures", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des factures: " + e.getMessage())
.build();
}
}
@GET
@Path("/{id}")
@Operation(summary = "Récupérer une facture par ID")
@APIResponse(responseCode = "200", description = "Facture récupérée avec succès")
@APIResponse(responseCode = "404", description = "Facture non trouvée")
public Response getFactureById(
@Parameter(description = "ID de la facture") @PathParam("id") String id) {
try {
UUID factureId = UUID.fromString(id);
return factureService
.findById(factureId)
.map(facture -> Response.ok(facture).build())
.orElse(
Response.status(Response.Status.NOT_FOUND)
.entity("Facture non trouvée avec l'ID: " + id)
.build());
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID de facture invalide: " + id)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération de la facture {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération de la facture: " + e.getMessage())
.build();
}
}
@GET
@Path("/count")
@Operation(summary = "Compter le nombre de factures")
@APIResponse(responseCode = "200", description = "Nombre de factures retourné avec succès")
public Response countFactures() {
try {
long count = factureService.count();
return Response.ok(new CountResponse(count)).build();
} catch (Exception e) {
logger.error("Erreur lors du comptage des factures", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du comptage des factures: " + e.getMessage())
.build();
}
}
@GET
@Path("/stats")
@Operation(summary = "Obtenir les statistiques des factures")
@APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès")
public Response getStats() {
try {
Object stats = factureService.getStatistics();
return Response.ok(stats).build();
} catch (Exception e) {
logger.error("Erreur lors de la génération des statistiques des factures", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la génération des statistiques: " + e.getMessage())
.build();
}
}
@GET
@Path("/chiffre-affaires")
@Operation(summary = "Calculer le chiffre d'affaires")
@APIResponse(responseCode = "200", description = "Chiffre d'affaires calculé avec succès")
@APIResponse(responseCode = "400", description = "Format de date invalide")
public Response getChiffreAffaires(
@Parameter(description = "Date de début (YYYY-MM-DD)") @QueryParam("dateDebut")
String dateDebut,
@Parameter(description = "Date de fin (YYYY-MM-DD)") @QueryParam("dateFin") String dateFin) {
try {
BigDecimal chiffre;
if (dateDebut != null && dateFin != null) {
LocalDate debut = LocalDate.parse(dateDebut);
LocalDate fin = LocalDate.parse(dateFin);
chiffre = factureService.getChiffreAffairesParPeriode(debut, fin);
} else {
chiffre = factureService.getChiffreAffaires();
}
return Response.ok(new ChiffreAffairesResponse(chiffre)).build();
} catch (Exception e) {
logger.error("Erreur lors du calcul du chiffre d'affaires", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du calcul du chiffre d'affaires: " + e.getMessage())
.build();
}
}
@GET
@Path("/echues")
@Operation(summary = "Récupérer les factures échues")
@APIResponse(
responseCode = "200",
description = "Liste des factures échues récupérée avec succès")
public Response getFacturesEchues() {
try {
List<Facture> factures = factureService.findEchues();
return Response.ok(factures).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des factures échues", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des factures échues: " + e.getMessage())
.build();
}
}
@GET
@Path("/proches-echeance")
@Operation(summary = "Récupérer les factures proches de l'échéance")
@APIResponse(
responseCode = "200",
description = "Liste des factures proches de l'échéance récupérée avec succès")
public Response getFacturesProchesEcheance(
@Parameter(description = "Nombre de jours avant l'échéance")
@QueryParam("jours")
@DefaultValue("7")
int jours) {
try {
List<Facture> factures = factureService.findProchesEcheance(jours);
return Response.ok(factures).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des factures proches de l'échéance", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(
"Erreur lors de la récupération des factures proches de l'échéance: "
+ e.getMessage())
.build();
}
}
// ===========================================
// ENDPOINTS DE GESTION
// ===========================================
@POST
@Operation(summary = "Créer une nouvelle facture")
@APIResponse(responseCode = "201", description = "Facture créée avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response createFacture(
@Parameter(description = "Données de la facture à créer") @NotNull
CreateFactureRequest request) {
try {
Facture facture =
factureService.create(
request.numero,
request.clientId,
request.chantierId,
request.montantHT,
request.description);
return Response.status(Response.Status.CREATED).entity(facture).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Données invalides: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la création de la facture", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la création de la facture: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}")
@Operation(summary = "Mettre à jour une facture")
@APIResponse(responseCode = "200", description = "Facture mise à jour avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
@APIResponse(responseCode = "404", description = "Facture non trouvée")
public Response updateFacture(
@Parameter(description = "ID de la facture") @PathParam("id") String id,
@Parameter(description = "Données de mise à jour de la facture") @NotNull
UpdateFactureRequest request) {
try {
UUID factureId = UUID.fromString(id);
Facture facture =
factureService.update(
factureId, request.description, request.montantHT, request.dateEcheance);
return Response.ok(facture).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Données invalides: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la mise à jour de la facture {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la mise à jour de la facture: " + e.getMessage())
.build();
}
}
@DELETE
@Path("/{id}")
@Operation(summary = "Supprimer une facture")
@APIResponse(responseCode = "204", description = "Facture supprimée avec succès")
@APIResponse(responseCode = "400", description = "ID invalide")
@APIResponse(responseCode = "404", description = "Facture non trouvée")
public Response deleteFacture(
@Parameter(description = "ID de la facture") @PathParam("id") String id) {
try {
UUID factureId = UUID.fromString(id);
factureService.delete(factureId);
return Response.noContent().build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID invalide: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la suppression de la facture {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la suppression de la facture: " + e.getMessage())
.build();
}
}
// ===========================================
// ENDPOINTS DE RECHERCHE AVANCÉE
// ===========================================
@GET
@Path("/date-range")
@Operation(summary = "Récupérer les factures par plage de dates")
@APIResponse(responseCode = "200", description = "Liste des factures récupérée avec succès")
@APIResponse(responseCode = "400", description = "Paramètres de date invalides")
public Response getFacturesByDateRange(
@Parameter(description = "Date de début (YYYY-MM-DD)") @QueryParam("dateDebut")
String dateDebut,
@Parameter(description = "Date de fin (YYYY-MM-DD)") @QueryParam("dateFin") String dateFin) {
try {
if (dateDebut == null || dateFin == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Les paramètres dateDebut et dateFin sont obligatoires")
.build();
}
LocalDate debut = LocalDate.parse(dateDebut);
LocalDate fin = LocalDate.parse(dateFin);
if (debut.isAfter(fin)) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("La date de début ne peut pas être après la date de fin")
.build();
}
List<Facture> factures = factureService.findByDateRange(debut, fin);
return Response.ok(factures).build();
} catch (Exception e) {
logger.error("Erreur lors de la recherche par plage de dates", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la recherche: " + e.getMessage())
.build();
}
}
@GET
@Path("/generate-numero")
@Operation(summary = "Générer un numéro de facture")
@APIResponse(responseCode = "200", description = "Numéro généré avec succès")
public Response generateNumero() {
try {
String numero = factureService.generateNextNumero();
return Response.ok(new NumeroResponse(numero)).build();
} catch (Exception e) {
logger.error("Erreur lors de la génération du numéro de facture", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la génération du numéro: " + e.getMessage())
.build();
}
}
// ===========================================
// CLASSES UTILITAIRES
// ===========================================
public static record CountResponse(long count) {}
public static record ChiffreAffairesResponse(BigDecimal montant) {}
public static record NumeroResponse(String numero) {}
public static record CreateFactureRequest(
String numero, UUID clientId, UUID chantierId, BigDecimal montantHT, String description) {}
public static record UpdateFactureRequest(
String description, BigDecimal montantHT, LocalDate dateEcheance) {}
// === ENDPOINTS WORKFLOW ET STATUTS ===
@PUT
@Path("/{id}/statut")
@Operation(summary = "Mettre à jour le statut d'une facture")
@APIResponse(responseCode = "200", description = "Statut mis à jour avec succès")
@APIResponse(responseCode = "404", description = "Facture non trouvée")
@APIResponse(responseCode = "400", description = "Transition de statut invalide")
public Response updateFactureStatut(
@Parameter(description = "ID de la facture") @PathParam("id") UUID id,
@Parameter(description = "Nouveau statut") @QueryParam("statut") @NotNull
Facture.StatutFacture statut) {
logger.debug("PUT /factures/{}/statut - nouveau statut: {}", id, statut);
Facture updatedFacture = factureService.updateStatut(id, statut);
return Response.ok(updatedFacture).build();
}
@PUT
@Path("/{id}/payer")
@Operation(summary = "Marquer une facture comme payée")
@APIResponse(responseCode = "200", description = "Facture marquée comme payée")
@APIResponse(responseCode = "404", description = "Facture non trouvée")
@APIResponse(responseCode = "400", description = "Facture ne peut pas être marquée comme payée")
public Response marquerFacturePayee(
@Parameter(description = "ID de la facture") @PathParam("id") UUID id) {
logger.debug("PUT /factures/{}/payer", id);
Facture facturePayee = factureService.marquerPayee(id);
return Response.ok(facturePayee).build();
}
// === ENDPOINTS CONVERSION DEVIS ===
@POST
@Path("/from-devis/{devisId}")
@Operation(summary = "Créer une facture à partir d'un devis")
@APIResponse(responseCode = "201", description = "Facture créée à partir du devis")
@APIResponse(responseCode = "404", description = "Devis non trouvé")
@APIResponse(responseCode = "400", description = "Devis ne peut pas être converti")
public Response createFactureFromDevis(
@Parameter(description = "ID du devis") @PathParam("devisId") UUID devisId) {
logger.debug("POST /factures/from-devis/{}", devisId);
Facture facture = factureService.createFromDevis(devisId);
return Response.status(Response.Status.CREATED).entity(facture).build();
}
// === ENDPOINTS RECHERCHE PAR STATUT ===
@GET
@Path("/statut/{statut}")
@Operation(summary = "Récupérer les factures par statut")
@APIResponse(responseCode = "200", description = "Factures par statut récupérées")
public Response getFacturesByStatut(
@Parameter(description = "Statut des factures") @PathParam("statut")
Facture.StatutFacture statut) {
logger.debug("GET /factures/statut/{}", statut);
List<Facture> factures = factureService.findByStatut(statut);
return Response.ok(factures).build();
}
@GET
@Path("/brouillons")
@Operation(summary = "Récupérer les factures brouillons")
@APIResponse(responseCode = "200", description = "Factures brouillons récupérées")
public Response getFacturesBrouillons() {
logger.debug("GET /factures/brouillons");
List<Facture> factures = factureService.findBrouillons();
return Response.ok(factures).build();
}
@GET
@Path("/envoyees")
@Operation(summary = "Récupérer les factures envoyées")
@APIResponse(responseCode = "200", description = "Factures envoyées récupérées")
public Response getFacturesEnvoyees() {
logger.debug("GET /factures/envoyees");
List<Facture> factures = factureService.findEnvoyees();
return Response.ok(factures).build();
}
@GET
@Path("/payees")
@Operation(summary = "Récupérer les factures payées")
@APIResponse(responseCode = "200", description = "Factures payées récupérées")
public Response getFacturesPayees() {
logger.debug("GET /factures/payees");
List<Facture> factures = factureService.findPayees();
return Response.ok(factures).build();
}
@GET
@Path("/en-retard")
@Operation(summary = "Récupérer les factures en retard")
@APIResponse(responseCode = "200", description = "Factures en retard récupérées")
public Response getFacturesEnRetard() {
logger.debug("GET /factures/en-retard");
List<Facture> factures = factureService.findEnRetard();
return Response.ok(factures).build();
}
// === ENDPOINTS PDF ===
@GET
@Path("/{id}/pdf")
@Operation(summary = "Générer le PDF d'une facture")
@APIResponse(responseCode = "200", description = "PDF généré avec succès")
@APIResponse(responseCode = "404", description = "Facture non trouvée")
public Response generateFacturePdf(
@Parameter(description = "ID de la facture") @PathParam("id") UUID id) {
logger.debug("GET /factures/{}/pdf", id);
Facture facture =
factureService
.findById(id)
.orElseThrow(() -> new jakarta.ws.rs.NotFoundException("Facture non trouvée"));
byte[] pdfContent = pdfGeneratorService.generateFacturePdf(facture);
String fileName = pdfGeneratorService.generateFileName("facture", facture.getNumero());
return Response.ok(pdfContent)
.header("Content-Type", "application/pdf")
.header("Content-Disposition", "attachment; filename=\"" + fileName + "\"")
.build();
}
}

View File

@@ -0,0 +1,26 @@
package dev.lions.btpxpress.adapter.http;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.time.LocalDateTime;
import java.util.Map;
/** Endpoint léger pour les health checks frontend Optimisé pour des vérifications fréquentes */
@Path("/api/v1/health")
@Produces(MediaType.APPLICATION_JSON)
public class HealthResource {
@GET
public Response health() {
// Réponse ultra-légère pour minimiser l'impact
return Response.ok(
Map.of(
"status", "UP",
"timestamp", LocalDateTime.now().toString(),
"service", "btpxpress-server"))
.build();
}
}

View File

@@ -0,0 +1,583 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.MaintenanceService;
import dev.lions.btpxpress.domain.core.entity.MaintenanceMateriel;
import dev.lions.btpxpress.domain.core.entity.StatutMaintenance;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour la gestion des maintenances - Architecture 2025 MAINTENANCE: API complète de
* maintenance du matériel BTP
*/
@Path("/api/v1/maintenances")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Maintenances", description = "Gestion de la maintenance du matériel BTP")
public class MaintenanceResource {
private static final Logger logger = LoggerFactory.getLogger(MaintenanceResource.class);
@Inject MaintenanceService maintenanceService;
// === ENDPOINTS DE CONSULTATION ===
@GET
@Operation(
summary = "Lister toutes les maintenances",
description = "Récupère la liste paginée de toutes les maintenances avec filtres optionnels")
@APIResponse(
responseCode = "200",
description = "Liste des maintenances récupérée avec succès",
content = @Content(schema = @Schema(implementation = MaintenanceMateriel.class)))
public Response getAllMaintenances(
@Parameter(description = "Numéro de page (0-indexé)", example = "0")
@QueryParam("page")
@DefaultValue("0")
int page,
@Parameter(description = "Taille de page", example = "20")
@QueryParam("size")
@DefaultValue("20")
int size,
@Parameter(description = "Filtrer par matériel (UUID)") @QueryParam("materielId")
UUID materielId,
@Parameter(description = "Filtrer par type de maintenance") @QueryParam("type") String type,
@Parameter(description = "Filtrer par statut") @QueryParam("statut") String statut,
@Parameter(description = "Filtrer par technicien") @QueryParam("technicien")
String technicien,
@Parameter(description = "Terme de recherche") @QueryParam("search") String search) {
logger.debug("Récupération des maintenances - page: {}, taille: {}", page, size);
List<MaintenanceMateriel> maintenances;
if (search != null || type != null || statut != null || technicien != null) {
maintenances = maintenanceService.search(search, type, statut, technicien);
} else if (materielId != null) {
maintenances = maintenanceService.findByMaterielId(materielId);
} else {
maintenances = maintenanceService.findAll(page, size);
}
return Response.ok(maintenances).build();
}
@GET
@Path("/{id}")
@Operation(
summary = "Récupérer une maintenance par ID",
description = "Récupère les détails d'une maintenance spécifique")
@APIResponse(
responseCode = "200",
description = "Maintenance trouvée",
content = @Content(schema = @Schema(implementation = MaintenanceMateriel.class)))
@APIResponse(responseCode = "404", description = "Maintenance non trouvée")
public Response getMaintenanceById(
@Parameter(description = "Identifiant unique de la maintenance", required = true)
@PathParam("id")
UUID id) {
logger.debug("Récupération de la maintenance avec l'ID: {}", id);
return maintenanceService
.findById(id)
.map(maintenance -> Response.ok(maintenance).build())
.orElse(Response.status(Response.Status.NOT_FOUND).build());
}
@GET
@Path("/planifiees")
@Operation(
summary = "Lister les maintenances planifiées",
description = "Récupère toutes les maintenances planifiées")
@APIResponse(
responseCode = "200",
description = "Maintenances planifiées récupérées",
content = @Content(schema = @Schema(implementation = MaintenanceMateriel.class)))
public Response getMaintenancesPlanifiees() {
logger.debug("Récupération des maintenances planifiées");
List<MaintenanceMateriel> maintenances = maintenanceService.findPlanifiees();
return Response.ok(maintenances).build();
}
@GET
@Path("/en-cours")
@Operation(
summary = "Lister les maintenances en cours",
description = "Récupère toutes les maintenances actuellement en cours")
@APIResponse(
responseCode = "200",
description = "Maintenances en cours récupérées",
content = @Content(schema = @Schema(implementation = MaintenanceMateriel.class)))
public Response getMaintenancesEnCours() {
logger.debug("Récupération des maintenances en cours");
List<MaintenanceMateriel> maintenances = maintenanceService.findEnCours();
return Response.ok(maintenances).build();
}
@GET
@Path("/terminees")
@Operation(
summary = "Lister les maintenances terminées",
description = "Récupère toutes les maintenances terminées")
@APIResponse(
responseCode = "200",
description = "Maintenances terminées récupérées",
content = @Content(schema = @Schema(implementation = MaintenanceMateriel.class)))
public Response getMaintenancesTerminees() {
logger.debug("Récupération des maintenances terminées");
List<MaintenanceMateriel> maintenances = maintenanceService.findTerminees();
return Response.ok(maintenances).build();
}
@GET
@Path("/en-retard")
@Operation(
summary = "Lister les maintenances en retard",
description = "Récupère toutes les maintenances planifiées en retard")
@APIResponse(
responseCode = "200",
description = "Maintenances en retard récupérées",
content = @Content(schema = @Schema(implementation = MaintenanceMateriel.class)))
public Response getMaintenancesEnRetard() {
logger.debug("Récupération des maintenances en retard");
List<MaintenanceMateriel> maintenances = maintenanceService.findEnRetard();
return Response.ok(maintenances).build();
}
@GET
@Path("/prochaines")
@Operation(
summary = "Lister les prochaines maintenances",
description = "Récupère les maintenances planifiées dans les prochains jours")
@APIResponse(
responseCode = "200",
description = "Prochaines maintenances récupérées",
content = @Content(schema = @Schema(implementation = MaintenanceMateriel.class)))
public Response getProchainesMaintenances(
@Parameter(description = "Nombre de jours à venir", example = "30")
@QueryParam("jours")
@DefaultValue("30")
int jours) {
logger.debug("Récupération des prochaines maintenances dans {} jours", jours);
List<MaintenanceMateriel> maintenances = maintenanceService.findProchainesMaintenances(jours);
return Response.ok(maintenances).build();
}
@GET
@Path("/preventives")
@Operation(
summary = "Lister les maintenances préventives",
description = "Récupère toutes les maintenances préventives")
@APIResponse(
responseCode = "200",
description = "Maintenances préventives récupérées",
content = @Content(schema = @Schema(implementation = MaintenanceMateriel.class)))
public Response getMaintenancesPreventives() {
logger.debug("Récupération des maintenances préventives");
List<MaintenanceMateriel> maintenances = maintenanceService.findMaintenancesPreventives();
return Response.ok(maintenances).build();
}
@GET
@Path("/correctives")
@Operation(
summary = "Lister les maintenances correctives",
description = "Récupère toutes les maintenances correctives")
@APIResponse(
responseCode = "200",
description = "Maintenances correctives récupérées",
content = @Content(schema = @Schema(implementation = MaintenanceMateriel.class)))
public Response getMaintenancesCorrectives() {
logger.debug("Récupération des maintenances correctives");
List<MaintenanceMateriel> maintenances = maintenanceService.findMaintenancesCorrectives();
return Response.ok(maintenances).build();
}
@GET
@Path("/periode")
@Operation(
summary = "Lister les maintenances pour une période",
description = "Récupère toutes les maintenances dans une période donnée")
@APIResponse(
responseCode = "200",
description = "Maintenances de la période récupérées",
content = @Content(schema = @Schema(implementation = MaintenanceMateriel.class)))
public Response getMaintenancesPourPeriode(
@Parameter(description = "Date de début (yyyy-mm-dd)", required = true)
@QueryParam("dateDebut")
@NotNull
LocalDate dateDebut,
@Parameter(description = "Date de fin (yyyy-mm-dd)", required = true)
@QueryParam("dateFin")
@NotNull
LocalDate dateFin) {
logger.debug("Récupération des maintenances pour la période {} - {}", dateDebut, dateFin);
List<MaintenanceMateriel> maintenances = maintenanceService.findByDateRange(dateDebut, dateFin);
return Response.ok(maintenances).build();
}
// === ENDPOINTS DE GESTION CRUD ===
@POST
@Operation(
summary = "Créer une nouvelle maintenance",
description = "Créé une nouvelle maintenance pour un matériel")
@APIResponse(
responseCode = "201",
description = "Maintenance créée avec succès",
content = @Content(schema = @Schema(implementation = MaintenanceMateriel.class)))
@APIResponse(responseCode = "400", description = "Données invalides")
public Response createMaintenance(@Valid @NotNull CreateMaintenanceRequest request) {
logger.info("Création d'une nouvelle maintenance pour le matériel: {}", request.materielId);
MaintenanceMateriel maintenance =
maintenanceService.createMaintenance(
request.materielId,
request.type,
request.description,
request.datePrevue,
request.technicien,
request.notes);
return Response.status(Response.Status.CREATED).entity(maintenance).build();
}
@PUT
@Path("/{id}")
@Operation(
summary = "Mettre à jour une maintenance",
description = "Met à jour les informations d'une maintenance existante")
@APIResponse(
responseCode = "200",
description = "Maintenance mise à jour avec succès",
content = @Content(schema = @Schema(implementation = MaintenanceMateriel.class)))
@APIResponse(responseCode = "404", description = "Maintenance non trouvée")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response updateMaintenance(
@Parameter(description = "Identifiant de la maintenance", required = true) @PathParam("id")
UUID id,
@Valid @NotNull UpdateMaintenanceRequest request) {
logger.info("Mise à jour de la maintenance: {}", id);
MaintenanceMateriel maintenance =
maintenanceService.updateMaintenance(
id,
request.description,
request.datePrevue,
request.technicien,
request.notes,
request.cout);
return Response.ok(maintenance).build();
}
@PUT
@Path("/{id}/statut")
@Operation(
summary = "Mettre à jour le statut d'une maintenance",
description = "Change le statut d'une maintenance existante")
@APIResponse(
responseCode = "200",
description = "Statut mis à jour avec succès",
content = @Content(schema = @Schema(implementation = MaintenanceMateriel.class)))
@APIResponse(responseCode = "404", description = "Maintenance non trouvée")
@APIResponse(responseCode = "400", description = "Transition de statut invalide")
public Response updateStatutMaintenance(
@Parameter(description = "Identifiant de la maintenance", required = true) @PathParam("id")
UUID id,
@Valid @NotNull UpdateStatutRequest request) {
logger.info("Mise à jour du statut de la maintenance: {}", id);
StatutMaintenance statut = StatutMaintenance.valueOf(request.statut.toUpperCase());
MaintenanceMateriel maintenance = maintenanceService.updateStatut(id, statut);
return Response.ok(maintenance).build();
}
@POST
@Path("/{id}/terminer")
@Operation(
summary = "Terminer une maintenance",
description = "Marque une maintenance comme terminée avec les détails finaux")
@APIResponse(
responseCode = "200",
description = "Maintenance terminée avec succès",
content = @Content(schema = @Schema(implementation = MaintenanceMateriel.class)))
@APIResponse(responseCode = "404", description = "Maintenance non trouvée")
@APIResponse(responseCode = "400", description = "Maintenance déjà terminée")
public Response terminerMaintenance(
@Parameter(description = "Identifiant de la maintenance", required = true) @PathParam("id")
UUID id,
@Valid @NotNull TerminerMaintenanceRequest request) {
logger.info("Finalisation de la maintenance: {}", id);
MaintenanceMateriel maintenance =
maintenanceService.terminerMaintenance(
id, request.dateRealisee, request.cout, request.notes);
return Response.ok(maintenance).build();
}
@DELETE
@Path("/{id}")
@Operation(
summary = "Supprimer une maintenance",
description = "Supprime définitivement une maintenance")
@APIResponse(responseCode = "204", description = "Maintenance supprimée avec succès")
@APIResponse(responseCode = "404", description = "Maintenance non trouvée")
@APIResponse(responseCode = "400", description = "Impossible de supprimer")
public Response deleteMaintenance(
@Parameter(description = "Identifiant de la maintenance", required = true) @PathParam("id")
UUID id) {
logger.info("Suppression de la maintenance: {}", id);
maintenanceService.deleteMaintenance(id);
return Response.noContent().build();
}
// === ENDPOINTS BUSINESS ===
@GET
@Path("/attention-requise")
@Operation(
summary = "Matériel nécessitant une attention",
description = "Récupère le matériel nécessitant une attention immédiate")
@APIResponse(
responseCode = "200",
description = "Matériel nécessitant attention récupéré",
content = @Content(schema = @Schema(implementation = MaintenanceMateriel.class)))
public Response getMaterielRequiringAttention() {
logger.debug("Récupération du matériel nécessitant attention");
List<MaintenanceMateriel> maintenances = maintenanceService.getMaterielRequiringAttention();
return Response.ok(maintenances).build();
}
@GET
@Path("/materiel/{materielId}/derniere")
@Operation(
summary = "Dernière maintenance d'un matériel",
description = "Récupère la dernière maintenance effectuée sur un matériel")
@APIResponse(
responseCode = "200",
description = "Dernière maintenance trouvée",
content = @Content(schema = @Schema(implementation = MaintenanceMateriel.class)))
@APIResponse(responseCode = "404", description = "Aucune maintenance trouvée")
public Response getLastMaintenanceForMateriel(
@Parameter(description = "Identifiant du matériel", required = true) @PathParam("materielId")
UUID materielId) {
logger.debug("Récupération de la dernière maintenance pour le matériel: {}", materielId);
return maintenanceService
.getLastMaintenanceForMateriel(materielId)
.map(maintenance -> Response.ok(maintenance).build())
.orElse(Response.status(Response.Status.NOT_FOUND).build());
}
@GET
@Path("/materiel/{materielId}/cout-total")
@Operation(
summary = "Coût total de maintenance d'un matériel",
description = "Calcule le coût total de maintenance d'un matériel")
@APIResponse(responseCode = "200", description = "Coût total calculé")
public Response getCoutTotalByMateriel(
@Parameter(description = "Identifiant du matériel", required = true) @PathParam("materielId")
UUID materielId) {
logger.debug("Calcul du coût total pour le matériel: {}", materielId);
BigDecimal coutTotalCalcule = maintenanceService.getCoutTotalByMateriel(materielId);
final UUID materielIdFinal = materielId;
return Response.ok(
new Object() {
public final UUID materielId = materielIdFinal;
public final BigDecimal coutTotal = coutTotalCalcule;
})
.build();
}
@GET
@Path("/cout-total-periode")
@Operation(
summary = "Coût total de maintenance pour une période",
description = "Calcule le coût total de maintenance pour une période donnée")
@APIResponse(responseCode = "200", description = "Coût total calculé")
public Response getCoutTotalByPeriode(
@Parameter(description = "Date de début", required = true) @QueryParam("dateDebut") @NotNull
LocalDate dateDebut,
@Parameter(description = "Date de fin", required = true) @QueryParam("dateFin") @NotNull
LocalDate dateFin) {
logger.debug("Calcul du coût total pour la période {} - {}", dateDebut, dateFin);
BigDecimal coutTotalCalcule = maintenanceService.getCoutTotalByPeriode(dateDebut, dateFin);
return Response.ok(
new Object() {
public final LocalDate periodeDebut = dateDebut;
public final LocalDate periodeFin = dateFin;
public final BigDecimal coutTotal = coutTotalCalcule;
})
.build();
}
// === ENDPOINTS STATISTIQUES ===
@GET
@Path("/statistiques")
@Operation(
summary = "Obtenir les statistiques des maintenances",
description = "Récupère les statistiques globales des maintenances")
@APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès")
public Response getStatistiques() {
logger.debug("Récupération des statistiques des maintenances");
Object statistiques = maintenanceService.getStatistics();
return Response.ok(statistiques).build();
}
@GET
@Path("/statistiques/par-type")
@Operation(
summary = "Statistiques par type de maintenance",
description = "Récupère les statistiques détaillées par type")
@APIResponse(responseCode = "200", description = "Statistiques par type récupérées")
public Response getStatistiquesParType() {
logger.debug("Récupération des statistiques par type");
List<Object> stats = maintenanceService.getStatsByType();
return Response.ok(stats).build();
}
@GET
@Path("/statistiques/par-statut")
@Operation(
summary = "Statistiques par statut",
description = "Récupère les statistiques par statut de maintenance")
@APIResponse(responseCode = "200", description = "Statistiques par statut récupérées")
public Response getStatistiquesParStatut() {
logger.debug("Récupération des statistiques par statut");
List<Object> stats = maintenanceService.getStatsByStatut();
return Response.ok(stats).build();
}
@GET
@Path("/statistiques/par-technicien")
@Operation(
summary = "Statistiques par technicien",
description = "Récupère les statistiques de maintenance par technicien")
@APIResponse(responseCode = "200", description = "Statistiques par technicien récupérées")
public Response getStatistiquesParTechnicien() {
logger.debug("Récupération des statistiques par technicien");
List<Object> stats = maintenanceService.getStatsByTechnicien();
return Response.ok(stats).build();
}
@GET
@Path("/statistiques/tendances-cout")
@Operation(
summary = "Tendances des coûts de maintenance",
description = "Récupère les tendances des coûts sur plusieurs mois")
@APIResponse(responseCode = "200", description = "Tendances des coûts récupérées")
public Response getTendancesCout(
@Parameter(description = "Nombre de mois", example = "12")
@QueryParam("mois")
@DefaultValue("12")
int mois) {
logger.debug("Récupération des tendances de coût sur {} mois", mois);
List<Object> tendances = maintenanceService.getCostTrends(mois);
return Response.ok(tendances).build();
}
// === CLASSES DE REQUÊTE ===
public static class CreateMaintenanceRequest {
@Schema(description = "Identifiant unique du matériel", required = true)
public UUID materielId;
@Schema(
description = "Type de maintenance",
required = true,
enumeration = {"PREVENTIVE", "CORRECTIVE", "REVISION", "CONTROLE_TECHNIQUE", "NETTOYAGE"})
public String type;
@Schema(
description = "Description détaillée de la maintenance",
required = true,
example = "Révision moteur et changement d'huile")
public String description;
@Schema(
description = "Date prévue pour la maintenance",
required = true,
example = "2024-04-15")
public LocalDate datePrevue;
@Schema(description = "Nom du technicien responsable", example = "Jean Dupont")
public String technicien;
@Schema(description = "Notes additionnelles", example = "Prévoir pièces de rechange")
public String notes;
}
public static class UpdateMaintenanceRequest {
@Schema(description = "Nouvelle description")
public String description;
@Schema(description = "Nouvelle date prévue")
public LocalDate datePrevue;
@Schema(description = "Nouveau technicien")
public String technicien;
@Schema(description = "Nouvelles notes")
public String notes;
@Schema(description = "Coût de la maintenance", example = "150.50")
public BigDecimal cout;
}
public static class UpdateStatutRequest {
@Schema(
description = "Nouveau statut de la maintenance",
required = true,
enumeration = {"PLANIFIEE", "EN_COURS", "TERMINEE", "REPORTEE", "ANNULEE"})
public String statut;
}
public static class TerminerMaintenanceRequest {
@Schema(description = "Date de réalisation effective")
public LocalDate dateRealisee;
@Schema(description = "Coût final de la maintenance", example = "175.25")
public BigDecimal cout;
@Schema(description = "Notes finales sur la maintenance")
public String notes;
}
}

View File

@@ -0,0 +1,267 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.MaterielService;
import dev.lions.btpxpress.domain.core.entity.Materiel;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour la gestion du matériel - Architecture 2025 MIGRATION: Préservation exacte de
* toutes les API endpoints et contrats
*/
@Path("/api/v1/materiels")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Matériels", description = "Gestion des matériels et équipements")
@Authenticated
public class MaterielResource {
private static final Logger logger = LoggerFactory.getLogger(MaterielResource.class);
@Inject MaterielService materielService;
// === ENDPOINTS DE LECTURE - API CONTRACTS PRÉSERVÉS EXACTEMENT ===
@GET
@Operation(summary = "Récupérer tous les matériels")
@APIResponse(responseCode = "200", description = "Liste des matériels récupérée avec succès")
public Response getAllMateriels(
@Parameter(description = "Numéro de page (0-based)") @QueryParam("page") @DefaultValue("0")
int page,
@Parameter(description = "Taille de la page") @QueryParam("size") @DefaultValue("20")
int size) {
logger.debug("GET /materiels - page: {}, size: {}", page, size);
List<Materiel> materiels;
if (page == 0 && size == 20) {
materiels = materielService.findAll();
} else {
materiels = materielService.findAll(page, size);
}
return Response.ok(materiels).build();
}
@GET
@Path("/{id}")
@Operation(summary = "Récupérer un matériel par ID")
@APIResponse(responseCode = "200", description = "Matériel trouvé")
@APIResponse(responseCode = "404", description = "Matériel non trouvé")
public Response getMaterielById(
@Parameter(description = "ID du matériel") @PathParam("id") UUID id) {
logger.debug("GET /materiels/{}", id);
Materiel materiel = materielService.findByIdRequired(id);
return Response.ok(materiel).build();
}
@GET
@Path("/search")
@Operation(summary = "Rechercher des matériels")
@APIResponse(responseCode = "200", description = "Résultats de recherche")
public Response searchMateriels(
@Parameter(description = "Nom du matériel") @QueryParam("nom") String nom,
@Parameter(description = "Type") @QueryParam("type") String type,
@Parameter(description = "Marque") @QueryParam("marque") String marque,
@Parameter(description = "Statut") @QueryParam("statut") String statut,
@Parameter(description = "Localisation") @QueryParam("localisation") String localisation) {
logger.debug(
"GET /materiels/search - nom: {}, type: {}, marque: {}, statut: {}, localisation: {}",
nom,
type,
marque,
statut,
localisation);
List<Materiel> materiels = materielService.search(nom, type, marque, statut, localisation);
return Response.ok(materiels).build();
}
@GET
@Path("/disponibles")
@Operation(summary = "Récupérer les matériels disponibles")
@APIResponse(responseCode = "200", description = "Liste des matériels disponibles")
public Response getMaterielsDisponibles(
@Parameter(description = "Date de début (YYYY-MM-DD)") @QueryParam("dateDebut")
String dateDebut,
@Parameter(description = "Date de fin (YYYY-MM-DD)") @QueryParam("dateFin") String dateFin,
@Parameter(description = "Type de matériel") @QueryParam("type") String type) {
logger.debug(
"GET /materiels/disponibles - dateDebut: {}, dateFin: {}, type: {}",
dateDebut,
dateFin,
type);
List<Materiel> materiels = materielService.findDisponibles(dateDebut, dateFin, type);
return Response.ok(materiels).build();
}
@GET
@Path("/maintenance-prevue")
@Operation(summary = "Récupérer les matériels avec maintenance prévue")
@APIResponse(
responseCode = "200",
description = "Liste des matériels nécessitant une maintenance")
public Response getMaterielsMaintenancePrevue(
@Parameter(description = "Nombre de jours à venir") @QueryParam("jours") @DefaultValue("30")
int jours) {
logger.debug("GET /materiels/maintenance-prevue - jours: {}", jours);
List<Materiel> materiels = materielService.findAvecMaintenancePrevue(jours);
return Response.ok(materiels).build();
}
@GET
@Path("/by-type/{type}")
@Operation(summary = "Récupérer les matériels par type")
@APIResponse(responseCode = "200", description = "Liste des matériels du type spécifié")
public Response getMaterielsByType(
@Parameter(description = "Type de matériel") @PathParam("type") String type) {
logger.debug("GET /materiels/by-type/{}", type);
List<Materiel> materiels = materielService.findByType(type);
return Response.ok(materiels).build();
}
// === ENDPOINTS D'ACTIONS - API CONTRACTS PRÉSERVÉS EXACTEMENT ===
@POST
@Path("/{id}/reserve")
@Operation(summary = "Réserver un matériel")
@APIResponse(responseCode = "200", description = "Matériel réservé avec succès")
@APIResponse(responseCode = "404", description = "Matériel non trouvé")
@APIResponse(responseCode = "400", description = "Matériel non disponible")
public Response reserverMateriel(
@Parameter(description = "ID du matériel") @PathParam("id") UUID id,
@Parameter(description = "Date de début de réservation") @QueryParam("dateDebut")
String dateDebut,
@Parameter(description = "Date de fin de réservation") @QueryParam("dateFin")
String dateFin) {
logger.debug("POST /materiels/{}/reserve - dateDebut: {}, dateFin: {}", id, dateDebut, dateFin);
materielService.reserver(id, dateDebut, dateFin);
return Response.ok().build();
}
@POST
@Path("/{id}/liberer")
@Operation(summary = "Libérer un matériel")
@APIResponse(responseCode = "200", description = "Matériel libéré avec succès")
@APIResponse(responseCode = "404", description = "Matériel non trouvé")
public Response libererMateriel(
@Parameter(description = "ID du matériel") @PathParam("id") UUID id) {
logger.debug("POST /materiels/{}/liberer", id);
materielService.liberer(id);
return Response.ok().build();
}
// === ENDPOINTS CRUD - API CONTRACTS PRÉSERVÉS EXACTEMENT ===
@POST
@Operation(summary = "Créer un nouveau matériel")
@APIResponse(responseCode = "201", description = "Matériel créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response createMateriel(@Valid @NotNull Materiel materiel) {
logger.debug("POST /materiels");
logger.info(
"Création matériel: nom={}, type={}, marque={}",
materiel.getNom(),
materiel.getType(),
materiel.getMarque());
try {
Materiel createdMateriel = materielService.create(materiel);
return Response.status(Response.Status.CREATED).entity(createdMateriel).build();
} catch (Exception e) {
logger.error("Erreur lors de la création du matériel: {}", e.getMessage(), e);
throw e;
}
}
@PUT
@Path("/{id}")
@Operation(summary = "Mettre à jour un matériel")
@APIResponse(responseCode = "200", description = "Matériel mis à jour avec succès")
@APIResponse(responseCode = "404", description = "Matériel non trouvé")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response updateMateriel(
@Parameter(description = "ID du matériel") @PathParam("id") UUID id,
@Valid @NotNull Materiel materiel) {
logger.debug("PUT /materiels/{}", id);
Materiel updatedMateriel = materielService.update(id, materiel);
return Response.ok(updatedMateriel).build();
}
@DELETE
@Path("/{id}")
@Operation(summary = "Supprimer un matériel")
@APIResponse(responseCode = "204", description = "Matériel supprimé avec succès")
@APIResponse(responseCode = "404", description = "Matériel non trouvé")
public Response deleteMateriel(
@Parameter(description = "ID du matériel") @PathParam("id") UUID id) {
logger.debug("DELETE /materiels/{}", id);
materielService.delete(id);
return Response.noContent().build();
}
// === ENDPOINTS STATISTIQUES - API CONTRACTS PRÉSERVÉS EXACTEMENT ===
@GET
@Path("/count")
@Operation(summary = "Compter le nombre de matériels")
@APIResponse(responseCode = "200", description = "Nombre de matériels")
public Response countMateriels() {
logger.debug("GET /materiels/count");
long count = materielService.count();
return Response.ok(count).build();
}
@GET
@Path("/stats")
@Operation(summary = "Récupérer les statistiques des matériels")
@APIResponse(responseCode = "200", description = "Statistiques des matériels")
public Response getStats() {
logger.debug("GET /materiels/stats");
var stats = materielService.getStatistics();
return Response.ok(stats).build();
}
@GET
@Path("/valeur-totale")
@Operation(summary = "Récupérer la valeur totale du parc matériel")
@APIResponse(responseCode = "200", description = "Valeur totale du parc matériel")
public Response getValeurTotale() {
logger.debug("GET /materiels/valeur-totale");
var valeur = materielService.getValeurTotale();
return Response.ok(valeur).build();
}
}

View File

@@ -0,0 +1,418 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.MessageService;
import dev.lions.btpxpress.domain.core.entity.Message;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour la gestion des messages - Architecture 2025 COMMUNICATION: API complète pour
* la messagerie BTP
*/
@Path("/api/v1/messages")
@Tag(name = "Messages", description = "Gestion de la messagerie interne")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@RolesAllowed({"USER", "ADMIN", "MANAGER"})
public class MessageResource {
private static final Logger logger = LoggerFactory.getLogger(MessageResource.class);
@Inject MessageService messageService;
// === CONSULTATION DES MESSAGES ===
@GET
@Operation(
summary = "Obtenir tous les messages",
description = "Récupère la liste de tous les messages actifs")
@APIResponse(responseCode = "200", description = "Liste des messages récupérée")
public Response getAllMessages(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size) {
logger.info("Récupération des messages - page: {}, taille: {}", page, size);
List<Message> messages =
size > 0 ? messageService.findAll(page, size) : messageService.findAll();
return Response.ok(messages).build();
}
@GET
@Path("/{id}")
@Operation(summary = "Obtenir un message par ID", description = "Récupère un message spécifique")
@APIResponse(responseCode = "200", description = "Message trouvé")
@APIResponse(responseCode = "404", description = "Message non trouvé")
public Response getMessageById(
@PathParam("id") @NotNull @Parameter(description = "ID du message") UUID id) {
logger.info("Récupération du message: {}", id);
Message message = messageService.findByIdRequired(id);
return Response.ok(message).build();
}
@GET
@Path("/boite-reception/{userId}")
@Operation(
summary = "Obtenir la boîte de réception",
description = "Messages reçus par un utilisateur")
@APIResponse(responseCode = "200", description = "Boîte de réception récupérée")
public Response getBoiteReception(
@PathParam("userId") @NotNull @Parameter(description = "ID de l'utilisateur") UUID userId) {
logger.info("Récupération boîte de réception pour: {}", userId);
List<Message> messages = messageService.findBoiteReception(userId);
return Response.ok(messages).build();
}
@GET
@Path("/boite-envoi/{userId}")
@Operation(
summary = "Obtenir la boîte d'envoi",
description = "Messages envoyés par un utilisateur")
@APIResponse(responseCode = "200", description = "Boîte d'envoi récupérée")
public Response getBoiteEnvoi(
@PathParam("userId") @NotNull @Parameter(description = "ID de l'utilisateur") UUID userId) {
logger.info("Récupération boîte d'envoi pour: {}", userId);
List<Message> messages = messageService.findBoiteEnvoi(userId);
return Response.ok(messages).build();
}
@GET
@Path("/non-lus/{userId}")
@Operation(
summary = "Obtenir les messages non lus",
description = "Messages non lus d'un utilisateur")
@APIResponse(responseCode = "200", description = "Messages non lus récupérés")
public Response getMessagesNonLus(
@PathParam("userId") @NotNull @Parameter(description = "ID de l'utilisateur") UUID userId) {
logger.info("Récupération messages non lus pour: {}", userId);
List<Message> messages = messageService.findNonLus(userId);
return Response.ok(messages).build();
}
@GET
@Path("/importants/{userId}")
@Operation(
summary = "Obtenir les messages importants",
description = "Messages marqués comme importants")
@APIResponse(responseCode = "200", description = "Messages importants récupérés")
public Response getMessagesImportants(
@PathParam("userId") @NotNull @Parameter(description = "ID de l'utilisateur") UUID userId) {
logger.info("Récupération messages importants pour: {}", userId);
List<Message> messages = messageService.findImportants(userId);
return Response.ok(messages).build();
}
@GET
@Path("/archives/{userId}")
@Operation(
summary = "Obtenir les messages archivés",
description = "Messages archivés d'un utilisateur")
@APIResponse(responseCode = "200", description = "Messages archivés récupérés")
public Response getMessagesArchives(
@PathParam("userId") @NotNull @Parameter(description = "ID de l'utilisateur") UUID userId) {
logger.info("Récupération messages archivés pour: {}", userId);
List<Message> messages = messageService.findArchives(userId);
return Response.ok(messages).build();
}
@GET
@Path("/conversation/{user1Id}/{user2Id}")
@Operation(
summary = "Obtenir une conversation",
description = "Conversation entre deux utilisateurs")
@APIResponse(responseCode = "200", description = "Conversation récupérée")
public Response getConversation(
@PathParam("user1Id") @NotNull @Parameter(description = "ID du premier utilisateur")
UUID user1Id,
@PathParam("user2Id") @NotNull @Parameter(description = "ID du second utilisateur")
UUID user2Id) {
logger.info("Récupération conversation entre {} et {}", user1Id, user2Id);
List<Message> messages = messageService.findConversation(user1Id, user2Id);
return Response.ok(messages).build();
}
@GET
@Path("/recherche")
@Operation(
summary = "Rechercher des messages",
description = "Recherche textuelle dans les messages")
@APIResponse(responseCode = "200", description = "Résultats de recherche")
public Response rechercherMessages(
@QueryParam("terme") @NotNull @Parameter(description = "Terme de recherche") String terme,
@QueryParam("userId") @Parameter(description = "ID utilisateur pour filtrer") UUID userId) {
logger.info("Recherche de messages avec le terme: {}", terme);
List<Message> messages =
userId != null ? messageService.searchForUser(userId, terme) : messageService.search(terme);
return Response.ok(messages).build();
}
// === ENVOI ET GESTION DES MESSAGES ===
@POST
@Operation(summary = "Envoyer un message", description = "Crée et envoie un nouveau message")
@APIResponse(responseCode = "201", description = "Message envoyé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response envoyerMessage(EnvoyerMessageForm form) {
logger.info("Envoi d'un message: {}", form.sujet);
Message message =
messageService.envoyerMessage(
form.sujet,
form.contenu,
form.type,
form.priorite,
form.expediteurId,
form.destinataireId,
form.chantierId,
form.equipeId,
form.documentIds);
return Response.status(Response.Status.CREATED).entity(message).build();
}
@POST
@Path("/{messageId}/repondre")
@Operation(
summary = "Répondre à un message",
description = "Crée une réponse à un message existant")
@APIResponse(responseCode = "201", description = "Réponse envoyée avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
@APIResponse(responseCode = "404", description = "Message parent non trouvé")
public Response repondreMessage(
@PathParam("messageId") @NotNull @Parameter(description = "ID du message parent")
UUID messageId,
RepondreMessageForm form) {
logger.info("Réponse au message: {}", messageId);
Message reponse =
messageService.repondreMessage(
messageId, form.contenu, form.expediteurId, form.priorite, form.documentIds);
return Response.status(Response.Status.CREATED).entity(reponse).build();
}
@POST
@Path("/diffuser")
@Operation(
summary = "Diffuser un message",
description = "Envoie un message à plusieurs destinataires")
@APIResponse(responseCode = "201", description = "Message diffusé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response diffuserMessage(DiffuserMessageForm form) {
logger.info("Diffusion d'un message à {} destinataires", form.destinataireIds.size());
List<Message> messages =
messageService.diffuserMessage(
form.sujet,
form.contenu,
form.type,
form.priorite,
form.expediteurId,
form.destinataireIds,
form.chantierId,
form.equipeId,
form.documentIds);
return Response.status(Response.Status.CREATED).entity(messages).build();
}
// === ACTIONS SUR LES MESSAGES ===
@PUT
@Path("/{messageId}/marquer-lu/{userId}")
@Operation(summary = "Marquer comme lu", description = "Marque un message comme lu")
@APIResponse(responseCode = "200", description = "Message marqué comme lu")
@APIResponse(responseCode = "400", description = "Action non autorisée")
@APIResponse(responseCode = "404", description = "Message non trouvé")
public Response marquerCommeLu(
@PathParam("messageId") @NotNull @Parameter(description = "ID du message") UUID messageId,
@PathParam("userId") @NotNull @Parameter(description = "ID de l'utilisateur") UUID userId) {
logger.info("Marquage du message {} comme lu par {}", messageId, userId);
Message message = messageService.marquerCommeLu(messageId, userId);
return Response.ok(message).build();
}
@PUT
@Path("/marquer-tous-lus/{userId}")
@Operation(
summary = "Marquer tous comme lus",
description = "Marque tous les messages non lus comme lus")
@APIResponse(responseCode = "200", description = "Messages marqués comme lus")
public Response marquerTousCommeLus(
@PathParam("userId") @NotNull @Parameter(description = "ID de l'utilisateur") UUID userId) {
logger.info("Marquage de tous les messages comme lus pour: {}", userId);
int count = messageService.marquerTousCommeLus(userId);
return Response.ok(
new Object() {
public final String message = count + " messages marqués comme lus";
public final int nombre = count;
})
.build();
}
@PUT
@Path("/{messageId}/marquer-important/{userId}")
@Operation(summary = "Marquer comme important", description = "Marque un message comme important")
@APIResponse(responseCode = "200", description = "Message marqué comme important")
@APIResponse(responseCode = "400", description = "Action non autorisée")
@APIResponse(responseCode = "404", description = "Message non trouvé")
public Response marquerCommeImportant(
@PathParam("messageId") @NotNull @Parameter(description = "ID du message") UUID messageId,
@PathParam("userId") @NotNull @Parameter(description = "ID de l'utilisateur") UUID userId) {
logger.info("Marquage du message {} comme important par {}", messageId, userId);
Message message = messageService.marquerCommeImportant(messageId, userId);
return Response.ok(message).build();
}
@PUT
@Path("/{messageId}/archiver/{userId}")
@Operation(summary = "Archiver un message", description = "Archive un message")
@APIResponse(responseCode = "200", description = "Message archivé")
@APIResponse(responseCode = "400", description = "Action non autorisée")
@APIResponse(responseCode = "404", description = "Message non trouvé")
public Response archiverMessage(
@PathParam("messageId") @NotNull @Parameter(description = "ID du message") UUID messageId,
@PathParam("userId") @NotNull @Parameter(description = "ID de l'utilisateur") UUID userId) {
logger.info("Archivage du message {} par {}", messageId, userId);
Message message = messageService.archiverMessage(messageId, userId);
return Response.ok(message).build();
}
@DELETE
@Path("/{messageId}/{userId}")
@Operation(summary = "Supprimer un message", description = "Supprime un message (soft delete)")
@APIResponse(responseCode = "204", description = "Message supprimé")
@APIResponse(responseCode = "400", description = "Action non autorisée")
@APIResponse(responseCode = "404", description = "Message non trouvé")
public Response supprimerMessage(
@PathParam("messageId") @NotNull @Parameter(description = "ID du message") UUID messageId,
@PathParam("userId") @NotNull @Parameter(description = "ID de l'utilisateur") UUID userId) {
logger.info("Suppression du message {} par {}", messageId, userId);
messageService.supprimerMessage(messageId, userId);
return Response.noContent().build();
}
// === STATISTIQUES ET TABLEAUX DE BORD ===
@GET
@Path("/statistiques")
@Operation(
summary = "Statistiques globales",
description = "Statistiques générales de la messagerie")
@APIResponse(responseCode = "200", description = "Statistiques récupérées")
@RolesAllowed({"ADMIN", "MANAGER"})
public Response getStatistiques() {
logger.info("Génération des statistiques globales des messages");
Object stats = messageService.getStatistiques();
return Response.ok(stats).build();
}
@GET
@Path("/statistiques/{userId}")
@Operation(
summary = "Statistiques utilisateur",
description = "Statistiques de messagerie d'un utilisateur")
@APIResponse(responseCode = "200", description = "Statistiques utilisateur récupérées")
public Response getStatistiquesUser(
@PathParam("userId") @NotNull @Parameter(description = "ID de l'utilisateur") UUID userId) {
logger.info("Génération des statistiques pour l'utilisateur: {}", userId);
Object stats = messageService.getStatistiquesUser(userId);
return Response.ok(stats).build();
}
@GET
@Path("/tableau-bord/{userId}")
@Operation(
summary = "Tableau de bord utilisateur",
description = "Tableau de bord personnalisé de messagerie")
@APIResponse(responseCode = "200", description = "Tableau de bord récupéré")
public Response getTableauBordUser(
@PathParam("userId") @NotNull @Parameter(description = "ID de l'utilisateur") UUID userId) {
logger.info("Génération du tableau de bord pour l'utilisateur: {}", userId);
Object dashboard = messageService.getTableauBordUser(userId);
return Response.ok(dashboard).build();
}
// === CLASSES DE FORMULAIRES ===
public static class EnvoyerMessageForm {
public String sujet;
public String contenu;
public String type;
public String priorite;
public UUID expediteurId;
public UUID destinataireId;
public UUID chantierId;
public UUID equipeId;
public List<UUID> documentIds;
}
public static class RepondreMessageForm {
public String contenu;
public UUID expediteurId;
public String priorite;
public List<UUID> documentIds;
}
public static class DiffuserMessageForm {
public String sujet;
public String contenu;
public String type;
public String priorite;
public UUID expediteurId;
public List<UUID> destinataireIds;
public UUID chantierId;
public UUID equipeId;
public List<UUID> documentIds;
}
}

View File

@@ -0,0 +1,592 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.*;
import dev.lions.btpxpress.domain.core.entity.*;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour les notifications - Architecture 2025 COMMUNICATION: API de gestion des
* notifications BTP
*/
@Path("/api/v1/notifications")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Notifications", description = "Gestion des notifications système BTP")
public class NotificationResource {
private static final Logger logger = LoggerFactory.getLogger(NotificationResource.class);
@Inject NotificationService notificationService;
@Inject UserService userService;
@Inject ChantierService chantierService;
@Inject MaintenanceService maintenanceService;
// === CONSULTATION DES NOTIFICATIONS ===
@GET
@Operation(
summary = "Lister toutes les notifications",
description = "Récupère toutes les notifications avec pagination et filtres")
@APIResponse(
responseCode = "200",
description = "Liste des notifications récupérée",
content = @Content(schema = @Schema(implementation = Notification.class)))
public Response getAllNotifications(
@Parameter(description = "Numéro de page (0-indexé)", example = "0")
@QueryParam("page")
@DefaultValue("0")
int page,
@Parameter(description = "Taille de page", example = "20")
@QueryParam("size")
@DefaultValue("20")
int size,
@Parameter(description = "Filtrer par utilisateur (UUID)") @QueryParam("userId") UUID userId,
@Parameter(description = "Filtrer par type de notification") @QueryParam("type")
String typeStr,
@Parameter(description = "Afficher seulement les non lues")
@QueryParam("nonLues")
@DefaultValue("false")
boolean nonLues,
@Parameter(description = "Filtrer par priorité") @QueryParam("priorite") String prioriteStr) {
logger.debug("Récupération des notifications - page: {}, taille: {}", page, size);
TypeNotification type = parseTypeNotification(typeStr);
PrioriteNotification priorite = parsePrioriteNotification(prioriteStr);
List<Notification> notifications;
if (userId != null) {
if (nonLues) {
notifications = notificationService.findNonLuesByUser(userId);
} else {
notifications = notificationService.findByUser(userId);
}
} else if (type != null) {
notifications = notificationService.findByType(type);
} else if (priorite != null) {
notifications = notificationService.findByPriorite(priorite);
} else if (nonLues) {
notifications = notificationService.findNonLues();
} else {
notifications = notificationService.findAll(page, size);
}
return Response.ok(notifications).build();
}
@GET
@Path("/{id}")
@Operation(
summary = "Récupérer une notification par ID",
description = "Récupère les détails d'une notification spécifique")
@APIResponse(
responseCode = "200",
description = "Notification trouvée",
content = @Content(schema = @Schema(implementation = Notification.class)))
@APIResponse(responseCode = "404", description = "Notification non trouvée")
public Response getNotificationById(
@Parameter(description = "Identifiant unique de la notification", required = true)
@PathParam("id")
UUID id) {
logger.debug("Récupération de la notification avec l'ID: {}", id);
return notificationService
.findById(id)
.map(notification -> Response.ok(notification).build())
.orElse(Response.status(Response.Status.NOT_FOUND).build());
}
@GET
@Path("/user/{userId}")
@Operation(
summary = "Notifications d'un utilisateur",
description = "Récupère toutes les notifications d'un utilisateur spécifique")
@APIResponse(
responseCode = "200",
description = "Notifications de l'utilisateur récupérées",
content = @Content(schema = @Schema(implementation = Notification.class)))
public Response getNotificationsByUser(
@Parameter(description = "Identifiant de l'utilisateur", required = true) @PathParam("userId")
UUID userId,
@Parameter(description = "Afficher seulement les non lues")
@QueryParam("nonLues")
@DefaultValue("false")
boolean nonLues) {
logger.debug("Récupération des notifications pour l'utilisateur: {}", userId);
List<Notification> notifications;
if (nonLues) {
notifications = notificationService.findNonLuesByUser(userId);
} else {
notifications = notificationService.findByUser(userId);
}
return Response.ok(notifications).build();
}
@GET
@Path("/non-lues")
@Operation(
summary = "Notifications non lues",
description = "Récupère toutes les notifications non lues du système")
@APIResponse(
responseCode = "200",
description = "Notifications non lues récupérées",
content = @Content(schema = @Schema(implementation = Notification.class)))
public Response getNotificationsNonLues(
@Parameter(description = "Filtrer par utilisateur (UUID)") @QueryParam("userId")
UUID userId) {
logger.debug("Récupération des notifications non lues");
List<Notification> notifications;
if (userId != null) {
notifications = notificationService.findNonLuesByUser(userId);
} else {
notifications = notificationService.findNonLues();
}
return Response.ok(notifications).build();
}
@GET
@Path("/recentes")
@Operation(
summary = "Notifications récentes",
description = "Récupère les notifications les plus récentes")
@APIResponse(
responseCode = "200",
description = "Notifications récentes récupérées",
content = @Content(schema = @Schema(implementation = Notification.class)))
public Response getNotificationsRecentes(
@Parameter(description = "Nombre de notifications à retourner", example = "10")
@QueryParam("limite")
@DefaultValue("10")
int limite,
@Parameter(description = "Filtrer par utilisateur (UUID)") @QueryParam("userId")
UUID userId) {
logger.debug("Récupération des {} notifications les plus récentes", limite);
List<Notification> notifications;
if (userId != null) {
notifications = notificationService.findRecentsByUser(userId, limite);
} else {
notifications = notificationService.findRecentes(limite);
}
return Response.ok(notifications).build();
}
// === CRÉATION ET ENVOI DE NOTIFICATIONS ===
@POST
@Operation(
summary = "Créer une nouvelle notification",
description = "Crée et envoie une nouvelle notification")
@APIResponse(
responseCode = "201",
description = "Notification créée avec succès",
content = @Content(schema = @Schema(implementation = Notification.class)))
@APIResponse(responseCode = "400", description = "Données invalides")
public Response createNotification(@Valid @NotNull CreateNotificationRequest request) {
logger.info("Création d'une nouvelle notification: {}", request.titre);
Notification notification =
notificationService.createNotification(
request.titre,
request.message,
request.type,
request.priorite,
request.userId,
request.chantierId,
request.lienAction,
request.donnees);
return Response.status(Response.Status.CREATED).entity(notification).build();
}
@POST
@Path("/broadcast")
@Operation(
summary = "Diffuser une notification",
description = "Envoie une notification à tous les utilisateurs ou à un groupe spécifique")
@APIResponse(responseCode = "201", description = "Notification diffusée avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response broadcastNotification(@Valid @NotNull BroadcastNotificationRequest request) {
logger.info("Diffusion d'une notification: {}", request.titre);
List<Notification> notifications =
notificationService.broadcastNotification(
request.titre,
request.message,
request.type,
request.priorite,
request.userIds,
request.roleTarget,
request.lienAction,
request.donnees);
final int nombreNotificationsBroadcast = notifications.size();
return Response.status(Response.Status.CREATED)
.entity(
new Object() {
public final int nombreNotifications = nombreNotificationsBroadcast;
public final List<Notification> notificationsList = notifications;
})
.build();
}
@POST
@Path("/automatiques/maintenance")
@Operation(
summary = "Générer notifications de maintenance",
description = "Génère automatiquement les notifications de maintenance en retard")
@APIResponse(responseCode = "201", description = "Notifications de maintenance générées")
public Response generateMaintenanceNotifications() {
logger.info("Génération des notifications de maintenance automatiques");
List<Notification> notifications = notificationService.generateMaintenanceNotifications();
final int nombreNotificationsGenere = notifications.size();
final String messageReponse = "Notifications de maintenance générées";
final List<Object> detailsNotifications =
notifications.stream()
.map(
n ->
new Object() {
public final String titre = n.getTitre();
public final String priorite = n.getPriorite().toString();
public final String destinataire = n.getUser().getEmail();
})
.collect(Collectors.toList());
return Response.status(Response.Status.CREATED)
.entity(
new Object() {
public final int nombreNotifications = nombreNotificationsGenere;
public final String message = messageReponse;
public final List<Object> details = detailsNotifications;
})
.build();
}
@POST
@Path("/automatiques/chantiers")
@Operation(
summary = "Générer notifications de chantiers",
description =
"Génère automatiquement les notifications pour les chantiers en retard ou critiques")
@APIResponse(responseCode = "201", description = "Notifications de chantiers générées")
public Response generateChantierNotifications() {
logger.info("Génération des notifications de chantiers automatiques");
List<Notification> notifications = notificationService.generateChantierNotifications();
final int nombreNotificationsChantier = notifications.size();
final String messageChantier = "Notifications de chantiers générées";
final List<Object> detailsChantier =
notifications.stream()
.map(
n ->
new Object() {
public final String titre = n.getTitre();
public final String priorite = n.getPriorite().toString();
public final String destinataire = n.getUser().getEmail();
})
.collect(Collectors.toList());
return Response.status(Response.Status.CREATED)
.entity(
new Object() {
public final int nombreNotifications = nombreNotificationsChantier;
public final String message = messageChantier;
public final List<Object> details = detailsChantier;
})
.build();
}
// === GESTION DES NOTIFICATIONS ===
@PUT
@Path("/{id}/marquer-lue")
@Operation(
summary = "Marquer une notification comme lue",
description = "Change le statut d'une notification à 'lue'")
@APIResponse(
responseCode = "200",
description = "Notification marquée comme lue",
content = @Content(schema = @Schema(implementation = Notification.class)))
@APIResponse(responseCode = "404", description = "Notification non trouvée")
public Response marquerCommeLue(
@Parameter(description = "Identifiant de la notification", required = true) @PathParam("id")
UUID id) {
logger.info("Marquage de la notification comme lue: {}", id);
Notification notification = notificationService.marquerCommeLue(id);
return Response.ok(notification).build();
}
@PUT
@Path("/{id}/marquer-non-lue")
@Operation(
summary = "Marquer une notification comme non lue",
description = "Change le statut d'une notification à 'non lue'")
@APIResponse(
responseCode = "200",
description = "Notification marquée comme non lue",
content = @Content(schema = @Schema(implementation = Notification.class)))
@APIResponse(responseCode = "404", description = "Notification non trouvée")
public Response marquerCommeNonLue(
@Parameter(description = "Identifiant de la notification", required = true) @PathParam("id")
UUID id) {
logger.info("Marquage de la notification comme non lue: {}", id);
Notification notification = notificationService.marquerCommeNonLue(id);
return Response.ok(notification).build();
}
@PUT
@Path("/user/{userId}/marquer-toutes-lues")
@Operation(
summary = "Marquer toutes les notifications d'un utilisateur comme lues",
description = "Marque toutes les notifications non lues d'un utilisateur comme lues")
@APIResponse(responseCode = "200", description = "Toutes les notifications marquées comme lues")
public Response marquerToutesCommeLues(
@Parameter(description = "Identifiant de l'utilisateur", required = true) @PathParam("userId")
UUID userId) {
logger.info("Marquage de toutes les notifications comme lues pour l'utilisateur: {}", userId);
int nombreMises = notificationService.marquerToutesCommeLues(userId);
final int nombreMisesFinal = nombreMises;
final String messageMises = "Toutes les notifications ont été marquées comme lues";
final UUID userIdFinal = userId;
return Response.ok(
new Object() {
public final int nombreNotificationsMises = nombreMisesFinal;
public final String message = messageMises;
public final UUID userId = userIdFinal;
})
.build();
}
@DELETE
@Path("/{id}")
@Operation(
summary = "Supprimer une notification",
description = "Supprime définitivement une notification")
@APIResponse(responseCode = "204", description = "Notification supprimée avec succès")
@APIResponse(responseCode = "404", description = "Notification non trouvée")
public Response deleteNotification(
@Parameter(description = "Identifiant de la notification", required = true) @PathParam("id")
UUID id) {
logger.info("Suppression de la notification: {}", id);
notificationService.deleteNotification(id);
return Response.noContent().build();
}
@DELETE
@Path("/user/{userId}/anciennes")
@Operation(
summary = "Supprimer les anciennes notifications",
description = "Supprime les notifications anciennes d'un utilisateur (plus de X jours)")
@APIResponse(responseCode = "200", description = "Anciennes notifications supprimées")
public Response deleteAnciennesNotifications(
@Parameter(description = "Identifiant de l'utilisateur", required = true) @PathParam("userId")
UUID userId,
@Parameter(description = "Nombre de jours (défaut: 30)", example = "30")
@QueryParam("jours")
@DefaultValue("30")
int jours) {
logger.info(
"Suppression des anciennes notifications (plus de {} jours) pour l'utilisateur: {}",
jours,
userId);
int nombreSupprimees = notificationService.deleteAnciennesNotifications(userId, jours);
final int nombreSupprimeesFinal = nombreSupprimees;
final String messageSuppr = "Anciennes notifications supprimées";
final int joursLimiteFinal = jours;
return Response.ok(
new Object() {
public final int nombreNotificationsSupprimees = nombreSupprimeesFinal;
public final String message = messageSuppr;
public final int joursLimite = joursLimiteFinal;
})
.build();
}
// === STATISTIQUES ET MÉTRIQUES ===
@GET
@Path("/statistiques")
@Operation(
summary = "Statistiques des notifications",
description = "Récupère les statistiques globales des notifications")
@APIResponse(responseCode = "200", description = "Statistiques récupérées")
public Response getStatistiques(
@Parameter(description = "Filtrer par utilisateur (UUID)") @QueryParam("userId")
UUID userId) {
logger.debug("Récupération des statistiques des notifications");
Object statistiques;
if (userId != null) {
statistiques = notificationService.getStatistiquesUser(userId);
} else {
statistiques = notificationService.getStatistiques();
}
return Response.ok(statistiques).build();
}
@GET
@Path("/tableau-bord")
@Operation(
summary = "Tableau de bord des notifications",
description = "Tableau de bord complet avec métriques et alertes")
@APIResponse(responseCode = "200", description = "Tableau de bord récupéré")
public Response getTableauBord(
@Parameter(description = "Filtrer par utilisateur (UUID)") @QueryParam("userId")
UUID userId) {
logger.debug("Génération du tableau de bord des notifications");
if (userId != null) {
Object tableauBordUser = notificationService.getTableauBordUser(userId);
return Response.ok(tableauBordUser).build();
} else {
Object tableauBordGlobal = notificationService.getTableauBordGlobal();
return Response.ok(tableauBordGlobal).build();
}
}
// === MÉTHODES PRIVÉES ===
private TypeNotification parseTypeNotification(String typeStr) {
if (typeStr == null || typeStr.trim().isEmpty()) {
return null;
}
try {
return TypeNotification.valueOf(typeStr.toUpperCase());
} catch (IllegalArgumentException e) {
logger.warn("Type de notification invalide: {}", typeStr);
return null;
}
}
private PrioriteNotification parsePrioriteNotification(String prioriteStr) {
if (prioriteStr == null || prioriteStr.trim().isEmpty()) {
return null;
}
try {
return PrioriteNotification.valueOf(prioriteStr.toUpperCase());
} catch (IllegalArgumentException e) {
logger.warn("Priorité de notification invalide: {}", prioriteStr);
return null;
}
}
// === CLASSES DE REQUÊTE ===
public static class CreateNotificationRequest {
@Schema(description = "Titre de la notification", required = true)
public String titre;
@Schema(description = "Message de la notification", required = true)
public String message;
@Schema(
description = "Type de notification",
required = true,
enumeration = {"INFO", "ALERTE", "MAINTENANCE", "CHANTIER", "SYSTEM"})
public String type;
@Schema(
description = "Priorité de la notification",
enumeration = {"BASSE", "NORMALE", "HAUTE", "CRITIQUE"})
public String priorite;
@Schema(description = "ID de l'utilisateur destinataire", required = true)
public UUID userId;
@Schema(description = "ID du chantier associé (optionnel)")
public UUID chantierId;
@Schema(description = "Lien vers une action (optionnel)")
public String lienAction;
@Schema(description = "Données supplémentaires au format JSON (optionnel)")
public String donnees;
}
public static class BroadcastNotificationRequest {
@Schema(description = "Titre de la notification", required = true)
public String titre;
@Schema(description = "Message de la notification", required = true)
public String message;
@Schema(
description = "Type de notification",
required = true,
enumeration = {"INFO", "ALERTE", "MAINTENANCE", "CHANTIER", "SYSTEM"})
public String type;
@Schema(
description = "Priorité de la notification",
enumeration = {"BASSE", "NORMALE", "HAUTE", "CRITIQUE"})
public String priorite;
@Schema(description = "Liste des IDs utilisateurs destinataires (optionnel)")
public List<UUID> userIds;
@Schema(
description = "Rôle cible pour diffusion (optionnel)",
enumeration = {"ADMIN", "CHEF_CHANTIER", "EMPLOYE", "CLIENT"})
public String roleTarget;
@Schema(description = "Lien vers une action (optionnel)")
public String lienAction;
@Schema(description = "Données supplémentaires au format JSON (optionnel)")
public String donnees;
}
}

View File

@@ -0,0 +1,479 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.PhaseChantierService;
import dev.lions.btpxpress.domain.core.entity.Chantier;
import dev.lions.btpxpress.domain.core.entity.PhaseChantier;
import dev.lions.btpxpress.domain.core.entity.StatutPhaseChantier;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Resource REST pour la gestion des phases de chantier */
@Path("/api/v1/phases-chantier")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Phases de Chantier", description = "Gestion des phases de chantier BTP")
public class PhaseChantierResource {
private static final Logger logger = LoggerFactory.getLogger(PhaseChantierResource.class);
@Inject PhaseChantierService phaseChantierService;
// === ENDPOINTS DE CONSULTATION ===
@GET
@Operation(summary = "Récupérer toutes les phases")
@APIResponse(responseCode = "200", description = "Liste des phases récupérée avec succès")
public Response getAllPhases(
@Parameter(description = "Statut de la phase") @QueryParam("statut") String statut,
@Parameter(description = "Filtrer par chantiers actifs seulement (true/false)")
@QueryParam("chantiersActifs")
@DefaultValue("false")
boolean chantiersActifs) {
try {
List<PhaseChantier> phases;
if (statut != null && !statut.isEmpty()) {
phases =
phaseChantierService.findByStatut(StatutPhaseChantier.valueOf(statut.toUpperCase()));
} else if (chantiersActifs) {
phases = phaseChantierService.findAllForActiveChantiers();
logger.debug("Récupération de {} phases pour chantiers actifs uniquement", phases.size());
} else {
phases = phaseChantierService.findAll();
logger.debug("Récupération de {} phases (tous chantiers)", phases.size());
}
return Response.ok(phases).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des phases", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des phases: " + e.getMessage())
.build();
}
}
@GET
@Path("/chantier/{chantierId}")
@Operation(summary = "Récupérer les phases d'un chantier")
@APIResponse(responseCode = "200", description = "Phases du chantier récupérées avec succès")
@APIResponse(responseCode = "400", description = "ID de chantier invalide")
public Response getPhasesByChantier(
@Parameter(description = "ID du chantier") @PathParam("chantierId") String chantierId) {
try {
UUID chantierUuid = UUID.fromString(chantierId);
List<PhaseChantier> phases = phaseChantierService.findByChantier(chantierUuid);
logger.debug("Récupération de {} phases pour le chantier {}", phases.size(), chantierId);
return Response.ok(phases).build();
} catch (IllegalArgumentException e) {
logger.warn("ID de chantier invalide: {}", chantierId);
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID de chantier invalide: " + chantierId)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des phases du chantier {}", chantierId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des phases: " + e.getMessage())
.build();
}
}
@GET
@Path("/{id}")
@Operation(summary = "Récupérer une phase par ID")
@APIResponse(responseCode = "200", description = "Phase récupérée avec succès")
@APIResponse(responseCode = "404", description = "Phase non trouvée")
public Response getPhaseById(
@Parameter(description = "ID de la phase") @PathParam("id") String id) {
try {
UUID phaseId = UUID.fromString(id);
PhaseChantier phase = phaseChantierService.findById(phaseId);
return Response.ok(phase).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID de phase invalide: " + id)
.build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Phase non trouvée avec l'ID: " + id)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération de la phase {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération de la phase: " + e.getMessage())
.build();
}
}
@GET
@Path("/en-retard")
@Operation(summary = "Récupérer les phases en retard")
public Response getPhasesEnRetard() {
try {
List<PhaseChantier> phases = phaseChantierService.findPhasesEnRetard();
return Response.ok(phases).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des phases en retard", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des phases en retard: " + e.getMessage())
.build();
}
}
@GET
@Path("/en-cours")
@Operation(summary = "Récupérer les phases en cours")
public Response getPhasesEnCours() {
try {
List<PhaseChantier> phases = phaseChantierService.findPhasesEnCours();
return Response.ok(phases).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des phases en cours", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des phases en cours: " + e.getMessage())
.build();
}
}
@GET
@Path("/critiques")
@Operation(summary = "Récupérer les phases critiques")
public Response getPhasesCritiques() {
try {
List<PhaseChantier> phases = phaseChantierService.findPhasesCritiques();
return Response.ok(phases).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des phases critiques", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des phases critiques: " + e.getMessage())
.build();
}
}
@GET
@Path("/statistiques")
@Operation(summary = "Récupérer les statistiques des phases")
public Response getStatistiques() {
try {
Map<String, Object> stats = phaseChantierService.getStatistiques();
return Response.ok(stats).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des statistiques", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des statistiques: " + e.getMessage())
.build();
}
}
// === ENDPOINTS DE MODIFICATION ===
@POST
@Operation(summary = "Créer une nouvelle phase")
@APIResponse(responseCode = "201", description = "Phase créée avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response createPhase(@Valid PhaseCreateRequest request) {
try {
PhaseChantier phase = new PhaseChantier();
phase.setNom(request.nom);
phase.setDescription(request.description);
phase.setOrdreExecution(request.ordreExecution);
if (request.dateDebutPrevue != null) {
phase.setDateDebutPrevue(LocalDate.parse(request.dateDebutPrevue));
}
if (request.dateFinPrevue != null) {
phase.setDateFinPrevue(LocalDate.parse(request.dateFinPrevue));
}
if (request.budgetPrevu != null) {
phase.setBudgetPrevu(new BigDecimal(request.budgetPrevu.toString()));
}
// Associer le chantier
Chantier chantier = new Chantier();
chantier.setId(UUID.fromString(request.chantierId));
phase.setChantier(chantier);
// Associer la phase parente si elle existe (pour les sous-phases)
if (request.phaseParentId != null && !request.phaseParentId.trim().isEmpty()) {
PhaseChantier phaseParent = new PhaseChantier();
phaseParent.setId(UUID.fromString(request.phaseParentId));
phase.setPhaseParent(phaseParent);
}
phase.setBloquante(request.critique != null ? request.critique : false);
PhaseChantier savedPhase = phaseChantierService.create(phase);
return Response.status(Response.Status.CREATED).entity(savedPhase).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Données invalides: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la création de la phase", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la création de la phase: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}")
@Operation(summary = "Mettre à jour une phase")
@APIResponse(responseCode = "200", description = "Phase mise à jour avec succès")
public Response updatePhase(
@Parameter(description = "ID de la phase") @PathParam("id") String id,
@Valid PhaseCreateRequest request) {
try {
UUID phaseId = UUID.fromString(id);
PhaseChantier phaseData = new PhaseChantier();
phaseData.setNom(request.nom);
phaseData.setDescription(request.description);
phaseData.setOrdreExecution(request.ordreExecution);
if (request.dateDebutPrevue != null) {
phaseData.setDateDebutPrevue(LocalDate.parse(request.dateDebutPrevue));
}
if (request.dateFinPrevue != null) {
phaseData.setDateFinPrevue(LocalDate.parse(request.dateFinPrevue));
}
if (request.budgetPrevu != null) {
phaseData.setBudgetPrevu(new BigDecimal(request.budgetPrevu.toString()));
}
phaseData.setBloquante(request.critique != null ? request.critique : false);
PhaseChantier updatedPhase = phaseChantierService.update(phaseId, phaseData);
return Response.ok(updatedPhase).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Données invalides: " + e.getMessage())
.build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Phase non trouvée avec l'ID: " + id)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la mise à jour de la phase {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la mise à jour de la phase: " + e.getMessage())
.build();
}
}
@DELETE
@Path("/{id}")
@Operation(summary = "Supprimer une phase")
@APIResponse(responseCode = "204", description = "Phase supprimée avec succès")
public Response deletePhase(
@Parameter(description = "ID de la phase") @PathParam("id") String id) {
try {
UUID phaseId = UUID.fromString(id);
phaseChantierService.delete(phaseId);
return Response.noContent().build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID de phase invalide: " + id)
.build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Phase non trouvée avec l'ID: " + id)
.build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.CONFLICT)
.entity("Impossible de supprimer la phase: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la suppression de la phase {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la suppression de la phase: " + e.getMessage())
.build();
}
}
// === ENDPOINTS D'ACTIONS ===
@POST
@Path("/{id}/demarrer")
@Operation(summary = "Démarrer une phase")
public Response demarrerPhase(
@Parameter(description = "ID de la phase") @PathParam("id") String id) {
try {
UUID phaseId = UUID.fromString(id);
PhaseChantier phase = phaseChantierService.demarrerPhase(phaseId);
return Response.ok(phase).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID de phase invalide: " + id)
.build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Phase non trouvée avec l'ID: " + id)
.build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.CONFLICT)
.entity("Impossible de démarrer la phase: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors du démarrage de la phase {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du démarrage de la phase: " + e.getMessage())
.build();
}
}
@POST
@Path("/{id}/terminer")
@Operation(summary = "Terminer une phase")
public Response terminerPhase(
@Parameter(description = "ID de la phase") @PathParam("id") String id) {
try {
UUID phaseId = UUID.fromString(id);
PhaseChantier phase = phaseChantierService.terminerPhase(phaseId);
return Response.ok(phase).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID de phase invalide: " + id)
.build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Phase non trouvée avec l'ID: " + id)
.build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.CONFLICT)
.entity("Impossible de terminer la phase: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la finalisation de la phase {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la finalisation de la phase: " + e.getMessage())
.build();
}
}
@POST
@Path("/{id}/suspendre")
@Operation(summary = "Suspendre une phase")
public Response suspendrePhase(
@Parameter(description = "ID de la phase") @PathParam("id") String id,
SuspendrePhaseRequest request) {
try {
UUID phaseId = UUID.fromString(id);
String motif = request != null ? request.motif : null;
PhaseChantier phase = phaseChantierService.suspendrPhase(phaseId, motif);
return Response.ok(phase).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID de phase invalide: " + id)
.build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Phase non trouvée avec l'ID: " + id)
.build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.CONFLICT)
.entity("Impossible de suspendre la phase: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la suspension de la phase {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la suspension de la phase: " + e.getMessage())
.build();
}
}
@POST
@Path("/{id}/reprendre")
@Operation(summary = "Reprendre une phase suspendue")
public Response reprendrePhase(
@Parameter(description = "ID de la phase") @PathParam("id") String id) {
try {
UUID phaseId = UUID.fromString(id);
PhaseChantier phase = phaseChantierService.reprendrePhase(phaseId);
return Response.ok(phase).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID de phase invalide: " + id)
.build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Phase non trouvée avec l'ID: " + id)
.build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.CONFLICT)
.entity("Impossible de reprendre la phase: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la reprise de la phase {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la reprise de la phase: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/avancement")
@Operation(summary = "Mettre à jour l'avancement d'une phase")
public Response updateAvancement(
@Parameter(description = "ID de la phase") @PathParam("id") String id,
@NotNull AvancementRequest request) {
try {
UUID phaseId = UUID.fromString(id);
BigDecimal pourcentage = new BigDecimal(request.pourcentage.toString());
PhaseChantier phase = phaseChantierService.updateAvancement(phaseId, pourcentage);
return Response.ok(phase).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Données invalides: " + e.getMessage())
.build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Phase non trouvée avec l'ID: " + id)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la mise à jour de l'avancement de la phase {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la mise à jour de l'avancement: " + e.getMessage())
.build();
}
}
// === CLASSES DE REQUÊTE ===
public static class PhaseCreateRequest {
public String nom;
public String description;
public String chantierId;
public String dateDebutPrevue;
public String dateFinPrevue;
public Integer ordreExecution = 1;
public Double budgetPrevu;
public Boolean critique;
public String responsableId;
public String phaseParentId;
}
public static class SuspendrePhaseRequest {
public String motif;
}
public static class AvancementRequest {
public Double pourcentage;
}
}

View File

@@ -0,0 +1,660 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.DocumentService;
import dev.lions.btpxpress.domain.core.entity.Document;
import dev.lions.btpxpress.domain.core.entity.TypeDocument;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.StreamingOutput;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.resteasy.reactive.PartType;
import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.multipart.FileUpload;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour la gestion des photos - Architecture 2025 PHOTOS: API spécialisée pour les
* photos de chantiers BTP
*/
@Path("/api/v1/photos")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Photos", description = "Gestion spécialisée des photos de chantiers BTP")
public class PhotoResource {
private static final Logger logger = LoggerFactory.getLogger(PhotoResource.class);
// Types MIME acceptés pour les photos
private static final String[] ALLOWED_IMAGE_TYPES = {
"image/jpeg", "image/jpg", "image/png", "image/bmp", "image/tiff", "image/webp"
};
// Taille maximale pour les photos (20MB)
private static final long MAX_PHOTO_SIZE = 20 * 1024 * 1024;
@Inject DocumentService documentService;
// === ENDPOINTS DE CONSULTATION ===
@GET
@Operation(
summary = "Lister toutes les photos",
description = "Récupère la liste de toutes les photos de chantiers")
@APIResponse(
responseCode = "200",
description = "Liste des photos récupérée avec succès",
content = @Content(schema = @Schema(implementation = Document.class)))
public Response getAllPhotos(
@Parameter(description = "Numéro de page (0-indexé)", example = "0")
@QueryParam("page")
@DefaultValue("0")
int page,
@Parameter(description = "Taille de page", example = "20")
@QueryParam("size")
@DefaultValue("20")
int size,
@Parameter(description = "Filtrer par chantier (UUID)") @QueryParam("chantierId")
UUID chantierId,
@Parameter(description = "Filtrer par employé (UUID)") @QueryParam("employeId")
UUID employeId,
@Parameter(description = "Terme de recherche dans les tags") @QueryParam("tags")
String tags) {
logger.debug("Récupération des photos - page: {}, taille: {}", page, size);
List<Document> photos;
if (chantierId != null || employeId != null || tags != null) {
photos = documentService.search(tags, "PHOTO_CHANTIER", chantierId, null, null);
// Filtrage supplémentaire par employé si spécifié
if (employeId != null) {
photos =
photos.stream()
.filter(
photo ->
photo.getEmploye() != null && photo.getEmploye().getId().equals(employeId))
.collect(Collectors.toList());
}
} else {
photos = documentService.findByType(TypeDocument.PHOTO_CHANTIER);
// Application de la pagination sur la liste complète
int fromIndex = page * size;
int toIndex = Math.min(fromIndex + size, photos.size());
if (fromIndex < photos.size()) {
photos = photos.subList(fromIndex, toIndex);
} else {
photos = List.of();
}
}
return Response.ok(photos).build();
}
@GET
@Path("/{id}")
@Operation(
summary = "Récupérer une photo par ID",
description = "Récupère les métadonnées d'une photo spécifique")
@APIResponse(
responseCode = "200",
description = "Photo trouvée",
content = @Content(schema = @Schema(implementation = Document.class)))
@APIResponse(responseCode = "404", description = "Photo non trouvée")
public Response getPhotoById(
@Parameter(description = "Identifiant unique de la photo", required = true) @PathParam("id")
UUID id) {
logger.debug("Récupération de la photo avec l'ID: {}", id);
return documentService
.findById(id)
.filter(doc -> doc.getTypeDocument() == TypeDocument.PHOTO_CHANTIER)
.map(photo -> Response.ok(photo).build())
.orElse(Response.status(Response.Status.NOT_FOUND).build());
}
@GET
@Path("/chantier/{chantierId}")
@Operation(
summary = "Photos d'un chantier",
description = "Récupère toutes les photos d'un chantier spécifique")
@APIResponse(
responseCode = "200",
description = "Photos du chantier récupérées",
content = @Content(schema = @Schema(implementation = Document.class)))
public Response getPhotosByChantier(
@Parameter(description = "Identifiant du chantier", required = true) @PathParam("chantierId")
UUID chantierId) {
logger.debug("Récupération des photos pour le chantier: {}", chantierId);
List<Document> photos =
documentService.findByChantier(chantierId).stream()
.filter(doc -> doc.getTypeDocument() == TypeDocument.PHOTO_CHANTIER)
.collect(Collectors.toList());
return Response.ok(photos).build();
}
@GET
@Path("/employe/{employeId}")
@Operation(
summary = "Photos prises par un employé",
description = "Récupère toutes les photos prises par un employé")
@APIResponse(
responseCode = "200",
description = "Photos de l'employé récupérées",
content = @Content(schema = @Schema(implementation = Document.class)))
public Response getPhotosByEmploye(
@Parameter(description = "Identifiant de l'employé", required = true) @PathParam("employeId")
UUID employeId) {
logger.debug("Récupération des photos pour l'employé: {}", employeId);
List<Document> photos =
documentService.findByEmploye(employeId).stream()
.filter(doc -> doc.getTypeDocument() == TypeDocument.PHOTO_CHANTIER)
.collect(Collectors.toList());
return Response.ok(photos).build();
}
@GET
@Path("/recentes")
@Operation(
summary = "Photos récentes",
description = "Récupère les photos les plus récemment ajoutées")
@APIResponse(
responseCode = "200",
description = "Photos récentes récupérées",
content = @Content(schema = @Schema(implementation = Document.class)))
public Response getPhotosRecentes(
@Parameter(description = "Nombre de photos à retourner", example = "10")
@QueryParam("limite")
@DefaultValue("10")
int limite) {
logger.debug("Récupération des {} photos les plus récentes", limite);
List<Document> photos =
documentService
.findRecents(limite * 2) // Récupérer plus pour filtrer
.stream()
.filter(doc -> doc.getTypeDocument() == TypeDocument.PHOTO_CHANTIER)
.limit(limite)
.collect(Collectors.toList());
return Response.ok(photos).build();
}
// === ENDPOINTS D'UPLOAD SPÉCIALISÉS ===
@POST
@Path("/upload")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Operation(
summary = "Uploader une photo de chantier",
description = "Upload une photo avec optimisations spécifiques aux images")
@APIResponse(
responseCode = "201",
description = "Photo uploadée avec succès",
content = @Content(schema = @Schema(implementation = Document.class)))
@APIResponse(responseCode = "400", description = "Fichier non valide ou trop volumineux")
public Response uploadPhoto(
@RestForm("nom") String nom,
@RestForm("description") String description,
@RestForm("file") FileUpload file,
@RestForm("fileName") String fileName,
@RestForm("contentType") String contentType,
@RestForm("chantierId") UUID chantierId,
@RestForm("materielId") UUID materielId,
@RestForm("equipeId") UUID equipeId,
@RestForm("employeId") UUID employeId,
@RestForm("localisation") String localisation,
@RestForm("latitude") Double latitude,
@RestForm("longitude") Double longitude) {
logger.info("Upload de photo: {}", nom);
// Validation spécifique aux images
validatePhotoUpload(file, fileName, contentType);
Document photo =
documentService.uploadDocument(
nom != null ? nom : "Photo_" + LocalDateTime.now(),
description,
"PHOTO_CHANTIER", // Type fixe pour les photos
file,
fileName,
contentType,
file != null ? file.size() : 0L,
chantierId,
materielId,
equipeId,
employeId,
null, // Pas de client pour les photos de chantier
null, // tags - ajouté si besoin
false, // estPublic - défaut
null); // userId - ajouté si besoin
return Response.status(Response.Status.CREATED).entity(photo).build();
}
@POST
@Path("/upload-multiple")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Operation(
summary = "Uploader plusieurs photos",
description = "Upload multiple de photos en une seule requête")
@APIResponse(responseCode = "201", description = "Photos uploadées avec succès")
@APIResponse(responseCode = "400", description = "Erreur dans l'upload")
public Response uploadMultiplePhotos(
@RestForm(FileUpload.ALL) List<FileUpload> files,
@RestForm("chantierId") UUID chantierId,
@RestForm("description") String description) {
logger.info("Upload multiple de {} photos", files != null ? files.size() : 0);
if (files == null || files.isEmpty()) {
throw new BadRequestException("Aucun fichier fourni");
}
if (files.size() > 10) {
throw new BadRequestException("Maximum 10 fichiers autorisés par upload");
}
List<Document> uploadedPhotos = new java.util.ArrayList<>();
for (int i = 0; i < files.size(); i++) {
FileUpload file = files.get(i);
String fileName = file.fileName();
String contentType = file.contentType();
long fileSize = file.size();
// Validation basique de chaque fichier
if (!isValidImageType(contentType)) {
throw new BadRequestException("Type de fichier non supporté: " + contentType);
}
Document photo =
documentService.uploadDocument(
"Photo_multiple_" + fileName,
description,
"PHOTO_CHANTIER",
file,
fileName,
contentType,
fileSize,
chantierId,
null, // materielId
null, // equipeId
null, // employeId
null, // clientId
null, // tags
false, // estPublic
null); // userId
uploadedPhotos.add(photo);
}
return Response.status(Response.Status.CREATED)
.entity(
new Object() {
public final int nombrePhotos = uploadedPhotos.size();
public final List<Document> photos = uploadedPhotos;
})
.build();
}
// === ENDPOINTS DE VISUALISATION ===
@GET
@Path("/{id}/thumbnail")
@Produces("image/*")
@Operation(
summary = "Miniature d'une photo",
description = "Récupère une version miniature de la photo")
@APIResponse(responseCode = "200", description = "Miniature récupérée")
@APIResponse(responseCode = "404", description = "Photo non trouvée")
public Response getThumbnail(
@Parameter(description = "Identifiant de la photo", required = true) @PathParam("id")
UUID id) {
logger.debug("Récupération de la miniature pour la photo: {}", id);
Document photo = documentService.findByIdRequired(id);
if (photo.getTypeDocument() != TypeDocument.PHOTO_CHANTIER) {
throw new BadRequestException("Ce document n'est pas une photo");
}
// Génération de miniature (simulation - en production utiliser une bibliothèque comme
// Thumbnailator)
InputStream inputStream =
generateThumbnail(documentService.downloadDocument(id), photo.getTypeMime());
StreamingOutput streamingOutput =
output -> {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
inputStream.close();
};
return Response.ok(streamingOutput)
.header("Content-Type", photo.getTypeMime())
.header("Cache-Control", "public, max-age=3600")
.build();
}
@GET
@Path("/{id}/view")
@Produces("image/*")
@Operation(summary = "Visualiser une photo", description = "Affiche la photo en taille originale")
@APIResponse(responseCode = "200", description = "Photo affichée")
@APIResponse(responseCode = "404", description = "Photo non trouvée")
public Response viewPhoto(
@Parameter(description = "Identifiant de la photo", required = true) @PathParam("id")
UUID id) {
logger.debug("Visualisation de la photo: {}", id);
Document photo = documentService.findByIdRequired(id);
if (photo.getTypeDocument() != TypeDocument.PHOTO_CHANTIER) {
throw new BadRequestException("Ce document n'est pas une photo");
}
InputStream inputStream = documentService.downloadDocument(id);
StreamingOutput streamingOutput =
output -> {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
inputStream.close();
};
return Response.ok(streamingOutput)
.header("Content-Type", photo.getTypeMime())
.header("Content-Disposition", "inline; filename=\"" + photo.getNomFichier() + "\"")
.header("Cache-Control", "public, max-age=3600")
.build();
}
// === ENDPOINTS STATISTIQUES SPÉCIALISÉS ===
@GET
@Path("/statistiques")
@Operation(
summary = "Statistiques des photos",
description = "Récupère les statistiques spécifiques aux photos")
@APIResponse(responseCode = "200", description = "Statistiques récupérées")
public Response getStatistiquesPhotos() {
logger.debug("Récupération des statistiques des photos");
List<Document> photos = documentService.findByType(TypeDocument.PHOTO_CHANTIER);
final long totalPhotosCount = photos.size();
final long tailleTotalBytes = photos.stream().mapToLong(Document::getTailleFichier).sum();
// Statistiques par chantier
final long chantiersAvecPhotosCount =
photos.stream()
.filter(p -> p.getChantier() != null)
.map(p -> p.getChantier().getId())
.distinct()
.count();
final double tailleMoyenneCalc =
totalPhotosCount > 0 ? (double) tailleTotalBytes / totalPhotosCount : 0;
return Response.ok(
new Object() {
public final long totalPhotos = totalPhotosCount;
public final String tailleTotale = formatFileSize(tailleTotalBytes);
public final long chantiersAvecPhotos = chantiersAvecPhotosCount;
public final double tailleMoyenne = tailleMoyenneCalc;
public final String tailleMoyenneFormatee = formatFileSize((long) tailleMoyenneCalc);
})
.build();
}
@GET
@Path("/galerie/{chantierId}")
@Operation(
summary = "Galerie photos d'un chantier",
description = "Récupère toutes les photos d'un chantier pour affichage galerie")
@APIResponse(responseCode = "200", description = "Galerie récupérée")
public Response getGalerieChantier(
@Parameter(description = "Identifiant du chantier", required = true) @PathParam("chantierId")
UUID chantierId) {
logger.debug("Récupération de la galerie pour le chantier: {}", chantierId);
List<Document> photos =
documentService.findByChantier(chantierId).stream()
.filter(doc -> doc.getTypeDocument() == TypeDocument.PHOTO_CHANTIER)
.collect(Collectors.toList());
// Informations de galerie enrichies
final UUID chantierIdFinal = chantierId;
final int nombrePhotosTotal = photos.size();
final List<Object> photosEnrichies =
photos.stream()
.map(
doc ->
new Object() {
public final UUID id = doc.getId();
public final String nom = doc.getNom();
public final String description = doc.getDescription();
public final String tailleFormatee = doc.getTailleFormatee();
public final LocalDateTime dateCreation = doc.getDateCreation();
public final String tags = doc.getTags();
public final String urlThumbnail = "/photos/" + doc.getId() + "/thumbnail";
public final String urlView = "/photos/" + doc.getId() + "/view";
})
.collect(Collectors.toList());
return Response.ok(
new Object() {
public final UUID chantierId = chantierIdFinal;
public final int nombrePhotos = nombrePhotosTotal;
public final List<Object> photos = photosEnrichies;
})
.build();
}
// === MÉTHODES PRIVÉES ===
private void validatePhotoUpload(FileUpload file, String fileName, String contentType) {
if (file == null) {
throw new BadRequestException("Aucun fichier fourni");
}
if (fileName == null || fileName.trim().isEmpty()) {
throw new BadRequestException("Nom de fichier manquant");
}
if (contentType == null || !isValidImageType(contentType)) {
throw new BadRequestException("Type de fichier non supporté pour les photos: " + contentType);
}
if (file.size() > MAX_PHOTO_SIZE) {
throw new BadRequestException(
"Photo trop volumineuse (max: " + formatFileSize(MAX_PHOTO_SIZE) + ")");
}
// Validation supplémentaire si nécessaire
// Le chantierId peut être null dans certains cas
}
private boolean isValidImageType(String contentType) {
if (contentType == null) return false;
return Arrays.stream(ALLOWED_IMAGE_TYPES).anyMatch(type -> type.equalsIgnoreCase(contentType));
}
private String formatFileSize(long bytes) {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
return String.format("%.1f GB", bytes / (1024.0 * 1024.0 * 1024.0));
}
/**
* Génère une miniature pour une image Simulation - en production, utiliser une bibliothèque comme
* Thumbnailator ou ImageIO
*/
private InputStream generateThumbnail(InputStream originalStream, String mimeType) {
try {
// Simulation simple - en production, implémenter une vraie génération de miniatures
// Utiliser des bibliothèques comme :
// - Thumbnailator: Thumbnails.of(originalStream).size(200,
// 200).outputFormat("jpg").toOutputStream()
// - ImageIO avec BufferedImage
// - Apache Commons Imaging
logger.debug("Génération de miniature (simulée) pour type MIME: {}", mimeType);
// Pour la simulation, retourner le stream original
// En production, générer une vraie miniature de 200x200 pixels
return originalStream;
} catch (Exception e) {
logger.error("Erreur lors de la génération de miniature: {}", e.getMessage());
// En cas d'erreur, retourner l'image originale
return originalStream;
}
}
// === CLASSES DE REQUÊTE ===
public static class UploadPhotoForm {
@RestForm("nom")
@Schema(description = "Nom de la photo")
public String nom;
@RestForm("description")
@Schema(description = "Description de la photo")
public String description;
@RestForm("file")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
@Schema(description = "Fichier image à uploader", required = true)
public InputStream file;
@RestForm("fileName")
@Schema(description = "Nom du fichier image", required = true)
public String fileName;
@RestForm("contentType")
@Schema(description = "Type MIME de l'image", required = true)
public String contentType;
@RestForm("fileSize")
@Schema(description = "Taille du fichier en bytes", required = true)
public long fileSize;
@RestForm("chantierId")
@Schema(description = "ID du chantier", required = true)
public UUID chantierId;
@RestForm("employeId")
@Schema(description = "ID de l'employé qui prend la photo")
public UUID employeId;
@RestForm("tags")
@Schema(description = "Tags descriptifs (ex: 'avancement,façade,jour1')")
public String tags;
@RestForm("estPublic")
@Schema(description = "Photo visible publiquement")
public Boolean estPublic;
@RestForm("userId")
@Schema(description = "ID de l'utilisateur qui upload")
public UUID userId;
}
public static class FileUploadInfo {
public InputStream file;
public String fileName;
public String contentType;
public long fileSize;
public FileUploadInfo(InputStream file, String fileName, String contentType, long fileSize) {
this.file = file;
this.fileName = fileName;
this.contentType = contentType;
this.fileSize = fileSize;
}
}
public static class UploadMultiplePhotosForm {
@RestForm("files")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
@Schema(description = "Fichiers images à uploader")
public List<InputStream> files;
@RestForm("fileNames")
@Schema(description = "Noms des fichiers")
public List<String> fileNames;
@RestForm("contentTypes")
@Schema(description = "Types MIME des fichiers")
public List<String> contentTypes;
@RestForm("fileSizes")
@Schema(description = "Tailles des fichiers")
public List<Long> fileSizes;
@RestForm("nomBase")
@Schema(description = "Nom de base pour les photos")
public String nomBase;
@RestForm("description")
@Schema(description = "Description commune aux photos")
public String description;
@RestForm("chantierId")
@Schema(description = "ID du chantier", required = true)
public UUID chantierId;
@RestForm("employeId")
@Schema(description = "ID de l'employé")
public UUID employeId;
@RestForm("tags")
@Schema(description = "Tags communs aux photos")
public String tags;
@RestForm("estPublic")
@Schema(description = "Photos visibles publiquement")
public Boolean estPublic;
@RestForm("userId")
@Schema(description = "ID de l'utilisateur qui upload")
public UUID userId;
}
}

View File

@@ -0,0 +1,431 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.PlanningService;
import dev.lions.btpxpress.domain.core.entity.PlanningEvent;
import dev.lions.btpxpress.domain.core.entity.TypePlanningEvent;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour la gestion du planning - Architecture 2025 MÉTIER: Gestion complète planning
* BTP avec détection conflits
*/
@Path("/api/v1/planning")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Planning", description = "Gestion du planning et des événements BTP")
public class PlanningResource {
private static final Logger logger = LoggerFactory.getLogger(PlanningResource.class);
@Inject PlanningService planningService;
// === ENDPOINTS VUE PLANNING GÉNÉRAL ===
@GET
@Operation(summary = "Récupérer la vue planning général")
@APIResponse(responseCode = "200", description = "Planning général récupéré avec succès")
@APIResponse(responseCode = "400", description = "Paramètres de date invalides")
public Response getPlanningGeneral(
@Parameter(description = "Date de début (YYYY-MM-DD)") @QueryParam("dateDebut")
String dateDebut,
@Parameter(description = "Date de fin (YYYY-MM-DD)") @QueryParam("dateFin") String dateFin,
@Parameter(description = "ID du chantier (optionnel)") @QueryParam("chantierId")
String chantierId,
@Parameter(description = "ID de l'équipe (optionnel)") @QueryParam("equipeId")
String equipeId,
@Parameter(description = "Type d'événement (optionnel)") @QueryParam("type") String type) {
try {
LocalDate debut = dateDebut != null ? LocalDate.parse(dateDebut) : LocalDate.now();
LocalDate fin = dateFin != null ? LocalDate.parse(dateFin) : debut.plusDays(30);
if (debut.isAfter(fin)) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("La date de début ne peut pas être après la date de fin")
.build();
}
UUID chantierUUID = chantierId != null ? UUID.fromString(chantierId) : null;
UUID equipeUUID = equipeId != null ? UUID.fromString(equipeId) : null;
TypePlanningEvent typeEvent =
type != null ? TypePlanningEvent.valueOf(type.toUpperCase()) : null;
Object planning =
planningService.getPlanningGeneral(debut, fin, chantierUUID, equipeUUID, typeEvent);
return Response.ok(planning).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Paramètres invalides: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération du planning général", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération du planning: " + e.getMessage())
.build();
}
}
@GET
@Path("/week/{date}")
@Operation(summary = "Récupérer le planning hebdomadaire")
@APIResponse(responseCode = "200", description = "Planning hebdomadaire récupéré avec succès")
@APIResponse(responseCode = "400", description = "Date invalide")
public Response getPlanningWeek(
@Parameter(description = "Date de référence (YYYY-MM-DD)") @PathParam("date") String date) {
try {
LocalDate dateRef = LocalDate.parse(date);
Object planningWeek = planningService.getPlanningWeek(dateRef);
return Response.ok(planningWeek).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST).entity("Date invalide: " + date).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération du planning hebdomadaire", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération du planning: " + e.getMessage())
.build();
}
}
@GET
@Path("/month/{date}")
@Operation(summary = "Récupérer le planning mensuel")
@APIResponse(responseCode = "200", description = "Planning mensuel récupéré avec succès")
@APIResponse(responseCode = "400", description = "Date invalide")
public Response getPlanningMonth(
@Parameter(description = "Date de référence (YYYY-MM-DD)") @PathParam("date") String date) {
try {
LocalDate dateRef = LocalDate.parse(date);
Object planningMonth = planningService.getPlanningMonth(dateRef);
return Response.ok(planningMonth).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST).entity("Date invalide: " + date).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération du planning mensuel", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération du planning: " + e.getMessage())
.build();
}
}
// === ENDPOINTS GESTION ÉVÉNEMENTS ===
@GET
@Path("/events")
@Operation(summary = "Récupérer tous les événements de planning")
@APIResponse(responseCode = "200", description = "Liste des événements récupérée avec succès")
public Response getAllEvents(
@Parameter(description = "Date de début (YYYY-MM-DD)") @QueryParam("dateDebut")
String dateDebut,
@Parameter(description = "Date de fin (YYYY-MM-DD)") @QueryParam("dateFin") String dateFin,
@Parameter(description = "Type d'événement") @QueryParam("type") String type,
@Parameter(description = "ID du chantier") @QueryParam("chantierId") String chantierId) {
try {
List<PlanningEvent> events;
if (dateDebut != null && dateFin != null) {
LocalDate debut = LocalDate.parse(dateDebut);
LocalDate fin = LocalDate.parse(dateFin);
events = planningService.findEventsByDateRange(debut, fin);
} else if (type != null) {
TypePlanningEvent typeEvent = TypePlanningEvent.valueOf(type.toUpperCase());
events = planningService.findEventsByType(typeEvent);
} else if (chantierId != null) {
UUID chantierUUID = UUID.fromString(chantierId);
events = planningService.findEventsByChantier(chantierUUID);
} else {
events = planningService.findAllEvents();
}
return Response.ok(events).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Paramètres invalides: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des événements", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des événements: " + e.getMessage())
.build();
}
}
@GET
@Path("/events/{id}")
@Operation(summary = "Récupérer un événement par ID")
@APIResponse(responseCode = "200", description = "Événement récupéré avec succès")
@APIResponse(responseCode = "404", description = "Événement non trouvé")
public Response getEventById(
@Parameter(description = "ID de l'événement") @PathParam("id") String id) {
try {
UUID eventId = UUID.fromString(id);
return planningService
.findEventById(eventId)
.map(event -> Response.ok(event).build())
.orElse(
Response.status(Response.Status.NOT_FOUND)
.entity("Événement non trouvé avec l'ID: " + id)
.build());
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID d'événement invalide: " + id)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération de l'événement {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération de l'événement: " + e.getMessage())
.build();
}
}
@POST
@Path("/events")
@Operation(summary = "Créer un nouvel événement de planning")
@APIResponse(responseCode = "201", description = "Événement créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
@APIResponse(responseCode = "409", description = "Conflit de ressources détecté")
public Response createEvent(
@Parameter(description = "Données du nouvel événement") @Valid @NotNull
CreateEventRequest request) {
try {
PlanningEvent event =
planningService.createEvent(
request.titre,
request.description,
request.type,
request.dateDebut,
request.dateFin,
request.chantierId,
request.equipeId,
request.employeIds,
request.materielIds);
return Response.status(Response.Status.CREATED).entity(event).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Données invalides: " + e.getMessage())
.build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.CONFLICT)
.entity("Conflit de ressources: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la création de l'événement", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la création de l'événement: " + e.getMessage())
.build();
}
}
@PUT
@Path("/events/{id}")
@Operation(summary = "Modifier un événement de planning")
@APIResponse(responseCode = "200", description = "Événement modifié avec succès")
@APIResponse(responseCode = "404", description = "Événement non trouvé")
@APIResponse(responseCode = "409", description = "Conflit de ressources détecté")
public Response updateEvent(
@Parameter(description = "ID de l'événement") @PathParam("id") String id,
@Parameter(description = "Nouvelles données de l'événement") @Valid @NotNull
UpdateEventRequest request) {
try {
UUID eventId = UUID.fromString(id);
PlanningEvent event =
planningService.updateEvent(
eventId,
request.titre,
request.description,
request.dateDebut,
request.dateFin,
request.equipeId,
request.employeIds,
request.materielIds);
return Response.ok(event).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Données invalides: " + e.getMessage())
.build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.CONFLICT)
.entity("Conflit de ressources: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la modification de l'événement {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la modification de l'événement: " + e.getMessage())
.build();
}
}
@DELETE
@Path("/events/{id}")
@Operation(summary = "Supprimer un événement de planning")
@APIResponse(responseCode = "204", description = "Événement supprimé avec succès")
@APIResponse(responseCode = "404", description = "Événement non trouvé")
public Response deleteEvent(
@Parameter(description = "ID de l'événement") @PathParam("id") String id) {
try {
UUID eventId = UUID.fromString(id);
planningService.deleteEvent(eventId);
return Response.noContent().build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID invalide: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la suppression de l'événement {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la suppression de l'événement: " + e.getMessage())
.build();
}
}
// === ENDPOINTS DÉTECTION CONFLITS ===
@GET
@Path("/conflicts")
@Operation(summary = "Détecter les conflits de ressources")
@APIResponse(responseCode = "200", description = "Conflits détectés avec succès")
@APIResponse(responseCode = "400", description = "Paramètres invalides")
public Response detectConflicts(
@Parameter(description = "Date de début pour la vérification") @QueryParam("dateDebut")
String dateDebut,
@Parameter(description = "Date de fin pour la vérification") @QueryParam("dateFin")
String dateFin,
@Parameter(description = "Type de ressource (EMPLOYE, MATERIEL, EQUIPE)")
@QueryParam("resourceType")
String resourceType) {
try {
LocalDate debut = dateDebut != null ? LocalDate.parse(dateDebut) : LocalDate.now();
LocalDate fin = dateFin != null ? LocalDate.parse(dateFin) : debut.plusDays(7);
List<Object> conflicts = planningService.detectConflicts(debut, fin, resourceType);
return Response.ok(new ConflictsResponse(conflicts)).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Paramètres invalides: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la détection des conflits", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la détection des conflits: " + e.getMessage())
.build();
}
}
@POST
@Path("/check-availability")
@Operation(summary = "Vérifier la disponibilité des ressources")
@APIResponse(responseCode = "200", description = "Disponibilité vérifiée avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response checkAvailability(
@Parameter(description = "Critères de vérification de disponibilité") @Valid @NotNull
AvailabilityCheckRequest request) {
try {
boolean available =
planningService.checkResourcesAvailability(
request.dateDebut,
request.dateFin,
request.employeIds,
request.materielIds,
request.equipeId);
Object details =
planningService.getAvailabilityDetails(
request.dateDebut,
request.dateFin,
request.employeIds,
request.materielIds,
request.equipeId);
return Response.ok(new AvailabilityResponse(available, details)).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Données invalides: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la vérification de disponibilité", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la vérification: " + e.getMessage())
.build();
}
}
// === ENDPOINTS STATISTIQUES ===
@GET
@Path("/stats")
@Operation(summary = "Obtenir les statistiques du planning")
@APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès")
public Response getPlanningStats(
@Parameter(description = "Période de début (YYYY-MM-DD)") @QueryParam("dateDebut")
String dateDebut,
@Parameter(description = "Période de fin (YYYY-MM-DD)") @QueryParam("dateFin")
String dateFin) {
try {
LocalDate debut =
dateDebut != null ? LocalDate.parse(dateDebut) : LocalDate.now().minusDays(30);
LocalDate fin = dateFin != null ? LocalDate.parse(dateFin) : LocalDate.now();
Object stats = planningService.getStatistics(debut, fin);
return Response.ok(stats).build();
} catch (Exception e) {
logger.error("Erreur lors de la génération des statistiques planning", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la génération des statistiques: " + e.getMessage())
.build();
}
}
// === CLASSES UTILITAIRES ===
public static record CreateEventRequest(
@Parameter(description = "Titre de l'événement") String titre,
@Parameter(description = "Description de l'événement") String description,
@Parameter(description = "Type d'événement") String type,
@Parameter(description = "Date et heure de début") LocalDateTime dateDebut,
@Parameter(description = "Date et heure de fin") LocalDateTime dateFin,
@Parameter(description = "ID du chantier concerné") UUID chantierId,
@Parameter(description = "ID de l'équipe assignée") UUID equipeId,
@Parameter(description = "Liste des IDs des employés") List<UUID> employeIds,
@Parameter(description = "Liste des IDs du matériel") List<UUID> materielIds) {}
public static record UpdateEventRequest(
@Parameter(description = "Nouveau titre") String titre,
@Parameter(description = "Nouvelle description") String description,
@Parameter(description = "Nouvelle date de début") LocalDateTime dateDebut,
@Parameter(description = "Nouvelle date de fin") LocalDateTime dateFin,
@Parameter(description = "Nouvel ID d'équipe") UUID equipeId,
@Parameter(description = "Nouveaux IDs des employés") List<UUID> employeIds,
@Parameter(description = "Nouveaux IDs du matériel") List<UUID> materielIds) {}
public static record AvailabilityCheckRequest(
@Parameter(description = "Date de début") LocalDateTime dateDebut,
@Parameter(description = "Date de fin") LocalDateTime dateFin,
@Parameter(description = "IDs des employés à vérifier") List<UUID> employeIds,
@Parameter(description = "IDs du matériel à vérifier") List<UUID> materielIds,
@Parameter(description = "ID de l'équipe à vérifier") UUID equipeId) {}
public static record ConflictsResponse(List<Object> conflicts) {}
public static record AvailabilityResponse(boolean available, Object details) {}
}

View File

@@ -0,0 +1,646 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.*;
import dev.lions.btpxpress.domain.core.entity.*;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.StreamingOutput;
import java.io.PrintWriter;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour les rapports et exports - Architecture 2025 REPORTING: API de génération de
* rapports BTP avec exports
*/
@Path("/api/v1/reports")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Reports", description = "Génération de rapports et exports BTP")
public class ReportResource {
private static final Logger logger = LoggerFactory.getLogger(ReportResource.class);
@Inject ChantierService chantierService;
@Inject EquipeService equipeService;
@Inject EmployeService employeService;
@Inject MaterielService materielService;
@Inject MaintenanceService maintenanceService;
@Inject DocumentService documentService;
@Inject DisponibiliteService disponibiliteService;
@Inject PlanningService planningService;
// === RAPPORTS DE CHANTIERS ===
@GET
@Path("/chantiers")
@Operation(
summary = "Rapport des chantiers",
description = "Génère un rapport détaillé des chantiers avec filtres")
@APIResponse(responseCode = "200", description = "Rapport généré avec succès")
public Response getRapportChantiers(
@Parameter(description = "Date de début (yyyy-mm-dd)") @QueryParam("dateDebut")
String dateDebutStr,
@Parameter(description = "Date de fin (yyyy-mm-dd)") @QueryParam("dateFin") String dateFinStr,
@Parameter(description = "Statut des chantiers") @QueryParam("statut") String statutStr,
@Parameter(description = "Format d'export", example = "json")
@QueryParam("format")
@DefaultValue("json")
String format) {
logger.info("Génération du rapport chantiers - format: {}", format);
LocalDate dateDebut = parseDate(dateDebutStr, LocalDate.now().minusMonths(1));
LocalDate dateFin = parseDate(dateFinStr, LocalDate.now());
StatutChantier statut = parseStatutChantier(statutStr);
List<Chantier> chantiers;
if (statut != null) {
chantiers = chantierService.findByStatut(statut);
} else {
chantiers = chantierService.findByDateRange(dateDebut, dateFin);
}
// Variables locales pour éviter les self-references
final LocalDate dateDebutRef = dateDebut;
final LocalDate dateFinRef = dateFin;
final String statutRef = statutStr;
// Enrichissement des données pour le rapport
final List<Object> chantiersEnrichis =
chantiers.stream()
.map(
chantier ->
new Object() {
public final UUID id = chantier.getId();
public final String nom = chantier.getNom();
public final String description = chantier.getDescription();
public final String adresse = chantier.getAdresse();
public final String statut = chantier.getStatut().toString();
public final LocalDate dateDebut = chantier.getDateDebut();
public final LocalDate dateFinPrevue = chantier.getDateFinPrevue();
public final LocalDate dateFinReelle = chantier.getDateFinReelle();
public final double montant =
chantier.getMontantPrevu() != null
? chantier.getMontantPrevu().doubleValue()
: 0.0;
public final String client =
chantier.getClient() != null
? chantier.getClient().getNom()
: "Non défini";
public final long nombreDocuments =
documentService.findByChantier(chantier.getId()).size();
})
.collect(Collectors.toList());
final int nombreChantiersRef = chantiersEnrichis.size();
final double budgetTotalRef =
chantiers.stream()
.filter(c -> c.getMontantPrevu() != null)
.mapToDouble(c -> c.getMontantPrevu().doubleValue())
.sum();
Object rapport =
new Object() {
public final String titre = "Rapport des Chantiers";
public final LocalDate dateDebut = dateDebutRef;
public final LocalDate dateFin = dateFinRef;
public final String statut = statutRef != null ? statutRef : "Tous";
public final int nombreChantiers = nombreChantiersRef;
public final double budgetTotal = budgetTotalRef;
public final List<Object> chantiers = chantiersEnrichis;
public final LocalDateTime genereA = LocalDateTime.now();
};
return handleFormatResponse(rapport, format, "rapport_chantiers");
}
@GET
@Path("/chantiers/{id}/detail")
@Operation(
summary = "Rapport détaillé d'un chantier",
description = "Génère un rapport complet pour un chantier spécifique")
@APIResponse(responseCode = "200", description = "Rapport de chantier généré")
@APIResponse(responseCode = "404", description = "Chantier non trouvé")
public Response getRapportChantierDetail(
@Parameter(description = "Identifiant du chantier", required = true) @PathParam("id")
UUID chantierId,
@Parameter(description = "Format d'export", example = "json")
@QueryParam("format")
@DefaultValue("json")
String format) {
logger.info("Génération du rapport détaillé pour le chantier: {}", chantierId);
Chantier chantierEntity =
chantierService
.findById(chantierId)
.orElseThrow(() -> new NotFoundException("Chantier non trouvé: " + chantierId));
List<Document> documents = documentService.findByChantier(chantierId);
Object rapportDetail =
new Object() {
public final String titre = "Rapport Détaillé du Chantier";
public final Object chantier =
new Object() {
public final UUID id = chantierEntity.getId();
public final String nom = chantierEntity.getNom();
public final String description = chantierEntity.getDescription();
public final String adresse = chantierEntity.getAdresse();
public final String statut = chantierEntity.getStatut().toString();
public final LocalDate dateDebut = chantierEntity.getDateDebut();
public final LocalDate dateFinPrevue = chantierEntity.getDateFinPrevue();
public final LocalDate dateFinReelle = chantierEntity.getDateFinReelle();
public final double montant =
chantierEntity.getMontantPrevu() != null
? chantierEntity.getMontantPrevu().doubleValue()
: 0.0;
public final String client =
chantierEntity.getClient() != null
? chantierEntity.getClient().getNom()
+ " ("
+ chantierEntity.getClient().getEmail()
+ ")"
: "Non défini";
};
public final Object statistiques =
new Object() {
public final int nombreDocuments = documents.size();
public final long tailleDocuments =
documents.stream().mapToLong(Document::getTailleFichier).sum();
public final long nombrePhotos =
documents.stream()
.filter(d -> d.getTypeDocument() == TypeDocument.PHOTO_CHANTIER)
.count();
public final long nombrePlans =
documents.stream()
.filter(d -> d.getTypeDocument() == TypeDocument.PLAN)
.count();
};
public final List<Object> documentsRecents =
documents.stream()
.limit(10)
.map(
doc ->
new Object() {
public final String nom = doc.getNom();
public final String type = doc.getTypeDocument().toString();
public final LocalDateTime dateCreation = doc.getDateCreation();
public final String taille = doc.getTailleFormatee();
})
.collect(Collectors.toList());
public final LocalDateTime genereA = LocalDateTime.now();
};
return handleFormatResponse(rapportDetail, format, "rapport_chantier_" + chantierId);
}
// === RAPPORTS DE MAINTENANCE ===
@GET
@Path("/maintenance")
@Operation(
summary = "Rapport de maintenance",
description = "Génère un rapport sur l'état de la maintenance du matériel")
@APIResponse(responseCode = "200", description = "Rapport de maintenance généré")
public Response getRapportMaintenance(
@Parameter(description = "Nombre de jours pour l'historique", example = "30")
@QueryParam("periode")
@DefaultValue("30")
int periodeJours,
@Parameter(description = "Format d'export", example = "json")
@QueryParam("format")
@DefaultValue("json")
String format) {
logger.info("Génération du rapport de maintenance - période: {} jours", periodeJours);
List<MaintenanceMateriel> maintenancesEnRetard = maintenanceService.findEnRetard();
List<MaintenanceMateriel> prochainesMaintenances =
maintenanceService.findProchainesMaintenances(30);
List<MaintenanceMateriel> maintenancesRecentes =
maintenanceService.findTerminees().stream().limit(50).collect(Collectors.toList());
final int periodeJoursRef = periodeJours;
final int maintenancesEnRetardCount = maintenancesEnRetard.size();
final int prochainesMaintenancesCount = prochainesMaintenances.size();
final int maintenancesRecentesCount = maintenancesRecentes.size();
Object rapport =
new Object() {
public final String titre = "Rapport de Maintenance";
public final int periodeJours = periodeJoursRef;
public final Object resume =
new Object() {
public final int maintenancesEnRetard = maintenancesEnRetardCount;
public final int prochainesMaintenances = prochainesMaintenancesCount;
public final int maintenancesRecentesTerminees = maintenancesRecentesCount;
public final boolean alerteCritique = maintenancesEnRetardCount > 0;
};
public final List<Object> enRetard =
maintenancesEnRetard.stream()
.map(
m ->
new Object() {
public final String materiel = m.getMateriel().getNom();
public final String type = m.getType().toString();
public final LocalDate datePrevue = m.getDatePrevue();
public final long joursRetard =
LocalDate.now().toEpochDay() - m.getDatePrevue().toEpochDay();
public final String technicien = m.getTechnicien();
public final String description = m.getDescription();
})
.collect(Collectors.toList());
public final List<Object> aVenir =
prochainesMaintenances.stream()
.map(
m ->
new Object() {
public final String materiel = m.getMateriel().getNom();
public final String type = m.getType().toString();
public final LocalDate datePrevue = m.getDatePrevue();
public final long joursDici =
m.getDatePrevue().toEpochDay() - LocalDate.now().toEpochDay();
public final String technicien = m.getTechnicien();
})
.collect(Collectors.toList());
public final List<Object> terminees =
maintenancesRecentes.stream()
.map(
m ->
new Object() {
public final String materiel = m.getMateriel().getNom();
public final String type = m.getType().toString();
public final LocalDate dateRealisee = m.getDateRealisee();
public final String technicien = m.getTechnicien();
public final String statut = m.getStatut().toString();
})
.collect(Collectors.toList());
public final LocalDateTime genereA = LocalDateTime.now();
};
return handleFormatResponse(rapport, format, "rapport_maintenance");
}
// === RAPPORTS DE RESSOURCES HUMAINES ===
@GET
@Path("/ressources-humaines")
@Operation(
summary = "Rapport des ressources humaines",
description = "Rapport sur les employés, équipes et disponibilités")
@APIResponse(responseCode = "200", description = "Rapport RH généré")
public Response getRapportRH(
@Parameter(description = "Format d'export", example = "json")
@QueryParam("format")
@DefaultValue("json")
String format) {
logger.info("Génération du rapport des ressources humaines");
List<Employe> employes = employeService.findAll();
List<Equipe> equipes = equipeService.findAll();
List<Disponibilite> disponibilitesEnAttente = disponibiliteService.findEnAttente();
List<Disponibilite> disponibilitesActuelles = disponibiliteService.findActuelles();
final int totalEmployesCount = employes.size();
final int totalEquipesCount = equipes.size();
final int disponibilitesEnAttenteCount = disponibilitesEnAttente.size();
final int disponibilitesActuellesCount = disponibilitesActuelles.size();
Object rapport =
new Object() {
public final String titre = "Rapport des Ressources Humaines";
public final Object resume =
new Object() {
public final int totalEmployes = totalEmployesCount;
public final int totalEquipes = totalEquipesCount;
public final int disponibilitesEnAttente = disponibilitesEnAttenteCount;
public final int disponibilitesActuelles = disponibilitesActuellesCount;
};
public final List<Object> equipesDetail =
equipes.stream()
.map(
equipeEntity ->
new Object() {
public final String nom = equipeEntity.getNom();
public final String specialites =
equipeEntity.getSpecialites() != null
? String.join(", ", equipeEntity.getSpecialites())
: "Non défini";
public final String statut = equipeEntity.getStatut().toString();
public final int nombreMembres =
employes.stream()
.filter(
emp ->
equipeEntity
.getId()
.equals(
emp.getEquipe() != null
? emp.getEquipe().getId()
: null))
.mapToInt(emp -> 1)
.sum();
})
.collect(Collectors.toList());
public final List<Object> disponibilitesEnCours =
disponibilitesEnAttente.stream()
.map(
dispo ->
new Object() {
public final String employe =
dispo.getEmploye().getNom() + " " + dispo.getEmploye().getPrenom();
public final String type = dispo.getType().toString();
public final LocalDateTime dateDebut = dispo.getDateDebut();
public final LocalDateTime dateFin = dispo.getDateFin();
public final String motif = dispo.getMotif();
public final String statut = "EN_ATTENTE";
})
.collect(Collectors.toList());
public final LocalDateTime genereA = LocalDateTime.now();
};
return handleFormatResponse(rapport, format, "rapport_rh");
}
// === RAPPORTS FINANCIERS ===
@GET
@Path("/financier")
@Operation(
summary = "Rapport financier",
description = "Rapport sur les budgets et coûts des chantiers")
@APIResponse(responseCode = "200", description = "Rapport financier généré")
public Response getRapportFinancier(
@Parameter(description = "Année de référence", example = "2025") @QueryParam("annee")
Integer annee,
@Parameter(description = "Format d'export", example = "json")
@QueryParam("format")
@DefaultValue("json")
String format) {
logger.info("Génération du rapport financier - année: {}", annee);
if (annee == null) {
annee = LocalDate.now().getYear();
}
// Filtrage par année des chantiers créés dans l'année
LocalDate debutAnnee = LocalDate.of(annee, 1, 1);
LocalDate finAnnee = LocalDate.of(annee, 12, 31);
List<Chantier> chantiers = chantierService.findByDateRange(debutAnnee, finAnnee);
List<Chantier> chantiersTermines =
chantiers.stream()
.filter(c -> c.getStatut() == StatutChantier.TERMINE)
.collect(Collectors.toList());
final int anneeRef = annee;
final int nombreChantiersCalc = chantiers.size();
final int chantiersTerminesCalc = chantiersTermines.size();
final double budgetTotalCalc =
chantiers.stream()
.filter(c -> c.getMontantPrevu() != null)
.mapToDouble(c -> c.getMontantPrevu().doubleValue())
.sum();
final double budgetMoyenCalc = chantiers.size() > 0 ? budgetTotalCalc / chantiers.size() : 0;
Object rapport =
new Object() {
public final String titre = "Rapport Financier";
public final int annee = anneeRef;
public final Object resume =
new Object() {
public final int nombreChantiers = nombreChantiersCalc;
public final int chantiersTermines = chantiersTerminesCalc;
public final double budgetTotal = budgetTotalCalc;
public final double budgetMoyen = budgetMoyenCalc;
public final String budgetTotalFormate = formatMontant(budgetTotalCalc);
};
public final List<Object> chantiersParBudget =
chantiers.stream()
.sorted(
(c1, c2) ->
Double.compare(
c2.getMontantPrevu() != null
? c2.getMontantPrevu().doubleValue()
: 0.0,
c1.getMontantPrevu() != null
? c1.getMontantPrevu().doubleValue()
: 0.0))
.limit(10)
.map(
chantier ->
new Object() {
public final String nom = chantier.getNom();
public final double budget =
chantier.getMontantPrevu() != null
? chantier.getMontantPrevu().doubleValue()
: 0.0;
public final String budgetFormate =
formatMontant(
chantier.getMontantPrevu() != null
? chantier.getMontantPrevu().doubleValue()
: 0.0);
public final String statut = chantier.getStatut().toString();
public final String client =
chantier.getClient() != null
? chantier.getClient().getNom()
: "Non défini";
})
.collect(Collectors.toList());
public final Object repartitionParStatut =
new Object() {
public final long enCours =
chantiers.stream()
.filter(c -> c.getStatut() == StatutChantier.EN_COURS)
.count();
public final long termines =
chantiers.stream().filter(c -> c.getStatut() == StatutChantier.TERMINE).count();
public final long planifies =
chantiers.stream()
.filter(c -> c.getStatut() == StatutChantier.PLANIFIE)
.count();
public final long suspendus =
chantiers.stream()
.filter(c -> c.getStatut() == StatutChantier.SUSPENDU)
.count();
};
public final LocalDateTime genereA = LocalDateTime.now();
};
return handleFormatResponse(rapport, format, "rapport_financier_" + annee);
}
// === EXPORTS SPÉCIALISÉS ===
@GET
@Path("/export/csv/chantiers")
@Produces("text/csv")
@Operation(
summary = "Export CSV des chantiers",
description = "Exporte la liste des chantiers au format CSV")
@APIResponse(responseCode = "200", description = "Export CSV généré")
public Response exportCsvChantiers() {
logger.info("Export CSV des chantiers");
List<Chantier> chantiers = chantierService.findAll();
StreamingOutput stream =
output -> {
try (PrintWriter writer = new PrintWriter(output)) {
// En-têtes CSV
writer.println(
"ID,Nom,Description,Adresse,Statut,Date Début,Date Fin Prévue,Date Fin"
+ " Réelle,Montant,Client");
// Données
for (Chantier chantier : chantiers) {
writer.printf(
"%s,%s,%s,%s,%s,%s,%s,%s,%.2f,%s%n",
csvEscape(chantier.getId().toString()),
csvEscape(chantier.getNom()),
csvEscape(chantier.getDescription()),
csvEscape(chantier.getAdresse()),
csvEscape(chantier.getStatut().toString()),
chantier.getDateDebut(),
chantier.getDateFinPrevue(),
chantier.getDateFinReelle() != null ? chantier.getDateFinReelle() : "",
chantier.getMontantPrevu() != null
? chantier.getMontantPrevu().doubleValue()
: 0.0,
csvEscape(chantier.getClient() != null ? chantier.getClient().getNom() : ""));
}
}
};
return Response.ok(stream)
.header(
"Content-Disposition",
"attachment; filename=\"chantiers_"
+ LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
+ ".csv\"")
.build();
}
@GET
@Path("/export/csv/maintenance")
@Produces("text/csv")
@Operation(
summary = "Export CSV des maintenances",
description = "Exporte la liste des maintenances au format CSV")
@APIResponse(responseCode = "200", description = "Export CSV généré")
public Response exportCsvMaintenance() {
logger.info("Export CSV des maintenances");
List<MaintenanceMateriel> maintenances = maintenanceService.findAll();
StreamingOutput stream =
output -> {
try (PrintWriter writer = new PrintWriter(output)) {
// En-têtes CSV
writer.println(
"ID,Matériel,Type,Date Prévue,Date Réalisée,Technicien,Description,Statut");
// Données
for (MaintenanceMateriel maintenance : maintenances) {
writer.printf(
"%s,%s,%s,%s,%s,%s,%s,%s%n",
csvEscape(maintenance.getId().toString()),
csvEscape(maintenance.getMateriel().getNom()),
csvEscape(maintenance.getType().toString()),
maintenance.getDatePrevue(),
maintenance.getDateRealisee() != null ? maintenance.getDateRealisee() : "",
csvEscape(maintenance.getTechnicien()),
csvEscape(maintenance.getDescription()),
csvEscape(maintenance.getStatut().toString()));
}
}
};
return Response.ok(stream)
.header(
"Content-Disposition",
"attachment; filename=\"maintenances_"
+ LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
+ ".csv\"")
.build();
}
// === MÉTHODES PRIVÉES ===
private LocalDate parseDate(String dateStr, LocalDate defaultValue) {
if (dateStr == null || dateStr.trim().isEmpty()) {
return defaultValue;
}
try {
return LocalDate.parse(dateStr);
} catch (Exception e) {
logger.warn("Date invalide: {}, utilisation de la valeur par défaut", dateStr);
return defaultValue;
}
}
private StatutChantier parseStatutChantier(String statutStr) {
if (statutStr == null || statutStr.trim().isEmpty()) {
return null;
}
try {
return StatutChantier.valueOf(statutStr.toUpperCase());
} catch (IllegalArgumentException e) {
logger.warn("Statut de chantier invalide: {}", statutStr);
return null;
}
}
private Response handleFormatResponse(Object data, String format, String filename) {
switch (format.toLowerCase()) {
case "csv":
return convertToCSV(data, filename);
case "json":
default:
return Response.ok(data).build();
}
}
private Response convertToCSV(Object data, String filename) {
// Pour l'instant, retourne le JSON - l'implémentation CSV complète nécessiterait
// une sérialisation plus complexe des objets
logger.warn("Conversion CSV non implémentée, retour du JSON");
return Response.ok(data)
.header("Content-Type", "application/json")
.header("Content-Disposition", "attachment; filename=\"" + filename + ".json\"")
.build();
}
private String csvEscape(String value) {
if (value == null) return "";
if (value.contains(",") || value.contains("\"") || value.contains("\n")) {
return "\"" + value.replace("\"", "\"\"") + "\"";
}
return value;
}
private String formatMontant(double montant) {
return String.format("%.2f €", montant);
}
}

View File

@@ -0,0 +1,224 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.TypeChantierService;
import dev.lions.btpxpress.domain.core.entity.TypeChantier;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Resource REST pour la gestion des types de chantier */
@Path("/api/v1/types-chantier")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Types de Chantier", description = "Gestion des types de chantier BTP")
public class TypeChantierResource {
private static final Logger logger = LoggerFactory.getLogger(TypeChantierResource.class);
@Inject TypeChantierService typeChantierService;
@GET
@Operation(summary = "Récupérer tous les types de chantier")
@APIResponse(
responseCode = "200",
description = "Liste des types de chantier récupérée avec succès")
public Response getAllTypes(
@Parameter(description = "Inclure les types inactifs")
@QueryParam("includeInactive")
@DefaultValue("false")
boolean includeInactive) {
try {
List<TypeChantier> types =
includeInactive
? typeChantierService.findAllIncludingInactive()
: typeChantierService.findAll();
logger.debug("Récupération de {} types de chantier", types.size());
return Response.ok(types).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des types de chantier", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des types de chantier: " + e.getMessage())
.build();
}
}
@GET
@Path("/par-categorie")
@Operation(summary = "Récupérer les types de chantier groupés par catégorie")
public Response getTypesByCategorie() {
try {
Map<String, List<TypeChantier>> types = typeChantierService.findByCategorie();
return Response.ok(types).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des types par catégorie", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération: " + e.getMessage())
.build();
}
}
@GET
@Path("/{id}")
@Operation(summary = "Récupérer un type de chantier par ID")
@APIResponse(responseCode = "200", description = "Type de chantier récupéré avec succès")
@APIResponse(responseCode = "404", description = "Type de chantier non trouvé")
public Response getTypeById(
@Parameter(description = "ID du type de chantier") @PathParam("id") String id) {
try {
UUID typeId = UUID.fromString(id);
TypeChantier type = typeChantierService.findById(typeId);
return Response.ok(type).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST).entity("ID invalide: " + id).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Type de chantier non trouvé avec l'ID: " + id)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération du type de chantier {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération: " + e.getMessage())
.build();
}
}
@GET
@Path("/code/{code}")
@Operation(summary = "Récupérer un type de chantier par code")
public Response getTypeByCode(
@Parameter(description = "Code du type de chantier") @PathParam("code") String code) {
try {
TypeChantier type = typeChantierService.findByCode(code);
return Response.ok(type).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Type de chantier non trouvé avec le code: " + code)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération du type de chantier par code {}", code, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération: " + e.getMessage())
.build();
}
}
@POST
@Operation(summary = "Créer un nouveau type de chantier")
@APIResponse(responseCode = "201", description = "Type de chantier créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response createType(@Valid TypeChantier typeChantier) {
try {
TypeChantier savedType = typeChantierService.create(typeChantier);
logger.info("Type de chantier créé avec succès: {}", savedType.getCode());
return Response.status(Response.Status.CREATED).entity(savedType).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.CONFLICT).entity("Conflit: " + e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la création du type de chantier", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la création: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}")
@Operation(summary = "Mettre à jour un type de chantier")
public Response updateType(
@Parameter(description = "ID du type de chantier") @PathParam("id") String id,
@Valid TypeChantier typeChantier) {
try {
UUID typeId = UUID.fromString(id);
TypeChantier updatedType = typeChantierService.update(typeId, typeChantier);
logger.info("Type de chantier mis à jour avec succès: {}", updatedType.getCode());
return Response.ok(updatedType).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Données invalides: " + e.getMessage())
.build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Type de chantier non trouvé avec l'ID: " + id)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la mise à jour du type de chantier {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la mise à jour: " + e.getMessage())
.build();
}
}
@DELETE
@Path("/{id}")
@Operation(summary = "Supprimer un type de chantier (soft delete)")
public Response deleteType(
@Parameter(description = "ID du type de chantier") @PathParam("id") String id) {
try {
UUID typeId = UUID.fromString(id);
typeChantierService.delete(typeId);
logger.info("Type de chantier supprimé (soft delete): {}", id);
return Response.noContent().build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST).entity("ID invalide: " + id).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Type de chantier non trouvé avec l'ID: " + id)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la suppression du type de chantier {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la suppression: " + e.getMessage())
.build();
}
}
@POST
@Path("/{id}/reactivate")
@Operation(summary = "Réactiver un type de chantier")
public Response reactivateType(
@Parameter(description = "ID du type de chantier") @PathParam("id") String id) {
try {
UUID typeId = UUID.fromString(id);
TypeChantier reactivatedType = typeChantierService.reactivate(typeId);
return Response.ok(reactivatedType).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST).entity("ID invalide: " + id).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Type de chantier non trouvé avec l'ID: " + id)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la réactivation du type de chantier {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la réactivation: " + e.getMessage())
.build();
}
}
@GET
@Path("/statistiques")
@Operation(summary = "Récupérer les statistiques des types de chantier")
public Response getStatistiques() {
try {
Map<String, Object> stats = typeChantierService.getStatistiques();
return Response.ok(stats).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des statistiques", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des statistiques: " + e.getMessage())
.build();
}
}
}

View File

@@ -0,0 +1,495 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.UserService;
import dev.lions.btpxpress.domain.core.entity.User;
import dev.lions.btpxpress.domain.core.entity.UserRole;
import dev.lions.btpxpress.domain.core.entity.UserStatus;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour la gestion des utilisateurs - Architecture 2025 SÉCURITÉ: Accès restreint aux
* administrateurs
*/
@Path("/api/v1/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Utilisateurs", description = "Gestion des utilisateurs du système")
@SecurityRequirement(name = "JWT")
// @Authenticated - Désactivé pour les tests
public class UserResource {
private static final Logger logger = LoggerFactory.getLogger(UserResource.class);
@Inject UserService userService;
// === ENDPOINTS DE CONSULTATION ===
@GET
@Operation(summary = "Récupérer tous les utilisateurs")
@APIResponse(responseCode = "200", description = "Liste des utilisateurs récupérée avec succès")
@APIResponse(responseCode = "401", description = "Non authentifié")
@APIResponse(responseCode = "403", description = "Accès refusé - droits administrateur requis")
public Response getAllUsers(
@Parameter(description = "Numéro de page (0-indexed)") @QueryParam("page") @DefaultValue("0")
int page,
@Parameter(description = "Taille de la page") @QueryParam("size") @DefaultValue("20")
int size,
@Parameter(description = "Terme de recherche") @QueryParam("search") String search,
@Parameter(description = "Filtrer par rôle") @QueryParam("role") String role,
@Parameter(description = "Filtrer par statut") @QueryParam("status") String status,
@Parameter(description = "Token d'authentification") @HeaderParam("Authorization")
String authorizationHeader) {
try {
List<User> users;
if (search != null && !search.isEmpty()) {
users = userService.searchUsers(search, page, size);
} else if (role != null && !role.isEmpty()) {
UserRole userRole = UserRole.valueOf(role.toUpperCase());
users = userService.findByRole(userRole, page, size);
} else if (status != null && !status.isEmpty()) {
UserStatus userStatus = UserStatus.valueOf(status.toUpperCase());
users = userService.findByStatus(userStatus, page, size);
} else {
users = userService.findAll(page, size);
}
// Convertir en DTO pour éviter d'exposer les données sensibles
List<UserResponse> userResponses = users.stream().map(this::toUserResponse).toList();
return Response.ok(userResponses).build();
} catch (SecurityException e) {
return Response.status(Response.Status.FORBIDDEN)
.entity("Accès refusé: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des utilisateurs", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des utilisateurs: " + e.getMessage())
.build();
}
}
@GET
@Path("/{id}")
@Operation(summary = "Récupérer un utilisateur par ID")
@APIResponse(responseCode = "200", description = "Utilisateur récupéré avec succès")
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé")
@APIResponse(responseCode = "403", description = "Accès refusé")
public Response getUserById(
@Parameter(description = "ID de l'utilisateur") @PathParam("id") String id,
@Parameter(description = "Token d'authentification") @HeaderParam("Authorization")
String authorizationHeader) {
try {
UUID userId = UUID.fromString(id);
return userService
.findById(userId)
.map(user -> Response.ok(toUserResponse(user)).build())
.orElse(
Response.status(Response.Status.NOT_FOUND)
.entity("Utilisateur non trouvé avec l'ID: " + id)
.build());
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID d'utilisateur invalide: " + id)
.build();
} catch (SecurityException e) {
return Response.status(Response.Status.FORBIDDEN)
.entity("Accès refusé: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération de l'utilisateur {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération de l'utilisateur: " + e.getMessage())
.build();
}
}
@GET
@Path("/count")
@Operation(summary = "Compter le nombre d'utilisateurs")
@APIResponse(responseCode = "200", description = "Nombre d'utilisateurs retourné avec succès")
@APIResponse(responseCode = "403", description = "Accès refusé")
public Response countUsers(
@Parameter(description = "Filtrer par statut") @QueryParam("status") String status,
@Parameter(description = "Token d'authentification") @HeaderParam("Authorization")
String authorizationHeader) {
try {
long count;
if (status != null && !status.isEmpty()) {
UserStatus userStatus = UserStatus.valueOf(status.toUpperCase());
count = userService.countByStatus(userStatus);
} else {
count = userService.count();
}
return Response.ok(new CountResponse(count)).build();
} catch (SecurityException e) {
return Response.status(Response.Status.FORBIDDEN)
.entity("Accès refusé: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors du comptage des utilisateurs", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du comptage des utilisateurs: " + e.getMessage())
.build();
}
}
@GET
@Path("/pending")
@Operation(summary = "Récupérer les utilisateurs en attente de validation")
@APIResponse(
responseCode = "200",
description = "Liste des utilisateurs en attente récupérée avec succès")
@APIResponse(responseCode = "403", description = "Accès refusé")
public Response getPendingUsers(
@Parameter(description = "Token d'authentification") @HeaderParam("Authorization")
String authorizationHeader) {
try {
List<User> pendingUsers = userService.findByStatus(UserStatus.PENDING, 0, 100);
List<UserResponse> userResponses = pendingUsers.stream().map(this::toUserResponse).toList();
return Response.ok(userResponses).build();
} catch (SecurityException e) {
return Response.status(Response.Status.FORBIDDEN)
.entity("Accès refusé: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des utilisateurs en attente", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des utilisateurs en attente: " + e.getMessage())
.build();
}
}
@GET
@Path("/stats")
@Operation(summary = "Obtenir les statistiques des utilisateurs")
@APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès")
@APIResponse(responseCode = "403", description = "Accès refusé")
public Response getUserStats(
@Parameter(description = "Token d'authentification") @HeaderParam("Authorization")
String authorizationHeader) {
try {
Object stats = userService.getStatistics();
return Response.ok(stats).build();
} catch (SecurityException e) {
return Response.status(Response.Status.FORBIDDEN)
.entity("Accès refusé: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la génération des statistiques utilisateurs", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la génération des statistiques: " + e.getMessage())
.build();
}
}
// === ENDPOINTS DE GESTION ===
@POST
@Operation(summary = "Créer un nouvel utilisateur")
@APIResponse(responseCode = "201", description = "Utilisateur créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
@APIResponse(responseCode = "409", description = "Email déjà utilisé")
@APIResponse(responseCode = "403", description = "Accès refusé")
public Response createUser(
@Parameter(description = "Données du nouvel utilisateur") @Valid @NotNull
CreateUserRequest request,
@Parameter(description = "Token d'authentification") @HeaderParam("Authorization")
String authorizationHeader) {
try {
User user =
userService.createUser(
request.email,
request.password,
request.nom,
request.prenom,
request.role,
request.status);
return Response.status(Response.Status.CREATED).entity(toUserResponse(user)).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Données invalides: " + e.getMessage())
.build();
} catch (SecurityException e) {
return Response.status(Response.Status.FORBIDDEN)
.entity("Accès refusé: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la création de l'utilisateur", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la création de l'utilisateur: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}")
@Operation(summary = "Modifier un utilisateur")
@APIResponse(responseCode = "200", description = "Utilisateur modifié avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé")
@APIResponse(responseCode = "403", description = "Accès refusé")
public Response updateUser(
@Parameter(description = "ID de l'utilisateur") @PathParam("id") String id,
@Parameter(description = "Nouvelles données utilisateur") @Valid @NotNull
UpdateUserRequest request,
@Parameter(description = "Token d'authentification") @HeaderParam("Authorization")
String authorizationHeader) {
try {
UUID userId = UUID.fromString(id);
User user = userService.updateUser(userId, request.nom, request.prenom, request.email);
return Response.ok(toUserResponse(user)).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Données invalides: " + e.getMessage())
.build();
} catch (SecurityException e) {
return Response.status(Response.Status.FORBIDDEN)
.entity("Accès refusé: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la modification de l'utilisateur {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la modification de l'utilisateur: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/status")
@Operation(summary = "Modifier le statut d'un utilisateur")
@APIResponse(responseCode = "200", description = "Statut modifié avec succès")
@APIResponse(responseCode = "400", description = "Statut invalide")
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé")
@APIResponse(responseCode = "403", description = "Accès refusé")
public Response updateUserStatus(
@Parameter(description = "ID de l'utilisateur") @PathParam("id") String id,
@Parameter(description = "Nouveau statut") @Valid @NotNull UpdateStatusRequest request,
@Parameter(description = "Token d'authentification") @HeaderParam("Authorization")
String authorizationHeader) {
try {
UUID userId = UUID.fromString(id);
UserStatus status = UserStatus.valueOf(request.status.toUpperCase());
User user = userService.updateStatus(userId, status);
return Response.ok(toUserResponse(user)).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Statut invalide: " + e.getMessage())
.build();
} catch (SecurityException e) {
return Response.status(Response.Status.FORBIDDEN)
.entity("Accès refusé: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la modification du statut utilisateur {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la modification du statut: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/role")
@Operation(summary = "Modifier le rôle d'un utilisateur")
@APIResponse(responseCode = "200", description = "Rôle modifié avec succès")
@APIResponse(responseCode = "400", description = "Rôle invalide")
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé")
@APIResponse(responseCode = "403", description = "Accès refusé")
public Response updateUserRole(
@Parameter(description = "ID de l'utilisateur") @PathParam("id") String id,
@Parameter(description = "Nouveau rôle") @Valid @NotNull UpdateRoleRequest request,
@Parameter(description = "Token d'authentification") @HeaderParam("Authorization")
String authorizationHeader) {
try {
UUID userId = UUID.fromString(id);
UserRole role = UserRole.valueOf(request.role.toUpperCase());
User user = userService.updateRole(userId, role);
return Response.ok(toUserResponse(user)).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Rôle invalide: " + e.getMessage())
.build();
} catch (SecurityException e) {
return Response.status(Response.Status.FORBIDDEN)
.entity("Accès refusé: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la modification du rôle utilisateur {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la modification du rôle: " + e.getMessage())
.build();
}
}
@POST
@Path("/{id}/approve")
@Operation(summary = "Approuver un utilisateur en attente")
@APIResponse(responseCode = "200", description = "Utilisateur approuvé avec succès")
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé")
@APIResponse(responseCode = "403", description = "Accès refusé")
public Response approveUser(
@Parameter(description = "ID de l'utilisateur") @PathParam("id") String id,
@Parameter(description = "Token d'authentification") @HeaderParam("Authorization")
String authorizationHeader) {
try {
UUID userId = UUID.fromString(id);
User user = userService.approveUser(userId);
return Response.ok(toUserResponse(user)).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Données invalides: " + e.getMessage())
.build();
} catch (SecurityException e) {
return Response.status(Response.Status.FORBIDDEN)
.entity("Accès refusé: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de l'approbation de l'utilisateur {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de l'approbation de l'utilisateur: " + e.getMessage())
.build();
}
}
@POST
@Path("/{id}/reject")
@Operation(summary = "Rejeter un utilisateur en attente")
@APIResponse(responseCode = "200", description = "Utilisateur rejeté avec succès")
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé")
@APIResponse(responseCode = "403", description = "Accès refusé")
public Response rejectUser(
@Parameter(description = "ID de l'utilisateur") @PathParam("id") String id,
@Parameter(description = "Raison du rejet") @Valid @NotNull RejectUserRequest request,
@Parameter(description = "Token d'authentification") @HeaderParam("Authorization")
String authorizationHeader) {
try {
UUID userId = UUID.fromString(id);
userService.rejectUser(userId, request.reason);
return Response.ok().entity("Utilisateur rejeté avec succès").build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Données invalides: " + e.getMessage())
.build();
} catch (SecurityException e) {
return Response.status(Response.Status.FORBIDDEN)
.entity("Accès refusé: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors du rejet de l'utilisateur {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du rejet de l'utilisateur: " + e.getMessage())
.build();
}
}
@DELETE
@Path("/{id}")
@Operation(summary = "Supprimer un utilisateur")
@APIResponse(responseCode = "204", description = "Utilisateur supprimé avec succès")
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé")
@APIResponse(responseCode = "403", description = "Accès refusé")
public Response deleteUser(
@Parameter(description = "ID de l'utilisateur") @PathParam("id") String id,
@Parameter(description = "Token d'authentification") @HeaderParam("Authorization")
String authorizationHeader) {
try {
UUID userId = UUID.fromString(id);
userService.deleteUser(userId);
return Response.noContent().build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID invalide: " + e.getMessage())
.build();
} catch (SecurityException e) {
return Response.status(Response.Status.FORBIDDEN)
.entity("Accès refusé: " + e.getMessage())
.build();
} catch (Exception e) {
logger.error("Erreur lors de la suppression de l'utilisateur {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la suppression de l'utilisateur: " + e.getMessage())
.build();
}
}
// === MÉTHODES UTILITAIRES ===
private UserResponse toUserResponse(User user) {
return new UserResponse(
user.getId(),
user.getEmail(),
user.getNom(),
user.getPrenom(),
user.getRole().toString(),
user.getStatus().toString(),
user.getDateCreation(),
user.getDateModification(),
user.getDerniereConnexion(),
user.getActif());
}
// === CLASSES UTILITAIRES ===
public static record CountResponse(long count) {}
public static record CreateUserRequest(
@Parameter(description = "Email de l'utilisateur") String email,
@Parameter(description = "Mot de passe") String password,
@Parameter(description = "Nom de famille") String nom,
@Parameter(description = "Prénom") String prenom,
@Parameter(description = "Rôle (USER, ADMIN, MANAGER)") String role,
@Parameter(description = "Statut (ACTIF, INACTIF, SUSPENDU)") String status) {}
public static record UpdateUserRequest(
@Parameter(description = "Nouveau nom") String nom,
@Parameter(description = "Nouveau prénom") String prenom,
@Parameter(description = "Nouvel email") String email) {}
public static record UpdateStatusRequest(
@Parameter(description = "Nouveau statut") String status) {}
public static record UpdateRoleRequest(@Parameter(description = "Nouveau rôle") String role) {}
public static record RejectUserRequest(
@Parameter(description = "Raison du rejet") String reason) {}
public static record UserResponse(
UUID id,
String email,
String nom,
String prenom,
String role,
String status,
LocalDateTime dateCreation,
LocalDateTime dateModification,
LocalDateTime derniereConnexion,
Boolean actif) {}
}