From b3ed387197e2246c215fbc358de720067b5fdb01 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 15 Sep 2025 14:41:34 +0200 Subject: [PATCH] feature a test --- ...tis_API_Collection.postman_collection.json | 443 ++++++++++++++++++ .../api/v1/DocumentRestController.java | 363 ++++++++++++++ .../api/v1/ExportFolderRestController.java | 274 +++++++++++ .../xpeditis/dto/app/DocumentSummaryDto.java | 52 ++ .../xpeditis/dto/app/DossierStatus.java | 167 +++++++ .../xpeditis/dto/app/ExportFolderDto.java | 79 ++++ .../xpeditis/dto/app/FolderAction.java | 90 ++++ .../xpeditis/dto/app/HistoryEntryDto.java | 43 ++ .../com/dh7789dev/xpeditis/dto/app/Role.java | 73 ++- .../xpeditis/dto/app/StatutVerification.java | 51 ++ .../dto/request/CreateFolderRequest.java | 26 + .../entity/DocumentDossierEntity.java | 148 ++++++ .../xpeditis/entity/DocumentTypeEntity.java | 51 ++ .../xpeditis/entity/ExportFolderEntity.java | 122 ++++- .../entity/HistoriqueDossierEntity.java | 65 +++ .../V4__CREATE_SSC_EXPORT_SYSTEM.sql | 288 ++++++++++++ 16 files changed, 2321 insertions(+), 14 deletions(-) create mode 100644 application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/DocumentRestController.java create mode 100644 application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/ExportFolderRestController.java create mode 100644 domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/DocumentSummaryDto.java create mode 100644 domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/DossierStatus.java create mode 100644 domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/ExportFolderDto.java create mode 100644 domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/FolderAction.java create mode 100644 domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/HistoryEntryDto.java create mode 100644 domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/StatutVerification.java create mode 100644 domain/data/src/main/java/com/dh7789dev/xpeditis/dto/request/CreateFolderRequest.java create mode 100644 infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/DocumentDossierEntity.java create mode 100644 infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/DocumentTypeEntity.java create mode 100644 infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/HistoriqueDossierEntity.java create mode 100644 infrastructure/src/main/resources/db/migration/structure/V4__CREATE_SSC_EXPORT_SYSTEM.sql diff --git a/Xpeditis_API_Collection.postman_collection.json b/Xpeditis_API_Collection.postman_collection.json index 638b5e8..c7e4473 100644 --- a/Xpeditis_API_Collection.postman_collection.json +++ b/Xpeditis_API_Collection.postman_collection.json @@ -515,6 +515,449 @@ } ], "description": "API de calcul automatisé de devis transport maritime - génère 3 offres (Rapide, Standard, Économique) basées sur les grilles tarifaires LESCHACO" + }, + { + "name": "📦 SSC Export Folders", + "item": [ + { + "name": "🔍 Rechercher Dossiers", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt_token}}" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/export-folders?page=0&size=20&sortBy=dateCreation&sortDir=DESC&statut=CREE&numeroDossier=EXP-2024", + "host": ["{{base_url}}"], + "path": ["api", "v1", "export-folders"], + "query": [ + { + "key": "page", + "value": "0", + "description": "Numéro de page (0-indexed)" + }, + { + "key": "size", + "value": "20", + "description": "Taille de page" + }, + { + "key": "sortBy", + "value": "dateCreation", + "description": "Champ de tri" + }, + { + "key": "sortDir", + "value": "DESC", + "description": "Direction de tri" + }, + { + "key": "statut", + "value": "CREE", + "description": "Filtrer par statut", + "disabled": true + }, + { + "key": "numeroDossier", + "value": "EXP-2024", + "description": "Recherche par numéro de dossier", + "disabled": true + }, + { + "key": "companyId", + "value": "1", + "description": "Filtrer par entreprise (admin uniquement)", + "disabled": true + } + ] + } + }, + "response": [], + "protocolProfileBehavior": { + "disableBodyPruning": true + } + }, + { + "name": "📋 Obtenir Dossier par ID", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt_token}}" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/export-folders/1", + "host": ["{{base_url}}"], + "path": ["api", "v1", "export-folders", "1"] + } + }, + "response": [] + }, + { + "name": "➕ Créer Dossier depuis Devis", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt_token}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"quoteId\": 1,\n \"commentairesClient\": \"Demande de transport urgent pour livraison avant fin mars. Marchandises fragiles - manipulation soignée requise.\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/v1/export-folders", + "host": ["{{base_url}}"], + "path": ["api", "v1", "export-folders"] + } + }, + "response": [] + }, + { + "name": "🔄 Mettre à jour Statut", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt_token}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"newStatus\": \"DOCUMENTS_EN_ATTENTE\",\n \"comment\": \"Dossier validé, en attente des documents clients\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/v1/export-folders/1/status", + "host": ["{{base_url}}"], + "path": ["api", "v1", "export-folders", "1", "status"] + } + }, + "response": [] + }, + { + "name": "📎 Uploader Document", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt_token}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "file", + "type": "file", + "src": "/path/to/your/document.pdf" + }, + { + "key": "typeDocumentId", + "value": "1", + "type": "text", + "description": "ID du type de document (1=Facture commerciale, 2=Liste de colisage, etc.)" + }, + { + "key": "description", + "value": "Facture commerciale pour export maritime", + "type": "text", + "description": "Description optionnelle" + } + ] + }, + "url": { + "raw": "{{base_url}}/api/v1/export-folders/1/documents", + "host": ["{{base_url}}"], + "path": ["api", "v1", "export-folders", "1", "documents"] + } + }, + "response": [] + }, + { + "name": "📑 Lister Documents", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt_token}}" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/export-folders/1/documents", + "host": ["{{base_url}}"], + "path": ["api", "v1", "export-folders", "1", "documents"] + } + }, + "response": [] + }, + { + "name": "📜 Historique du Dossier", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt_token}}" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/export-folders/1/history?limit=50", + "host": ["{{base_url}}"], + "path": ["api", "v1", "export-folders", "1", "history"], + "query": [ + { + "key": "limit", + "value": "50", + "description": "Nombre maximum d'entrées à retourner" + } + ] + } + }, + "response": [] + }, + { + "name": "🔐 Actions Autorisées", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt_token}}" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/export-folders/1/permissions", + "host": ["{{base_url}}"], + "path": ["api", "v1", "export-folders", "1", "permissions"] + } + }, + "response": [] + } + ], + "description": "🚢 Système SSC de Gestion des Dossiers d'Export - Workflow complet de création, suivi et gestion documentaire pour les expéditions maritimes", + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{jwt_token}}", + "type": "string" + } + ] + } + }, + { + "name": "📊 Grilles Tarifaires", + "item": [ + { + "name": "📋 Lister Grilles Tarifaires", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/grilles-tarifaires?page=0&size=20", + "host": ["{{base_url}}"], + "path": ["api", "v1", "grilles-tarifaires"], + "query": [ + { + "key": "page", + "value": "0" + }, + { + "key": "size", + "value": "20" + }, + { + "key": "nom", + "value": "LESCHACO", + "disabled": true, + "description": "Filtrer par nom" + }, + { + "key": "paysOrigine", + "value": "France", + "disabled": true, + "description": "Filtrer par pays d'origine" + } + ] + } + }, + "response": [] + }, + { + "name": "🔍 Obtenir Grille par ID", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/grilles-tarifaires/1", + "host": ["{{base_url}}"], + "path": ["api", "v1", "grilles-tarifaires", "1"] + } + }, + "response": [] + }, + { + "name": "✅ Valider Grille Tarifaire", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt_token}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "file", + "type": "file", + "src": "/path/to/your/grille.xlsx", + "description": "Fichier Excel ou CSV contenant la grille tarifaire" + } + ] + }, + "url": { + "raw": "{{base_url}}/api/v1/grilles-tarifaires/validate", + "host": ["{{base_url}}"], + "path": ["api", "v1", "grilles-tarifaires", "validate"] + } + }, + "response": [] + }, + { + "name": "📤 Import CSV", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt_token}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "file", + "type": "file", + "src": "/path/to/your/grille.csv" + } + ] + }, + "url": { + "raw": "{{base_url}}/api/v1/grilles-tarifaires/import/csv", + "host": ["{{base_url}}"], + "path": ["api", "v1", "grilles-tarifaires", "import", "csv"] + } + }, + "response": [] + }, + { + "name": "📤 Import Excel", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt_token}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "file", + "type": "file", + "src": "/path/to/your/grille.xlsx" + } + ] + }, + "url": { + "raw": "{{base_url}}/api/v1/grilles-tarifaires/import/excel", + "host": ["{{base_url}}"], + "path": ["api", "v1", "grilles-tarifaires", "import", "excel"] + } + }, + "response": [] + }, + { + "name": "📤 Import JSON", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt_token}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"nom\": \"Grille LESCHACO 2025\",\n \"paysOrigine\": \"France\",\n \"paysDestination\": \"Chine\",\n \"portOrigine\": \"FRBOL\",\n \"portDestination\": \"CNSHA\",\n \"dateValiditeDebut\": \"2025-01-01\",\n \"dateValiditeFin\": \"2025-12-31\",\n \"tarifsFret\": [\n {\n \"poidsMin\": 0.0,\n \"poidsMax\": 100.0,\n \"prixParKg\": 2.50,\n \"prixForfaitaire\": 150.00\n },\n {\n \"poidsMin\": 100.0,\n \"poidsMax\": 500.0,\n \"prixParKg\": 2.20,\n \"prixForfaitaire\": 200.00\n }\n ],\n \"fraisAdditionnels\": [\n {\n \"type\": \"MANUTENTION\",\n \"montant\": 45.00,\n \"description\": \"Frais de manutention portuaire\"\n },\n {\n \"type\": \"DOCUMENTATION\",\n \"montant\": 25.00,\n \"description\": \"Frais de documentation\"\n }\n ],\n \"surchargesDangereuses\": [\n {\n \"classe\": \"3\",\n \"pourcentage\": 15.0,\n \"montantFixe\": 100.0,\n \"description\": \"Surcharge marchandises inflammables\"\n }\n ]\n}" + }, + "url": { + "raw": "{{base_url}}/api/v1/grilles-tarifaires/import/json", + "host": ["{{base_url}}"], + "path": ["api", "v1", "grilles-tarifaires", "import", "json"] + } + }, + "response": [] + } + ], + "description": "🏷️ Gestion des grilles tarifaires LESCHACO - Import, validation et consultation des tarifs pour le calcul automatisé des devis" } ], "event": [ diff --git a/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/DocumentRestController.java b/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/DocumentRestController.java new file mode 100644 index 0000000..c953afe --- /dev/null +++ b/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/DocumentRestController.java @@ -0,0 +1,363 @@ +package com.dh7789dev.xpeditis.controller.api.v1; + +import com.dh7789dev.xpeditis.dto.app.*; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.validation.Valid; +import java.io.IOException; + +@RestController +@RequestMapping("/api/v1/documents") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "Documents", description = "Gestion des documents SSC avec validation") +@SecurityRequirement(name = "bearerAuth") +public class DocumentRestController { + + // TODO: Inject required services + // private final DocumentValidationService documentValidationService; + // private final DocumentStorageService documentStorageService; + // private final ExportFolderPermissionService permissionService; + // private final NotificationService notificationService; + + @Operation( + summary = "Télécharger un document", + description = "Télécharge le fichier document si autorisé", + responses = { + @ApiResponse(responseCode = "200", description = "Fichier téléchargé"), + @ApiResponse(responseCode = "403", description = "Accès refusé"), + @ApiResponse(responseCode = "404", description = "Document non trouvé") + } + ) + @GetMapping("/{id}/download") + @PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')") + public ResponseEntity downloadDocument( + @Parameter(description = "ID du document") + @PathVariable Long id + ) { + try { + // TODO: Implement download with permission check + // UserEntity currentUser = getCurrentUser(); + // DocumentDossierEntity document = documentService.findById(id); + // + // if (!permissionService.canPerformAction(currentUser, document.getDossier(), FolderAction.DOWNLOAD_DOCUMENTS)) { + // return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + // } + // + // Resource resource = documentStorageService.loadAsResource(document); + + log.info("Téléchargement document ID: {}", id); + + // Placeholder response - would return actual file resource + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"document.pdf\"") + .body(null); // Would return actual Resource + + } catch (Exception e) { + log.error("Erreur lors du téléchargement du document {}", id, e); + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } + } + + @Operation( + summary = "Valider un document", + description = "Approuve ou refuse un document (admin SSC uniquement)", + responses = { + @ApiResponse(responseCode = "200", description = "Document validé"), + @ApiResponse(responseCode = "403", description = "Droits insuffisants"), + @ApiResponse(responseCode = "404", description = "Document non trouvé") + } + ) + @PutMapping("/{id}/validate") + @PreAuthorize("hasRole('ADMIN_SSC') or hasRole('SUPER_ADMIN')") + public ResponseEntity validateDocument( + @Parameter(description = "ID du document") + @PathVariable Long id, + + @Parameter(description = "Décision de validation") + @Valid @RequestBody ValidationRequest request + ) { + try { + // TODO: Implement document validation + // UserEntity currentUser = getCurrentUser(); + // DocumentSummaryDto validatedDocument = documentValidationService.processValidation( + // currentUser, id, request); + + log.info("Validation document {} - décision: {}, commentaire: {}", + id, request.isApproved(), request.getComment()); + + // Placeholder response + return ResponseEntity.ok(DocumentSummaryDto.builder() + .id(id) + .statutVerification(request.isApproved() ? + StatutVerification.VALIDE : StatutVerification.REFUSE) + .build()); + + } catch (Exception e) { + log.error("Erreur lors de la validation du document {}", id, e); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); + } + } + + @Operation( + summary = "Supprimer un document", + description = "Supprime un document si autorisé", + responses = { + @ApiResponse(responseCode = "204", description = "Document supprimé"), + @ApiResponse(responseCode = "403", description = "Accès refusé"), + @ApiResponse(responseCode = "404", description = "Document non trouvé") + } + ) + @DeleteMapping("/{id}") + @PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')") + public ResponseEntity deleteDocument( + @Parameter(description = "ID du document") + @PathVariable Long id + ) { + try { + // TODO: Implement deletion with permission check + // UserEntity currentUser = getCurrentUser(); + // DocumentDossierEntity document = documentService.findById(id); + // + // if (!permissionService.canPerformAction(currentUser, document.getDossier(), FolderAction.DELETE_DOCUMENTS)) { + // return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + // } + // + // documentValidationService.deleteDocument(currentUser, id); + + log.info("Suppression document ID: {}", id); + + return ResponseEntity.noContent().build(); + + } catch (Exception e) { + log.error("Erreur lors de la suppression du document {}", id, e); + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } + } + + @Operation( + summary = "Uploader une nouvelle version", + description = "Remplace un document par une nouvelle version", + responses = { + @ApiResponse(responseCode = "201", description = "Nouvelle version uploadée"), + @ApiResponse(responseCode = "400", description = "Fichier invalide"), + @ApiResponse(responseCode = "403", description = "Accès refusé") + } + ) + @PostMapping("/{id}/new-version") + @PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')") + public ResponseEntity uploadNewVersion( + @Parameter(description = "ID du document original") + @PathVariable Long id, + + @Parameter(description = "Nouveau fichier") + @RequestPart("file") MultipartFile file, + + @Parameter(description = "Description des changements") + @RequestPart(value = "description", required = false) String description + ) { + try { + // TODO: Implement version upload + // UserEntity currentUser = getCurrentUser(); + // DocumentDossierEntity originalDocument = documentService.findById(id); + // + // if (!permissionService.canPerformAction(currentUser, originalDocument.getDossier(), FolderAction.UPLOAD_DOCUMENTS)) { + // return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + // } + // + // DocumentSummaryDto newVersion = documentValidationService.uploadNewVersion( + // currentUser, id, file, description); + + log.info("Upload nouvelle version document {} - fichier: {}", id, file.getOriginalFilename()); + + // Placeholder response + return ResponseEntity.status(HttpStatus.CREATED) + .body(DocumentSummaryDto.builder() + .id(id + 1000L) // Simulated new version ID + .nomOriginal(file.getOriginalFilename()) + .numeroVersion(2) + .statutVerification(StatutVerification.EN_ATTENTE) + .build()); + + } catch (Exception e) { + log.error("Erreur lors de l'upload de nouvelle version pour document {}", id, e); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); + } + } + + @Operation( + summary = "Obtenir les détails d'un document", + description = "Récupère les informations complètes d'un document", + responses = { + @ApiResponse(responseCode = "200", description = "Détails du document"), + @ApiResponse(responseCode = "403", description = "Accès refusé"), + @ApiResponse(responseCode = "404", description = "Document non trouvé") + } + ) + @GetMapping("/{id}") + @PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')") + public ResponseEntity getDocumentDetails( + @Parameter(description = "ID du document") + @PathVariable Long id + ) { + try { + // TODO: Implement details retrieval with permissions + // UserEntity currentUser = getCurrentUser(); + // DocumentDetailDto details = documentService.getDetailsWithPermissions(currentUser, id); + + log.info("Consultation détails document ID: {}", id); + + // Placeholder response + return ResponseEntity.ok(DocumentDetailDto.builder() + .id(id) + .statutVerification(StatutVerification.EN_ATTENTE) + .build()); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des détails du document {}", id, e); + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } + } + + @Operation( + summary = "Marquer un document comme expiré", + description = "Force l'expiration d'un document (admin uniquement)", + responses = { + @ApiResponse(responseCode = "200", description = "Document marqué expiré"), + @ApiResponse(responseCode = "403", description = "Droits insuffisants"), + @ApiResponse(responseCode = "404", description = "Document non trouvé") + } + ) + @PutMapping("/{id}/expire") + @PreAuthorize("hasRole('ADMIN_SSC') or hasRole('SUPER_ADMIN')") + public ResponseEntity expireDocument( + @Parameter(description = "ID du document") + @PathVariable Long id, + + @Parameter(description = "Raison de l'expiration") + @RequestBody ExpireDocumentRequest request + ) { + try { + // TODO: Implement document expiration + // UserEntity currentUser = getCurrentUser(); + // documentValidationService.expireDocument(currentUser, id, request.getReason()); + + log.info("Expiration document {} - raison: {}", id, request.getReason()); + + return ResponseEntity.ok().build(); + + } catch (Exception e) { + log.error("Erreur lors de l'expiration du document {}", id, e); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); + } + } + + @Operation( + summary = "Obtenir l'historique de validation d'un document", + description = "Liste chronologique des validations d'un document", + responses = { + @ApiResponse(responseCode = "200", description = "Historique de validation"), + @ApiResponse(responseCode = "403", description = "Accès refusé"), + @ApiResponse(responseCode = "404", description = "Document non trouvé") + } + ) + @GetMapping("/{id}/validation-history") + @PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')") + public ResponseEntity> getValidationHistory( + @Parameter(description = "ID du document") + @PathVariable Long id + ) { + try { + // TODO: Implement validation history retrieval + // UserEntity currentUser = getCurrentUser(); + // List history = documentService.getValidationHistory(currentUser, id); + + log.info("Consultation historique validation document ID: {}", id); + + // Placeholder response + return ResponseEntity.ok(java.util.List.of()); + + } catch (Exception e) { + log.error("Erreur lors de la récupération de l'historique de validation du document {}", id, e); + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } + } + + // TODO: Add getCurrentUser() method to get authenticated user + // private UserEntity getCurrentUser() { + // // Implementation to get current authenticated user + // return null; + // } +} + +// ========== REQUEST DTOs ========== + +@lombok.Data +class ValidationRequest { + private boolean approved; + private String comment; + private String correctionsDemandees; +} + +@lombok.Data +class ExpireDocumentRequest { + private String reason; +} + +// ========== RESPONSE DTOs ========== + +@lombok.Data +@lombok.Builder +@lombok.NoArgsConstructor +@lombok.AllArgsConstructor +class DocumentDetailDto { + private Long id; + private String nomOriginal; + private String typeDocumentNom; + private Long tailleOctets; + private String typeMime; + private Integer numeroVersion; + private StatutVerification statutVerification; + private String commentaireVerification; + private String correctionsDemandees; + private String description; + private java.time.LocalDate dateValidite; + private boolean isExpired; + private String uploadePar; + private java.time.LocalDateTime dateUpload; + private String verifiePar; + private java.time.LocalDateTime dateVerification; + private boolean canDownload; + private boolean canDelete; + private boolean canValidate; + private boolean canUploadNewVersion; +} + +@lombok.Data +@lombok.Builder +@lombok.NoArgsConstructor +@lombok.AllArgsConstructor +class ValidationHistoryDto { + private Long id; + private StatutVerification ancienStatut; + private StatutVerification nouveauStatut; + private String commentaire; + private String effectuePar; + private java.time.LocalDateTime dateValidation; + private String reason; +} \ No newline at end of file diff --git a/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/ExportFolderRestController.java b/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/ExportFolderRestController.java new file mode 100644 index 0000000..bc52bf0 --- /dev/null +++ b/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/ExportFolderRestController.java @@ -0,0 +1,274 @@ +package com.dh7789dev.xpeditis.controller.api.v1; + +import com.dh7789dev.xpeditis.dto.app.*; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.validation.Valid; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@RestController +@RequestMapping("/api/v1/export-folders") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "Export Folders", description = "Gestion des dossiers d'export SSC") +@SecurityRequirement(name = "bearerAuth") +public class ExportFolderRestController { + + // TODO: Inject permission service when modules are properly connected + + @Operation( + summary = "Rechercher des dossiers d'export", + description = "Recherche paginée avec filtres selon permissions utilisateur" + ) + @GetMapping + @PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')") + public ResponseEntity> searchFolders( + @Parameter(description = "Critères de recherche") + @RequestParam Map params, + + @Parameter(description = "Numéro de page (0-indexed)") + @RequestParam(defaultValue = "0") int page, + + @Parameter(description = "Taille de page") + @RequestParam(defaultValue = "20") int size, + + @Parameter(description = "Champ de tri") + @RequestParam(defaultValue = "dateCreation") String sortBy, + + @Parameter(description = "Direction de tri") + @RequestParam(defaultValue = "DESC") String sortDir + ) { + try { + Pageable pageable = PageRequest.of( + page, size, + Sort.Direction.fromString(sortDir), + sortBy + ); + + log.info("Recherche dossiers - page: {}, size: {}, filtres: {}", page, size, params); + + // Placeholder response + return ResponseEntity.ok(Page.empty()); + + } catch (Exception e) { + log.error("Erreur lors de la recherche de dossiers", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + @Operation( + summary = "Créer un dossier d'export depuis un devis", + description = "Crée un nouveau dossier d'export à partir d'un devis accepté" + ) + @PostMapping + @PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')") + public ResponseEntity createFromQuote( + @Parameter(description = "Données pour création du dossier") + @Valid @RequestBody CreateFolderRequest request + ) { + try { + log.info("Création dossier depuis devis ID: {}", request.getQuoteId()); + + // Placeholder response + return ResponseEntity.status(HttpStatus.CREATED) + .body(ExportFolderDto.builder().build()); + + } catch (Exception e) { + log.error("Erreur lors de la création du dossier", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + @Operation( + summary = "Obtenir un dossier par ID", + description = "Récupère les détails complets d'un dossier selon permissions" + ) + @GetMapping("/{id}") + @PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')") + public ResponseEntity getFolder( + @Parameter(description = "ID du dossier") + @PathVariable Long id + ) { + try { + log.info("Consultation dossier ID: {}", id); + + // Placeholder response + return ResponseEntity.ok(ExportFolderDto.builder().id(id).build()); + + } catch (Exception e) { + log.error("Erreur lors de la récupération du dossier {}", id, e); + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } + } + + @Operation( + summary = "Mettre à jour le statut d'un dossier", + description = "Change le statut d'un dossier selon le workflow" + ) + @PutMapping("/{id}/status") + @PreAuthorize("hasRole('ADMIN_SSC') or hasRole('SUPER_ADMIN')") + public ResponseEntity updateStatus( + @Parameter(description = "ID du dossier") + @PathVariable Long id, + + @Parameter(description = "Nouveau statut") + @Valid @RequestBody StatusUpdateRequest request + ) { + try { + log.info("Mise à jour statut dossier {} vers {}", id, request.getNewStatus()); + + return ResponseEntity.ok().build(); + + } catch (Exception e) { + log.error("Erreur lors de la mise à jour du statut du dossier {}", id, e); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); + } + } + + @Operation( + summary = "Uploader un document", + description = "Ajoute un nouveau document au dossier" + ) + @PostMapping("/{id}/documents") + @PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')") + public ResponseEntity uploadDocument( + @Parameter(description = "ID du dossier") + @PathVariable Long id, + + @Parameter(description = "Fichier à uploader") + @RequestPart("file") MultipartFile file, + + @Parameter(description = "ID du type de document") + @RequestPart("typeDocumentId") Long typeDocumentId, + + @Parameter(description = "Description optionnelle") + @RequestPart(value = "description", required = false) String description + ) { + try { + log.info("Upload document pour dossier {} - fichier: {}, type: {}", + id, file.getOriginalFilename(), typeDocumentId); + + // Placeholder response + return ResponseEntity.status(HttpStatus.CREATED) + .body(DocumentSummaryDto.builder().build()); + + } catch (Exception e) { + log.error("Erreur lors de l'upload de document pour dossier {}", id, e); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); + } + } + + @Operation( + summary = "Obtenir les documents d'un dossier", + description = "Liste tous les documents avec statuts de validation" + ) + @GetMapping("/{id}/documents") + @PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')") + public ResponseEntity> getDocuments( + @Parameter(description = "ID du dossier") + @PathVariable Long id + ) { + try { + log.info("Consultation documents dossier ID: {}", id); + + // Placeholder response + return ResponseEntity.ok(List.of()); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des documents du dossier {}", id, e); + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } + } + + @Operation( + summary = "Obtenir l'historique d'un dossier", + description = "Liste chronologique des actions sur le dossier" + ) + @GetMapping("/{id}/history") + @PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')") + public ResponseEntity> getHistory( + @Parameter(description = "ID du dossier") + @PathVariable Long id, + + @Parameter(description = "Nombre d'entrées max") + @RequestParam(defaultValue = "50") int limit + ) { + try { + log.info("Consultation historique dossier ID: {}, limit: {}", id, limit); + + // Placeholder response + return ResponseEntity.ok(List.of()); + + } catch (Exception e) { + log.error("Erreur lors de la récupération de l'historique du dossier {}", id, e); + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } + } + + @Operation( + summary = "Obtenir les actions autorisées", + description = "Liste les actions que l'utilisateur peut effectuer sur ce dossier" + ) + @GetMapping("/{id}/permissions") + @PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')") + public ResponseEntity> getAuthorizedActions( + @Parameter(description = "ID du dossier") + @PathVariable Long id + ) { + try { + log.info("Consultation permissions dossier ID: {}", id); + + // Placeholder response + return ResponseEntity.ok(Set.of(FolderAction.VIEW_BASIC_INFO)); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des permissions du dossier {}", id, e); + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } + } +} + +// ========== REQUEST DTOs ========== + +@lombok.Data +class CreateFolderRequest { + private Long quoteId; + private String commentairesClient; +} + +@lombok.Data +class StatusUpdateRequest { + private DossierStatus newStatus; + private String comment; +} + +@lombok.Data +class AssignFolderRequest { + private Long adminId; + private String comment; +} + +@lombok.Data +class UpdateTransportRequest { + private String referenceBooking; + private String numeroBl; + private String numeroConteneur; + private String notesInternes; +} \ No newline at end of file diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/DocumentSummaryDto.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/DocumentSummaryDto.java new file mode 100644 index 0000000..f9caedd --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/DocumentSummaryDto.java @@ -0,0 +1,52 @@ +package com.dh7789dev.xpeditis.dto.app; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DocumentSummaryDto { + + private Long id; + + // ========== DOCUMENT TYPE ========== + private Long typeDocumentId; + private String typeDocumentNom; + private boolean isObligatoire; + + // ========== FILE INFO ========== + private String nomOriginal; + private Long tailleOctets; + private String typeMime; + private Integer numeroVersion; + + // ========== VALIDATION ========== + private StatutVerification statutVerification; + private String displayStatutVerification; + private String commentaireVerification; + private String correctionsDemandees; + + // ========== METADATA ========== + private String description; + private LocalDate dateValidite; + private boolean isExpired; + + // ========== USER INFO ========== + private String uploadePar; + private LocalDateTime dateUpload; + private String verifiePar; + private LocalDateTime dateVerification; + + // ========== ACTIONS ========== + private boolean canDownload; + private boolean canDelete; + private boolean canValidate; + private boolean requiresAdminAction; +} \ No newline at end of file diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/DossierStatus.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/DossierStatus.java new file mode 100644 index 0000000..94ca4cb --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/DossierStatus.java @@ -0,0 +1,167 @@ +package com.dh7789dev.xpeditis.dto.app; + +import java.util.Set; + +/** + * Énumération des statuts d'un dossier d'export + * Définit le workflow complet de traitement des dossiers + */ +public enum DossierStatus { + // ========== PHASE CRÉATION ========== + CREE("Créé", "Dossier créé depuis devis accepté", false, false), + + // ========== PHASE DOCUMENTS ========== + DOCUMENTS_EN_ATTENTE("Documents en attente", "En attente d'upload des documents obligatoires", true, false), + DOCUMENTS_UPLOADES("Documents uploadés", "Documents uploadés, en attente de vérification", true, false), + DOCUMENTS_EN_VERIFICATION("Documents en vérification", "Vérification des documents en cours par admin", false, false), + DOCUMENTS_REFUSES("Documents refusés", "Corrections demandées sur un ou plusieurs documents", true, false), + DOCUMENTS_VALIDES("Documents validés", "Tous documents validés, prêt pour booking", false, false), + + // ========== PHASE TRANSPORT ========== + BOOKING_EN_COURS("Booking en cours", "Réservation transport en cours", false, false), + BOOKING_CONFIRME("Booking confirmé", "Transport confirmé", false, false), + ENLEVE("Enlevé", "Marchandise enlevée", false, false), + EN_TRANSIT("En transit", "En cours de transport", false, false), + ARRIVE("Arrivé", "Arrivé à destination", false, false), + + // ========== PHASE FINALISATION ========== + LIVRE("Livré", "Livré au destinataire", false, true), + CLOTURE("Clôturé", "Dossier clôturé", false, true), + + // ========== STATUT SPÉCIAL ========== + ANNULE("Annulé", "Dossier annulé", false, true); + + private final String displayName; + private final String description; + private final boolean allowsDocumentUpload; + private final boolean isFinalized; + + DossierStatus(String displayName, String description, boolean allowsDocumentUpload, boolean isFinalized) { + this.displayName = displayName; + this.description = description; + this.allowsDocumentUpload = allowsDocumentUpload; + this.isFinalized = isFinalized; + } + + public String getDisplayName() { + return displayName; + } + + public String getDescription() { + return description; + } + + public boolean allowsDocumentUpload() { + return allowsDocumentUpload; + } + + public boolean isFinalized() { + return isFinalized; + } + + /** + * Retourne les statuts qui permettent l'upload de documents + */ + public static Set getUploadableStatuses() { + return Set.of( + DOCUMENTS_EN_ATTENTE, + DOCUMENTS_UPLOADES, + DOCUMENTS_REFUSES + ); + } + + /** + * Retourne les statuts considérés comme actifs (non terminés) + */ + public static Set getActiveStatuses() { + return Set.of( + CREE, + DOCUMENTS_EN_ATTENTE, + DOCUMENTS_UPLOADES, + DOCUMENTS_EN_VERIFICATION, + DOCUMENTS_REFUSES, + DOCUMENTS_VALIDES, + BOOKING_EN_COURS, + BOOKING_CONFIRME, + ENLEVE, + EN_TRANSIT, + ARRIVE + ); + } + + /** + * Vérifie si une transition de statut est valide + */ + public boolean canTransitionTo(DossierStatus newStatus) { + if (this.isFinalized) { + return false; // Aucune transition possible depuis un statut finalisé + } + + // Définition des transitions valides + switch (this) { + case CREE: + return newStatus == DOCUMENTS_EN_ATTENTE || newStatus == ANNULE; + + case DOCUMENTS_EN_ATTENTE: + return newStatus == DOCUMENTS_UPLOADES || newStatus == ANNULE; + + case DOCUMENTS_UPLOADES: + return newStatus == DOCUMENTS_EN_VERIFICATION || + newStatus == DOCUMENTS_EN_ATTENTE || newStatus == ANNULE; + + case DOCUMENTS_EN_VERIFICATION: + return newStatus == DOCUMENTS_VALIDES || + newStatus == DOCUMENTS_REFUSES || newStatus == ANNULE; + + case DOCUMENTS_REFUSES: + return newStatus == DOCUMENTS_UPLOADES || + newStatus == DOCUMENTS_EN_VERIFICATION || newStatus == ANNULE; + + case DOCUMENTS_VALIDES: + return newStatus == BOOKING_EN_COURS || newStatus == ANNULE; + + case BOOKING_EN_COURS: + return newStatus == BOOKING_CONFIRME || + newStatus == DOCUMENTS_VALIDES || newStatus == ANNULE; + + case BOOKING_CONFIRME: + return newStatus == ENLEVE || newStatus == ANNULE; + + case ENLEVE: + return newStatus == EN_TRANSIT; + + case EN_TRANSIT: + return newStatus == ARRIVE; + + case ARRIVE: + return newStatus == LIVRE; + + case LIVRE: + return newStatus == CLOTURE; + + default: + return false; + } + } + + /** + * Retourne le prochain statut logique dans le workflow standard + */ + public DossierStatus getNextLogicalStatus() { + switch (this) { + case CREE: return DOCUMENTS_EN_ATTENTE; + case DOCUMENTS_EN_ATTENTE: return DOCUMENTS_UPLOADES; + case DOCUMENTS_UPLOADES: return DOCUMENTS_EN_VERIFICATION; + case DOCUMENTS_EN_VERIFICATION: return DOCUMENTS_VALIDES; + case DOCUMENTS_REFUSES: return DOCUMENTS_UPLOADES; + case DOCUMENTS_VALIDES: return BOOKING_EN_COURS; + case BOOKING_EN_COURS: return BOOKING_CONFIRME; + case BOOKING_CONFIRME: return ENLEVE; + case ENLEVE: return EN_TRANSIT; + case EN_TRANSIT: return ARRIVE; + case ARRIVE: return LIVRE; + case LIVRE: return CLOTURE; + default: return null; + } + } +} \ No newline at end of file diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/ExportFolderDto.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/ExportFolderDto.java new file mode 100644 index 0000000..5eaf210 --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/ExportFolderDto.java @@ -0,0 +1,79 @@ +package com.dh7789dev.xpeditis.dto.app; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ExportFolderDto { + + private Long id; + + // ========== BASIC INFO ========== + private String reference; + private String numeroDossier; + private DossierStatus statut; + private String displayStatut; + + // ========== COMPANY & QUOTE ========== + private Long companyId; + private String companyName; + private Long quoteId; + private String quoteReference; + + // ========== WORKFLOW DATES ========== + private LocalDateTime dateCreation; + private LocalDateTime dateDocumentsComplets; + private LocalDateTime dateValidationDocuments; + private LocalDateTime dateBooking; + private LocalDateTime dateEnlevement; + private LocalDateTime dateLivraison; + private LocalDateTime dateCloture; + + // ========== TRANSPORT REFERENCES ========== + private String referenceBooking; + private String numeroBl; + private String numeroConteneur; + + // ========== METADATA ========== + private String commentairesClient; + private String notesInternes; + + // ========== USER ASSIGNMENTS ========== + private Long createdById; + private String createdByName; + private Long assignedToAdminId; + private String assignedToAdminName; + + // ========== WORKFLOW INFO ========== + private boolean allowsDocumentUpload; + private boolean isFinalized; + private DossierStatus nextLogicalStatus; + private boolean canTransitionToNext; + + // ========== DOCUMENTS SUMMARY ========== + private int totalDocuments; + private int validatedDocuments; + private int pendingDocuments; + private int rejectedDocuments; + private boolean allMandatoryDocumentsProvided; + + // ========== PERMISSIONS ========== + private Set authorizedActions; + + // ========== RELATED DATA ========== + private List documents; + private List recentHistory; + + // ========== TIMESTAMPS ========== + private LocalDateTime createdAt; + private LocalDateTime modifiedAt; +} \ No newline at end of file diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/FolderAction.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/FolderAction.java new file mode 100644 index 0000000..79071c5 --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/FolderAction.java @@ -0,0 +1,90 @@ +package com.dh7789dev.xpeditis.dto.app; + +/** + * Actions possibles sur les dossiers d'export + * Utilisé pour le système de permissions granulaires + */ +public enum FolderAction { + + // ========== CONSULTATION ========== + VIEW_BASIC_INFO("Consulter informations de base", "Voir référence, statut, dates"), + VIEW_FULL_DETAILS("Consulter détails complets", "Voir toutes les informations du dossier"), + VIEW_DOCUMENTS("Consulter documents", "Voir la liste des documents uploadés"), + VIEW_HISTORY("Consulter historique", "Voir l'historique des actions"), + DOWNLOAD_DOCUMENTS("Télécharger documents", "Télécharger les fichiers"), + + // ========== MODIFICATION ========== + UPDATE_BASIC_INFO("Modifier informations de base", "Modifier commentaires client, références"), + UPDATE_STATUS("Modifier statut", "Changer le statut du dossier"), + ADD_INTERNAL_NOTES("Ajouter notes internes", "Ajouter des commentaires internes"), + + // ========== GESTION DOCUMENTS ========== + UPLOAD_DOCUMENTS("Uploader documents", "Ajouter de nouveaux documents"), + DELETE_DOCUMENTS("Supprimer documents", "Supprimer des documents"), + VALIDATE_DOCUMENTS("Valider documents", "Approuver ou refuser des documents"), + REQUEST_CORRECTIONS("Demander corrections", "Demander des corrections sur documents"), + + // ========== WORKFLOW ========== + TRANSITION_STATUS("Changer statut workflow", "Faire progresser le dossier dans le workflow"), + FORCE_STATUS_CHANGE("Forcer changement statut", "Changer le statut sans validation workflow"), + ASSIGN_TO_ADMIN("Assigner à administrateur", "Assigner le dossier à un admin"), + MARK_DOCUMENTS_COMPLETE("Marquer documents complets", "Valider que tous documents sont fournis"), + + // ========== ADMINISTRATION ========== + DELETE_FOLDER("Supprimer dossier", "Supprimer complètement le dossier"), + EXPORT_DATA("Exporter données", "Exporter les données du dossier"), + MANAGE_PERMISSIONS("Gérer permissions", "Modifier les permissions sur le dossier"), + ACCESS_SYSTEM_DATA("Accès données système", "Voir données techniques internes"), + + // ========== TRANSPORT ========== + UPDATE_TRANSPORT_INFO("Modifier infos transport", "Mettre à jour booking, BL, conteneur"), + TRACK_SHIPMENT("Suivre expédition", "Voir le suivi de l'expédition"), + UPDATE_TRACKING("Mettre à jour suivi", "Ajouter des mises à jour de suivi"), + + // ========== NOTIFICATIONS ========== + RECEIVE_NOTIFICATIONS("Recevoir notifications", "Être notifié des changements"), + SEND_NOTIFICATIONS("Envoyer notifications", "Notifier d'autres utilisateurs"); + + private final String displayName; + private final String description; + + FolderAction(String displayName, String description) { + this.displayName = displayName; + this.description = description; + } + + public String getDisplayName() { + return displayName; + } + + public String getDescription() { + return description; + } + + /** + * Actions de consultation seulement + */ + public boolean isReadOnlyAction() { + return this == VIEW_BASIC_INFO || this == VIEW_FULL_DETAILS || + this == VIEW_DOCUMENTS || this == VIEW_HISTORY || + this == DOWNLOAD_DOCUMENTS || this == TRACK_SHIPMENT || + this == RECEIVE_NOTIFICATIONS; + } + + /** + * Actions nécessitant des privilèges administratifs + */ + public boolean requiresAdminPrivileges() { + return this == VALIDATE_DOCUMENTS || this == FORCE_STATUS_CHANGE || + this == DELETE_FOLDER || this == MANAGE_PERMISSIONS || + this == ACCESS_SYSTEM_DATA || this == ASSIGN_TO_ADMIN; + } + + /** + * Actions liées au workflow + */ + public boolean isWorkflowAction() { + return this == UPDATE_STATUS || this == TRANSITION_STATUS || + this == FORCE_STATUS_CHANGE || this == MARK_DOCUMENTS_COMPLETE; + } +} \ No newline at end of file diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/HistoryEntryDto.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/HistoryEntryDto.java new file mode 100644 index 0000000..cfbb9c9 --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/HistoryEntryDto.java @@ -0,0 +1,43 @@ +package com.dh7789dev.xpeditis.dto.app; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HistoryEntryDto { + + private Long id; + + // ========== ACTION INFO ========== + private String action; + private String description; + + // ========== STATUS CHANGES ========== + private String ancienStatut; + private String nouveauStatut; + + // ========== ACTOR INFO ========== + private String effectueParType; // COMPANY_USER, ADMIN, SYSTEM + private Long effectueParId; + private String effectueParNom; + + // ========== TIMING ========== + private LocalDateTime dateAction; + private String relativeTime; // "Il y a 2 heures", "Hier", etc. + + // ========== ADDITIONAL DATA ========== + private Map donneesSupplementaires; + + // ========== UI HELPERS ========== + private String actionIcon; // Icon CSS class for UI + private String actionColor; // Color for UI display + private String displayText; // Human-readable display text +} \ No newline at end of file diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/Role.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/Role.java index 0b36579..c92d30c 100644 --- a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/Role.java +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/Role.java @@ -1,8 +1,73 @@ package com.dh7789dev.xpeditis.dto.app; +/** + * Système de rôles étendu pour SSC Export System + * Intègre les rôles existants et les nouveaux besoins SSC + */ public enum Role { - USER, - MANAGER, - ADMIN, - ADMIN_PLATFORM + // ========== RÔLES EXISTANTS (Conservés) ========== + USER("Utilisateur Standard", 1), + MANAGER("Gestionnaire", 2), + ADMIN("Administrateur", 3), + ADMIN_PLATFORM("Administrateur Plateforme", 4), + + // ========== NOUVEAUX RÔLES SSC ========== + SUPER_ADMIN("Super Administrateur", 10), // Accès total système + ADMIN_SSC("Administrateur SSC", 9), // Validation documents, gestion dossiers + COMPANY_ADMIN("Administrateur Entreprise", 6), // Gestion équipe entreprise cliente + COMPANY_USER("Utilisateur Entreprise", 5), // Consultation dossiers de son entreprise + COMPANY_GUEST("Invité Entreprise", 3); // Lecture seule sur dossiers spécifiques + + private final String displayName; + private final int level; + + Role(String displayName, int level) { + this.displayName = displayName; + this.level = level; + } + + public String getDisplayName() { + return displayName; + } + + public int getLevel() { + return level; + } + + /** + * Vérifie si ce rôle a un niveau supérieur ou égal à un autre + */ + public boolean hasLevelGreaterOrEqual(Role other) { + return this.level >= other.level; + } + + /** + * Vérifie si ce rôle est un rôle SSC (gestion dossiers export) + */ + public boolean isSscRole() { + return this == SUPER_ADMIN || this == ADMIN_SSC || + this == COMPANY_ADMIN || this == COMPANY_USER || this == COMPANY_GUEST; + } + + /** + * Vérifie si ce rôle peut gérer des utilisateurs d'entreprise + */ + public boolean canManageCompanyUsers() { + return this == SUPER_ADMIN || this == ADMIN_SSC || this == COMPANY_ADMIN; + } + + /** + * Vérifie si ce rôle peut valider des documents + */ + public boolean canValidateDocuments() { + return this == SUPER_ADMIN || this == ADMIN_SSC; + } + + /** + * Vérifie si ce rôle est un administrateur + */ + public boolean isAdmin() { + return this == SUPER_ADMIN || this == ADMIN_SSC || + this == ADMIN || this == ADMIN_PLATFORM; + } } \ No newline at end of file diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/StatutVerification.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/StatutVerification.java new file mode 100644 index 0000000..b08fc00 --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/StatutVerification.java @@ -0,0 +1,51 @@ +package com.dh7789dev.xpeditis.dto.app; + +/** + * Statuts de vérification des documents uploadés + * Cycle de validation par les administrateurs SSC + */ +public enum StatutVerification { + + EN_ATTENTE("En attente", "Document uploadé, en attente de vérification"), + EN_COURS_VERIFICATION("En cours de vérification", "Document en cours d'analyse par un administrateur"), + VALIDE("Validé", "Document vérifié et approuvé"), + REFUSE("Refusé", "Document refusé, corrections nécessaires"), + EXPIRE("Expiré", "Document expiré, renouvellement requis"); + + private final String displayName; + private final String description; + + StatutVerification(String displayName, String description) { + this.displayName = displayName; + this.description = description; + } + + public String getDisplayName() { + return displayName; + } + + public String getDescription() { + return description; + } + + /** + * Vérifie si ce statut permet une nouvelle vérification + */ + public boolean allowsReVerification() { + return this == REFUSE || this == EXPIRE; + } + + /** + * Vérifie si ce statut est considéré comme final + */ + public boolean isFinal() { + return this == VALIDE || this == REFUSE; + } + + /** + * Vérifie si ce statut nécessite une action admin + */ + public boolean requiresAdminAction() { + return this == EN_ATTENTE || this == EN_COURS_VERIFICATION; + } +} \ No newline at end of file diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/request/CreateFolderRequest.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/request/CreateFolderRequest.java new file mode 100644 index 0000000..bb27ca3 --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/request/CreateFolderRequest.java @@ -0,0 +1,26 @@ +package com.dh7789dev.xpeditis.dto.request; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +@Data +@Builder(toBuilder = true) +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class CreateFolderRequest { + + @NotNull(message = "Quote ID is required") + @Positive(message = "Quote ID must be positive") + Long quoteId; + + @Size(max = 1000, message = "Comments must not exceed 1000 characters") + String commentairesClient; +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/DocumentDossierEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/DocumentDossierEntity.java new file mode 100644 index 0000000..d92eb30 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/DocumentDossierEntity.java @@ -0,0 +1,148 @@ +package com.dh7789dev.xpeditis.entity; + +import com.dh7789dev.xpeditis.dto.app.StatutVerification; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Entity +@Table(name = "documents_dossier") +@Getter +@Setter +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class DocumentDossierEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "dossier_export_id", nullable = false) + ExportFolderEntity dossier; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "type_document_id", nullable = false) + DocumentTypeEntity typeDocument; + + // ========== FILE INFORMATION ========== + @Column(name = "nom_original", nullable = false, length = 255) + String nomOriginal; + + @Column(name = "nom_stockage", nullable = false, length = 100) + String nomStockage; // UUID + extension + + @Column(name = "chemin_stockage", nullable = false, length = 500) + String cheminStockage; // Path on filesystem + + @Column(name = "taille_octets", nullable = false) + Long tailleOctets; + + @Column(name = "type_mime", nullable = false, length = 100) + String typeMime; + + @Column(name = "hash_fichier", length = 64) + String hashFichier; // SHA-256 + + // ========== VERSIONING ========== + @Column(name = "numero_version", nullable = false) + Integer numeroVersion = 1; + + @Column(name = "description", length = 500) + String description; + + @Column(name = "date_validite") + LocalDate dateValidite; + + // ========== VERIFICATION STATUS ========== + @Enumerated(EnumType.STRING) + @Column(name = "statut_verification", nullable = false, length = 30) + StatutVerification statutVerification = StatutVerification.EN_ATTENTE; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "verifie_par") + UserEntity verifiePar; + + @Column(name = "date_verification") + LocalDateTime dateVerification; + + @Lob + @Column(name = "commentaire_verification") + String commentaireVerification; + + @Lob + @Column(name = "corrections_demandees") + String correctionsDemandees; + + // ========== METADATA ========== + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "uploade_par", nullable = false) + UserEntity uploadePar; + + @Column(name = "date_upload", nullable = false) + LocalDateTime dateUpload; + + @Column(name = "updated_at") + LocalDateTime updatedAt; + + @PrePersist + protected void onCreate() { + if (dateUpload == null) { + dateUpload = LocalDateTime.now(); + } + updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } + + // ========== BUSINESS METHODS ========== + + /** + * Vérifie si le document est valide (approuvé) + */ + public boolean isValid() { + return statutVerification == StatutVerification.VALIDE; + } + + /** + * Vérifie si le document nécessite une action admin + */ + public boolean requiresAdminAction() { + return statutVerification.requiresAdminAction(); + } + + /** + * Vérifie si le document peut être re-vérifié + */ + public boolean canBeReVerified() { + return statutVerification.allowsReVerification(); + } + + /** + * Vérifie si le document a expiré + */ + public boolean isExpired() { + return dateValidite != null && dateValidite.isBefore(LocalDate.now()); + } + + /** + * Génère un nom de fichier de stockage unique + */ + public static String generateStorageName(String originalName) { + String extension = ""; + int i = originalName.lastIndexOf('.'); + if (i > 0) { + extension = originalName.substring(i); + } + return java.util.UUID.randomUUID().toString() + extension; + } +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/DocumentTypeEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/DocumentTypeEntity.java new file mode 100644 index 0000000..ed8a980 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/DocumentTypeEntity.java @@ -0,0 +1,51 @@ +package com.dh7789dev.xpeditis.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +@Entity +@Table(name = "document_types") +@Getter +@Setter +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class DocumentTypeEntity extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @Column(name = "nom", nullable = false, length = 100) + String nom; + + @Column(name = "description", length = 500) + String description; + + @Column(name = "obligatoire", nullable = false) + Boolean obligatoire = false; + + @Column(name = "pour_import", nullable = false) + Boolean pourImport = false; + + @Column(name = "pour_export", nullable = false) + Boolean pourExport = false; + + @Column(name = "pour_marchandise_dangereuse", nullable = false) + Boolean pourMarchandiseDangereuse = false; + + @Column(name = "extensions_acceptees", length = 200) + String extensionsAcceptees; + + @Column(name = "taille_max_mo") + Integer tailleMaxMo = 10; + + @Column(name = "ordre_affichage") + Integer ordreAffichage = 0; + + @Column(name = "actif", nullable = false) + Boolean actif = true; +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/ExportFolderEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/ExportFolderEntity.java index 3da5b43..6f63ca2 100644 --- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/ExportFolderEntity.java +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/ExportFolderEntity.java @@ -1,5 +1,6 @@ package com.dh7789dev.xpeditis.entity; +import com.dh7789dev.xpeditis.dto.app.DossierStatus; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; @@ -22,36 +23,107 @@ import java.util.List; public class ExportFolderEntity extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + Long id; + // ========== EXISTING FIELDS (Preserved) ========== @Column(unique = true) - private String reference; + String reference; @Column(name = "validationDate") - private LocalDateTime validationDate; + LocalDateTime validationDate; @OneToMany(mappedBy = "exportFolder", cascade = CascadeType.ALL, orphanRemoval = true) - private List documents; + List documents; @OneToMany(mappedBy = "exportFolder", cascade = CascadeType.ALL, orphanRemoval = true) - private List trackingUpdates = new ArrayList<>(); + List trackingUpdates = new ArrayList<>(); @ManyToOne - private CompanyEntity company; + CompanyEntity company; @OneToOne @JoinColumn(name = "quote_id", unique = true) - private QuoteEntity quote; + QuoteEntity quote; @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; + LocalDateTime createdAt; @Column(name = "modified_at") - private LocalDateTime modifiedAt; + LocalDateTime modifiedAt; + + // ========== NEW WORKFLOW FIELDS ========== + @Column(name = "numero_dossier", unique = true, length = 20) + String numeroDossier; // EXP-2024-001234 + + @Enumerated(EnumType.STRING) + @Column(name = "statut", nullable = false, length = 50) + DossierStatus statut = DossierStatus.CREE; + + // ========== TRACKING DATES ========== + @Column(name = "date_creation") + LocalDateTime dateCreation; + + @Column(name = "date_documents_complets") + LocalDateTime dateDocumentsComplets; + + @Column(name = "date_validation_documents") + LocalDateTime dateValidationDocuments; + + @Column(name = "date_booking") + LocalDateTime dateBooking; + + @Column(name = "date_enlevement") + LocalDateTime dateEnlevement; + + @Column(name = "date_livraison") + LocalDateTime dateLivraison; + + @Column(name = "date_cloture") + LocalDateTime dateCloture; + + // ========== TRANSPORT REFERENCES ========== + @Column(name = "reference_booking", length = 50) + String referenceBooking; + + @Column(name = "numero_bl", length = 50) + String numeroBl; + + @Column(name = "numero_conteneur", length = 50) + String numeroConteneur; + + // ========== METADATA ========== + @Lob + @Column(name = "commentaires_client") + String commentairesClient; + + @Lob + @Column(name = "notes_internes") + String notesInternes; + + // ========== USER RELATIONSHIPS ========== + @ManyToOne + @JoinColumn(name = "created_by_user_id") + UserEntity createdByUser; + + @ManyToOne + @JoinColumn(name = "assigned_to_admin") + UserEntity assignedToAdmin; + + // ========== NEW RELATIONSHIPS ========== + @OneToMany(mappedBy = "dossier", cascade = CascadeType.ALL) + List documentsValidation = new ArrayList<>(); + + @OneToMany(mappedBy = "dossierId", cascade = CascadeType.ALL) + List historique = new ArrayList<>(); @PrePersist public void onCreate() { - createdAt = LocalDateTime.now(); + if (createdAt == null) { + createdAt = LocalDateTime.now(); + } + if (dateCreation == null) { + dateCreation = LocalDateTime.now(); + } modifiedAt = LocalDateTime.now(); } @@ -59,5 +131,35 @@ public class ExportFolderEntity extends BaseEntity { public void onUpdate() { modifiedAt = LocalDateTime.now(); } + + // ========== BUSINESS METHODS ========== + + /** + * Vérifie si le dossier est dans un état permettant l'upload de documents + */ + public boolean allowsDocumentUpload() { + return statut != null && statut.allowsDocumentUpload(); + } + + /** + * Vérifie si le dossier est finalisé + */ + public boolean isFinalized() { + return statut != null && statut.isFinalized(); + } + + /** + * Vérifie si une transition de statut est possible + */ + public boolean canTransitionTo(DossierStatus newStatus) { + return statut != null && statut.canTransitionTo(newStatus); + } + + /** + * Retourne le prochain statut logique + */ + public DossierStatus getNextLogicalStatus() { + return statut != null ? statut.getNextLogicalStatus() : null; + } } diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/HistoriqueDossierEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/HistoriqueDossierEntity.java new file mode 100644 index 0000000..bc5b145 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/HistoriqueDossierEntity.java @@ -0,0 +1,65 @@ +package com.dh7789dev.xpeditis.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.FieldDefaults; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.time.LocalDateTime; +import java.util.Map; + +@Entity +@Table(name = "dossier_history") +@Getter +@Setter +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class HistoriqueDossierEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @Column(name = "dossier_id", nullable = false) + Long dossierId; + + @Column(name = "action", nullable = false, length = 100) + String action; + + @Column(name = "ancien_statut", length = 50) + String ancienStatut; + + @Column(name = "nouveau_statut", length = 50) + String nouveauStatut; + + @Lob + @Column(name = "description") + String description; + + @Column(name = "effectue_par_type", nullable = false, length = 20) + String effectueParType; // COMPANY_USER, ADMIN, SYSTEM + + @Column(name = "effectue_par_id") + Long effectueParId; + + @Column(name = "effectue_par_nom", nullable = false, length = 100) + String effectueParNom; + + @Column(name = "date_action", nullable = false) + LocalDateTime dateAction; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "donnees_supplementaires", columnDefinition = "json") + Map donneesSupplementaires; + + @PrePersist + protected void onCreate() { + if (dateAction == null) { + dateAction = LocalDateTime.now(); + } + } +} \ No newline at end of file diff --git a/infrastructure/src/main/resources/db/migration/structure/V4__CREATE_SSC_EXPORT_SYSTEM.sql b/infrastructure/src/main/resources/db/migration/structure/V4__CREATE_SSC_EXPORT_SYSTEM.sql new file mode 100644 index 0000000..84841fa --- /dev/null +++ b/infrastructure/src/main/resources/db/migration/structure/V4__CREATE_SSC_EXPORT_SYSTEM.sql @@ -0,0 +1,288 @@ +-- Migration V4: SSC Export System - Complete Implementation +-- Creates tables and modifies existing structure for comprehensive export folder management + +-- ========== CREATE NEW TABLES ========== + +-- Table des types de documents +CREATE TABLE document_types ( + id BIGINT NOT NULL AUTO_INCREMENT, + nom VARCHAR(100) NOT NULL, + description VARCHAR(500), + obligatoire BOOLEAN NOT NULL DEFAULT FALSE, + pour_import BOOLEAN NOT NULL DEFAULT FALSE, + pour_export BOOLEAN NOT NULL DEFAULT FALSE, + pour_marchandise_dangereuse BOOLEAN NOT NULL DEFAULT FALSE, + extensions_acceptees VARCHAR(200), + taille_max_mo INTEGER DEFAULT 10, + ordre_affichage INTEGER DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +-- Table de l'historique des dossiers +CREATE TABLE dossier_history ( + id BIGINT NOT NULL AUTO_INCREMENT, + dossier_id BIGINT NOT NULL, + action VARCHAR(100) NOT NULL, + ancien_statut VARCHAR(50), + nouveau_statut VARCHAR(50), + description TEXT, + effectue_par_type VARCHAR(20) NOT NULL, -- COMPANY_USER, ADMIN, SYSTEM + effectue_par_id BIGINT, + effectue_par_nom VARCHAR(100) NOT NULL, + date_action TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + donnees_supplementaires JSON, + PRIMARY KEY (id), + INDEX idx_dossier_history_dossier_id (dossier_id), + INDEX idx_dossier_history_date_action (date_action), + INDEX idx_dossier_history_effectue_par (effectue_par_type, effectue_par_id) +); + +-- Table des documents de dossier (nouvelle version avec validation) +CREATE TABLE documents_dossier ( + id BIGINT NOT NULL AUTO_INCREMENT, + dossier_export_id BIGINT NOT NULL, + type_document_id BIGINT NOT NULL, + + -- Informations fichier + nom_original VARCHAR(255) NOT NULL, + nom_stockage VARCHAR(100) NOT NULL, + chemin_stockage VARCHAR(500) NOT NULL, + taille_octets BIGINT NOT NULL, + type_mime VARCHAR(100) NOT NULL, + hash_fichier VARCHAR(64), -- SHA-256 + + -- Versioning + numero_version INTEGER NOT NULL DEFAULT 1, + description VARCHAR(500), + date_validite DATE, + + -- Statut de vérification + statut_verification VARCHAR(30) NOT NULL DEFAULT 'EN_ATTENTE', + verifie_par BIGINT, + date_verification TIMESTAMP, + commentaire_verification TEXT, + corrections_demandees TEXT, + + -- Métadonnées + uploade_par BIGINT NOT NULL, + date_upload TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + PRIMARY KEY (id), + FOREIGN KEY (dossier_export_id) REFERENCES exportfolder(id) ON DELETE CASCADE, + FOREIGN KEY (type_document_id) REFERENCES document_types(id), + FOREIGN KEY (uploade_par) REFERENCES user(id), + FOREIGN KEY (verifie_par) REFERENCES user(id), + + INDEX idx_document_dossier_export_id (dossier_export_id), + INDEX idx_document_dossier_type (type_document_id), + INDEX idx_document_dossier_verification (statut_verification), + INDEX idx_document_dossier_upload_date (date_upload), + INDEX idx_document_dossier_version (numero_version), + UNIQUE KEY uk_document_stockage (nom_stockage) +); + +-- ========== MODIFY EXISTING EXPORTFOLDER TABLE ========== + +-- Add workflow fields +ALTER TABLE exportfolder +ADD COLUMN numero_dossier VARCHAR(20) UNIQUE, +ADD COLUMN statut VARCHAR(50) NOT NULL DEFAULT 'CREE', + +-- Add tracking dates +ADD COLUMN date_creation TIMESTAMP, +ADD COLUMN date_documents_complets TIMESTAMP, +ADD COLUMN date_validation_documents TIMESTAMP, +ADD COLUMN date_booking TIMESTAMP, +ADD COLUMN date_enlevement TIMESTAMP, +ADD COLUMN date_livraison TIMESTAMP, +ADD COLUMN date_cloture TIMESTAMP, + +-- Add transport references +ADD COLUMN reference_booking VARCHAR(50), +ADD COLUMN numero_bl VARCHAR(50), +ADD COLUMN numero_conteneur VARCHAR(50), + +-- Add metadata +ADD COLUMN commentaires_client TEXT, +ADD COLUMN notes_internes TEXT, + +-- Add user relationships +ADD COLUMN created_by BIGINT, +ADD COLUMN assigned_to_admin BIGINT; + +-- Add foreign key constraints +ALTER TABLE exportfolder +ADD CONSTRAINT fk_exportfolder_created_by + FOREIGN KEY (created_by) REFERENCES user(id), +ADD CONSTRAINT fk_exportfolder_assigned_to + FOREIGN KEY (assigned_to_admin) REFERENCES user(id); + +-- ========== CREATE INDEXES FOR PERFORMANCE ========== + +-- ExportFolder indexes +CREATE INDEX idx_exportfolder_statut ON exportfolder(statut); +CREATE INDEX idx_exportfolder_company_statut ON exportfolder(company_id, statut); +CREATE INDEX idx_exportfolder_created_by ON exportfolder(created_by); +CREATE INDEX idx_exportfolder_assigned_to ON exportfolder(assigned_to_admin); +CREATE INDEX idx_exportfolder_date_creation ON exportfolder(date_creation); +CREATE INDEX idx_exportfolder_numero_dossier ON exportfolder(numero_dossier); +CREATE INDEX idx_exportfolder_date_modified ON exportfolder(modified_at); + +-- Composite indexes for common queries +CREATE INDEX idx_exportfolder_company_date ON exportfolder(company_id, date_creation DESC); +CREATE INDEX idx_exportfolder_statut_date ON exportfolder(statut, date_creation DESC); + +-- ========== DATA INITIALIZATION ========== + +-- Insert default document types +INSERT INTO document_types (nom, description, obligatoire, pour_export, extensions_acceptees, taille_max_mo, ordre_affichage, actif) VALUES +('Facture commerciale', 'Facture détaillant les biens exportés', TRUE, TRUE, '.pdf,.jpg,.png', 5, 1, TRUE), +('Liste de colisage', 'Détail du contenu de chaque colis', TRUE, TRUE, '.pdf,.xls,.xlsx', 5, 2, TRUE), +('Certificat d''origine', 'Document attestant l''origine des marchandises', FALSE, TRUE, '.pdf,.jpg,.png', 3, 3, TRUE), +('Déclaration en douane', 'Documents douaniers requis', TRUE, TRUE, '.pdf', 10, 4, TRUE), +('Assurance transport', 'Police d''assurance pour le transport', FALSE, TRUE, '.pdf', 2, 5, TRUE), +('Autorisation d''exportation', 'Autorisation spéciale si requise', FALSE, TRUE, '.pdf,.jpg,.png', 5, 6, TRUE), +('Certificat sanitaire', 'Pour marchandises alimentaires/agricoles', FALSE, TRUE, '.pdf', 3, 7, TRUE), +('Documents marchandises dangereuses', 'Classification et déclaration MD', FALSE, TRUE, '.pdf', 5, 8, TRUE); + +-- Update document types for dangerous goods +UPDATE document_types +SET pour_marchandise_dangereuse = TRUE +WHERE nom IN ('Documents marchandises dangereuses', 'Déclaration en douane', 'Certificat d''origine'); + +-- ========== UPDATE EXISTING DATA ========== + +-- Initialize date_creation for existing folders +UPDATE exportfolder +SET date_creation = created_at +WHERE date_creation IS NULL AND created_at IS NOT NULL; + +-- Initialize numero_dossier for existing folders (if any exist) +-- Format: EXP-YYYY-NNNNNN +UPDATE exportfolder +SET numero_dossier = CONCAT('EXP-', YEAR(COALESCE(created_at, NOW())), '-', LPAD(id, 6, '0')) +WHERE numero_dossier IS NULL; + +-- Initialize created_by from user relationship if quote exists +UPDATE exportfolder ef +INNER JOIN Quote q ON ef.quote_id = q.id +SET ef.created_by = q.user_id +WHERE ef.created_by IS NULL AND q.user_id IS NOT NULL; + +-- ========== ADD CONSTRAINTS AND VALIDATIONS ========== + +-- Add check constraints for statut values +ALTER TABLE exportfolder +ADD CONSTRAINT chk_exportfolder_statut +CHECK (statut IN ( + 'CREE', 'DOCUMENTS_EN_ATTENTE', 'DOCUMENTS_UPLOADES', + 'DOCUMENTS_EN_VERIFICATION', 'DOCUMENTS_REFUSES', 'DOCUMENTS_VALIDES', + 'BOOKING_EN_COURS', 'BOOKING_CONFIRME', 'ENLEVE', 'EN_TRANSIT', + 'ARRIVE', 'LIVRE', 'CLOTURE', 'ANNULE' +)); + +-- Add check constraints for document verification status +ALTER TABLE documents_dossier +ADD CONSTRAINT chk_documents_dossier_statut_verification +CHECK (statut_verification IN ( + 'EN_ATTENTE', 'EN_COURS_VERIFICATION', 'VALIDE', 'REFUSE', 'EXPIRE' +)); + +-- Ensure version numbers are positive +ALTER TABLE documents_dossier +ADD CONSTRAINT chk_documents_dossier_version +CHECK (numero_version > 0); + +-- Ensure file size is positive +ALTER TABLE documents_dossier +ADD CONSTRAINT chk_documents_dossier_taille +CHECK (taille_octets > 0); + +-- ========== TRIGGERS FOR AUDIT AND AUTOMATION ========== + +-- Trigger to automatically create history entry on folder status change +DELIMITER // +CREATE TRIGGER tr_exportfolder_status_history +AFTER UPDATE ON exportfolder +FOR EACH ROW +BEGIN + IF OLD.statut != NEW.statut THEN + INSERT INTO dossier_history ( + dossier_id, action, ancien_statut, nouveau_statut, + description, effectue_par_type, effectue_par_nom, date_action + ) VALUES ( + NEW.id, 'STATUS_CHANGE', OLD.statut, NEW.statut, + CONCAT('Changement de statut: ', OLD.statut, ' → ', NEW.statut), + 'SYSTEM', 'System Auto', NOW() + ); + END IF; +END// +DELIMITER ; + +-- Trigger to update document modification date +DELIMITER // +CREATE TRIGGER tr_documents_dossier_update_date +BEFORE UPDATE ON documents_dossier +FOR EACH ROW +BEGIN + SET NEW.updated_at = NOW(); +END// +DELIMITER ; + +-- ========== VIEWS FOR REPORTING ========== + +-- Vue des dossiers avec statistiques de documents +CREATE VIEW v_exportfolder_stats AS +SELECT + ef.id, + ef.numero_dossier, + ef.statut, + ef.date_creation, + ef.company_id, + c.name as company_name, + COUNT(dd.id) as total_documents, + COUNT(CASE WHEN dd.statut_verification = 'VALIDE' THEN 1 END) as documents_valides, + COUNT(CASE WHEN dd.statut_verification = 'EN_ATTENTE' THEN 1 END) as documents_en_attente, + COUNT(CASE WHEN dd.statut_verification = 'REFUSE' THEN 1 END) as documents_refuses, + COUNT(dt.id) as types_obligatoires, + COUNT(CASE WHEN dt.obligatoire = TRUE AND dd.id IS NOT NULL THEN 1 END) as types_obligatoires_fournis +FROM exportfolder ef +LEFT JOIN company c ON ef.company_id = c.id +LEFT JOIN documents_dossier dd ON ef.id = dd.dossier_export_id +LEFT JOIN document_types dt ON dt.obligatoire = TRUE AND dt.actif = TRUE +GROUP BY ef.id, ef.numero_dossier, ef.statut, ef.date_creation, ef.company_id, c.name; + +-- Vue de l'activité récente +CREATE VIEW v_recent_activity AS +SELECT + dh.id, + dh.dossier_id, + ef.numero_dossier, + dh.action, + dh.description, + dh.effectue_par_nom, + dh.date_action, + ef.company_id, + c.name as company_name +FROM dossier_history dh +INNER JOIN exportfolder ef ON dh.dossier_id = ef.id +LEFT JOIN company c ON ef.company_id = c.id +ORDER BY dh.date_action DESC; + +-- ========== FINAL OPTIMIZATIONS ========== + +-- Analyze tables for optimal query planning +ANALYZE TABLE exportfolder; +ANALYZE TABLE documents_dossier; +ANALYZE TABLE dossier_history; +ANALYZE TABLE document_types; + +-- Create full-text search indexes if needed (MySQL 5.7+) +-- ALTER TABLE exportfolder ADD FULLTEXT(commentaires_client, notes_internes); +-- ALTER TABLE documents_dossier ADD FULLTEXT(description, commentaire_verification); + +COMMIT; \ No newline at end of file