Refactor: Backend Frontend-Centric Auth - Suppression OIDC, validation JWT

Architecture modifiée pour Frontend-Centric Authentication:

1. **Suppression des dépendances OIDC**
   - quarkus-oidc → quarkus-smallrye-jwt
   - quarkus-keycloak-authorization → quarkus-smallrye-jwt-build
   - Le backend ne gère plus l'authentification OAuth

2. **Configuration JWT simple**
   - Validation des tokens JWT envoyés par le frontend
   - mp.jwt.verify.publickey.location (JWKS de Keycloak)
   - mp.jwt.verify.issuer (Keycloak realm)
   - Authentification via Authorization: Bearer header

3. **Suppression configurations OIDC**
   - application.properties: Suppression %dev.quarkus.oidc.*
   - application.properties: Suppression %prod.quarkus.oidc.*
   - application-prod.properties: Remplacement par mp.jwt.*
   - Logging: io.quarkus.oidc → io.quarkus.smallrye.jwt

4. **Sécurité simplifiée**
   - quarkus.security.auth.proactive=false
   - @Authenticated sur les endpoints
   - CORS configuré pour le frontend
   - Endpoints publics: /q/*, /openapi, /swagger-ui/*

Flux d'authentification:
1️⃣ Frontend → Keycloak (OAuth login)
2️⃣ Frontend ← Keycloak (access_token)
3️⃣ Frontend → Backend (Authorization: Bearer token)
4️⃣ Backend valide le token JWT (signature + issuer)
5️⃣ Backend → Frontend (données API)

Avantages:
 Pas de secret backend à gérer
 Pas de client btpxpress-backend dans Keycloak
 Séparation claire frontend/backend
 Backend devient une API REST stateless
 Tokens gérés par le frontend (localStorage/sessionStorage)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
DahoudG
2025-10-31 17:05:11 +00:00
parent 7a72d13ffa
commit 7df5f346f1
60 changed files with 6095 additions and 4932 deletions

View File

@@ -0,0 +1,359 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.AbonnementService;
import dev.lions.btpxpress.domain.core.entity.Abonnement;
import dev.lions.btpxpress.domain.core.entity.StatutAbonnement;
import dev.lions.btpxpress.domain.core.entity.TypeAbonnement;
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.HashMap;
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 abonnements
* Architecture 2025 : API complète pour la gestion des abonnements
*/
@Path("/api/v1/abonnements")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Abonnements", description = "Gestion des abonnements d'entreprise")
public class AbonnementResource {
private static final Logger logger = LoggerFactory.getLogger(AbonnementResource.class);
@Inject AbonnementService abonnementService;
// === ENDPOINTS DE LECTURE ===
@GET
@Operation(summary = "Récupérer tous les abonnements")
@APIResponse(responseCode = "200", description = "Liste des abonnements récupérée avec succès")
public Response getAllAbonnements(
@Parameter(description = "Statut") @QueryParam("statut") String statut,
@Parameter(description = "Type d'abonnement") @QueryParam("type") String type) {
logger.debug("GET /abonnements - statut: {}, type: {}", statut, type);
List<Abonnement> abonnements;
if (statut != null && !statut.trim().isEmpty()) {
try {
StatutAbonnement statutEnum = StatutAbonnement.valueOf(statut.toUpperCase());
abonnements = abonnementService.findByStatut(statutEnum);
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Statut invalide: " + statut))
.build();
}
} else if (type != null && !type.trim().isEmpty()) {
try {
TypeAbonnement typeEnum = TypeAbonnement.valueOf(type.toUpperCase());
abonnements = abonnementService.findByType(typeEnum);
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Type invalide: " + type))
.build();
}
} else {
abonnements = abonnementService.findAll();
}
Map<String, Object> response = new HashMap<>();
response.put("abonnements", abonnements);
response.put("total", abonnements.size());
return Response.ok(response).build();
}
@GET
@Path("/{id}")
@Operation(summary = "Récupérer un abonnement par ID")
@APIResponse(responseCode = "200", description = "Abonnement trouvé")
@APIResponse(responseCode = "404", description = "Abonnement non trouvé")
public Response getAbonnementById(@Parameter(description = "ID de l'abonnement") @PathParam("id") UUID id) {
logger.debug("GET /abonnements/{}", id);
Abonnement abonnement = abonnementService.findByIdRequired(id);
return Response.ok(abonnement).build();
}
@GET
@Path("/actifs")
@Operation(summary = "Récupérer tous les abonnements actifs")
@APIResponse(responseCode = "200", description = "Liste des abonnements actifs")
public Response getAbonnementsActifs() {
logger.debug("GET /abonnements/actifs");
List<Abonnement> abonnements = abonnementService.findActifs();
Map<String, Object> response = new HashMap<>();
response.put("abonnements", abonnements);
response.put("total", abonnements.size());
return Response.ok(response).build();
}
@GET
@Path("/expires")
@Operation(summary = "Récupérer tous les abonnements expirés")
@APIResponse(responseCode = "200", description = "Liste des abonnements expirés")
public Response getAbonnementsExpires() {
logger.debug("GET /abonnements/expires");
List<Abonnement> abonnements = abonnementService.findExpires();
Map<String, Object> response = new HashMap<>();
response.put("abonnements", abonnements);
response.put("total", abonnements.size());
return Response.ok(response).build();
}
@GET
@Path("/bientot-expires")
@Operation(summary = "Récupérer les abonnements qui arrivent à expiration")
@APIResponse(responseCode = "200", description = "Liste des abonnements bientôt expirés")
public Response getAbonnementsBientotExpires(
@Parameter(description = "Nombre de jours") @QueryParam("jours") @DefaultValue("7") int jours) {
logger.debug("GET /abonnements/bientot-expires - jours: {}", jours);
List<Abonnement> abonnements = abonnementService.findBientotExpires(jours);
Map<String, Object> response = new HashMap<>();
response.put("abonnements", abonnements);
response.put("total", abonnements.size());
return Response.ok(response).build();
}
@GET
@Path("/entreprise/{entrepriseId}")
@Operation(summary = "Récupérer l'abonnement actif d'une entreprise")
@APIResponse(responseCode = "200", description = "Abonnement actif trouvé")
@APIResponse(responseCode = "404", description = "Aucun abonnement actif pour cette entreprise")
public Response getAbonnementActifByEntreprise(
@Parameter(description = "ID de l'entreprise") @PathParam("entrepriseId") UUID entrepriseId) {
logger.debug("GET /abonnements/entreprise/{}", entrepriseId);
return abonnementService
.findAbonnementActifByEntreprise(entrepriseId)
.map(abonnement -> Response.ok(abonnement).build())
.orElse(
Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "Aucun abonnement actif pour cette entreprise"))
.build());
}
@GET
@Path("/entreprise/{entrepriseId}/historique")
@Operation(summary = "Récupérer l'historique des abonnements d'une entreprise")
@APIResponse(responseCode = "200", description = "Historique récupéré avec succès")
public Response getHistoriqueByEntreprise(
@Parameter(description = "ID de l'entreprise") @PathParam("entrepriseId") UUID entrepriseId) {
logger.debug("GET /abonnements/entreprise/{}/historique", entrepriseId);
List<Abonnement> abonnements = abonnementService.findByEntreprise(entrepriseId);
Map<String, Object> response = new HashMap<>();
response.put("abonnements", abonnements);
response.put("total", abonnements.size());
return Response.ok(response).build();
}
@GET
@Path("/statistics")
@Operation(summary = "Récupérer les statistiques des abonnements")
@APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès")
public Response getStatistics() {
logger.debug("GET /abonnements/statistics");
Map<String, Object> stats = abonnementService.getStatistics();
return Response.ok(stats).build();
}
@GET
@Path("/plans")
@Operation(summary = "Récupérer les plans tarifaires disponibles")
@APIResponse(responseCode = "200", description = "Plans récupérés avec succès")
public Response getPlans() {
logger.debug("GET /abonnements/plans");
Map<String, Object> plans = abonnementService.getPlans();
return Response.ok(plans).build();
}
// === ENDPOINTS DE CRÉATION ===
@POST
@Authenticated
@Operation(summary = "Créer un nouvel abonnement")
@APIResponse(responseCode = "201", description = "Abonnement créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
@APIResponse(responseCode = "409", description = "Un abonnement actif existe déjà")
public Response createAbonnement(@Valid @NotNull Abonnement abonnement) {
logger.info("POST /abonnements - Création d'un abonnement");
Abonnement created = abonnementService.create(abonnement);
return Response.status(Response.Status.CREATED).entity(created).build();
}
@POST
@Path("/mensuel")
@Authenticated
@Operation(summary = "Créer un abonnement mensuel")
@APIResponse(responseCode = "201", description = "Abonnement mensuel créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response createAbonnementMensuel(
@Parameter(description = "ID de l'entreprise") @QueryParam("entrepriseId") @NotNull UUID entrepriseId,
@Parameter(description = "Type d'abonnement") @QueryParam("type") @NotNull String type,
@Parameter(description = "Méthode de paiement") @QueryParam("methodePaiement") String methodePaiement) {
logger.info(
"POST /abonnements/mensuel - entrepriseId: {}, type: {}", entrepriseId, type);
try {
TypeAbonnement typeEnum = TypeAbonnement.valueOf(type.toUpperCase());
Abonnement created =
abonnementService.createAbonnementMensuel(entrepriseId, typeEnum, methodePaiement);
return Response.status(Response.Status.CREATED).entity(created).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Type d'abonnement invalide: " + type))
.build();
}
}
@POST
@Path("/annuel")
@Authenticated
@Operation(summary = "Créer un abonnement annuel")
@APIResponse(responseCode = "201", description = "Abonnement annuel créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response createAbonnementAnnuel(
@Parameter(description = "ID de l'entreprise") @QueryParam("entrepriseId") @NotNull UUID entrepriseId,
@Parameter(description = "Type d'abonnement") @QueryParam("type") @NotNull String type,
@Parameter(description = "Méthode de paiement") @QueryParam("methodePaiement") String methodePaiement) {
logger.info("POST /abonnements/annuel - entrepriseId: {}, type: {}", entrepriseId, type);
try {
TypeAbonnement typeEnum = TypeAbonnement.valueOf(type.toUpperCase());
Abonnement created =
abonnementService.createAbonnementAnnuel(entrepriseId, typeEnum, methodePaiement);
return Response.status(Response.Status.CREATED).entity(created).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Type d'abonnement invalide: " + type))
.build();
}
}
// === ENDPOINTS DE MISE À JOUR ===
@PUT
@Path("/{id}")
@Authenticated
@Operation(summary = "Mettre à jour un abonnement")
@APIResponse(responseCode = "200", description = "Abonnement mis à jour avec succès")
@APIResponse(responseCode = "404", description = "Abonnement non trouvé")
public Response updateAbonnement(
@Parameter(description = "ID de l'abonnement") @PathParam("id") UUID id,
@Valid @NotNull Abonnement abonnementUpdate) {
logger.info("PUT /abonnements/{}", id);
Abonnement updated = abonnementService.update(id, abonnementUpdate);
return Response.ok(updated).build();
}
@POST
@Path("/{id}/renouveler")
@Authenticated
@Operation(summary = "Renouveler un abonnement")
@APIResponse(responseCode = "200", description = "Abonnement renouvelé avec succès")
@APIResponse(responseCode = "404", description = "Abonnement non trouvé")
public Response renouvelerAbonnement(
@Parameter(description = "ID de l'abonnement") @PathParam("id") UUID id,
@Parameter(description = "Renouvellement annuel") @QueryParam("annuel") @DefaultValue("false") boolean annuel) {
logger.info("POST /abonnements/{}/renouveler - annuel: {}", id, annuel);
Abonnement renewed = abonnementService.renouveler(id, annuel);
return Response.ok(renewed).build();
}
@PUT
@Path("/{id}/changer-type")
@Authenticated
@Operation(summary = "Changer le type d'abonnement (upgrade/downgrade)")
@APIResponse(responseCode = "200", description = "Type changé avec succès")
@APIResponse(responseCode = "404", description = "Abonnement non trouvé")
@APIResponse(responseCode = "400", description = "Type invalide")
public Response changerType(
@Parameter(description = "ID de l'abonnement") @PathParam("id") UUID id,
@Parameter(description = "Nouveau type") @QueryParam("type") @NotNull String type) {
logger.info("PUT /abonnements/{}/changer-type - nouveauType: {}", id, type);
try {
TypeAbonnement typeEnum = TypeAbonnement.valueOf(type.toUpperCase());
Abonnement updated = abonnementService.changerType(id, typeEnum);
return Response.ok(updated).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Type d'abonnement invalide: " + type))
.build();
}
}
@PUT
@Path("/{id}/toggle-auto-renew")
@Authenticated
@Operation(summary = "Activer/désactiver le renouvellement automatique")
@APIResponse(responseCode = "200", description = "Renouvellement automatique modifié")
@APIResponse(responseCode = "404", description = "Abonnement non trouvé")
public Response toggleAutoRenew(@Parameter(description = "ID de l'abonnement") @PathParam("id") UUID id) {
logger.info("PUT /abonnements/{}/toggle-auto-renew", id);
Abonnement updated = abonnementService.toggleAutoRenouvellement(id);
return Response.ok(updated).build();
}
@POST
@Path("/{id}/annuler")
@Authenticated
@Operation(summary = "Annuler un abonnement")
@APIResponse(responseCode = "204", description = "Abonnement annulé avec succès")
@APIResponse(responseCode = "404", description = "Abonnement non trouvé")
public Response annulerAbonnement(@Parameter(description = "ID de l'abonnement") @PathParam("id") UUID id) {
logger.info("POST /abonnements/{}/annuler", id);
abonnementService.annuler(id);
return Response.status(Response.Status.NO_CONTENT).build();
}
@POST
@Path("/{id}/suspendre")
@Authenticated
@Operation(summary = "Suspendre un abonnement")
@APIResponse(responseCode = "204", description = "Abonnement suspendu avec succès")
@APIResponse(responseCode = "404", description = "Abonnement non trouvé")
public Response suspendreAbonnement(@Parameter(description = "ID de l'abonnement") @PathParam("id") UUID id) {
logger.info("POST /abonnements/{}/suspendre", id);
abonnementService.suspendre(id);
return Response.status(Response.Status.NO_CONTENT).build();
}
@POST
@Path("/{id}/reactiver")
@Authenticated
@Operation(summary = "Réactiver un abonnement suspendu")
@APIResponse(responseCode = "204", description = "Abonnement réactivé avec succès")
@APIResponse(responseCode = "404", description = "Abonnement non trouvé")
public Response reactiverAbonnement(@Parameter(description = "ID de l'abonnement") @PathParam("id") UUID id) {
logger.info("POST /abonnements/{}/reactiver", id);
abonnementService.reactiver(id);
return Response.status(Response.Status.NO_CONTENT).build();
}
// === ENDPOINTS DE SUPPRESSION ===
@DELETE
@Path("/{id}")
@Authenticated
@Operation(summary = "Supprimer définitivement un abonnement")
@APIResponse(responseCode = "204", description = "Abonnement supprimé définitivement")
@APIResponse(responseCode = "404", description = "Abonnement non trouvé")
public Response deleteAbonnement(@Parameter(description = "ID de l'abonnement") @PathParam("id") UUID id) {
logger.info("DELETE /abonnements/{}", id);
abonnementService.deletePermanently(id);
return Response.status(Response.Status.NO_CONTENT).build();
}
}

View File

@@ -9,6 +9,8 @@ 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.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.Map;
import org.eclipse.microprofile.jwt.JsonWebToken;
@@ -32,6 +34,52 @@ public class AuthResource {
@Inject
JsonWebToken jwt;
/**
* Redirige vers Keycloak pour l'authentification
* Architecture 2025 : Redirection directe vers https://security.lions.dev pour l'authentification
*/
@GET
@Path("/login")
@PermitAll
@Operation(
summary = "Initier l'authentification Keycloak",
description = "Redirige l'utilisateur vers Keycloak (https://security.lions.dev) pour l'authentification OAuth2/OIDC")
@APIResponse(responseCode = "302", description = "Redirection vers Keycloak pour authentification")
public Response login(@Context SecurityContext securityContext) {
try {
logger.info("Redirection vers Keycloak pour authentification");
// Construction de l'URL Keycloak pour l'authentification
String keycloakUrl = "https://security.lions.dev/realms/btpxpress/protocol/openid_connect/auth";
String clientId = "btpxpress-backend";
String redirectUri = "http://localhost:8080/api/v1/auth/callback"; // Peut être configuré dynamiquement
String responseType = "code";
String scope = "openid profile email";
// Construction de l'URL complète avec paramètres
java.net.URI authUri = java.net.URI.create(
String.format(
"%s?client_id=%s&redirect_uri=%s&response_type=%s&scope=%s",
keycloakUrl,
clientId,
URLEncoder.encode(redirectUri, StandardCharsets.UTF_8),
responseType,
URLEncoder.encode(scope, StandardCharsets.UTF_8)
)
);
logger.debug("Redirection vers Keycloak: {}", authUri);
return Response.status(Response.Status.FOUND)
.location(authUri)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la redirection vers Keycloak", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la redirection vers Keycloak", "message", e.getMessage()))
.build();
}
}
/**
* Récupère les informations de l'utilisateur connecté depuis le token JWT
*/

View File

@@ -0,0 +1,337 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.BonCommandeService;
import dev.lions.btpxpress.domain.core.entity.BonCommande;
import dev.lions.btpxpress.domain.core.entity.PrioriteBonCommande;
import dev.lions.btpxpress.domain.core.entity.StatutBonCommande;
import dev.lions.btpxpress.domain.core.entity.TypeBonCommande;
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.HashMap;
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 bons de commande
* Architecture 2025 : API complète pour la gestion des bons de commande BTP
*/
@Path("/api/v1/bons-commande")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Bons de Commande", description = "Gestion des bons de commande BTP")
public class BonCommandeResource {
private static final Logger logger = LoggerFactory.getLogger(BonCommandeResource.class);
@Inject BonCommandeService bonCommandeService;
// === ENDPOINTS DE LECTURE - ARCHITECTURE 2025 ===
@GET
@Operation(summary = "Récupérer tous les bons de commande", description = "Retourne la liste complète des bons de commande")
@APIResponse(responseCode = "200", description = "Liste des bons de commande récupérée avec succès")
public Response getAllBonsCommande() {
logger.debug("GET /bons-commande");
try {
List<BonCommande> bonsCommande = bonCommandeService.findAll();
Map<String, Object> response = new HashMap<>();
response.put("bonsCommande", bonsCommande);
response.put("total", bonsCommande.size());
return Response.ok(response).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des bons de commande", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération des bons de commande", "message", e.getMessage()))
.build();
}
}
@GET
@Path("/{id}")
@Operation(summary = "Récupérer un bon de commande par ID", description = "Retourne les détails d'un bon de commande")
@APIResponse(responseCode = "200", description = "Bon de commande trouvé")
@APIResponse(responseCode = "404", description = "Bon de commande non trouvé")
public Response getBonCommandeById(@Parameter(description = "ID du bon de commande") @PathParam("id") UUID id) {
logger.debug("GET /bons-commande/{}", id);
try {
BonCommande bonCommande = bonCommandeService.findById(id);
return Response.ok(bonCommande).build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération du bon de commande: {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération du bon de commande"))
.build();
}
}
@GET
@Path("/numero/{numero}")
@Operation(summary = "Récupérer un bon de commande par numéro", description = "Recherche un bon de commande par son numéro")
@APIResponse(responseCode = "200", description = "Bon de commande trouvé")
@APIResponse(responseCode = "404", description = "Bon de commande non trouvé")
public Response getBonCommandeByNumero(@Parameter(description = "Numéro du bon de commande") @PathParam("numero") String numero) {
logger.debug("GET /bons-commande/numero/{}", numero);
try {
BonCommande bonCommande = bonCommandeService.findByNumero(numero);
if (bonCommande == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "Bon de commande non trouvé"))
.build();
}
return Response.ok(bonCommande).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération du bon de commande par numéro: {}", numero, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération du bon de commande"))
.build();
}
}
@GET
@Path("/statut/{statut}")
@Operation(summary = "Récupérer les bons de commande par statut", description = "Filtre les bons de commande par statut")
@APIResponse(responseCode = "200", description = "Liste des bons de commande filtrés")
public Response getBonsCommandeByStatut(@Parameter(description = "Statut du bon de commande") @PathParam("statut") StatutBonCommande statut) {
logger.debug("GET /bons-commande/statut/{}", statut);
try {
List<BonCommande> bonsCommande = bonCommandeService.findByStatut(statut);
Map<String, Object> response = new HashMap<>();
response.put("bonsCommande", bonsCommande);
response.put("total", bonsCommande.size());
return Response.ok(response).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des bons de commande par statut: {}", statut, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération des bons de commande"))
.build();
}
}
@GET
@Path("/urgents")
@Operation(summary = "Récupérer les bons de commande urgents", description = "Liste les bons de commande prioritaires")
@APIResponse(responseCode = "200", description = "Liste des bons de commande urgents")
public Response getBonsCommandeUrgents() {
logger.debug("GET /bons-commande/urgents");
try {
List<BonCommande> bonsCommande = bonCommandeService.findUrgents();
Map<String, Object> response = new HashMap<>();
response.put("bonsCommande", bonsCommande);
response.put("total", bonsCommande.size());
return Response.ok(response).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des bons de commande urgents", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération des bons de commande"))
.build();
}
}
@GET
@Path("/search")
@Operation(summary = "Rechercher des bons de commande", description = "Recherche textuelle dans les bons de commande")
@APIResponse(responseCode = "200", description = "Résultats de recherche")
@APIResponse(responseCode = "400", description = "Terme de recherche requis")
public Response searchBonsCommande(@Parameter(description = "Terme de recherche") @QueryParam("term") String searchTerm) {
logger.debug("GET /bons-commande/search - term: {}", searchTerm);
try {
if (searchTerm == null || searchTerm.trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Terme de recherche requis"))
.build();
}
List<BonCommande> bonsCommande = bonCommandeService.searchCommandes(searchTerm);
Map<String, Object> response = new HashMap<>();
response.put("bonsCommande", bonsCommande);
response.put("total", bonsCommande.size());
return Response.ok(response).build();
} catch (Exception e) {
logger.error("Erreur lors de la recherche de bons de commande: {}", searchTerm, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la recherche"))
.build();
}
}
@GET
@Path("/statistiques")
@Operation(summary = "Récupérer les statistiques des bons de commande", description = "Retourne des statistiques globales")
@APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès")
public Response getStatistiques() {
logger.debug("GET /bons-commande/statistiques");
try {
Map<String, Object> stats = bonCommandeService.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(Map.of("error", "Erreur lors de la récupération des statistiques"))
.build();
}
}
// === ENDPOINTS DE CRÉATION - ARCHITECTURE 2025 ===
@POST
@Authenticated
@Operation(summary = "Créer un nouveau bon de commande", description = "Crée un nouveau bon de commande")
@APIResponse(responseCode = "201", description = "Bon de commande créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response createBonCommande(@Valid @NotNull BonCommande bonCommande) {
logger.info("POST /bons-commande - Création d'un bon de commande");
try {
BonCommande nouveauBonCommande = bonCommandeService.create(bonCommande);
return Response.status(Response.Status.CREATED).entity(nouveauBonCommande).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
logger.error("Erreur lors de la création du bon de commande", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la création du bon de commande"))
.build();
}
}
// === ENDPOINTS DE MISE À JOUR - ARCHITECTURE 2025 ===
@PUT
@Path("/{id}")
@Authenticated
@Operation(summary = "Mettre à jour un bon de commande", description = "Met à jour les informations d'un bon de commande")
@APIResponse(responseCode = "200", description = "Bon de commande mis à jour avec succès")
@APIResponse(responseCode = "404", description = "Bon de commande non trouvé")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response updateBonCommande(
@Parameter(description = "ID du bon de commande") @PathParam("id") UUID id,
@Valid @NotNull BonCommande bonCommandeData) {
logger.info("PUT /bons-commande/{} - Mise à jour", id);
try {
BonCommande bonCommande = bonCommandeService.update(id, bonCommandeData);
return Response.ok(bonCommande).build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
logger.error("Erreur lors de la mise à jour du bon de commande: {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la mise à jour du bon de commande"))
.build();
}
}
@POST
@Path("/{id}/valider")
@Authenticated
@Operation(summary = "Valider un bon de commande", description = "Valide un bon de commande en attente")
@APIResponse(responseCode = "200", description = "Bon de commande validé avec succès")
@APIResponse(responseCode = "404", description = "Bon de commande non trouvé")
@APIResponse(responseCode = "400", description = "Le bon de commande ne peut pas être validé")
public Response validerBonCommande(
@Parameter(description = "ID du bon de commande") @PathParam("id") UUID id,
Map<String, String> payload) {
logger.info("POST /bons-commande/{}/valider", id);
try {
String commentaires = payload != null ? payload.get("commentaires") : null;
BonCommande bonCommande = bonCommandeService.validerBonCommande(id, commentaires);
return Response.ok(bonCommande).build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
logger.error("Erreur lors de la validation du bon de commande: {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la validation du bon de commande"))
.build();
}
}
@POST
@Path("/{id}/annuler")
@Authenticated
@Operation(summary = "Annuler un bon de commande", description = "Annule un bon de commande")
@APIResponse(responseCode = "200", description = "Bon de commande annulé avec succès")
@APIResponse(responseCode = "404", description = "Bon de commande non trouvé")
@APIResponse(responseCode = "400", description = "Le bon de commande ne peut pas être annulé")
public Response annulerBonCommande(
@Parameter(description = "ID du bon de commande") @PathParam("id") UUID id,
Map<String, String> payload) {
logger.info("POST /bons-commande/{}/annuler", id);
try {
String motif = payload != null ? payload.get("motif") : null;
BonCommande bonCommande = bonCommandeService.annulerBonCommande(id, motif);
return Response.ok(bonCommande).build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
logger.error("Erreur lors de l'annulation du bon de commande: {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de l'annulation du bon de commande"))
.build();
}
}
// === ENDPOINTS DE SUPPRESSION - ARCHITECTURE 2025 ===
@DELETE
@Path("/{id}")
@Authenticated
@Operation(summary = "Supprimer un bon de commande", description = "Supprime un bon de commande")
@APIResponse(responseCode = "204", description = "Bon de commande supprimé avec succès")
@APIResponse(responseCode = "404", description = "Bon de commande non trouvé")
public Response deleteBonCommande(@Parameter(description = "ID du bon de commande") @PathParam("id") UUID id) {
logger.info("DELETE /bons-commande/{}", id);
try {
bonCommandeService.delete(id);
return Response.noContent().build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
logger.error("Erreur lors de la suppression du bon de commande: {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la suppression du bon de commande"))
.build();
}
}
}

View File

@@ -0,0 +1,519 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.ComparaisonFournisseurService;
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.math.BigDecimal;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* API REST pour la comparaison des fournisseurs EXPOSITION: Endpoints pour l'aide à la décision et
* l'optimisation des achats BTP
*/
@Path("/api/v1/comparaisons-fournisseurs")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ComparaisonFournisseurResource {
private static final Logger logger =
LoggerFactory.getLogger(ComparaisonFournisseurResource.class);
@Inject ComparaisonFournisseurService comparaisonService;
// === ENDPOINTS DE CONSULTATION ===
@GET
@Path("/")
public Response findAll(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("50") int size) {
try {
logger.debug("GET /api/comparaisons-fournisseurs/ - page: {}, size: {}", page, size);
List<ComparaisonFournisseur> comparaisons;
if (page > 0 || size < 1000) {
comparaisons = comparaisonService.findAll(page, size);
} else {
comparaisons = comparaisonService.findAll();
}
return Response.ok(comparaisons).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des comparaisons", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des comparaisons: " + e.getMessage())
.build();
}
}
@GET
@Path("/{id}")
public Response findById(@PathParam("id") UUID id) {
try {
logger.debug("GET /api/comparaisons-fournisseurs/{}", id);
ComparaisonFournisseur comparaison = comparaisonService.findByIdRequired(id);
return Response.ok(comparaison).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération de la comparaison: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération de la comparaison: " + e.getMessage())
.build();
}
}
@GET
@Path("/materiel/{materielId}")
public Response findByMateriel(@PathParam("materielId") UUID materielId) {
try {
logger.debug("GET /api/comparaisons-fournisseurs/materiel/{}", materielId);
List<ComparaisonFournisseur> comparaisons = comparaisonService.findByMateriel(materielId);
return Response.ok(comparaisons).build();
} catch (Exception e) {
logger.error(
"Erreur lors de la récupération des comparaisons pour matériel: " + materielId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des comparaisons: " + e.getMessage())
.build();
}
}
@GET
@Path("/fournisseur/{fournisseurId}")
public Response findByFournisseur(@PathParam("fournisseurId") UUID fournisseurId) {
try {
logger.debug("GET /api/comparaisons-fournisseurs/fournisseur/{}", fournisseurId);
List<ComparaisonFournisseur> comparaisons =
comparaisonService.findByFournisseur(fournisseurId);
return Response.ok(comparaisons).build();
} catch (Exception e) {
logger.error(
"Erreur lors de la récupération des comparaisons pour fournisseur: " + fournisseurId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des comparaisons: " + e.getMessage())
.build();
}
}
@GET
@Path("/session/{sessionId}")
public Response findBySession(@PathParam("sessionId") String sessionId) {
try {
logger.debug("GET /api/comparaisons-fournisseurs/session/{}", sessionId);
List<ComparaisonFournisseur> comparaisons = comparaisonService.findBySession(sessionId);
return Response.ok(comparaisons).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des comparaisons pour session: " + sessionId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des comparaisons: " + e.getMessage())
.build();
}
}
@GET
@Path("/search")
public Response search(@QueryParam("terme") String terme) {
try {
logger.debug("GET /api/comparaisons-fournisseurs/search?terme={}", terme);
List<ComparaisonFournisseur> resultats = comparaisonService.search(terme);
return Response.ok(resultats).build();
} catch (Exception e) {
logger.error("Erreur lors de la recherche avec terme: " + terme, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la recherche: " + e.getMessage())
.build();
}
}
// === ENDPOINTS MÉTIER SPÉCIALISÉS ===
@GET
@Path("/meilleures-offres/{materielId}")
public Response findMeilleuresOffres(
@PathParam("materielId") UUID materielId,
@QueryParam("limite") @DefaultValue("5") int limite) {
try {
logger.debug("GET /api/comparaisons-fournisseurs/meilleures-offres/{}", materielId);
List<ComparaisonFournisseur> meilleures =
comparaisonService.findMeilleuresOffres(materielId, limite);
return Response.ok(meilleures).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des meilleures offres: " + materielId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des meilleures offres: " + e.getMessage())
.build();
}
}
@GET
@Path("/recommandees")
public Response findOffresRecommandees() {
try {
logger.debug("GET /api/comparaisons-fournisseurs/recommandees");
List<ComparaisonFournisseur> recommandees = comparaisonService.findOffresRecommandees();
return Response.ok(recommandees).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des offres recommandées", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des offres recommandées: " + e.getMessage())
.build();
}
}
@GET
@Path("/gamme-prix")
public Response findByGammePrix(
@QueryParam("prixMin") @NotNull BigDecimal prixMin,
@QueryParam("prixMax") @NotNull BigDecimal prixMax) {
try {
logger.debug(
"GET /api/comparaisons-fournisseurs/gamme-prix?prixMin={}&prixMax={}", prixMin, prixMax);
List<ComparaisonFournisseur> comparaisons =
comparaisonService.findByGammePrix(prixMin, prixMax);
return Response.ok(comparaisons).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la recherche par gamme de prix", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la recherche par gamme de prix: " + e.getMessage())
.build();
}
}
@GET
@Path("/disponibles-delai")
public Response findDisponiblesDansDelai(
@QueryParam("maxJours") @DefaultValue("30") int maxJours) {
try {
logger.debug("GET /api/comparaisons-fournisseurs/disponibles-delai?maxJours={}", maxJours);
List<ComparaisonFournisseur> disponibles =
comparaisonService.findDisponiblesDansDelai(maxJours);
return Response.ok(disponibles).build();
} catch (Exception e) {
logger.error("Erreur lors de la recherche par délai", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la recherche par délai: " + e.getMessage())
.build();
}
}
// === ENDPOINTS DE CRÉATION ET GESTION ===
@POST
@Path("/lancer-comparaison")
public Response lancerComparaison(@Valid LancerComparaisonRequest request) {
try {
logger.info("POST /api/comparaisons-fournisseurs/lancer-comparaison");
String sessionId =
comparaisonService.lancerComparaison(
request.materielId,
request.quantiteDemandee,
request.uniteDemandee,
request.dateDebutSouhaitee,
request.dateFinSouhaitee,
request.lieuLivraison,
request.evaluateur);
return Response.status(Response.Status.CREATED)
.entity(Map.of("sessionId", sessionId))
.build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors du lancement de la comparaison", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du lancement de la comparaison: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}")
public Response updateComparaison(
@PathParam("id") UUID id, @Valid UpdateComparaisonRequest request) {
try {
logger.info("PUT /api/comparaisons-fournisseurs/{}", id);
ComparaisonFournisseurService.ComparaisonUpdateRequest updateRequest =
mapToServiceRequest(request);
ComparaisonFournisseur comparaison = comparaisonService.updateComparaison(id, updateRequest);
return Response.ok(comparaison).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 la comparaison: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la mise à jour de la comparaison: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/calculer-scores")
public Response calculerScores(@PathParam("id") UUID id, @Valid CalculerScoresRequest request) {
try {
logger.info("PUT /api/comparaisons-fournisseurs/{}/calculer-scores", id);
ComparaisonFournisseur comparaison = comparaisonService.findByIdRequired(id);
comparaisonService.calculerScores(comparaison, request.poidsCriteres);
return Response.ok(comparaison).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors du calcul des scores: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du calcul des scores: " + e.getMessage())
.build();
}
}
@PUT
@Path("/session/{sessionId}/classer")
public Response classerComparaisons(@PathParam("sessionId") String sessionId) {
try {
logger.info("PUT /api/comparaisons-fournisseurs/session/{}/classer", sessionId);
comparaisonService.classerComparaisons(sessionId);
List<ComparaisonFournisseur> comparaisons = comparaisonService.findBySession(sessionId);
return Response.ok(
Map.of("message", "Classement effectué avec succès", "comparaisons", comparaisons))
.build();
} catch (Exception e) {
logger.error("Erreur lors du classement des comparaisons: " + sessionId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du classement des comparaisons: " + e.getMessage())
.build();
}
}
// === ENDPOINTS D'ANALYSE ET RAPPORTS ===
@GET
@Path("/statistiques")
public Response getStatistiques() {
try {
logger.debug("GET /api/comparaisons-fournisseurs/statistiques");
Map<String, Object> statistiques = comparaisonService.getStatistiques();
return Response.ok(statistiques).build();
} catch (Exception e) {
logger.error("Erreur lors de la génération des statistiques", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la génération des statistiques: " + e.getMessage())
.build();
}
}
@GET
@Path("/evolution-prix/{materielId}")
public Response analyserEvolutionPrix(
@PathParam("materielId") UUID materielId,
@QueryParam("dateDebut") @NotNull LocalDate dateDebut,
@QueryParam("dateFin") @NotNull LocalDate dateFin) {
try {
logger.debug("GET /api/comparaisons-fournisseurs/evolution-prix/{}", materielId);
List<Object> evolution =
comparaisonService.analyserEvolutionPrix(materielId, dateDebut, dateFin);
return Response.ok(evolution).build();
} catch (Exception e) {
logger.error("Erreur lors de l'analyse d'évolution des prix: " + materielId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de l'analyse d'évolution des prix: " + e.getMessage())
.build();
}
}
@GET
@Path("/delais-fournisseurs")
public Response analyserDelaisFournisseurs() {
try {
logger.debug("GET /api/comparaisons-fournisseurs/delais-fournisseurs");
List<Object> delais = comparaisonService.analyserDelaisFournisseurs();
return Response.ok(delais).build();
} catch (Exception e) {
logger.error("Erreur lors de l'analyse des délais fournisseurs", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de l'analyse des délais fournisseurs: " + e.getMessage())
.build();
}
}
@GET
@Path("/rapport/{sessionId}")
public Response genererRapportComparaison(@PathParam("sessionId") String sessionId) {
try {
logger.debug("GET /api/comparaisons-fournisseurs/rapport/{}", sessionId);
Map<String, Object> rapport = comparaisonService.genererRapportComparaison(sessionId);
return Response.ok(rapport).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la génération du rapport: " + sessionId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la génération du rapport: " + e.getMessage())
.build();
}
}
// === ENDPOINTS UTILITAIRES ===
@GET
@Path("/criteres-comparaison")
public Response getCriteresComparaison() {
try {
CritereComparaison[] criteres = CritereComparaison.values();
List<Map<String, Object>> criteresInfo =
Arrays.stream(criteres)
.map(
critere -> {
Map<String, Object> map = new HashMap<>();
map.put("code", critere.name());
map.put("libelle", critere.getLibelle());
map.put("description", critere.getDescription());
map.put("poidsDefaut", critere.getPoidsDefaut());
map.put("uniteMesure", critere.getUniteMesure());
map.put("icone", critere.getIcone());
map.put("couleur", critere.getCouleur());
return map;
})
.collect(Collectors.toList());
return Response.ok(criteresInfo).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des critères", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des critères: " + e.getMessage())
.build();
}
}
// === MÉTHODES UTILITAIRES ===
private ComparaisonFournisseurService.ComparaisonUpdateRequest mapToServiceRequest(
UpdateComparaisonRequest request) {
ComparaisonFournisseurService.ComparaisonUpdateRequest serviceRequest =
new ComparaisonFournisseurService.ComparaisonUpdateRequest();
serviceRequest.disponible = request.disponible;
serviceRequest.quantiteDisponible = request.quantiteDisponible;
serviceRequest.dateDisponibilite = request.dateDisponibilite;
serviceRequest.delaiLivraisonJours = request.delaiLivraisonJours;
serviceRequest.prixUnitaireHT = request.prixUnitaireHT;
serviceRequest.fraisLivraison = request.fraisLivraison;
serviceRequest.fraisInstallation = request.fraisInstallation;
serviceRequest.fraisMaintenance = request.fraisMaintenance;
serviceRequest.cautionDemandee = request.cautionDemandee;
serviceRequest.remiseAppliquee = request.remiseAppliquee;
serviceRequest.dureeValiditeOffre = request.dureeValiditeOffre;
serviceRequest.delaiPaiement = request.delaiPaiement;
serviceRequest.garantieMois = request.garantieMois;
serviceRequest.maintenanceIncluse = request.maintenanceIncluse;
serviceRequest.formationIncluse = request.formationIncluse;
serviceRequest.noteQualite = request.noteQualite;
serviceRequest.noteFiabilite = request.noteFiabilite;
serviceRequest.distanceKm = request.distanceKm;
serviceRequest.conditionsParticulieres = request.conditionsParticulieres;
serviceRequest.avantages = request.avantages;
serviceRequest.inconvenients = request.inconvenients;
serviceRequest.commentairesEvaluateur = request.commentairesEvaluateur;
serviceRequest.recommandations = request.recommandations;
serviceRequest.poidsCriteres = request.poidsCriteres;
return serviceRequest;
}
// === CLASSES DE REQUÊTE ===
public static class LancerComparaisonRequest {
@NotNull public UUID materielId;
@NotNull public BigDecimal quantiteDemandee;
public String uniteDemandee;
public LocalDate dateDebutSouhaitee;
public LocalDate dateFinSouhaitee;
public String lieuLivraison;
public String evaluateur;
}
public static class UpdateComparaisonRequest {
public Boolean disponible;
public BigDecimal quantiteDisponible;
public LocalDate dateDisponibilite;
public Integer delaiLivraisonJours;
public BigDecimal prixUnitaireHT;
public BigDecimal fraisLivraison;
public BigDecimal fraisInstallation;
public BigDecimal fraisMaintenance;
public BigDecimal cautionDemandee;
public BigDecimal remiseAppliquee;
public Integer dureeValiditeOffre;
public Integer delaiPaiement;
public Integer garantieMois;
public Boolean maintenanceIncluse;
public Boolean formationIncluse;
public BigDecimal noteQualite;
public BigDecimal noteFiabilite;
public BigDecimal distanceKm;
public String conditionsParticulieres;
public String avantages;
public String inconvenients;
public String commentairesEvaluateur;
public String recommandations;
public Map<CritereComparaison, Integer> poidsCriteres;
}
public static class CalculerScoresRequest {
public Map<CritereComparaison, Integer> poidsCriteres;
}
}

View File

@@ -0,0 +1,322 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.EntrepriseProfileService;
import dev.lions.btpxpress.domain.core.entity.EntrepriseProfile;
import dev.lions.btpxpress.domain.core.entity.TypeAbonnement;
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.math.BigDecimal;
import java.util.HashMap;
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 profils d'entreprise
* Architecture 2025 : API complète pour la gestion des profils d'entreprise et leurs notations
*/
@Path("/api/v1/entreprise-profiles")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Profils d'Entreprise", description = "Gestion des profils d'entreprise")
public class EntrepriseProfileResource {
private static final Logger logger =
LoggerFactory.getLogger(EntrepriseProfileResource.class);
@Inject EntrepriseProfileService entrepriseProfileService;
// === ENDPOINTS DE LECTURE - ARCHITECTURE 2025 ===
@GET
@Operation(
summary = "Récupérer tous les profils d'entreprise visibles",
description = "Retourne la liste complète des profils d'entreprise visibles, triés par note")
@APIResponse(responseCode = "200", description = "Liste des profils récupérée avec succès")
public Response getAllProfiles(
@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 /entreprise-profiles - page: {}, size: {}", page, size);
List<EntrepriseProfile> profiles;
if (page == 0 && size == 20) {
profiles = entrepriseProfileService.findAll();
} else {
profiles = entrepriseProfileService.findAll(page, size);
}
Map<String, Object> response = new HashMap<>();
response.put("profiles", profiles);
response.put("total", profiles.size());
return Response.ok(response).build();
}
@GET
@Path("/{id}")
@Operation(
summary = "Récupérer un profil d'entreprise par ID",
description = "Retourne les détails complets d'un profil d'entreprise")
@APIResponse(responseCode = "200", description = "Profil trouvé")
@APIResponse(responseCode = "404", description = "Profil non trouvé")
public Response getProfileById(@Parameter(description = "ID du profil") @PathParam("id") UUID id) {
logger.debug("GET /entreprise-profiles/{}", id);
EntrepriseProfile profile = entrepriseProfileService.findByIdRequired(id);
return Response.ok(profile).build();
}
@GET
@Path("/search")
@Operation(
summary = "Rechercher des profils d'entreprise",
description = "Recherche textuelle complète dans les profils")
@APIResponse(responseCode = "200", description = "Résultats de recherche")
public Response searchProfiles(
@Parameter(description = "Terme de recherche") @QueryParam("q") String searchTerm,
@Parameter(description = "Zone d'intervention") @QueryParam("zone") String zone,
@Parameter(description = "Spécialité") @QueryParam("specialite") String specialite,
@Parameter(description = "Région") @QueryParam("region") String region,
@Parameter(description = "Ville") @QueryParam("ville") String ville,
@Parameter(description = "Certifié uniquement") @QueryParam("certifie") Boolean certifie,
@Parameter(description = "Type d'abonnement") @QueryParam("typeAbonnement")
TypeAbonnement typeAbonnement) {
logger.debug(
"GET /entreprise-profiles/search - q: {}, zone: {}, specialite: {}, region: {}, ville: {}, certifie: {}",
searchTerm,
zone,
specialite,
region,
ville,
certifie);
List<EntrepriseProfile> profiles;
// Recherche par terme de recherche complet
if (searchTerm != null && !searchTerm.trim().isEmpty()) {
profiles = entrepriseProfileService.searchFullText(searchTerm);
}
// Recherche par zone d'intervention
else if (zone != null && !zone.trim().isEmpty()) {
profiles = entrepriseProfileService.findByZoneIntervention(zone);
}
// Recherche par spécialité
else if (specialite != null && !specialite.trim().isEmpty()) {
profiles = entrepriseProfileService.findBySpecialite(specialite);
}
// Recherche par région
else if (region != null && !region.trim().isEmpty()) {
profiles = entrepriseProfileService.findByRegion(region);
}
// Recherche par ville
else if (ville != null && !ville.trim().isEmpty()) {
profiles = entrepriseProfileService.findByVille(ville);
}
// Recherche par certification
else if (certifie != null) {
profiles = entrepriseProfileService.findByCertifie(certifie);
}
// Recherche par type d'abonnement
else if (typeAbonnement != null) {
profiles = entrepriseProfileService.findByTypeAbonnement(typeAbonnement);
}
// Par défaut, retourner tous les profils
else {
profiles = entrepriseProfileService.findAll();
}
Map<String, Object> response = new HashMap<>();
response.put("profiles", profiles);
response.put("total", profiles.size());
return Response.ok(response).build();
}
@GET
@Path("/top-rated")
@Operation(
summary = "Récupérer les profils les mieux notés",
description = "Retourne les profils d'entreprise avec les meilleures notes")
@APIResponse(responseCode = "200", description = "Liste des profils les mieux notés")
public Response getTopRated(
@Parameter(description = "Nombre de profils à retourner") @QueryParam("limit")
@DefaultValue("10")
int limit) {
logger.debug("GET /entreprise-profiles/top-rated - limit: {}", limit);
List<EntrepriseProfile> profiles = entrepriseProfileService.findTopRated(limit);
Map<String, Object> response = new HashMap<>();
response.put("profiles", profiles);
response.put("total", profiles.size());
return Response.ok(response).build();
}
@GET
@Path("/statistics")
@Operation(
summary = "Récupérer les statistiques des profils",
description = "Retourne des statistiques globales sur les profils d'entreprise")
@APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès")
public Response getStatistics() {
logger.debug("GET /entreprise-profiles/statistics");
Map<String, Object> stats = entrepriseProfileService.getStatistics();
return Response.ok(stats).build();
}
@GET
@Path("/user/{userId}")
@Operation(
summary = "Récupérer le profil d'un utilisateur",
description = "Retourne le profil d'entreprise associé à un utilisateur")
@APIResponse(responseCode = "200", description = "Profil trouvé")
@APIResponse(responseCode = "404", description = "Profil non trouvé pour cet utilisateur")
public Response getProfileByUserId(
@Parameter(description = "ID de l'utilisateur") @PathParam("userId") UUID userId) {
logger.debug("GET /entreprise-profiles/user/{}", userId);
return entrepriseProfileService
.findByUserId(userId)
.map(profile -> Response.ok(profile).build())
.orElse(Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "Profil non trouvé pour cet utilisateur"))
.build());
}
// === ENDPOINTS DE CRÉATION - ARCHITECTURE 2025 ===
@POST
@Authenticated
@Operation(
summary = "Créer un nouveau profil d'entreprise",
description = "Crée un nouveau profil d'entreprise pour un utilisateur")
@APIResponse(responseCode = "201", description = "Profil créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
@APIResponse(responseCode = "409", description = "Un profil existe déjà pour cet utilisateur")
public Response createProfile(@Valid @NotNull EntrepriseProfile profile) {
logger.info("POST /entreprise-profiles - Création d'un profil: {}", profile.getNomCommercial());
EntrepriseProfile created = entrepriseProfileService.create(profile);
return Response.status(Response.Status.CREATED).entity(created).build();
}
// === ENDPOINTS DE MISE À JOUR - ARCHITECTURE 2025 ===
@PUT
@Path("/{id}")
@Authenticated
@Operation(
summary = "Mettre à jour un profil d'entreprise",
description = "Met à jour les informations d'un profil d'entreprise existant")
@APIResponse(responseCode = "200", description = "Profil mis à jour avec succès")
@APIResponse(responseCode = "404", description = "Profil non trouvé")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response updateProfile(
@Parameter(description = "ID du profil") @PathParam("id") UUID id,
@Valid @NotNull EntrepriseProfile profileUpdate) {
logger.info("PUT /entreprise-profiles/{} - Mise à jour", id);
EntrepriseProfile updated = entrepriseProfileService.update(id, profileUpdate);
return Response.ok(updated).build();
}
@PUT
@Path("/{id}/note")
@Authenticated
@Operation(
summary = "Mettre à jour la note d'un profil",
description = "Met à jour la note globale et le nombre d'avis d'un profil")
@APIResponse(responseCode = "200", description = "Note mise à jour avec succès")
@APIResponse(responseCode = "404", description = "Profil non trouvé")
public Response updateNote(
@Parameter(description = "ID du profil") @PathParam("id") UUID id,
@Parameter(description = "Nouvelle note globale") @QueryParam("note")
@NotNull
BigDecimal note,
@Parameter(description = "Nouveau nombre d'avis") @QueryParam("nombreAvis")
@DefaultValue("0")
int nombreAvis) {
logger.info("PUT /entreprise-profiles/{}/note - note: {}, nombreAvis: {}", id, note, nombreAvis);
EntrepriseProfile updated = entrepriseProfileService.updateNote(id, note, nombreAvis);
return Response.ok(updated).build();
}
@PUT
@Path("/{id}/increment-projects")
@Authenticated
@Operation(
summary = "Incrémenter le nombre de projets réalisés",
description = "Incrémente le compteur de projets réalisés pour un profil")
@APIResponse(responseCode = "200", description = "Compteur incrémenté avec succès")
@APIResponse(responseCode = "404", description = "Profil non trouvé")
public Response incrementProjects(
@Parameter(description = "ID du profil") @PathParam("id") UUID id) {
logger.debug("PUT /entreprise-profiles/{}/increment-projects", id);
EntrepriseProfile updated = entrepriseProfileService.incrementerProjets(id);
return Response.ok(updated).build();
}
@PUT
@Path("/{id}/increment-clients")
@Authenticated
@Operation(
summary = "Incrémenter le nombre de clients servis",
description = "Incrémente le compteur de clients servis pour un profil")
@APIResponse(responseCode = "200", description = "Compteur incrémenté avec succès")
@APIResponse(responseCode = "404", description = "Profil non trouvé")
public Response incrementClients(@Parameter(description = "ID du profil") @PathParam("id") UUID id) {
logger.debug("PUT /entreprise-profiles/{}/increment-clients", id);
EntrepriseProfile updated = entrepriseProfileService.incrementerClients(id);
return Response.ok(updated).build();
}
// === ENDPOINTS DE SUPPRESSION - ARCHITECTURE 2025 ===
@DELETE
@Path("/{id}")
@Authenticated
@Operation(
summary = "Supprimer un profil d'entreprise",
description = "Supprime (désactive) un profil d'entreprise")
@APIResponse(responseCode = "204", description = "Profil supprimé avec succès")
@APIResponse(responseCode = "404", description = "Profil non trouvé")
public Response deleteProfile(@Parameter(description = "ID du profil") @PathParam("id") UUID id) {
logger.info("DELETE /entreprise-profiles/{}", id);
entrepriseProfileService.delete(id);
return Response.status(Response.Status.NO_CONTENT).build();
}
@DELETE
@Path("/{id}/permanent")
@Authenticated
@Operation(
summary = "Supprimer définitivement un profil",
description = "Supprime définitivement un profil d'entreprise de la base de données")
@APIResponse(responseCode = "204", description = "Profil supprimé définitivement")
@APIResponse(responseCode = "404", description = "Profil non trouvé")
public Response deleteProfilePermanently(
@Parameter(description = "ID du profil") @PathParam("id") UUID id) {
logger.info("DELETE /entreprise-profiles/{}/permanent", id);
entrepriseProfileService.deletePermanently(id);
return Response.status(Response.Status.NO_CONTENT).build();
}
}

View File

@@ -0,0 +1,194 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.FournisseurService;
import dev.lions.btpxpress.domain.core.entity.Fournisseur;
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 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 java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Resource REST pour la gestion des fournisseurs BTP
* Architecture 2025 : API complète pour la gestion des fournisseurs
*/
@Path("/api/v1/fournisseurs")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Fournisseurs", description = "Gestion des fournisseurs BTP")
public class FournisseurResource {
@Inject
FournisseurService fournisseurService;
// === ENDPOINTS DE LECTURE - ARCHITECTURE 2025 ===
@GET
@Operation(summary = "Récupère tous les fournisseurs")
@APIResponse(responseCode = "200", description = "Liste des fournisseurs")
public Response getAllFournisseurs(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("10") int size,
@QueryParam("search") String search) {
try {
List<Fournisseur> fournisseurs;
if (search != null && !search.trim().isEmpty()) {
fournisseurs = fournisseurService.searchFournisseurs(search);
} else {
fournisseurs = fournisseurService.getAllFournisseurs(page, size);
}
return Response.ok(fournisseurs).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération des fournisseurs"))
.build();
}
}
@GET
@Path("/{id}")
@Operation(summary = "Récupère un fournisseur par ID")
@APIResponse(responseCode = "200", description = "Fournisseur trouvé")
@APIResponse(responseCode = "404", description = "Fournisseur non trouvé")
public Response getFournisseurById(
@Parameter(description = "ID du fournisseur") @PathParam("id") UUID id) {
try {
Fournisseur fournisseur = fournisseurService.getFournisseurById(id);
return Response.ok(fournisseur).build();
} catch (Exception e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "Fournisseur non trouvé"))
.build();
}
}
@GET
@Path("/search")
@Operation(summary = "Recherche des fournisseurs")
@APIResponse(responseCode = "200", description = "Résultats de la recherche")
public Response searchFournisseurs(@QueryParam("q") String query) {
try {
List<Fournisseur> fournisseurs = fournisseurService.searchFournisseurs(query);
return Response.ok(fournisseurs).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la recherche"))
.build();
}
}
@GET
@Path("/stats")
@Operation(summary = "Récupère les statistiques des fournisseurs")
@APIResponse(responseCode = "200", description = "Statistiques des fournisseurs")
public Response getFournisseurStats() {
try {
Map<String, Object> stats = fournisseurService.getFournisseurStats();
return Response.ok(stats).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors du calcul des statistiques"))
.build();
}
}
// === ENDPOINTS DE CRÉATION - ARCHITECTURE 2025 ===
@POST
@Operation(summary = "Crée un nouveau fournisseur")
@APIResponse(responseCode = "201", description = "Fournisseur créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
@APIResponse(responseCode = "409", description = "Conflit - fournisseur existant")
public Response createFournisseur(@Valid Fournisseur fournisseur) {
try {
Fournisseur created = fournisseurService.createFournisseur(fournisseur);
return Response.status(Response.Status.CREATED)
.entity(created)
.build();
} catch (Exception e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Erreur lors de la création du fournisseur"))
.build();
}
}
@PUT
@Path("/{id}")
@Operation(summary = "Met à jour un fournisseur existant")
@APIResponse(responseCode = "200", description = "Fournisseur mis à jour avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
@APIResponse(responseCode = "404", description = "Fournisseur non trouvé")
public Response updateFournisseur(
@Parameter(description = "ID du fournisseur") @PathParam("id") UUID id,
@Valid Fournisseur fournisseur) {
try {
Fournisseur updated = fournisseurService.updateFournisseur(id, fournisseur);
return Response.ok(updated).build();
} catch (Exception e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "Fournisseur non trouvé"))
.build();
}
}
@DELETE
@Path("/{id}")
@Operation(summary = "Supprime un fournisseur")
@APIResponse(responseCode = "204", description = "Fournisseur supprimé avec succès")
@APIResponse(responseCode = "404", description = "Fournisseur non trouvé")
public Response deleteFournisseur(
@Parameter(description = "ID du fournisseur") @PathParam("id") UUID id) {
try {
fournisseurService.deleteFournisseur(id);
return Response.noContent().build();
} catch (Exception e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "Fournisseur non trouvé"))
.build();
}
}
// === ENDPOINTS DE MODIFICATION - ARCHITECTURE 2025 ===
@PUT
@Path("/{id}/activate")
@Operation(summary = "Active un fournisseur")
@APIResponse(responseCode = "200", description = "Fournisseur activé avec succès")
@APIResponse(responseCode = "404", description = "Fournisseur non trouvé")
public Response activateFournisseur(
@Parameter(description = "ID du fournisseur") @PathParam("id") UUID id) {
try {
fournisseurService.activateFournisseur(id);
return Response.ok(Map.of("message", "Fournisseur activé avec succès")).build();
} catch (Exception e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "Fournisseur non trouvé"))
.build();
}
}
@PUT
@Path("/{id}/deactivate")
@Operation(summary = "Désactive un fournisseur")
@APIResponse(responseCode = "200", description = "Fournisseur désactivé avec succès")
@APIResponse(responseCode = "404", description = "Fournisseur non trouvé")
public Response deactivateFournisseur(
@Parameter(description = "ID du fournisseur") @PathParam("id") UUID id) {
try {
fournisseurService.deactivateFournisseur(id);
return Response.ok(Map.of("message", "Fournisseur désactivé avec succès")).build();
} catch (Exception e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "Fournisseur non trouvé"))
.build();
}
}
}

View File

@@ -0,0 +1,849 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.LivraisonMaterielService;
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.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource REST pour la gestion des livraisons de matériel
* Architecture 2025 : API complète pour la logistique et le suivi des livraisons BTP
*/
@Path("/api/v1/livraisons-materiel")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class LivraisonMaterielResource {
private static final Logger logger = LoggerFactory.getLogger(LivraisonMaterielResource.class);
@Inject LivraisonMaterielService livraisonService;
// === ENDPOINTS DE LECTURE - ARCHITECTURE 2025 ===
@GET
@Path("/")
public Response findAll(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("50") int size) {
try {
logger.debug("GET /api/livraisons-materiel/ - page: {}, size: {}", page, size);
List<LivraisonMateriel> livraisons;
if (page > 0 || size < 1000) {
livraisons = livraisonService.findAll(page, size);
} else {
livraisons = livraisonService.findAll();
}
return Response.ok(livraisons).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des livraisons", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des livraisons: " + e.getMessage())
.build();
}
}
@GET
@Path("/{id}")
public Response findById(@PathParam("id") UUID id) {
try {
logger.debug("GET /api/livraisons-materiel/{}", id);
LivraisonMateriel livraison = livraisonService.findByIdRequired(id);
return Response.ok(livraison).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération de la livraison: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération de la livraison: " + e.getMessage())
.build();
}
}
@GET
@Path("/numero/{numero}")
public Response findByNumero(@PathParam("numero") String numeroLivraison) {
try {
logger.debug("GET /api/livraisons-materiel/numero/{}", numeroLivraison);
return livraisonService
.findByNumero(numeroLivraison)
.map(livraison -> Response.ok(livraison).build())
.orElse(
Response.status(Response.Status.NOT_FOUND)
.entity("Livraison non trouvée avec le numéro: " + numeroLivraison)
.build());
} catch (Exception e) {
logger.error(
"Erreur lors de la récupération de la livraison par numéro: " + numeroLivraison, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération de la livraison: " + e.getMessage())
.build();
}
}
@GET
@Path("/reservation/{reservationId}")
public Response findByReservation(@PathParam("reservationId") UUID reservationId) {
try {
logger.debug("GET /api/livraisons-materiel/reservation/{}", reservationId);
List<LivraisonMateriel> livraisons = livraisonService.findByReservation(reservationId);
return Response.ok(livraisons).build();
} catch (Exception e) {
logger.error(
"Erreur lors de la récupération des livraisons pour réservation: " + reservationId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des livraisons: " + e.getMessage())
.build();
}
}
@GET
@Path("/chantier/{chantierId}")
public Response findByChantier(@PathParam("chantierId") UUID chantierId) {
try {
logger.debug("GET /api/livraisons-materiel/chantier/{}", chantierId);
List<LivraisonMateriel> livraisons = livraisonService.findByChantier(chantierId);
return Response.ok(livraisons).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des livraisons pour chantier: " + chantierId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des livraisons: " + e.getMessage())
.build();
}
}
@GET
@Path("/statut/{statut}")
public Response findByStatut(@PathParam("statut") String statutStr) {
try {
logger.debug("GET /api/livraisons-materiel/statut/{}", statutStr);
StatutLivraison statut = StatutLivraison.fromString(statutStr);
List<LivraisonMateriel> livraisons = livraisonService.findByStatut(statut);
return Response.ok(livraisons).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des livraisons par statut: " + statutStr, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des livraisons: " + e.getMessage())
.build();
}
}
@GET
@Path("/transporteur/{transporteur}")
public Response findByTransporteur(@PathParam("transporteur") String transporteur) {
try {
logger.debug("GET /api/livraisons-materiel/transporteur/{}", transporteur);
List<LivraisonMateriel> livraisons = livraisonService.findByTransporteur(transporteur);
return Response.ok(livraisons).build();
} catch (Exception e) {
logger.error(
"Erreur lors de la récupération des livraisons pour transporteur: " + transporteur, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des livraisons: " + e.getMessage())
.build();
}
}
@GET
@Path("/search")
public Response search(@QueryParam("terme") String terme) {
try {
logger.debug("GET /api/livraisons-materiel/search?terme={}", terme);
List<LivraisonMateriel> resultats = livraisonService.search(terme);
return Response.ok(resultats).build();
} catch (Exception e) {
logger.error("Erreur lors de la recherche avec terme: " + terme, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la recherche: " + e.getMessage())
.build();
}
}
// === ENDPOINTS MÉTIER SPÉCIALISÉS ===
@GET
@Path("/du-jour")
public Response findLivraisonsDuJour() {
try {
logger.debug("GET /api/livraisons-materiel/du-jour");
List<LivraisonMateriel> livraisons = livraisonService.findLivraisonsDuJour();
return Response.ok(livraisons).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des livraisons du jour", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des livraisons: " + e.getMessage())
.build();
}
}
@GET
@Path("/en-cours")
public Response findLivraisonsEnCours() {
try {
logger.debug("GET /api/livraisons-materiel/en-cours");
List<LivraisonMateriel> livraisons = livraisonService.findLivraisonsEnCours();
return Response.ok(livraisons).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des livraisons en cours", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des livraisons: " + e.getMessage())
.build();
}
}
@GET
@Path("/en-retard")
public Response findLivraisonsEnRetard() {
try {
logger.debug("GET /api/livraisons-materiel/en-retard");
List<LivraisonMateriel> livraisons = livraisonService.findLivraisonsEnRetard();
return Response.ok(livraisons).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des livraisons en retard", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des livraisons: " + e.getMessage())
.build();
}
}
@GET
@Path("/avec-incidents")
public Response findAvecIncidents() {
try {
logger.debug("GET /api/livraisons-materiel/avec-incidents");
List<LivraisonMateriel> livraisons = livraisonService.findAvecIncidents();
return Response.ok(livraisons).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des livraisons avec incidents", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des livraisons: " + e.getMessage())
.build();
}
}
@GET
@Path("/prioritaires")
public Response findLivraisonsPrioritaires() {
try {
logger.debug("GET /api/livraisons-materiel/prioritaires");
List<LivraisonMateriel> livraisons = livraisonService.findLivraisonsPrioritaires();
return Response.ok(livraisons).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des livraisons prioritaires", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des livraisons: " + e.getMessage())
.build();
}
}
@GET
@Path("/tracking-actif")
public Response findAvecTrackingActif() {
try {
logger.debug("GET /api/livraisons-materiel/tracking-actif");
List<LivraisonMateriel> livraisons = livraisonService.findAvecTrackingActif();
return Response.ok(livraisons).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des livraisons avec tracking", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des livraisons: " + e.getMessage())
.build();
}
}
@GET
@Path("/necessitant-action")
public Response findNecessitantAction() {
try {
logger.debug("GET /api/livraisons-materiel/necessitant-action");
List<LivraisonMateriel> livraisons = livraisonService.findNecessitantAction();
return Response.ok(livraisons).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des livraisons nécessitant action", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des livraisons: " + e.getMessage())
.build();
}
}
// === ENDPOINTS DE CRÉATION ET MODIFICATION ===
@POST
@Path("/")
public Response creerLivraison(@Valid CreerLivraisonRequest request) {
try {
logger.info("POST /api/livraisons-materiel/ - création livraison");
LivraisonMateriel livraison =
livraisonService.creerLivraison(
request.reservationId,
request.typeTransport,
request.dateLivraisonPrevue,
request.heureLivraisonPrevue,
request.transporteur,
request.planificateur);
return Response.status(Response.Status.CREATED).entity(livraison).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la création de la livraison", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la création de la livraison: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}")
public Response updateLivraison(@PathParam("id") UUID id, @Valid UpdateLivraisonRequest request) {
try {
logger.info("PUT /api/livraisons-materiel/{}", id);
LivraisonMaterielService.LivraisonUpdateRequest updateRequest = mapToServiceRequest(request);
LivraisonMateriel livraison = livraisonService.updateLivraison(id, updateRequest);
return Response.ok(livraison).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la mise à jour de la livraison: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la mise à jour de la livraison: " + e.getMessage())
.build();
}
}
// === ENDPOINTS DE GESTION DU WORKFLOW ===
@PUT
@Path("/{id}/demarrer-preparation")
public Response demarrerPreparation(@PathParam("id") UUID id, @Valid OperateurRequest request) {
try {
logger.info("PUT /api/livraisons-materiel/{}/demarrer-preparation", id);
LivraisonMateriel livraison = livraisonService.demarrerPreparation(id, request.operateur);
return Response.ok(livraison).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors du démarrage de la préparation: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du démarrage de la préparation: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/marquer-prete")
public Response marquerPrete(@PathParam("id") UUID id, @Valid MarquerPreteRequest request) {
try {
logger.info("PUT /api/livraisons-materiel/{}/marquer-prete", id);
LivraisonMateriel livraison =
livraisonService.marquerPrete(id, request.operateur, request.observations);
return Response.ok(livraison).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors du marquage prêt: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du marquage prêt: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/demarrer-transit")
public Response demarrerTransit(@PathParam("id") UUID id, @Valid DemarrerTransitRequest request) {
try {
logger.info("PUT /api/livraisons-materiel/{}/demarrer-transit", id);
LivraisonMateriel livraison =
livraisonService.demarrerTransit(id, request.chauffeur, request.heureDepart);
return Response.ok(livraison).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors du démarrage du transit: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du démarrage du transit: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/signaler-arrivee")
public Response signalerArrivee(@PathParam("id") UUID id, @Valid SignalerArriveeRequest request) {
try {
logger.info("PUT /api/livraisons-materiel/{}/signaler-arrivee", id);
LivraisonMateriel livraison =
livraisonService.signalerArrivee(
id, request.chauffeur, request.heureArrivee, request.latitude, request.longitude);
return Response.ok(livraison).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors du signalement d'arrivée: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du signalement d'arrivée: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/commencer-dechargement")
public Response commencerDechargement(@PathParam("id") UUID id, @Valid OperateurRequest request) {
try {
logger.info("PUT /api/livraisons-materiel/{}/commencer-dechargement", id);
LivraisonMateriel livraison = livraisonService.commencerDechargement(id, request.operateur);
return Response.ok(livraison).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors du début de déchargement: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du début de déchargement: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/finaliser")
public Response finaliserLivraison(@PathParam("id") UUID id, @Valid FinalisationRequest request) {
try {
logger.info("PUT /api/livraisons-materiel/{}/finaliser", id);
LivraisonMaterielService.FinalisationLivraisonRequest finalisationRequest =
mapToFinalisationRequest(request);
LivraisonMateriel livraison = livraisonService.finaliserLivraison(id, finalisationRequest);
return Response.ok(livraison).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la finalisation: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la finalisation: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/signaler-incident")
public Response signalerIncident(
@PathParam("id") UUID id, @Valid SignalerIncidentRequest request) {
try {
logger.info("PUT /api/livraisons-materiel/{}/signaler-incident", id);
LivraisonMaterielService.IncidentRequest incidentRequest = mapToIncidentRequest(request);
LivraisonMateriel livraison = livraisonService.signalerIncident(id, incidentRequest);
return Response.ok(livraison).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors du signalement d'incident: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du signalement d'incident: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/retarder")
public Response retarderLivraison(
@PathParam("id") UUID id, @Valid RetarderLivraisonRequest request) {
try {
logger.info("PUT /api/livraisons-materiel/{}/retarder", id);
LivraisonMateriel livraison =
livraisonService.retarderLivraison(
id,
request.nouvelleDatePrevue,
request.nouvelleHeurePrevue,
request.motif,
request.operateur);
return Response.ok(livraison).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors du retard de livraison: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du retard de livraison: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/annuler")
public Response annulerLivraison(
@PathParam("id") UUID id, @Valid AnnulerLivraisonRequest request) {
try {
logger.info("PUT /api/livraisons-materiel/{}/annuler", id);
LivraisonMateriel livraison =
livraisonService.annulerLivraison(id, request.motifAnnulation, request.operateur);
return Response.ok(livraison).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de l'annulation de livraison: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de l'annulation de livraison: " + e.getMessage())
.build();
}
}
// === ENDPOINTS DE SUIVI ET TRACKING ===
@PUT
@Path("/{id}/position-gps")
public Response mettreAJourPositionGPS(
@PathParam("id") UUID id, @Valid PositionGPSRequest request) {
try {
livraisonService.mettreAJourPositionGPS(
id, request.latitude, request.longitude, request.vitesseKmh);
return Response.ok(Map.of("message", "Position mise à jour")).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 GPS: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la mise à jour GPS: " + e.getMessage())
.build();
}
}
@GET
@Path("/{id}/eta")
public Response calculerETA(@PathParam("id") UUID id) {
try {
logger.debug("GET /api/livraisons-materiel/{}/eta", id);
Map<String, Object> eta = livraisonService.calculerETA(id);
return Response.ok(eta).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors du calcul ETA: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du calcul ETA: " + e.getMessage())
.build();
}
}
// === ENDPOINTS D'OPTIMISATION ===
@POST
@Path("/optimiser-itineraires")
public Response optimiserItineraires(@Valid OptimiserItinerairesRequest request) {
try {
logger.info("POST /api/livraisons-materiel/optimiser-itineraires");
List<LivraisonMateriel> itineraireOptimise =
livraisonService.optimiserItineraires(request.date, request.transporteur);
return Response.ok(
Map.of(
"itineraireOptimise",
itineraireOptimise,
"nombreLivraisons",
itineraireOptimise.size()))
.build();
} catch (Exception e) {
logger.error("Erreur lors de l'optimisation des itinéraires", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de l'optimisation des itinéraires: " + e.getMessage())
.build();
}
}
// === ENDPOINTS STATISTIQUES ===
@GET
@Path("/statistiques")
public Response getStatistiques() {
try {
logger.debug("GET /api/livraisons-materiel/statistiques");
Map<String, Object> statistiques = livraisonService.getStatistiques();
return Response.ok(statistiques).build();
} catch (Exception e) {
logger.error("Erreur lors de la génération des statistiques", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la génération des statistiques: " + e.getMessage())
.build();
}
}
@GET
@Path("/tableau-bord")
public Response getTableauBordLogistique() {
try {
logger.debug("GET /api/livraisons-materiel/tableau-bord");
Map<String, Object> tableauBord = livraisonService.getTableauBordLogistique();
return Response.ok(tableauBord).build();
} catch (Exception e) {
logger.error("Erreur lors de la génération du tableau de bord", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la génération du tableau de bord: " + e.getMessage())
.build();
}
}
@GET
@Path("/performance-transporteurs")
public Response analyserPerformanceTransporteurs() {
try {
logger.debug("GET /api/livraisons-materiel/performance-transporteurs");
List<Object> performance = livraisonService.analyserPerformanceTransporteurs();
return Response.ok(performance).build();
} catch (Exception e) {
logger.error("Erreur lors de l'analyse des performances", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de l'analyse des performances: " + e.getMessage())
.build();
}
}
// === MÉTHODES UTILITAIRES ===
private LivraisonMaterielService.LivraisonUpdateRequest mapToServiceRequest(
UpdateLivraisonRequest request) {
LivraisonMaterielService.LivraisonUpdateRequest serviceRequest =
new LivraisonMaterielService.LivraisonUpdateRequest();
serviceRequest.dateLivraisonPrevue = request.dateLivraisonPrevue;
serviceRequest.heureLivraisonPrevue = request.heureLivraisonPrevue;
serviceRequest.transporteur = request.transporteur;
serviceRequest.chauffeur = request.chauffeur;
serviceRequest.telephoneChauffeur = request.telephoneChauffeur;
serviceRequest.immatriculation = request.immatriculation;
serviceRequest.contactReception = request.contactReception;
serviceRequest.telephoneContact = request.telephoneContact;
serviceRequest.instructionsSpeciales = request.instructionsSpeciales;
serviceRequest.accesChantier = request.accesChantier;
serviceRequest.modifiePar = request.modifiePar;
return serviceRequest;
}
private LivraisonMaterielService.FinalisationLivraisonRequest mapToFinalisationRequest(
FinalisationRequest request) {
LivraisonMaterielService.FinalisationLivraisonRequest finalisationRequest =
new LivraisonMaterielService.FinalisationLivraisonRequest();
finalisationRequest.quantiteLivree = request.quantiteLivree;
finalisationRequest.etatMateriel = request.etatMateriel;
finalisationRequest.observations = request.observations;
finalisationRequest.receptionnaire = request.receptionnaire;
finalisationRequest.conforme = request.conforme;
finalisationRequest.photoLivraison = request.photoLivraison;
return finalisationRequest;
}
private LivraisonMaterielService.IncidentRequest mapToIncidentRequest(
SignalerIncidentRequest request) {
LivraisonMaterielService.IncidentRequest incidentRequest =
new LivraisonMaterielService.IncidentRequest();
incidentRequest.typeIncident = request.typeIncident;
incidentRequest.description = request.description;
incidentRequest.impact = request.impact;
incidentRequest.actionsCorrectives = request.actionsCorrectives;
incidentRequest.declarant = request.declarant;
return incidentRequest;
}
// === CLASSES DE REQUÊTE ===
public static class CreerLivraisonRequest {
@NotNull public UUID reservationId;
@NotNull public TypeTransport typeTransport;
@NotNull public LocalDate dateLivraisonPrevue;
public LocalTime heureLivraisonPrevue;
public String transporteur;
public String planificateur;
}
public static class UpdateLivraisonRequest {
public LocalDate dateLivraisonPrevue;
public LocalTime heureLivraisonPrevue;
public String transporteur;
public String chauffeur;
public String telephoneChauffeur;
public String immatriculation;
public String contactReception;
public String telephoneContact;
public String instructionsSpeciales;
public String accesChantier;
public String modifiePar;
}
public static class OperateurRequest {
@NotNull public String operateur;
}
public static class MarquerPreteRequest {
@NotNull public String operateur;
public String observations;
}
public static class DemarrerTransitRequest {
@NotNull public String chauffeur;
public LocalTime heureDepart;
}
public static class SignalerArriveeRequest {
@NotNull public String chauffeur;
public LocalTime heureArrivee;
public BigDecimal latitude;
public BigDecimal longitude;
}
public static class FinalisationRequest {
@NotNull public BigDecimal quantiteLivree;
public String etatMateriel;
public String observations;
@NotNull public String receptionnaire;
public Boolean conforme;
public String photoLivraison;
}
public static class SignalerIncidentRequest {
@NotNull public String typeIncident;
@NotNull public String description;
public String impact;
public String actionsCorrectives;
@NotNull public String declarant;
}
public static class RetarderLivraisonRequest {
@NotNull public LocalDate nouvelleDatePrevue;
public LocalTime nouvelleHeurePrevue;
@NotNull public String motif;
@NotNull public String operateur;
}
public static class AnnulerLivraisonRequest {
@NotNull public String motifAnnulation;
@NotNull public String operateur;
}
public static class PositionGPSRequest {
@NotNull public BigDecimal latitude;
@NotNull public BigDecimal longitude;
public Integer vitesseKmh;
}
public static class OptimiserItinerairesRequest {
@NotNull public LocalDate date;
public String transporteur;
}
}

View File

@@ -0,0 +1,354 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.PermissionService;
import dev.lions.btpxpress.domain.core.entity.Permission;
import dev.lions.btpxpress.domain.core.entity.Permission.PermissionCategory;
import dev.lions.btpxpress.domain.core.entity.UserRole;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* API REST pour la gestion des permissions EXPOSITION: Consultation des droits d'accès et
* permissions par rôle
*/
@Path("/api/permissions")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class PermissionResource {
private static final Logger logger = LoggerFactory.getLogger(PermissionResource.class);
@Inject PermissionService permissionService;
// === ENDPOINTS DE CONSULTATION ===
/** Récupère toutes les permissions disponibles */
@GET
@Path("/all")
public Response getAllPermissions() {
try {
logger.debug("GET /api/permissions/all");
List<Object> permissions =
Arrays.stream(Permission.values())
.map(
p ->
Map.of(
"code", p.getCode(),
"description", p.getDescription(),
"category", p.getCategory().name(),
"categoryDisplay", p.getCategory().getDisplayName()))
.collect(Collectors.toList());
return Response.ok(permissions).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des permissions", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des permissions: " + e.getMessage())
.build();
}
}
/** Récupère les permissions par catégorie */
@GET
@Path("/categories")
public Response getPermissionsByCategory() {
try {
logger.debug("GET /api/permissions/categories");
Map<String, Object> result =
Arrays.stream(PermissionCategory.values())
.collect(
Collectors.toMap(
category -> category.name(),
category ->
Map.of(
"displayName", category.getDisplayName(),
"permissions",
Permission.getByCategory(category).stream()
.map(
p ->
Map.of(
"code", p.getCode(),
"description", p.getDescription()))
.collect(Collectors.toList()))));
return Response.ok(result).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des permissions par catégorie", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des permissions: " + e.getMessage())
.build();
}
}
/** Récupère les permissions d'un rôle spécifique */
@GET
@Path("/role/{role}")
public Response getPermissionsByRole(@PathParam("role") String roleStr) {
try {
logger.debug("GET /api/permissions/role/{}", roleStr);
UserRole role = UserRole.valueOf(roleStr.toUpperCase());
Map<String, Object> summary = permissionService.getPermissionSummary(role);
return Response.ok(summary).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Rôle invalide: " + roleStr)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des permissions pour le rôle: " + roleStr, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des permissions: " + e.getMessage())
.build();
}
}
/** Vérifie si un rôle a une permission spécifique */
@GET
@Path("/check/{role}/{permission}")
public Response checkPermission(
@PathParam("role") String roleStr, @PathParam("permission") String permissionCode) {
try {
logger.debug("GET /api/permissions/check/{}/{}", roleStr, permissionCode);
UserRole role = UserRole.valueOf(roleStr.toUpperCase());
boolean hasPermission = permissionService.hasPermission(role, permissionCode);
return Response.ok(
Map.of(
"role", role.getDisplayName(),
"permission", permissionCode,
"hasPermission", hasPermission))
.build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Rôle invalide: " + roleStr)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la vérification de permission", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la vérification de permission: " + e.getMessage())
.build();
}
}
/** Récupère tous les rôles avec leurs permissions */
@GET
@Path("/roles")
public Response getAllRolesWithPermissions() {
try {
logger.debug("GET /api/permissions/roles");
Map<String, Object> result =
Arrays.stream(UserRole.values())
.collect(
Collectors.toMap(
role -> role.name(),
role ->
Map.of(
"displayName", role.getDisplayName(),
"description", role.getDescription(),
"hierarchyLevel", role.getHierarchyLevel(),
"isManagementRole", role.isManagementRole(),
"isFieldRole", role.isFieldRole(),
"isAdministrativeRole", role.isAdministrativeRole(),
"permissions",
permissionService.getPermissions(role).stream()
.map(Permission::getCode)
.collect(Collectors.toList()),
"permissionCount", permissionService.getPermissions(role).size())));
return Response.ok(result).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des rôles et permissions", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des rôles: " + e.getMessage())
.build();
}
}
/** Compare les permissions entre deux rôles */
@GET
@Path("/compare/{role1}/{role2}")
public Response compareRoles(
@PathParam("role1") String role1Str, @PathParam("role2") String role2Str) {
try {
logger.debug("GET /api/permissions/compare/{}/{}", role1Str, role2Str);
UserRole role1 = UserRole.valueOf(role1Str.toUpperCase());
UserRole role2 = UserRole.valueOf(role2Str.toUpperCase());
Set<Permission> permissions1 = permissionService.getPermissions(role1);
Set<Permission> permissions2 = permissionService.getPermissions(role2);
Set<Permission> missing1to2 = permissionService.getMissingPermissions(role1, role2);
Set<Permission> missing2to1 = permissionService.getMissingPermissions(role2, role1);
Set<Permission> common =
permissions1.stream().filter(permissions2::contains).collect(Collectors.toSet());
return Response.ok(
Map.of(
"role1",
Map.of(
"name", role1.getDisplayName(),
"permissionCount", permissions1.size(),
"permissions",
permissions1.stream()
.map(Permission::getCode)
.collect(Collectors.toList())),
"role2",
Map.of(
"name", role2.getDisplayName(),
"permissionCount", permissions2.size(),
"permissions",
permissions2.stream()
.map(Permission::getCode)
.collect(Collectors.toList())),
"common",
Map.of(
"count", common.size(),
"permissions",
common.stream()
.map(Permission::getCode)
.collect(Collectors.toList())),
"onlyInRole1",
Map.of(
"count", missing2to1.size(),
"permissions",
missing2to1.stream()
.map(Permission::getCode)
.collect(Collectors.toList())),
"onlyInRole2",
Map.of(
"count", missing1to2.size(),
"permissions",
missing1to2.stream()
.map(Permission::getCode)
.collect(Collectors.toList()))))
.build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST).entity("Rôle invalide").build();
} catch (Exception e) {
logger.error("Erreur lors de la comparaison des rôles", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la comparaison: " + e.getMessage())
.build();
}
}
/** Récupère les permissions spécifiques au gestionnaire de projet */
@GET
@Path("/gestionnaire")
public Response getGestionnairePermissions() {
try {
logger.debug("GET /api/permissions/gestionnaire");
UserRole gestionnaireRole = UserRole.GESTIONNAIRE_PROJET;
Map<String, Object> summary = permissionService.getPermissionSummary(gestionnaireRole);
// Ajout d'informations spécifiques au gestionnaire
Map<PermissionCategory, List<Permission>> byCategory =
permissionService.getPermissionsByCategory(gestionnaireRole);
return Response.ok(
Map.of(
"role", gestionnaireRole.getDisplayName(),
"description", gestionnaireRole.getDescription(),
"summary", summary,
"specificities",
Map.of(
"clientManagement", "Gestion limitée aux clients assignés",
"projectScope", "Chantiers et projets sous sa responsabilité uniquement",
"budgetAccess", "Consultation et planification budgétaire",
"materialReservation", "Réservation de matériel pour ses chantiers",
"reportingLevel", "Rapports et statistiques de ses projets"),
"categoriesDetails",
byCategory.entrySet().stream()
.collect(
Collectors.toMap(
entry -> entry.getKey().getDisplayName(),
entry ->
entry.getValue().stream()
.map(
p ->
Map.of(
"code", p.getCode(),
"description", p.getDescription()))
.collect(Collectors.toList())))))
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des permissions gestionnaire", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des permissions: " + e.getMessage())
.build();
}
}
/** Valide les permissions requises pour une fonctionnalité */
@POST
@Path("/validate")
public Response validatePermissions(ValidationRequest request) {
try {
logger.debug("POST /api/permissions/validate");
UserRole role = UserRole.valueOf(request.role.toUpperCase());
Set<Permission> requiredPermissions =
request.requiredPermissions.stream()
.map(Permission::fromCode)
.collect(Collectors.toSet());
boolean hasMinimum = permissionService.hasMinimumPermissions(role, requiredPermissions);
Map<String, Boolean> permissionChecks =
request.requiredPermissions.stream()
.collect(
Collectors.toMap(
code -> code, code -> permissionService.hasPermission(role, code)));
return Response.ok(
Map.of(
"role",
role.getDisplayName(),
"hasMinimumPermissions",
hasMinimum,
"permissionChecks",
permissionChecks,
"missingPermissions",
request.requiredPermissions.stream()
.filter(code -> !permissionService.hasPermission(role, code))
.collect(Collectors.toList())))
.build();
} catch (Exception e) {
logger.error("Erreur lors de la validation des permissions", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la validation: " + e.getMessage())
.build();
}
}
// === CLASSES DE REQUÊTE ===
public static class ValidationRequest {
public String role;
public List<String> requiredPermissions;
}
}

View File

@@ -0,0 +1,351 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.PhaseTemplateService;
import dev.lions.btpxpress.domain.core.entity.PhaseChantier;
import dev.lions.btpxpress.domain.core.entity.PhaseTemplate;
import dev.lions.btpxpress.domain.core.entity.TypeChantierBTP;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
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;
/**
* API REST pour la gestion des templates de phases BTP Expose les fonctionnalités de création,
* consultation et administration des templates
*/
@Path("/api/v1/phase-templates")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Phase Templates", description = "Gestion des templates de phases BTP")
public class PhaseTemplateResource {
@Inject PhaseTemplateService phaseTemplateService;
// ===================================
// CONSULTATION DES TEMPLATES
// ===================================
@GET
@Path("/types-chantier")
@Operation(summary = "Récupère tous les types de chantiers disponibles")
@APIResponse(responseCode = "200", description = "Liste des types de chantiers")
public Response getTypesChantierDisponibles() {
TypeChantierBTP[] types = phaseTemplateService.getTypesChantierDisponibles();
return Response.ok(types).build();
}
@GET
@Path("/by-type/{typeChantier}")
@Operation(summary = "Récupère tous les templates pour un type de chantier")
@APIResponse(responseCode = "200", description = "Liste des templates pour le type de chantier")
@APIResponse(responseCode = "400", description = "Type de chantier invalide")
public Response getTemplatesByType(
@Parameter(description = "Type de chantier BTP") @PathParam("typeChantier")
String typeChantierStr) {
try {
TypeChantierBTP typeChantier = TypeChantierBTP.valueOf(typeChantierStr.toUpperCase());
List<PhaseTemplate> templates = phaseTemplateService.getTemplatesByType(typeChantier);
return Response.ok(templates).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Type de chantier invalide: " + typeChantierStr)
.build();
}
}
@GET
@Path("/{id}")
@Operation(summary = "Récupère un template par son ID avec ses sous-phases")
@APIResponse(responseCode = "200", description = "Template trouvé")
@APIResponse(responseCode = "404", description = "Template non trouvé")
public Response getTemplateById(
@Parameter(description = "Identifiant du template") @PathParam("id") UUID id) {
return phaseTemplateService
.getTemplateById(id)
.map(template -> Response.ok(template).build())
.orElse(Response.status(Response.Status.NOT_FOUND).build());
}
@GET
@Operation(summary = "Récupère tous les templates actifs")
@APIResponse(responseCode = "200", description = "Liste de tous les templates actifs")
public Response getAllTemplatesActifs() {
List<PhaseTemplate> templates = phaseTemplateService.getAllTemplatesActifs();
return Response.ok(templates).build();
}
@GET
@Path("/previsualisation/{typeChantier}")
@Operation(summary = "Prévisualise les phases qui seraient générées pour un type de chantier")
@APIResponse(responseCode = "200", description = "Prévisualisation des phases")
@APIResponse(responseCode = "400", description = "Type de chantier invalide")
public Response previsualiserPhases(
@Parameter(description = "Type de chantier BTP") @PathParam("typeChantier")
String typeChantierStr) {
try {
TypeChantierBTP typeChantier = TypeChantierBTP.valueOf(typeChantierStr.toUpperCase());
List<PhaseTemplate> templates = phaseTemplateService.previsualiserPhases(typeChantier);
return Response.ok(templates).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Type de chantier invalide: " + typeChantierStr)
.build();
}
}
@GET
@Path("/duree-estimee/{typeChantier}")
@Operation(summary = "Calcule la durée totale estimée pour un type de chantier")
@APIResponse(responseCode = "200", description = "Durée totale en jours")
@APIResponse(responseCode = "400", description = "Type de chantier invalide")
public Response calculerDureeTotaleEstimee(
@Parameter(description = "Type de chantier BTP") @PathParam("typeChantier")
String typeChantierStr) {
try {
TypeChantierBTP typeChantier = TypeChantierBTP.valueOf(typeChantierStr.toUpperCase());
Integer dureeTotal = phaseTemplateService.calculerDureeTotaleEstimee(typeChantier);
return Response.ok(dureeTotal).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Type de chantier invalide: " + typeChantierStr)
.build();
}
}
@GET
@Path("/complexite/{typeChantier}")
@Operation(summary = "Analyse la complexité d'un type de chantier")
@APIResponse(responseCode = "200", description = "Analyse de complexité")
@APIResponse(responseCode = "400", description = "Type de chantier invalide")
public Response analyserComplexite(
@Parameter(description = "Type de chantier BTP") @PathParam("typeChantier")
String typeChantierStr) {
try {
TypeChantierBTP typeChantier = TypeChantierBTP.valueOf(typeChantierStr.toUpperCase());
PhaseTemplateService.ComplexiteChantier complexite =
phaseTemplateService.analyserComplexite(typeChantier);
return Response.ok(complexite).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Type de chantier invalide: " + typeChantierStr)
.build();
}
}
// ===================================
// GÉNÉRATION AUTOMATIQUE DE PHASES
// ===================================
@POST
@Path("/generer-phases")
@Operation(summary = "Génère automatiquement les phases pour un chantier")
@APIResponse(responseCode = "201", description = "Phases générées avec succès")
@APIResponse(responseCode = "400", description = "Paramètres invalides")
@APIResponse(responseCode = "404", description = "Chantier non trouvé")
public Response genererPhasesAutomatiquement(GenerationPhasesRequest request) {
if (request.chantierId == null || request.dateDebutChantier == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("L'ID du chantier et la date de début sont obligatoires")
.build();
}
try {
List<PhaseChantier> phasesCreees =
phaseTemplateService.genererPhasesAutomatiquement(
request.chantierId,
request.dateDebutChantier,
request.inclureSousPhases != null ? request.inclureSousPhases : true);
return Response.status(Response.Status.CREATED).entity(phasesCreees).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
}
}
// ===================================
// ADMINISTRATION DES TEMPLATES
// ===================================
@POST
@Path("/initialize")
@Operation(summary = "Initialise les templates de phases par défaut")
@APIResponse(responseCode = "200", description = "Templates initialisés avec succès")
@APIResponse(responseCode = "409", description = "Templates déjà existants")
public Response initializeTemplates() {
try {
// Vérifier si des templates existent déjà
List<PhaseTemplate> existingTemplates = phaseTemplateService.getAllTemplatesActifs();
if (!existingTemplates.isEmpty()) {
return Response.status(Response.Status.CONFLICT)
.entity(Map.of("message", "Des templates existent déjà", "count", existingTemplates.size()))
.build();
}
// Initialisation des templates de phases par défaut
List<PhaseTemplate> defaultTemplates = createDefaultPhaseTemplates();
for (PhaseTemplate template : defaultTemplates) {
phaseTemplateService.creerTemplate(template);
}
return Response.ok()
.entity(Map.of(
"message", "Templates initialisés avec succès",
"count", defaultTemplates.size()
))
.build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de l'initialisation : " + e.getMessage())
.build();
}
}
@POST
@Operation(summary = "Crée un nouveau template de phase")
@APIResponse(responseCode = "201", description = "Template créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
@APIResponse(responseCode = "409", description = "Conflit - template existant pour cet ordre")
public Response creerTemplate(@Valid PhaseTemplate template) {
try {
PhaseTemplate nouveauTemplate = phaseTemplateService.creerTemplate(template);
return Response.status(Response.Status.CREATED).entity(nouveauTemplate).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.CONFLICT).entity(e.getMessage()).build();
}
}
@PUT
@Path("/{id}")
@Operation(summary = "Met à jour un template de phase")
@APIResponse(responseCode = "200", description = "Template mis à jour avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
@APIResponse(responseCode = "404", description = "Template non trouvé")
@APIResponse(responseCode = "409", description = "Conflit - ordre d'exécution déjà utilisé")
public Response updateTemplate(
@Parameter(description = "Identifiant du template") @PathParam("id") UUID id,
@Valid PhaseTemplate templateData) {
try {
PhaseTemplate templateMisAJour = phaseTemplateService.updateTemplate(id, templateData);
return Response.ok(templateMisAJour).build();
} catch (IllegalArgumentException e) {
if (e.getMessage().contains("non trouvé")) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} else {
return Response.status(Response.Status.CONFLICT).entity(e.getMessage()).build();
}
}
}
@DELETE
@Path("/{id}")
@Operation(summary = "Supprime un template (désactivation)")
@APIResponse(responseCode = "204", description = "Template supprimé avec succès")
@APIResponse(responseCode = "404", description = "Template non trouvé")
public Response supprimerTemplate(
@Parameter(description = "Identifiant du template") @PathParam("id") UUID id) {
try {
phaseTemplateService.supprimerTemplate(id);
return Response.noContent().build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
}
}
// ===================================
// CLASSES INTERNES - REQUESTS/RESPONSES
// ===================================
/** Classe pour la requête de génération automatique de phases */
public static class GenerationPhasesRequest {
@NotNull(message = "L'ID du chantier est obligatoire")
public UUID chantierId;
@NotNull(message = "La date de début du chantier est obligatoire")
public LocalDate dateDebutChantier;
public Boolean inclureSousPhases = true;
// Constructeurs
public GenerationPhasesRequest() {}
public GenerationPhasesRequest(
UUID chantierId, LocalDate dateDebutChantier, Boolean inclureSousPhases) {
this.chantierId = chantierId;
this.dateDebutChantier = dateDebutChantier;
this.inclureSousPhases = inclureSousPhases;
}
}
/** Classe pour la réponse de génération de phases */
public static class GenerationPhasesResponse {
public List<PhaseChantier> phasesCreees;
public int nombrePhasesGenerees;
public int nombreSousPhasesGenerees;
public String message;
public GenerationPhasesResponse(List<PhaseChantier> phasesCreees) {
this.phasesCreees = phasesCreees;
this.nombrePhasesGenerees = phasesCreees.size();
this.nombreSousPhasesGenerees = calculerNombreSousPhases(phasesCreees);
this.message =
String.format(
"Génération réussie: %d phases principales et %d sous-phases créées",
nombrePhasesGenerees, nombreSousPhasesGenerees);
}
private int calculerNombreSousPhases(List<PhaseChantier> phases) {
return phases.stream()
.mapToInt(phase -> phase.getSousPhases() != null ? phase.getSousPhases().size() : 0)
.sum();
}
}
private List<PhaseTemplate> createDefaultPhaseTemplates() {
List<PhaseTemplate> templates = new ArrayList<>();
// Template pour construction neuve
PhaseTemplate constructionNeuve = new PhaseTemplate();
constructionNeuve.setNom("Construction neuve - Standard");
constructionNeuve.setDescription("Template pour construction de maison individuelle");
constructionNeuve.setActif(true);
templates.add(constructionNeuve);
// Template pour rénovation
PhaseTemplate renovation = new PhaseTemplate();
renovation.setNom("Rénovation - Standard");
renovation.setDescription("Template pour rénovation complète");
renovation.setActif(true);
templates.add(renovation);
return templates;
}
}

View File

@@ -0,0 +1,626 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.PlanningMaterielService;
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.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* API REST pour la gestion des plannings matériel EXPOSITION: Endpoints pour planification et
* visualisation graphique
*/
@Path("/api/v1/plannings-materiel")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class PlanningMaterielResource {
private static final Logger logger = LoggerFactory.getLogger(PlanningMaterielResource.class);
@Inject PlanningMaterielService planningService;
// === ENDPOINTS DE CONSULTATION ===
@GET
@Path("/")
public Response findAll(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("50") int size) {
try {
logger.debug("GET /api/plannings-materiel/ - page: {}, size: {}", page, size);
List<PlanningMateriel> plannings;
if (page > 0 || size < 1000) {
plannings = planningService.findAll(page, size);
} else {
plannings = planningService.findAll();
}
return Response.ok(plannings).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des plannings", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des plannings: " + e.getMessage())
.build();
}
}
@GET
@Path("/{id}")
public Response findById(@PathParam("id") UUID id) {
try {
logger.debug("GET /api/plannings-materiel/{}", id);
PlanningMateriel planning = planningService.findByIdRequired(id);
return Response.ok(planning).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération du planning: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération du planning: " + e.getMessage())
.build();
}
}
@GET
@Path("/materiel/{materielId}")
public Response findByMateriel(@PathParam("materielId") UUID materielId) {
try {
logger.debug("GET /api/plannings-materiel/materiel/{}", materielId);
List<PlanningMateriel> plannings = planningService.findByMateriel(materielId);
return Response.ok(plannings).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des plannings pour matériel: " + materielId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des plannings: " + e.getMessage())
.build();
}
}
@GET
@Path("/periode")
public Response findByPeriode(
@QueryParam("dateDebut") LocalDate dateDebut, @QueryParam("dateFin") LocalDate dateFin) {
try {
logger.debug(
"GET /api/plannings-materiel/periode?dateDebut={}&dateFin={}", dateDebut, dateFin);
if (dateDebut == null || dateFin == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Les paramètres dateDebut et dateFin sont obligatoires")
.build();
}
List<PlanningMateriel> plannings = planningService.findByPeriode(dateDebut, dateFin);
return Response.ok(plannings).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des plannings par période", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des plannings: " + e.getMessage())
.build();
}
}
@GET
@Path("/statut/{statut}")
public Response findByStatut(@PathParam("statut") String statutStr) {
try {
logger.debug("GET /api/plannings-materiel/statut/{}", statutStr);
StatutPlanning statut = StatutPlanning.fromString(statutStr);
List<PlanningMateriel> plannings = planningService.findByStatut(statut);
return Response.ok(plannings).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des plannings par statut: " + statutStr, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des plannings: " + e.getMessage())
.build();
}
}
@GET
@Path("/type/{type}")
public Response findByType(@PathParam("type") String typeStr) {
try {
logger.debug("GET /api/plannings-materiel/type/{}", typeStr);
TypePlanning type = TypePlanning.fromString(typeStr);
List<PlanningMateriel> plannings = planningService.findByType(type);
return Response.ok(plannings).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des plannings par type: " + typeStr, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des plannings: " + e.getMessage())
.build();
}
}
@GET
@Path("/search")
public Response search(@QueryParam("terme") String terme) {
try {
logger.debug("GET /api/plannings-materiel/search?terme={}", terme);
List<PlanningMateriel> resultats = planningService.search(terme);
return Response.ok(resultats).build();
} catch (Exception e) {
logger.error("Erreur lors de la recherche avec terme: " + terme, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la recherche: " + e.getMessage())
.build();
}
}
// === ENDPOINTS MÉTIER SPÉCIALISÉS ===
@GET
@Path("/avec-conflits")
public Response findAvecConflits() {
try {
logger.debug("GET /api/plannings-materiel/avec-conflits");
List<PlanningMateriel> plannings = planningService.findAvecConflits();
return Response.ok(plannings).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des plannings avec conflits", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des plannings: " + e.getMessage())
.build();
}
}
@GET
@Path("/necessitant-attention")
public Response findNecessitantAttention() {
try {
logger.debug("GET /api/plannings-materiel/necessitant-attention");
List<PlanningMateriel> plannings = planningService.findNecessitantAttention();
return Response.ok(plannings).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des plannings nécessitant attention", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des plannings: " + e.getMessage())
.build();
}
}
@GET
@Path("/en-retard-validation")
public Response findEnRetardValidation() {
try {
logger.debug("GET /api/plannings-materiel/en-retard-validation");
List<PlanningMateriel> plannings = planningService.findEnRetardValidation();
return Response.ok(plannings).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des plannings en retard", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des plannings: " + e.getMessage())
.build();
}
}
@GET
@Path("/prioritaires")
public Response findPrioritaires() {
try {
logger.debug("GET /api/plannings-materiel/prioritaires");
List<PlanningMateriel> plannings = planningService.findPrioritaires();
return Response.ok(plannings).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des plannings prioritaires", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des plannings: " + e.getMessage())
.build();
}
}
@GET
@Path("/en-cours")
public Response findEnCours() {
try {
logger.debug("GET /api/plannings-materiel/en-cours");
List<PlanningMateriel> plannings = planningService.findEnCours();
return Response.ok(plannings).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des plannings en cours", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des plannings: " + e.getMessage())
.build();
}
}
// === ENDPOINTS DE CRÉATION ET MODIFICATION ===
@POST
@Path("/")
public Response createPlanning(@Valid CreatePlanningRequest request) {
try {
logger.info("POST /api/plannings-materiel/ - création planning");
PlanningMateriel planning =
planningService.createPlanning(
request.materielId,
request.nomPlanning,
request.descriptionPlanning,
request.dateDebut,
request.dateFin,
request.typePlanning,
request.planificateur);
return Response.status(Response.Status.CREATED).entity(planning).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la création du planning", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la création du planning: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}")
public Response updatePlanning(@PathParam("id") UUID id, @Valid UpdatePlanningRequest request) {
try {
logger.info("PUT /api/plannings-materiel/{}", id);
PlanningMateriel planning =
planningService.updatePlanning(
id,
request.nomPlanning,
request.descriptionPlanning,
request.dateDebut,
request.dateFin,
request.modifiePar);
return Response.ok(planning).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la mise à jour du planning: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la mise à jour du planning: " + e.getMessage())
.build();
}
}
// === ENDPOINTS DE GESTION DU WORKFLOW ===
@PUT
@Path("/{id}/valider")
public Response validerPlanning(@PathParam("id") UUID id, @Valid ValiderPlanningRequest request) {
try {
logger.info("PUT /api/plannings-materiel/{}/valider", id);
PlanningMateriel planning =
planningService.validerPlanning(id, request.valideur, request.commentaires);
return Response.ok(planning).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la validation du planning: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la validation du planning: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/mettre-en-revision")
public Response mettreEnRevision(
@PathParam("id") UUID id, @Valid RevisionPlanningRequest request) {
try {
logger.info("PUT /api/plannings-materiel/{}/mettre-en-revision", id);
PlanningMateriel planning = planningService.mettreEnRevision(id, request.motif);
return Response.ok(planning).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 en révision du planning: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la mise en révision du planning: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/archiver")
public Response archiverPlanning(@PathParam("id") UUID id) {
try {
logger.info("PUT /api/plannings-materiel/{}/archiver", id);
PlanningMateriel planning = planningService.archiverPlanning(id);
return Response.ok(planning).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de l'archivage du planning: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de l'archivage du planning: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/suspendre")
public Response suspendrePlanning(@PathParam("id") UUID id) {
try {
logger.info("PUT /api/plannings-materiel/{}/suspendre", id);
PlanningMateriel planning = planningService.suspendrePlanning(id);
return Response.ok(planning).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la suspension du planning: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la suspension du planning: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/reactiver")
public Response reactiverPlanning(@PathParam("id") UUID id) {
try {
logger.info("PUT /api/plannings-materiel/{}/reactiver", id);
PlanningMateriel planning = planningService.reactiverPlanning(id);
return Response.ok(planning).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la réactivation du planning: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la réactivation du planning: " + e.getMessage())
.build();
}
}
// === ENDPOINTS DE VÉRIFICATION ET ANALYSE ===
@GET
@Path("/check-conflits")
public Response checkConflits(
@QueryParam("materielId") @NotNull UUID materielId,
@QueryParam("dateDebut") @NotNull LocalDate dateDebut,
@QueryParam("dateFin") @NotNull LocalDate dateFin,
@QueryParam("excludeId") UUID excludeId) {
try {
logger.debug("GET /api/plannings-materiel/check-conflits");
List<PlanningMateriel> conflitsList =
planningService.checkConflits(materielId, dateDebut, dateFin, excludeId);
return Response.ok(
new Object() {
public boolean disponible = conflitsList.isEmpty();
public int nombreConflits = conflitsList.size();
public List<PlanningMateriel> conflits = conflitsList;
})
.build();
} catch (Exception e) {
logger.error("Erreur lors de la vérification des conflits", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la vérification des conflits: " + e.getMessage())
.build();
}
}
@GET
@Path("/disponibilite/{materielId}")
public Response analyserDisponibilite(
@PathParam("materielId") UUID materielId,
@QueryParam("dateDebut") @NotNull LocalDate dateDebut,
@QueryParam("dateFin") @NotNull LocalDate dateFin) {
try {
logger.debug("GET /api/plannings-materiel/disponibilite/{}", materielId);
Map<String, Object> disponibilite =
planningService.analyserDisponibilite(materielId, dateDebut, dateFin);
return Response.ok(disponibilite).build();
} catch (Exception e) {
logger.error("Erreur lors de l'analyse de disponibilité: " + materielId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de l'analyse de disponibilité: " + e.getMessage())
.build();
}
}
// === ENDPOINTS D'OPTIMISATION ===
@POST
@Path("/optimiser")
public Response optimiserPlannings() {
try {
logger.info("POST /api/plannings-materiel/optimiser");
List<PlanningMateriel> optimises = planningService.optimiserPlannings();
return Response.ok(Map.of("nombreOptimises", optimises.size(), "plannings", optimises))
.build();
} catch (Exception e) {
logger.error("Erreur lors de l'optimisation des plannings", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de l'optimisation: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/optimiser")
public Response optimiserPlanning(@PathParam("id") UUID id) {
try {
logger.info("PUT /api/plannings-materiel/{}/optimiser", id);
PlanningMateriel planning = planningService.findByIdRequired(id);
planningService.optimiserPlanning(planning);
return Response.ok(planning).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de l'optimisation du planning: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de l'optimisation du planning: " + e.getMessage())
.build();
}
}
// === ENDPOINTS STATISTIQUES ===
@GET
@Path("/statistiques")
public Response getStatistiques() {
try {
logger.debug("GET /api/plannings-materiel/statistiques");
Map<String, Object> statistiques = planningService.getStatistiques();
return Response.ok(statistiques).build();
} catch (Exception e) {
logger.error("Erreur lors de la génération des statistiques", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la génération des statistiques: " + e.getMessage())
.build();
}
}
@GET
@Path("/tableau-bord")
public Response getTableauBord() {
try {
logger.debug("GET /api/plannings-materiel/tableau-bord");
Map<String, Object> tableauBord = planningService.getTableauBordPlannings();
return Response.ok(tableauBord).build();
} catch (Exception e) {
logger.error("Erreur lors de la génération du tableau de bord", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la génération du tableau de bord: " + e.getMessage())
.build();
}
}
@GET
@Path("/taux-utilisation")
public Response analyserTauxUtilisation(
@QueryParam("dateDebut") @NotNull LocalDate dateDebut,
@QueryParam("dateFin") @NotNull LocalDate dateFin) {
try {
logger.debug("GET /api/plannings-materiel/taux-utilisation");
List<Object> analyse = planningService.analyserTauxUtilisation(dateDebut, dateFin);
return Response.ok(analyse).build();
} catch (Exception e) {
logger.error("Erreur lors de l'analyse des taux d'utilisation", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de l'analyse: " + e.getMessage())
.build();
}
}
// === MAINTENANCE AUTOMATIQUE ===
@POST
@Path("/verifier-conflits")
public Response verifierTousConflits() {
try {
logger.info("POST /api/plannings-materiel/verifier-conflits");
planningService.verifierTousConflits();
return Response.ok(Map.of("message", "Vérification des conflits terminée")).build();
} catch (Exception e) {
logger.error("Erreur lors de la vérification des conflits", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la vérification des conflits: " + e.getMessage())
.build();
}
}
// === CLASSES DE REQUÊTE ===
public static class CreatePlanningRequest {
@NotNull public UUID materielId;
@NotNull public String nomPlanning;
public String descriptionPlanning;
@NotNull public LocalDate dateDebut;
@NotNull public LocalDate dateFin;
@NotNull public TypePlanning typePlanning;
public String planificateur;
}
public static class UpdatePlanningRequest {
public String nomPlanning;
public String descriptionPlanning;
public LocalDate dateDebut;
public LocalDate dateFin;
public String modifiePar;
}
public static class ValiderPlanningRequest {
@NotNull public String valideur;
public String commentaires;
}
public static class RevisionPlanningRequest {
@NotNull public String motif;
}
}

View File

@@ -0,0 +1,574 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.ReservationMaterielService;
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.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* API REST pour la gestion des réservations matériel EXPOSITION: Endpoints pour l'affectation et
* planification matériel/chantier
*/
@Path("/api/v1/reservations-materiel")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ReservationMaterielResource {
private static final Logger logger = LoggerFactory.getLogger(ReservationMaterielResource.class);
@Inject ReservationMaterielService reservationService;
// === ENDPOINTS DE CONSULTATION ===
@GET
@Path("/")
public Response findAll(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("50") int size) {
try {
logger.debug("GET /api/reservations-materiel/ - page: {}, size: {}", page, size);
List<ReservationMateriel> reservations;
if (page > 0 || size < 1000) {
reservations = reservationService.findAll(page, size);
} else {
reservations = reservationService.findAll();
}
return Response.ok(reservations).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des réservations", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des réservations: " + e.getMessage())
.build();
}
}
@GET
@Path("/{id}")
public Response findById(@PathParam("id") UUID id) {
try {
logger.debug("GET /api/reservations-materiel/{}", id);
ReservationMateriel reservation = reservationService.findByIdRequired(id);
return Response.ok(reservation).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération de la réservation: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération de la réservation: " + e.getMessage())
.build();
}
}
@GET
@Path("/reference/{reference}")
public Response findByReference(@PathParam("reference") String reference) {
try {
logger.debug("GET /api/reservations-materiel/reference/{}", reference);
return reservationService
.findByReference(reference)
.map(reservation -> Response.ok(reservation).build())
.orElse(
Response.status(Response.Status.NOT_FOUND)
.entity("Réservation non trouvée avec la référence: " + reference)
.build());
} catch (Exception e) {
logger.error(
"Erreur lors de la récupération de la réservation par référence: " + reference, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération de la réservation: " + e.getMessage())
.build();
}
}
@GET
@Path("/materiel/{materielId}")
public Response findByMateriel(@PathParam("materielId") UUID materielId) {
try {
logger.debug("GET /api/reservations-materiel/materiel/{}", materielId);
List<ReservationMateriel> reservations = reservationService.findByMateriel(materielId);
return Response.ok(reservations).build();
} catch (Exception e) {
logger.error(
"Erreur lors de la récupération des réservations pour matériel: " + materielId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des réservations: " + e.getMessage())
.build();
}
}
@GET
@Path("/chantier/{chantierId}")
public Response findByChantier(@PathParam("chantierId") UUID chantierId) {
try {
logger.debug("GET /api/reservations-materiel/chantier/{}", chantierId);
List<ReservationMateriel> reservations = reservationService.findByChantier(chantierId);
return Response.ok(reservations).build();
} catch (Exception e) {
logger.error(
"Erreur lors de la récupération des réservations pour chantier: " + chantierId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des réservations: " + e.getMessage())
.build();
}
}
@GET
@Path("/statut/{statut}")
public Response findByStatut(@PathParam("statut") String statutStr) {
try {
logger.debug("GET /api/reservations-materiel/statut/{}", statutStr);
StatutReservationMateriel statut = StatutReservationMateriel.fromString(statutStr);
List<ReservationMateriel> reservations = reservationService.findByStatut(statut);
return Response.ok(reservations).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des réservations par statut: " + statutStr, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des réservations: " + e.getMessage())
.build();
}
}
@GET
@Path("/periode")
public Response findByPeriode(
@QueryParam("dateDebut") LocalDate dateDebut, @QueryParam("dateFin") LocalDate dateFin) {
try {
logger.debug(
"GET /api/reservations-materiel/periode?dateDebut={}&dateFin={}", dateDebut, dateFin);
if (dateDebut == null || dateFin == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Les paramètres dateDebut et dateFin sont obligatoires")
.build();
}
List<ReservationMateriel> reservations = reservationService.findByPeriode(dateDebut, dateFin);
return Response.ok(reservations).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des réservations par période", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des réservations: " + e.getMessage())
.build();
}
}
// === ENDPOINTS MÉTIER SPÉCIALISÉS ===
@GET
@Path("/en-attente-validation")
public Response findEnAttenteValidation() {
try {
logger.debug("GET /api/reservations-materiel/en-attente-validation");
List<ReservationMateriel> reservations = reservationService.findEnAttenteValidation();
return Response.ok(reservations).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des réservations en attente", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des réservations: " + e.getMessage())
.build();
}
}
@GET
@Path("/en-retard")
public Response findEnRetard() {
try {
logger.debug("GET /api/reservations-materiel/en-retard");
List<ReservationMateriel> reservations = reservationService.findEnRetard();
return Response.ok(reservations).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des réservations en retard", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des réservations: " + e.getMessage())
.build();
}
}
@GET
@Path("/prioritaires")
public Response findPrioritaires() {
try {
logger.debug("GET /api/reservations-materiel/prioritaires");
List<ReservationMateriel> reservations = reservationService.findPrioritaires();
return Response.ok(reservations).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des réservations prioritaires", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des réservations: " + e.getMessage())
.build();
}
}
@GET
@Path("/search")
public Response search(@QueryParam("terme") String terme) {
try {
logger.debug("GET /api/reservations-materiel/search?terme={}", terme);
List<ReservationMateriel> resultats = reservationService.search(terme);
return Response.ok(resultats).build();
} catch (Exception e) {
logger.error("Erreur lors de la recherche avec terme: " + terme, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la recherche: " + e.getMessage())
.build();
}
}
// === ENDPOINTS DE CRÉATION ET MODIFICATION ===
@POST
@Path("/")
public Response createReservation(@Valid CreateReservationRequest request) {
try {
logger.info("POST /api/reservations-materiel/ - création réservation");
ReservationMateriel reservation =
reservationService.createReservation(
request.materielId,
request.chantierId,
request.phaseId,
request.dateDebut,
request.dateFin,
request.quantite,
request.unite,
request.demandeur,
request.lieuLivraison);
return Response.status(Response.Status.CREATED).entity(reservation).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la création de la réservation", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la création de la réservation: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}")
public Response updateReservation(
@PathParam("id") UUID id, @Valid UpdateReservationRequest request) {
try {
logger.info("PUT /api/reservations-materiel/{}", id);
ReservationMateriel reservation =
reservationService.updateReservation(
id,
request.dateDebut,
request.dateFin,
request.quantite,
request.lieuLivraison,
request.instructionsLivraison,
request.priorite);
return Response.ok(reservation).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la mise à jour de la réservation: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la mise à jour de la réservation: " + e.getMessage())
.build();
}
}
// === ENDPOINTS DE GESTION DU WORKFLOW ===
@PUT
@Path("/{id}/valider")
public Response validerReservation(
@PathParam("id") UUID id, @Valid ValiderReservationRequest request) {
try {
logger.info("PUT /api/reservations-materiel/{}/valider", id);
ReservationMateriel reservation = reservationService.validerReservation(id, request.valideur);
return Response.ok(reservation).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la validation de la réservation: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la validation de la réservation: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/refuser")
public Response refuserReservation(
@PathParam("id") UUID id, @Valid RefuserReservationRequest request) {
try {
logger.info("PUT /api/reservations-materiel/{}/refuser", id);
ReservationMateriel reservation =
reservationService.refuserReservation(id, request.valideur, request.motifRefus);
return Response.ok(reservation).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors du refus de la réservation: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du refus de la réservation: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/livrer")
public Response livrerMateriel(@PathParam("id") UUID id, @Valid LivrerMaterielRequest request) {
try {
logger.info("PUT /api/reservations-materiel/{}/livrer", id);
ReservationMateriel reservation =
reservationService.livrerMateriel(
id, request.dateLivraison, request.observations, request.etatMateriel);
return Response.ok(reservation).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de la livraison du matériel: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la livraison du matériel: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/retourner")
public Response retournerMateriel(
@PathParam("id") UUID id, @Valid RetournerMaterielRequest request) {
try {
logger.info("PUT /api/reservations-materiel/{}/retourner", id);
ReservationMateriel reservation =
reservationService.retournerMateriel(
id, request.dateRetour, request.observations, request.etatMateriel, request.prixReel);
return Response.ok(reservation).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors du retour du matériel: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors du retour du matériel: " + e.getMessage())
.build();
}
}
@PUT
@Path("/{id}/annuler")
public Response annulerReservation(
@PathParam("id") UUID id, @Valid AnnulerReservationRequest request) {
try {
logger.info("PUT /api/reservations-materiel/{}/annuler", id);
ReservationMateriel reservation =
reservationService.annulerReservation(id, request.motifAnnulation);
return Response.ok(reservation).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} catch (BadRequestException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
} catch (Exception e) {
logger.error("Erreur lors de l'annulation de la réservation: " + id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de l'annulation de la réservation: " + e.getMessage())
.build();
}
}
// === ENDPOINTS DE VÉRIFICATION ===
@GET
@Path("/check-conflits")
public Response checkConflits(
@QueryParam("materielId") @NotNull UUID materielId,
@QueryParam("dateDebut") @NotNull LocalDate dateDebut,
@QueryParam("dateFin") @NotNull LocalDate dateFin,
@QueryParam("excludeId") UUID excludeId) {
try {
logger.debug("GET /api/reservations-materiel/check-conflits");
List<ReservationMateriel> conflitsList =
reservationService.checkConflits(materielId, dateDebut, dateFin, excludeId);
return Response.ok(
new Object() {
public boolean disponible = conflitsList.isEmpty();
public int nombreConflits = conflitsList.size();
public List<ReservationMateriel> conflits = conflitsList;
})
.build();
} catch (Exception e) {
logger.error("Erreur lors de la vérification des conflits", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la vérification des conflits: " + e.getMessage())
.build();
}
}
@GET
@Path("/disponibilite/{materielId}")
public Response getDisponibiliteMateriel(
@PathParam("materielId") UUID materielId,
@QueryParam("dateDebut") @NotNull LocalDate dateDebut,
@QueryParam("dateFin") @NotNull LocalDate dateFin) {
try {
logger.debug("GET /api/reservations-materiel/disponibilite/{}", materielId);
Map<String, Object> disponibilite =
reservationService.getDisponibiliteMateriel(materielId, dateDebut, dateFin);
return Response.ok(disponibilite).build();
} catch (Exception e) {
logger.error("Erreur lors de l'analyse de disponibilité: " + materielId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de l'analyse de disponibilité: " + e.getMessage())
.build();
}
}
// === ENDPOINTS STATISTIQUES ===
@GET
@Path("/statistiques")
public Response getStatistiques() {
try {
logger.debug("GET /api/reservations-materiel/statistiques");
Object statistiques = reservationService.getStatistiques();
return Response.ok(statistiques).build();
} catch (Exception e) {
logger.error("Erreur lors de la génération des statistiques", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la génération des statistiques: " + e.getMessage())
.build();
}
}
@GET
@Path("/tableau-bord")
public Response getTableauBord() {
try {
logger.debug("GET /api/reservations-materiel/tableau-bord");
Object tableauBord = reservationService.getTableauBordReservations();
return Response.ok(tableauBord).build();
} catch (Exception e) {
logger.error("Erreur lors de la génération du tableau de bord", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la génération du tableau de bord: " + e.getMessage())
.build();
}
}
// === CLASSES DE REQUÊTE ===
public static class CreateReservationRequest {
@NotNull public UUID materielId;
@NotNull public UUID chantierId;
public UUID phaseId;
@NotNull public LocalDate dateDebut;
@NotNull public LocalDate dateFin;
@NotNull public BigDecimal quantite;
public String unite;
public String demandeur;
public String lieuLivraison;
}
public static class UpdateReservationRequest {
public LocalDate dateDebut;
public LocalDate dateFin;
public BigDecimal quantite;
public String lieuLivraison;
public String instructionsLivraison;
public PrioriteReservation priorite;
}
public static class ValiderReservationRequest {
@NotNull public String valideur;
}
public static class RefuserReservationRequest {
@NotNull public String valideur;
@NotNull public String motifRefus;
}
public static class LivrerMaterielRequest {
public LocalDate dateLivraison;
public String observations;
public String etatMateriel;
}
public static class RetournerMaterielRequest {
public LocalDate dateRetour;
public String observations;
public String etatMateriel;
public BigDecimal prixReel;
}
public static class AnnulerReservationRequest {
@NotNull public String motifAnnulation;
}
}

View File

@@ -0,0 +1,365 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.domain.core.entity.PhaseTemplate;
import dev.lions.btpxpress.domain.core.entity.SousPhaseTemplate;
import dev.lions.btpxpress.domain.infrastructure.repository.PhaseTemplateRepository;
import dev.lions.btpxpress.domain.infrastructure.repository.SousPhaseTemplateRepository;
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.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;
/**
* API REST pour la gestion des templates de sous-phases BTP Fournit les opérations CRUD pour les
* sous-phases de templates
*/
@Path("/api/v1/sous-phase-templates")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Sous-Phase Templates", description = "Gestion des templates de sous-phases BTP")
public class SousPhaseTemplateResource {
@Inject SousPhaseTemplateRepository sousPhaseTemplateRepository;
@Inject PhaseTemplateRepository phaseTemplateRepository;
// ===================================
// CONSULTATION DES SOUS-PHASE TEMPLATES
// ===================================
@GET
@Path("/by-phase/{phaseTemplateId}")
@Operation(summary = "Récupère toutes les sous-phases d'une phase template")
@APIResponse(responseCode = "200", description = "Liste des sous-phases pour la phase template")
@APIResponse(responseCode = "404", description = "Phase template non trouvée")
public Response getSousPhasesByPhaseTemplate(
@Parameter(description = "ID de la phase template parent") @PathParam("phaseTemplateId")
UUID phaseTemplateId) {
// Vérifier que la phase template existe
PhaseTemplate phaseTemplate = phaseTemplateRepository.findById(phaseTemplateId);
if (phaseTemplate == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Phase template non trouvée avec l'ID: " + phaseTemplateId)
.build();
}
List<SousPhaseTemplate> sousPhases =
sousPhaseTemplateRepository.findByPhaseTemplate(phaseTemplate);
return Response.ok(sousPhases).build();
}
@GET
@Path("/{id}")
@Operation(summary = "Récupère une sous-phase template par son ID")
@APIResponse(responseCode = "200", description = "Sous-phase template trouvée")
@APIResponse(responseCode = "404", description = "Sous-phase template non trouvée")
public Response getSousPhaseTemplateById(
@Parameter(description = "Identifiant de la sous-phase template") @PathParam("id") UUID id) {
SousPhaseTemplate sousPhaseTemplate = sousPhaseTemplateRepository.findById(id);
if (sousPhaseTemplate == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
return Response.ok(sousPhaseTemplate).build();
}
@GET
@Path("/critiques/by-phase/{phaseTemplateId}")
@Operation(summary = "Récupère les sous-phases critiques d'une phase template")
@APIResponse(responseCode = "200", description = "Liste des sous-phases critiques")
@APIResponse(responseCode = "404", description = "Phase template non trouvée")
public Response getSousPhasesCritiques(
@Parameter(description = "ID de la phase template parent") @PathParam("phaseTemplateId")
UUID phaseTemplateId) {
PhaseTemplate phaseTemplate = phaseTemplateRepository.findById(phaseTemplateId);
if (phaseTemplate == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Phase template non trouvée avec l'ID: " + phaseTemplateId)
.build();
}
List<SousPhaseTemplate> sousPhasesCritiques =
sousPhaseTemplateRepository.findCritiquesByPhase(phaseTemplate);
return Response.ok(sousPhasesCritiques).build();
}
@GET
@Path("/with-qualified-workers/by-phase/{phaseTemplateId}")
@Operation(summary = "Récupère les sous-phases nécessitant du personnel qualifié")
@APIResponse(
responseCode = "200",
description = "Liste des sous-phases avec personnel qualifié requis")
@APIResponse(responseCode = "404", description = "Phase template non trouvée")
public Response getSousPhaseAvecPersonnelQualifie(
@Parameter(description = "ID de la phase template parent") @PathParam("phaseTemplateId")
UUID phaseTemplateId) {
PhaseTemplate phaseTemplate = phaseTemplateRepository.findById(phaseTemplateId);
if (phaseTemplate == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Phase template non trouvée avec l'ID: " + phaseTemplateId)
.build();
}
List<SousPhaseTemplate> sousPhases =
sousPhaseTemplateRepository.findRequiringQualifiedWorkers(phaseTemplate);
return Response.ok(sousPhases).build();
}
@GET
@Path("/with-materials/by-phase/{phaseTemplateId}")
@Operation(summary = "Récupère les sous-phases avec matériels spécifiques")
@APIResponse(responseCode = "200", description = "Liste des sous-phases avec matériels")
@APIResponse(responseCode = "404", description = "Phase template non trouvée")
public Response getSousPhaseAvecMateriels(
@Parameter(description = "ID de la phase template parent") @PathParam("phaseTemplateId")
UUID phaseTemplateId) {
PhaseTemplate phaseTemplate = phaseTemplateRepository.findById(phaseTemplateId);
if (phaseTemplate == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Phase template non trouvée avec l'ID: " + phaseTemplateId)
.build();
}
List<SousPhaseTemplate> sousPhases =
sousPhaseTemplateRepository.findWithSpecificMaterials(phaseTemplate);
return Response.ok(sousPhases).build();
}
@GET
@Path("/duree-totale/by-phase/{phaseTemplateId}")
@Operation(summary = "Calcule la durée totale des sous-phases d'une phase template")
@APIResponse(responseCode = "200", description = "Durée totale en jours")
@APIResponse(responseCode = "404", description = "Phase template non trouvée")
public Response calculerDureeTotaleSousPhases(
@Parameter(description = "ID de la phase template parent") @PathParam("phaseTemplateId")
UUID phaseTemplateId) {
PhaseTemplate phaseTemplate = phaseTemplateRepository.findById(phaseTemplateId);
if (phaseTemplate == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Phase template non trouvée avec l'ID: " + phaseTemplateId)
.build();
}
Integer dureeTotal = sousPhaseTemplateRepository.calculateDureeTotale(phaseTemplate);
return Response.ok(dureeTotal).build();
}
@GET
@Path("/count/by-phase/{phaseTemplateId}")
@Operation(summary = "Compte le nombre de sous-phases pour une phase template")
@APIResponse(responseCode = "200", description = "Nombre de sous-phases actives")
@APIResponse(responseCode = "404", description = "Phase template non trouvée")
public Response compterSousPhases(
@Parameter(description = "ID de la phase template parent") @PathParam("phaseTemplateId")
UUID phaseTemplateId) {
PhaseTemplate phaseTemplate = phaseTemplateRepository.findById(phaseTemplateId);
if (phaseTemplate == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Phase template non trouvée avec l'ID: " + phaseTemplateId)
.build();
}
long count = sousPhaseTemplateRepository.countByPhaseTemplate(phaseTemplate);
return Response.ok(count).build();
}
@GET
@Path("/search")
@Operation(summary = "Recherche de sous-phases par nom")
@APIResponse(responseCode = "200", description = "Liste des sous-phases correspondantes")
@APIResponse(responseCode = "400", description = "Paramètres de recherche invalides")
@APIResponse(responseCode = "404", description = "Phase template non trouvée")
public Response rechercherSousPhases(
@Parameter(description = "ID de la phase template parent") @QueryParam("phaseTemplateId")
UUID phaseTemplateId,
@Parameter(description = "Terme de recherche") @QueryParam("searchTerm") String searchTerm) {
if (phaseTemplateId == null || searchTerm == null || searchTerm.trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("L'ID de la phase template et le terme de recherche sont obligatoires")
.build();
}
PhaseTemplate phaseTemplate = phaseTemplateRepository.findById(phaseTemplateId);
if (phaseTemplate == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Phase template non trouvée avec l'ID: " + phaseTemplateId)
.build();
}
List<SousPhaseTemplate> resultats =
sousPhaseTemplateRepository.searchByNom(phaseTemplate, searchTerm.trim());
return Response.ok(resultats).build();
}
// ===================================
// ADMINISTRATION DES SOUS-PHASE TEMPLATES
// ===================================
@POST
@Operation(summary = "Crée une nouvelle sous-phase template")
@APIResponse(responseCode = "201", description = "Sous-phase template créée avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
@APIResponse(responseCode = "404", description = "Phase template parent non trouvée")
@APIResponse(responseCode = "409", description = "Conflit - sous-phase existante pour cet ordre")
public Response creerSousPhaseTemplate(@Valid SousPhaseTemplate sousPhaseTemplate) {
// Vérifier que la phase template parent existe
if (sousPhaseTemplate.getPhaseParent() == null
|| sousPhaseTemplate.getPhaseParent().getId() == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("La phase template parent est obligatoire")
.build();
}
PhaseTemplate phaseTemplate =
phaseTemplateRepository.findById(sousPhaseTemplate.getPhaseParent().getId());
if (phaseTemplate == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Phase template parent non trouvée")
.build();
}
// Vérifier l'unicité de l'ordre d'exécution
if (sousPhaseTemplate.getOrdreExecution() != null
&& sousPhaseTemplateRepository.existsByPhaseAndOrdre(
phaseTemplate, sousPhaseTemplate.getOrdreExecution(), null)) {
return Response.status(Response.Status.CONFLICT)
.entity(
"Une sous-phase existe déjà pour cette phase à l'ordre "
+ sousPhaseTemplate.getOrdreExecution())
.build();
}
// Si aucun ordre spécifié, utiliser le prochain disponible
if (sousPhaseTemplate.getOrdreExecution() == null) {
sousPhaseTemplate.setOrdreExecution(
sousPhaseTemplateRepository.getNextOrdreExecution(phaseTemplate));
}
// Assigner la phase template parent
sousPhaseTemplate.setPhaseParent(phaseTemplate);
sousPhaseTemplateRepository.persist(sousPhaseTemplate);
return Response.status(Response.Status.CREATED).entity(sousPhaseTemplate).build();
}
@PUT
@Path("/{id}")
@Operation(summary = "Met à jour une sous-phase template")
@APIResponse(responseCode = "200", description = "Sous-phase template mise à jour avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
@APIResponse(responseCode = "404", description = "Sous-phase template non trouvée")
@APIResponse(responseCode = "409", description = "Conflit - ordre d'exécution déjà utilisé")
public Response updateSousPhaseTemplate(
@Parameter(description = "Identifiant de la sous-phase template") @PathParam("id") UUID id,
@Valid SousPhaseTemplate sousPhaseData) {
SousPhaseTemplate existingSousPhase = sousPhaseTemplateRepository.findById(id);
if (existingSousPhase == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Sous-phase template non trouvée avec l'ID: " + id)
.build();
}
// Vérifier l'unicité de l'ordre d'exécution si modifié
if (sousPhaseData.getOrdreExecution() != null
&& !sousPhaseData.getOrdreExecution().equals(existingSousPhase.getOrdreExecution())
&& sousPhaseTemplateRepository.existsByPhaseAndOrdre(
existingSousPhase.getPhaseParent(), sousPhaseData.getOrdreExecution(), id)) {
return Response.status(Response.Status.CONFLICT)
.entity(
"Une autre sous-phase existe déjà pour cette phase à l'ordre "
+ sousPhaseData.getOrdreExecution())
.build();
}
// Mettre à jour les champs
existingSousPhase.setNom(sousPhaseData.getNom());
existingSousPhase.setDescription(sousPhaseData.getDescription());
existingSousPhase.setOrdreExecution(sousPhaseData.getOrdreExecution());
existingSousPhase.setDureePrevueJours(sousPhaseData.getDureePrevueJours());
existingSousPhase.setDureeEstimeeHeures(sousPhaseData.getDureeEstimeeHeures());
existingSousPhase.setCritique(sousPhaseData.getCritique());
existingSousPhase.setPriorite(sousPhaseData.getPriorite());
existingSousPhase.setMaterielsTypes(sousPhaseData.getMaterielsTypes());
existingSousPhase.setCompetencesRequises(sousPhaseData.getCompetencesRequises());
existingSousPhase.setOutilsNecessaires(sousPhaseData.getOutilsNecessaires());
existingSousPhase.setInstructionsExecution(sousPhaseData.getInstructionsExecution());
existingSousPhase.setPointsControle(sousPhaseData.getPointsControle());
existingSousPhase.setCriteresValidation(sousPhaseData.getCriteresValidation());
existingSousPhase.setPrecautionsSecurite(sousPhaseData.getPrecautionsSecurite());
existingSousPhase.setConditionsExecution(sousPhaseData.getConditionsExecution());
existingSousPhase.setTempsPreparationMinutes(sousPhaseData.getTempsPreparationMinutes());
existingSousPhase.setTempsFinitionMinutes(sousPhaseData.getTempsFinitionMinutes());
existingSousPhase.setNombreOperateursRequis(sousPhaseData.getNombreOperateursRequis());
existingSousPhase.setNiveauQualification(sousPhaseData.getNiveauQualification());
return Response.ok(existingSousPhase).build();
}
@DELETE
@Path("/{id}")
@Operation(summary = "Supprime une sous-phase template (désactivation)")
@APIResponse(responseCode = "204", description = "Sous-phase template supprimée avec succès")
@APIResponse(responseCode = "404", description = "Sous-phase template non trouvée")
public Response supprimerSousPhaseTemplate(
@Parameter(description = "Identifiant de la sous-phase template") @PathParam("id") UUID id) {
SousPhaseTemplate sousPhaseTemplate = sousPhaseTemplateRepository.findById(id);
if (sousPhaseTemplate == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Sous-phase template non trouvée avec l'ID: " + id)
.build();
}
int updated = sousPhaseTemplateRepository.desactiver(id);
if (updated > 0) {
return Response.noContent().build();
} else {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la suppression de la sous-phase template")
.build();
}
}
@PUT
@Path("/{id}/reactiver")
@Operation(summary = "Réactive une sous-phase template")
@APIResponse(responseCode = "200", description = "Sous-phase template réactivée avec succès")
@APIResponse(responseCode = "404", description = "Sous-phase template non trouvée")
public Response reactiverSousPhaseTemplate(
@Parameter(description = "Identifiant de la sous-phase template") @PathParam("id") UUID id) {
SousPhaseTemplate sousPhaseTemplate = sousPhaseTemplateRepository.findById(id);
if (sousPhaseTemplate == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Sous-phase template non trouvée avec l'ID: " + id)
.build();
}
int updated = sousPhaseTemplateRepository.reactiver(id);
if (updated > 0) {
sousPhaseTemplate.setActif(true);
return Response.ok(sousPhaseTemplate).build();
} else {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la réactivation de la sous-phase template")
.build();
}
}
}

View File

@@ -0,0 +1,385 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.StockService;
import dev.lions.btpxpress.domain.core.entity.CategorieStock;
import dev.lions.btpxpress.domain.core.entity.StatutStock;
import dev.lions.btpxpress.domain.core.entity.Stock;
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.math.BigDecimal;
import java.util.HashMap;
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 stocks et inventaires BTP
* Architecture 2025 : API complète pour la gestion des stocks, entrées/sorties, réservations
*/
@Path("/api/v1/stocks")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Stocks", description = "Gestion des stocks et inventaires BTP")
public class StockResource {
private static final Logger logger = LoggerFactory.getLogger(StockResource.class);
@Inject StockService stockService;
// === ENDPOINTS DE LECTURE - ARCHITECTURE 2025 ===
@GET
@Operation(summary = "Récupérer tous les stocks", description = "Retourne la liste complète des articles en stock")
@APIResponse(responseCode = "200", description = "Liste des stocks récupérée avec succès")
public Response getAllStocks() {
logger.debug("GET /stocks");
try {
List<Stock> stocks = stockService.findAll();
Map<String, Object> response = new HashMap<>();
response.put("stocks", stocks);
response.put("total", stocks.size());
return Response.ok(response).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des stocks", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération des stocks", "message", e.getMessage()))
.build();
}
}
@GET
@Path("/{id}")
@Operation(summary = "Récupérer un stock par ID", description = "Retourne les détails d'un article en stock")
@APIResponse(responseCode = "200", description = "Stock trouvé")
@APIResponse(responseCode = "404", description = "Stock non trouvé")
public Response getStockById(@Parameter(description = "ID du stock") @PathParam("id") UUID id) {
logger.debug("GET /stocks/{}", id);
try {
Stock stock = stockService.findById(id);
return Response.ok(stock).build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération du stock: {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération du stock"))
.build();
}
}
@GET
@Path("/reference/{reference}")
@Operation(summary = "Récupérer un stock par référence", description = "Recherche un article en stock par sa référence")
@APIResponse(responseCode = "200", description = "Stock trouvé")
@APIResponse(responseCode = "404", description = "Stock non trouvé")
public Response getStockByReference(@Parameter(description = "Référence du stock") @PathParam("reference") String reference) {
logger.debug("GET /stocks/reference/{}", reference);
try {
Stock stock = stockService.findByReference(reference);
if (stock == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "Stock non trouvé"))
.build();
}
return Response.ok(stock).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération du stock par référence: {}", reference, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération du stock"))
.build();
}
}
@GET
@Path("/search")
@Operation(summary = "Rechercher des stocks", description = "Recherche textuelle dans les stocks")
@APIResponse(responseCode = "200", description = "Résultats de recherche")
@APIResponse(responseCode = "400", description = "Terme de recherche requis")
public Response searchStocks(@Parameter(description = "Terme de recherche") @QueryParam("term") String searchTerm) {
logger.debug("GET /stocks/search - term: {}", searchTerm);
try {
if (searchTerm == null || searchTerm.trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Terme de recherche requis"))
.build();
}
List<Stock> stocks = stockService.searchStocks(searchTerm);
Map<String, Object> response = new HashMap<>();
response.put("stocks", stocks);
response.put("total", stocks.size());
return Response.ok(response).build();
} catch (Exception e) {
logger.error("Erreur lors de la recherche de stocks: {}", searchTerm, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la recherche"))
.build();
}
}
@GET
@Path("/categorie/{categorie}")
@Operation(summary = "Récupérer les stocks par catégorie", description = "Filtre les stocks par catégorie")
@APIResponse(responseCode = "200", description = "Liste des stocks filtrés")
public Response getStocksByCategorie(@Parameter(description = "Catégorie de stock") @PathParam("categorie") String categorieStr) {
logger.debug("GET /stocks/categorie/{}", categorieStr);
try {
CategorieStock categorie = CategorieStock.valueOf(categorieStr.toUpperCase());
List<Stock> stocks = stockService.findByCategorie(categorie);
Map<String, Object> response = new HashMap<>();
response.put("stocks", stocks);
response.put("total", stocks.size());
return Response.ok(response).build();
} catch (IllegalArgumentException e) {
logger.error("Catégorie invalide: {}", categorieStr, e);
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Catégorie invalide", "valeurs_acceptees", java.util.Arrays.toString(CategorieStock.values())))
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des stocks par catégorie: {}", categorieStr, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération des stocks"))
.build();
}
}
@GET
@Path("/statut/{statut}")
@Operation(summary = "Récupérer les stocks par statut", description = "Filtre les stocks par statut")
@APIResponse(responseCode = "200", description = "Liste des stocks filtrés")
public Response getStocksByStatut(@Parameter(description = "Statut du stock") @PathParam("statut") StatutStock statut) {
logger.debug("GET /stocks/statut/{}", statut);
try {
List<Stock> stocks = stockService.findByStatut(statut);
Map<String, Object> response = new HashMap<>();
response.put("stocks", stocks);
response.put("total", stocks.size());
return Response.ok(response).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des stocks par statut: {}", statut, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération des stocks"))
.build();
}
}
@GET
@Path("/rupture")
@Operation(summary = "Récupérer les stocks en rupture", description = "Liste les articles en rupture de stock")
@APIResponse(responseCode = "200", description = "Liste des stocks en rupture")
public Response getStocksEnRupture() {
logger.debug("GET /stocks/rupture");
try {
List<Stock> stocks = stockService.findStocksEnRupture();
Map<String, Object> response = new HashMap<>();
response.put("stocks", stocks);
response.put("total", stocks.size());
return Response.ok(response).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des stocks en rupture", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération des stocks"))
.build();
}
}
@GET
@Path("/statistiques")
@Operation(summary = "Récupérer les statistiques des stocks", description = "Retourne des statistiques globales sur les stocks")
@APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès")
public Response getStatistiques() {
logger.debug("GET /stocks/statistiques");
try {
Map<String, Object> stats = stockService.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(Map.of("error", "Erreur lors de la récupération des statistiques"))
.build();
}
}
@GET
@Path("/valeur-totale")
@Operation(summary = "Calculer la valeur totale du stock", description = "Calcule la valeur totale de tous les stocks")
@APIResponse(responseCode = "200", description = "Valeur totale calculée")
public Response getValeurTotaleStock() {
logger.debug("GET /stocks/valeur-totale");
try {
BigDecimal valeurTotale = stockService.calculateValeurTotaleStock();
return Response.ok(Map.of("valeurTotale", valeurTotale)).build();
} catch (Exception e) {
logger.error("Erreur lors du calcul de la valeur totale", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors du calcul de la valeur totale"))
.build();
}
}
// === ENDPOINTS DE CRÉATION - ARCHITECTURE 2025 ===
@POST
@Authenticated
@Operation(summary = "Créer un nouveau stock", description = "Crée un nouvel article en stock")
@APIResponse(responseCode = "201", description = "Stock créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response createStock(@Valid @NotNull Stock stock) {
logger.info("POST /stocks - Création d'un stock: {}", stock.getReference());
try {
Stock nouveauStock = stockService.create(stock);
return Response.status(Response.Status.CREATED).entity(nouveauStock).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
logger.error("Erreur lors de la création du stock", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la création du stock"))
.build();
}
}
// === ENDPOINTS DE MISE À JOUR - ARCHITECTURE 2025 ===
@PUT
@Path("/{id}")
@Authenticated
@Operation(summary = "Mettre à jour un stock", description = "Met à jour les informations d'un article en stock")
@APIResponse(responseCode = "200", description = "Stock mis à jour avec succès")
@APIResponse(responseCode = "404", description = "Stock non trouvé")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response updateStock(
@Parameter(description = "ID du stock") @PathParam("id") UUID id,
@Valid @NotNull Stock stockData) {
logger.info("PUT /stocks/{} - Mise à jour", id);
try {
Stock stock = stockService.update(id, stockData);
return Response.ok(stock).build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
logger.error("Erreur lors de la mise à jour du stock: {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la mise à jour du stock"))
.build();
}
}
@POST
@Path("/{id}/entree")
@Authenticated
@Operation(summary = "Enregistrer une entrée de stock", description = "Ajoute une quantité au stock")
@APIResponse(responseCode = "200", description = "Entrée enregistrée avec succès")
@APIResponse(responseCode = "404", description = "Stock non trouvé")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response entreeStock(
@Parameter(description = "ID du stock") @PathParam("id") UUID id,
Map<String, Object> payload) {
logger.info("POST /stocks/{}/entree", id);
try {
BigDecimal quantite = new BigDecimal(payload.get("quantite").toString());
String motif = payload.get("motif") != null ? payload.get("motif").toString() : null;
String numeroDocument = payload.get("numeroDocument") != null ? payload.get("numeroDocument").toString() : null;
Stock stock = stockService.entreeStock(id, quantite, motif, numeroDocument);
return Response.ok(stock).build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Quantité ou données invalides"))
.build();
} catch (Exception e) {
logger.error("Erreur lors de l'entrée de stock: {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de l'entrée de stock"))
.build();
}
}
@POST
@Path("/{id}/sortie")
@Authenticated
@Operation(summary = "Enregistrer une sortie de stock", description = "Retire une quantité du stock")
@APIResponse(responseCode = "200", description = "Sortie enregistrée avec succès")
@APIResponse(responseCode = "404", description = "Stock non trouvé")
@APIResponse(responseCode = "400", description = "Quantité insuffisante")
public Response sortieStock(
@Parameter(description = "ID du stock") @PathParam("id") UUID id,
Map<String, Object> payload) {
logger.info("POST /stocks/{}/sortie", id);
try {
BigDecimal quantite = new BigDecimal(payload.get("quantite").toString());
String motif = payload.get("motif") != null ? payload.get("motif").toString() : null;
String numeroDocument = payload.get("numeroDocument") != null ? payload.get("numeroDocument").toString() : null;
Stock stock = stockService.sortieStock(id, quantite, motif, numeroDocument);
return Response.ok(stock).build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Quantité insuffisante ou données invalides"))
.build();
} catch (Exception e) {
logger.error("Erreur lors de la sortie de stock: {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la sortie de stock"))
.build();
}
}
// === ENDPOINTS DE SUPPRESSION - ARCHITECTURE 2025 ===
@DELETE
@Path("/{id}")
@Authenticated
@Operation(summary = "Supprimer un stock", description = "Supprime un article du stock")
@APIResponse(responseCode = "204", description = "Stock supprimé avec succès")
@APIResponse(responseCode = "404", description = "Stock non trouvé")
public Response deleteStock(@Parameter(description = "ID du stock") @PathParam("id") UUID id) {
logger.info("DELETE /stocks/{}", id);
try {
stockService.delete(id);
return Response.noContent().build();
} catch (jakarta.ws.rs.NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
logger.error("Erreur lors de la suppression du stock: {}", id, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la suppression du stock"))
.build();
}
}
}

View File

@@ -0,0 +1,443 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.TacheTemplateService;
import dev.lions.btpxpress.domain.core.entity.SousPhaseTemplate;
import dev.lions.btpxpress.domain.core.entity.TacheTemplate;
import dev.lions.btpxpress.domain.core.entity.TypeChantierBTP;
import dev.lions.btpxpress.domain.infrastructure.repository.SousPhaseTemplateRepository;
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.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;
/**
* API REST pour la gestion des templates de tâches BTP Permet aux utilisateurs de gérer
* complètement les tâches après déploiement Fournit les opérations CRUD pour la gestion granulaire
* des tâches
*/
@Path("/api/v1/tache-templates")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Tâche Templates", description = "Gestion granulaire des templates de tâches BTP")
public class TacheTemplateResource {
@Inject TacheTemplateService tacheTemplateService;
@Inject SousPhaseTemplateRepository sousPhaseTemplateRepository;
// ===================================
// CONSULTATION DES TÂCHES TEMPLATES
// ===================================
/** Récupère toutes les tâches d'une sous-phase */
@GET
@Path("/by-sous-phase/{sousPhaseId}")
@Operation(summary = "Récupère toutes les tâches d'une sous-phase template")
@APIResponse(responseCode = "200", description = "Liste des tâches pour la sous-phase template")
@APIResponse(responseCode = "404", description = "Sous-phase template non trouvée")
public Response getTachesBySousPhase(
@Parameter(description = "ID de la sous-phase template parent") @PathParam("sousPhaseId")
UUID sousPhaseId) {
// Vérifier que la sous-phase template existe
SousPhaseTemplate sousPhaseTemplate = sousPhaseTemplateRepository.findById(sousPhaseId);
if (sousPhaseTemplate == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Sous-phase template non trouvée avec l'ID: " + sousPhaseId)
.build();
}
List<TacheTemplate> taches = tacheTemplateService.getTachesBySousPhase(sousPhaseId);
return Response.ok(taches).build();
}
/** Récupère toutes les tâches actives d'une sous-phase */
@GET
@Path("/by-sous-phase/{sousPhaseId}/actives")
@Operation(summary = "Récupère toutes les tâches actives d'une sous-phase template")
@APIResponse(responseCode = "200", description = "Liste des tâches actives")
@APIResponse(responseCode = "404", description = "Sous-phase template non trouvée")
public Response getActiveTachesBySousPhase(
@Parameter(description = "ID de la sous-phase template") @PathParam("sousPhaseId")
UUID sousPhaseId) {
SousPhaseTemplate sousPhaseTemplate = sousPhaseTemplateRepository.findById(sousPhaseId);
if (sousPhaseTemplate == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Sous-phase template non trouvée avec l'ID: " + sousPhaseId)
.build();
}
List<TacheTemplate> taches = tacheTemplateService.getActiveTachesBySousPhase(sousPhaseId);
return Response.ok(taches).build();
}
/** Récupère une tâche template par son ID */
@GET
@Path("/{id}")
@Operation(summary = "Récupère une tâche template par son ID")
@APIResponse(responseCode = "200", description = "Tâche template trouvée")
@APIResponse(responseCode = "404", description = "Tâche template non trouvée")
public Response getTacheTemplate(
@Parameter(description = "Identifiant de la tâche template") @PathParam("id") UUID id) {
try {
TacheTemplate tache = tacheTemplateService.getTacheTemplateById(id);
return Response.ok(tache).build();
} catch (Exception e) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Tâche template non trouvée avec l'ID: " + id)
.build();
}
}
/** Recherche des tâches par nom ou description */
@GET
@Path("/search")
@Operation(summary = "Recherche de tâches par nom ou description")
@APIResponse(responseCode = "200", description = "Liste des tâches correspondantes")
@APIResponse(responseCode = "400", description = "Paramètre de recherche manquant")
public Response searchTaches(
@Parameter(description = "Terme de recherche") @QueryParam("q") String searchTerm) {
if (searchTerm == null || searchTerm.trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Le terme de recherche est obligatoire")
.build();
}
List<TacheTemplate> taches = tacheTemplateService.searchTaches(searchTerm.trim());
return Response.ok(taches).build();
}
/** Récupère toutes les tâches d'un type de chantier */
@GET
@Path("/by-type-chantier/{typeChantier}")
@Operation(summary = "Récupère toutes les tâches d'un type de chantier")
@APIResponse(responseCode = "200", description = "Liste des tâches pour le type de chantier")
@APIResponse(responseCode = "400", description = "Type de chantier invalide")
public Response getTachesByTypeChantier(
@Parameter(description = "Type de chantier BTP") @PathParam("typeChantier")
String typeChantierStr) {
try {
TypeChantierBTP typeChantier = TypeChantierBTP.valueOf(typeChantierStr.toUpperCase());
List<TacheTemplate> taches = tacheTemplateService.getTachesByTypeChantier(typeChantier);
return Response.ok(taches).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Type de chantier invalide: " + typeChantierStr)
.build();
}
}
/** Récupère les statistiques d'une sous-phase basées sur ses tâches */
@GET
@Path("/stats/by-sous-phase/{sousPhaseId}")
@Operation(summary = "Calcule les statistiques d'une sous-phase basées sur ses tâches")
@APIResponse(responseCode = "200", description = "Statistiques de la sous-phase")
@APIResponse(responseCode = "404", description = "Sous-phase template non trouvée")
public Response getSousPhaseStatistics(
@Parameter(description = "ID de la sous-phase template") @PathParam("sousPhaseId")
UUID sousPhaseId) {
SousPhaseTemplate sousPhaseTemplate = sousPhaseTemplateRepository.findById(sousPhaseId);
if (sousPhaseTemplate == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Sous-phase template non trouvée avec l'ID: " + sousPhaseId)
.build();
}
TacheTemplateService.SousPhaseStatistics stats =
tacheTemplateService.calculateSousPhaseStatistics(sousPhaseId);
return Response.ok(stats).build();
}
/** Récupère toutes les tâches critiques d'une sous-phase */
@GET
@Path("/critiques/by-sous-phase/{sousPhaseId}")
@Operation(summary = "Récupère toutes les tâches critiques d'une sous-phase")
@APIResponse(responseCode = "200", description = "Liste des tâches critiques")
@APIResponse(responseCode = "404", description = "Sous-phase template non trouvée")
public Response getCriticalTachesBySousPhase(
@Parameter(description = "ID de la sous-phase template") @PathParam("sousPhaseId")
UUID sousPhaseId) {
SousPhaseTemplate sousPhaseTemplate = sousPhaseTemplateRepository.findById(sousPhaseId);
if (sousPhaseTemplate == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Sous-phase template non trouvée avec l'ID: " + sousPhaseId)
.build();
}
List<TacheTemplate> taches = tacheTemplateService.getCriticalTachesBySousPhase(sousPhaseId);
return Response.ok(taches).build();
}
/** Récupère toutes les tâches bloquantes d'une sous-phase */
@GET
@Path("/bloquantes/by-sous-phase/{sousPhaseId}")
@Operation(summary = "Récupère toutes les tâches bloquantes d'une sous-phase")
@APIResponse(responseCode = "200", description = "Liste des tâches bloquantes")
@APIResponse(responseCode = "404", description = "Sous-phase template non trouvée")
public Response getBlockingTachesBySousPhase(
@Parameter(description = "ID de la sous-phase template") @PathParam("sousPhaseId")
UUID sousPhaseId) {
SousPhaseTemplate sousPhaseTemplate = sousPhaseTemplateRepository.findById(sousPhaseId);
if (sousPhaseTemplate == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Sous-phase template non trouvée avec l'ID: " + sousPhaseId)
.build();
}
List<TacheTemplate> taches = tacheTemplateService.getBlockingTachesBySousPhase(sousPhaseId);
return Response.ok(taches).build();
}
// ===================================
// ADMINISTRATION DES TÂCHES TEMPLATES
// ===================================
/** Crée une nouvelle tâche template */
@POST
@Operation(summary = "Crée une nouvelle tâche template")
@APIResponse(responseCode = "201", description = "Tâche template créée avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
@APIResponse(responseCode = "404", description = "Sous-phase template parent non trouvée")
public Response createTacheTemplate(@Valid TacheTemplate tacheTemplate) {
// Vérifier que la sous-phase template parent existe
if (tacheTemplate.getSousPhaseParent() == null
|| tacheTemplate.getSousPhaseParent().getId() == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("La sous-phase template parent est obligatoire")
.build();
}
SousPhaseTemplate sousPhaseTemplate =
sousPhaseTemplateRepository.findById(tacheTemplate.getSousPhaseParent().getId());
if (sousPhaseTemplate == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Sous-phase template parent non trouvée")
.build();
}
try {
TacheTemplate createdTache = tacheTemplateService.createTacheTemplate(tacheTemplate);
return Response.status(Response.Status.CREATED).entity(createdTache).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
}
}
/** Met à jour une tâche template */
@PUT
@Path("/{id}")
@Operation(summary = "Met à jour une tâche template")
@APIResponse(responseCode = "200", description = "Tâche template mise à jour avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
@APIResponse(responseCode = "404", description = "Tâche template non trouvée")
public Response updateTacheTemplate(
@Parameter(description = "Identifiant de la tâche template") @PathParam("id") UUID id,
@Valid TacheTemplate tacheTemplateData) {
try {
TacheTemplate updatedTache = tacheTemplateService.updateTacheTemplate(id, tacheTemplateData);
return Response.ok(updatedTache).build();
} catch (IllegalArgumentException e) {
if (e.getMessage().contains("non trouvé")) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
} else {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
}
}
}
/** Duplique une tâche template vers une autre sous-phase */
@POST
@Path("/{id}/duplicate")
@Operation(summary = "Duplique une tâche template vers une autre sous-phase")
@APIResponse(responseCode = "201", description = "Tâche template dupliquée avec succès")
@APIResponse(
responseCode = "404",
description = "Tâche template ou sous-phase de destination non trouvée")
public Response duplicateTacheTemplate(
@Parameter(description = "ID de la tâche template à dupliquer") @PathParam("id") UUID id,
@Parameter(description = "ID de la nouvelle sous-phase parent") @QueryParam("newSousPhaseId")
UUID newSousPhaseId) {
if (newSousPhaseId == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("L'ID de la nouvelle sous-phase est obligatoire")
.build();
}
try {
TacheTemplate duplicatedTache =
tacheTemplateService.duplicateTacheTemplate(id, newSousPhaseId);
return Response.status(Response.Status.CREATED).entity(duplicatedTache).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
}
}
/** Désactive une tâche template */
@PUT
@Path("/{id}/deactivate")
@Operation(summary = "Désactive une tâche template")
@APIResponse(responseCode = "204", description = "Tâche template désactivée avec succès")
@APIResponse(responseCode = "404", description = "Tâche template non trouvée")
public Response deactivateTacheTemplate(
@Parameter(description = "Identifiant de la tâche template") @PathParam("id") UUID id) {
try {
tacheTemplateService.deactivateTacheTemplate(id);
return Response.noContent().build();
} catch (Exception e) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Tâche template non trouvée avec l'ID: " + id)
.build();
}
}
/** Supprime définitivement une tâche template */
@DELETE
@Path("/{id}")
@Operation(summary = "Supprime définitivement une tâche template")
@APIResponse(responseCode = "204", description = "Tâche template supprimée avec succès")
@APIResponse(responseCode = "404", description = "Tâche template non trouvée")
public Response deleteTacheTemplate(
@Parameter(description = "Identifiant de la tâche template") @PathParam("id") UUID id) {
try {
tacheTemplateService.deleteTacheTemplate(id);
return Response.noContent().build();
} catch (Exception e) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Tâche template non trouvée avec l'ID: " + id)
.build();
}
}
/** Réorganise l'ordre des tâches dans une sous-phase */
@PUT
@Path("/reorder/by-sous-phase/{sousPhaseId}")
@Operation(summary = "Réorganise l'ordre des tâches dans une sous-phase")
@APIResponse(responseCode = "200", description = "Tâches réorganisées avec succès")
@APIResponse(responseCode = "400", description = "Liste des IDs invalide")
@APIResponse(responseCode = "404", description = "Sous-phase template non trouvée")
public Response reorderTaches(
@Parameter(description = "ID de la sous-phase template") @PathParam("sousPhaseId")
UUID sousPhaseId,
List<UUID> tacheIds) {
if (tacheIds == null || tacheIds.isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("La liste des IDs de tâches ne peut pas être vide")
.build();
}
SousPhaseTemplate sousPhaseTemplate = sousPhaseTemplateRepository.findById(sousPhaseId);
if (sousPhaseTemplate == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Sous-phase template non trouvée avec l'ID: " + sousPhaseId)
.build();
}
try {
tacheTemplateService.reorderTaches(sousPhaseId, tacheIds);
return Response.ok().build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
}
}
/** Crée plusieurs tâches en lot */
@POST
@Path("/batch")
@Operation(summary = "Crée plusieurs tâches templates en lot")
@APIResponse(responseCode = "201", description = "Tâches créées avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response createTachesBatch(@Valid List<TacheTemplate> taches) {
if (taches == null || taches.isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("La liste des tâches ne peut pas être vide")
.build();
}
try {
List<TacheTemplate> createdTaches =
taches.stream().map(tacheTemplateService::createTacheTemplate).toList();
return Response.status(Response.Status.CREATED).entity(createdTaches).build();
} catch (Exception e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Erreur lors de la création des tâches: " + e.getMessage())
.build();
}
}
/** Récupère un template vide pour la création */
@GET
@Path("/template-vide")
@Operation(summary = "Récupère un template vide pour la création d'une nouvelle tâche")
@APIResponse(responseCode = "200", description = "Template vide")
public Response getEmptyTemplate() {
TacheTemplate emptyTemplate = new TacheTemplate();
emptyTemplate.setNombreOperateursRequis(1);
emptyTemplate.setCritique(false);
emptyTemplate.setBloquante(false);
emptyTemplate.setActif(true);
emptyTemplate.setConditionsMeteo(TacheTemplate.ConditionMeteo.TOUS_TEMPS);
return Response.ok(emptyTemplate).build();
}
/** Validation des données d'une tâche template */
@POST
@Path("/validate")
@Operation(summary = "Valide les données d'une tâche template")
@APIResponse(responseCode = "200", description = "Résultat de validation")
public Response validateTacheTemplate(TacheTemplate tacheTemplate) {
ValidationResult result = new ValidationResult();
result.valid = true;
if (tacheTemplate.getNom() == null || tacheTemplate.getNom().trim().isEmpty()) {
result.valid = false;
result.errors.add("Le nom de la tâche est obligatoire");
}
if (tacheTemplate.getSousPhaseParent() == null) {
result.valid = false;
result.errors.add("La sous-phase parente est obligatoire");
}
if (tacheTemplate.getNombreOperateursRequis() != null
&& tacheTemplate.getNombreOperateursRequis() < 1) {
result.valid = false;
result.errors.add("Le nombre d'opérateurs requis doit être au moins 1");
}
if (tacheTemplate.getDureeEstimeeMinutes() != null
&& tacheTemplate.getDureeEstimeeMinutes() < 1) {
result.valid = false;
result.errors.add("La durée estimée doit être au moins 1 minute");
}
return Response.ok(result).build();
}
/** Classe pour le résultat de validation */
public static class ValidationResult {
public boolean valid;
public List<String> errors = new java.util.ArrayList<>();
}
}

View File

@@ -0,0 +1,435 @@
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.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.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* API REST pour la gestion des utilisateurs BTP
* Expose les fonctionnalités de création, consultation et administration des utilisateurs
*/
@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;
// ===================================
// CONSULTATION DES UTILISATEURS
// ===================================
@GET
@Operation(summary = "Récupère tous les utilisateurs")
@APIResponse(responseCode = "200", description = "Liste des utilisateurs")
@APIResponse(responseCode = "401", description = "Non authentifié")
@APIResponse(responseCode = "403", description = "Accès refusé")
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) {
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 (Exception e) {
logger.error("Erreur lors de la récupération des utilisateurs", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération des utilisateurs"))
.build();
}
}
@GET
@Path("/{id}")
@Operation(summary = "Récupère un utilisateur par ID")
@APIResponse(responseCode = "200", description = "Utilisateur trouvé")
@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) {
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(Map.of("error", "Utilisateur non trouvé avec l'ID: " + id))
.build());
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "ID d'utilisateur invalide: " + id))
.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(Map.of("error", "Erreur lors de la récupération de l'utilisateur"))
.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 = "Récupère les statistiques des utilisateurs")
@APIResponse(responseCode = "200", description = "Statistiques des utilisateurs")
@APIResponse(responseCode = "403", description = "Accès refusé")
public Response getUserStats() {
try {
Object stats = userService.getStatistics();
return Response.ok(stats).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(Map.of("error", "Erreur lors de la génération des statistiques"))
.build();
}
}
// ===================================
// GESTION DES UTILISATEURS
// ===================================
@POST
@Operation(summary = "Crée 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) {
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(Map.of("error", "Données invalides: " + 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(Map.of("error", "Erreur lors de la création de l'utilisateur"))
.build();
}
}
@PUT
@Path("/{id}")
@Operation(summary = "Met à jour un utilisateur")
@APIResponse(responseCode = "200", description = "Utilisateur mis à jour 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) {
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(Map.of("error", "Données invalides: " + 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(Map.of("error", "Erreur lors de la modification de l'utilisateur"))
.build();
}
}
@PUT
@Path("/{id}/status")
@Operation(summary = "Met à jour le statut d'un utilisateur")
@APIResponse(responseCode = "200", description = "Statut mis à jour 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) {
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(Map.of("error", "Statut invalide: " + 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(Map.of("error", "Erreur lors de la modification du statut"))
.build();
}
}
@PUT
@Path("/{id}/role")
@Operation(summary = "Met à jour le rôle d'un utilisateur")
@APIResponse(responseCode = "200", description = "Rôle mis à jour 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) {
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(Map.of("error", "Rôle invalide: " + 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(Map.of("error", "Erreur lors de la modification du rôle"))
.build();
}
}
@POST
@Path("/{id}/approve")
@Operation(summary = "Approuve 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) {
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(Map.of("error", "Données invalides: " + 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(Map.of("error", "Erreur lors de l'approbation de l'utilisateur"))
.build();
}
}
@POST
@Path("/{id}/reject")
@Operation(summary = "Rejette 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) {
try {
UUID userId = UUID.fromString(id);
userService.rejectUser(userId, request.reason);
return Response.ok(Map.of("message", "Utilisateur rejeté avec succès")).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Données invalides: " + 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(Map.of("error", "Erreur lors du rejet de l'utilisateur"))
.build();
}
}
@DELETE
@Path("/{id}")
@Operation(summary = "Supprime 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) {
try {
UUID userId = UUID.fromString(id);
userService.deleteUser(userId);
return Response.noContent().build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "ID invalide: " + 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(Map.of("error", "Erreur lors de la suppression de l'utilisateur"))
.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) {}
}

View File

@@ -0,0 +1,275 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.ZoneClimatiqueService;
import dev.lions.btpxpress.domain.core.entity.ZoneClimatique;
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.math.BigDecimal;
import java.util.HashMap;
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;
/**
* Resource REST pour la gestion des zones climatiques africaines
* Architecture 2025 : API complète pour la gestion des contraintes climatiques de construction
*/
@Path("/api/v1/zones-climatiques")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Zones Climatiques", description = "Gestion des zones climatiques et contraintes BTP")
public class ZoneClimatiqueResource {
private static final Logger logger = LoggerFactory.getLogger(ZoneClimatiqueResource.class);
@Inject ZoneClimatiqueService zoneClimatiqueService;
// === ENDPOINTS DE LECTURE - ARCHITECTURE 2025 ===
@GET
@Operation(
summary = "Récupérer toutes les zones climatiques actives",
description = "Retourne la liste complète des zones climatiques actives")
@APIResponse(responseCode = "200", description = "Liste des zones climatiques récupérée avec succès")
public Response getAllZones() {
logger.debug("GET /zones-climatiques");
List<ZoneClimatique> zones = zoneClimatiqueService.findAll();
Map<String, Object> response = new HashMap<>();
response.put("zones", zones);
response.put("total", zones.size());
return Response.ok(response).build();
}
@GET
@Path("/all")
@Operation(
summary = "Récupérer toutes les zones (actives et inactives)",
description = "Retourne la liste complète incluant les zones désactivées")
@APIResponse(responseCode = "200", description = "Liste complète des zones climatiques")
public Response getAllZonesIncludingInactive() {
logger.debug("Récupération de toutes les zones (actives et inactives)");
List<ZoneClimatique> zones = zoneClimatiqueService.findAllIncludingInactive();
Map<String, Object> response = new HashMap<>();
response.put("zones", zones);
response.put("total", zones.size());
return Response.ok(response).build();
}
@GET
@Path("/{id}")
@Operation(summary = "Récupérer une zone par ID", description = "Retourne les détails d'une zone climatique")
@APIResponse(responseCode = "200", description = "Zone climatique trouvée")
@APIResponse(responseCode = "404", description = "Zone climatique non trouvée")
public Response getZoneById(@Parameter(description = "ID de la zone climatique") @PathParam("id") Long id) {
logger.debug("GET /zones-climatiques/{}", id);
ZoneClimatique zone = zoneClimatiqueService.findById(id);
return Response.ok(zone).build();
}
@GET
@Path("/code/{code}")
@Operation(
summary = "Récupérer une zone par code",
description = "Retourne les détails d'une zone climatique par son code unique")
@APIResponse(responseCode = "200", description = "Zone climatique trouvée")
@APIResponse(responseCode = "404", description = "Zone climatique non trouvée")
public Response getZoneByCode(@PathParam("code") String code) {
logger.debug("Récupération de la zone climatique avec code: {}", code);
ZoneClimatique zone = zoneClimatiqueService.findByCode(code);
return Response.ok(zone).build();
}
@GET
@Path("/temperature-range")
@Operation(
summary = "Rechercher par plage de température",
description = "Retourne les zones dont la température moyenne est dans la plage spécifiée")
@APIResponse(responseCode = "200", description = "Zones trouvées")
public Response getByTemperatureRange(
@Parameter(description = "Température minimale (°C)", example = "15")
@QueryParam("min")
BigDecimal min,
@Parameter(description = "Température maximale (°C)", example = "40")
@QueryParam("max")
BigDecimal max) {
logger.debug("Recherche zones climatiques par température: {} - {}", min, max);
List<ZoneClimatique> zones = zoneClimatiqueService.findByTemperatureRange(min, max);
Map<String, Object> response = new HashMap<>();
response.put("zones", zones);
response.put("total", zones.size());
return Response.ok(response).build();
}
@GET
@Path("/pluviometrie")
@Operation(
summary = "Rechercher par pluviométrie",
description = "Retourne les zones dont la pluviométrie annuelle est dans la plage spécifiée")
@APIResponse(responseCode = "200", description = "Zones trouvées")
public Response getByPluviometrie(
@Parameter(description = "Pluviométrie minimale (mm)", example = "500") @QueryParam("min")
Integer min,
@Parameter(description = "Pluviométrie maximale (mm)", example = "2000") @QueryParam("max")
Integer max) {
logger.debug("Recherche zones climatiques par pluviométrie: {} - {}", min, max);
List<ZoneClimatique> zones = zoneClimatiqueService.findByPluviometrie(min, max);
Map<String, Object> response = new HashMap<>();
response.put("zones", zones);
response.put("total", zones.size());
return Response.ok(response).build();
}
@GET
@Path("/risque-seisme")
@Operation(
summary = "Zones avec risque sismique",
description = "Retourne toutes les zones présentant un risque sismique")
@APIResponse(responseCode = "200", description = "Zones sismiques trouvées")
public Response getWithSeismicRisk() {
logger.debug("Recherche zones avec risque sismique");
List<ZoneClimatique> zones = zoneClimatiqueService.findAvecRisqueSeisme();
Map<String, Object> response = new HashMap<>();
response.put("zones", zones);
response.put("total", zones.size());
return Response.ok(response).build();
}
@GET
@Path("/risque-cyclones")
@Operation(
summary = "Zones avec risque cyclonique",
description = "Retourne toutes les zones présentant un risque cyclonique")
@APIResponse(responseCode = "200", description = "Zones cycloniques trouvées")
public Response getWithCycloneRisk() {
logger.debug("Recherche zones avec risque cyclonique");
List<ZoneClimatique> zones = zoneClimatiqueService.findAvecRisqueCyclones();
Map<String, Object> response = new HashMap<>();
response.put("zones", zones);
response.put("total", zones.size());
return Response.ok(response).build();
}
@GET
@Path("/search")
@Operation(
summary = "Recherche avancée",
description = "Recherche avancée avec critères multiples")
@APIResponse(responseCode = "200", description = "Résultats de la recherche")
public Response search(
@QueryParam("tempMin") BigDecimal tempMin,
@QueryParam("tempMax") BigDecimal tempMax,
@QueryParam("pluvioMin") Integer pluvioMin,
@QueryParam("pluvioMax") Integer pluvioMax,
@QueryParam("risqueSeisme") Boolean risqueSeisme,
@QueryParam("corrosionMarine") Boolean corrosionMarine,
@QueryParam("texte") String texte) {
logger.debug(
"Recherche avancée - temp: {}-{}, pluvio: {}-{}, seisme: {}, corrosion: {}, texte: {}",
tempMin,
tempMax,
pluvioMin,
pluvioMax,
risqueSeisme,
corrosionMarine,
texte);
List<ZoneClimatique> zones =
zoneClimatiqueService.search(
tempMin, tempMax, pluvioMin, pluvioMax, risqueSeisme, corrosionMarine, texte);
Map<String, Object> response = new HashMap<>();
response.put("zones", zones);
response.put("total", zones.size());
return Response.ok(response).build();
}
@GET
@Path("/statistiques")
@Operation(
summary = "Statistiques des zones climatiques",
description = "Retourne des statistiques globales sur les zones climatiques")
@APIResponse(responseCode = "200", description = "Statistiques calculées")
public Response getStatistics() {
logger.debug("Récupération des statistiques des zones climatiques");
Map<String, Object> stats = zoneClimatiqueService.getStatistics();
return Response.ok(stats).build();
}
// =================== ENDPOINTS DE CRÉATION ===================
@POST
@Authenticated
@Operation(summary = "Créer une nouvelle zone climatique", description = "Crée une nouvelle zone climatique")
@APIResponse(responseCode = "201", description = "Zone climatique créée avec succès")
@APIResponse(responseCode = "400", description = "Données invalides ou code déjà existant")
public Response createZone(@Valid @NotNull ZoneClimatique zone) {
logger.info("Création d'une nouvelle zone climatique: {}", zone.getCode());
ZoneClimatique created = zoneClimatiqueService.create(zone);
return Response.status(Response.Status.CREATED).entity(created).build();
}
// === ENDPOINTS DE MODIFICATION - ARCHITECTURE 2025 ===
@PUT
@Path("/{id}")
@Authenticated
@Operation(summary = "Modifier une zone climatique", description = "Met à jour les informations d'une zone")
@APIResponse(responseCode = "200", description = "Zone modifiée avec succès")
@APIResponse(responseCode = "404", description = "Zone non trouvée")
public Response updateZone(@PathParam("id") Long id, @Valid @NotNull ZoneClimatique zone) {
logger.info("PUT /zones-climatiques/{} - Modification", id);
ZoneClimatique updated = zoneClimatiqueService.update(id, zone);
return Response.ok(updated).build();
}
@PUT
@Path("/{id}/activate")
@Authenticated
@Operation(summary = "Activer une zone climatique", description = "Réactive une zone désactivée")
@APIResponse(responseCode = "200", description = "Zone activée avec succès")
public Response activateZone(@PathParam("id") Long id) {
logger.info("Activation de la zone climatique ID: {}", id);
zoneClimatiqueService.activate(id);
Map<String, Object> response = new HashMap<>();
response.put("message", "Zone climatique activée avec succès");
response.put("id", id);
return Response.ok(response).build();
}
@PUT
@Path("/{id}/deactivate")
@Authenticated
@Operation(summary = "Désactiver une zone climatique", description = "Désactive une zone")
@APIResponse(responseCode = "200", description = "Zone désactivée avec succès")
public Response deactivateZone(@PathParam("id") Long id) {
logger.info("Désactivation de la zone climatique ID: {}", id);
zoneClimatiqueService.deactivate(id);
Map<String, Object> response = new HashMap<>();
response.put("message", "Zone climatique désactivée avec succès");
response.put("id", id);
return Response.ok(response).build();
}
// === ENDPOINT DE SUPPRESSION - ARCHITECTURE 2025 ===
@DELETE
@Path("/{id}")
@Authenticated
@Operation(summary = "Supprimer une zone climatique", description = "Supprime définitivement une zone")
@APIResponse(responseCode = "204", description = "Zone supprimée avec succès")
@APIResponse(responseCode = "404", description = "Zone non trouvée")
public Response deleteZone(@PathParam("id") Long id) {
logger.info("DELETE /zones-climatiques/{} - Suppression", id);
zoneClimatiqueService.delete(id);
return Response.noContent().build();
}
}