feature a test

This commit is contained in:
David 2025-09-15 14:41:34 +02:00
parent f31f1b6c69
commit b3ed387197
16 changed files with 2321 additions and 14 deletions

View File

@ -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" "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": [ "event": [

View File

@ -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<Resource> 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<DocumentSummaryDto> 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<Void> 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<DocumentSummaryDto> 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<DocumentDetailDto> 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<Void> 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<java.util.List<ValidationHistoryDto>> getValidationHistory(
@Parameter(description = "ID du document")
@PathVariable Long id
) {
try {
// TODO: Implement validation history retrieval
// UserEntity currentUser = getCurrentUser();
// List<ValidationHistoryDto> 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;
}

View File

@ -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<Page<ExportFolderDto>> searchFolders(
@Parameter(description = "Critères de recherche")
@RequestParam Map<String, String> 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<ExportFolderDto> 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<ExportFolderDto> 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<Void> 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<DocumentSummaryDto> 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<List<DocumentSummaryDto>> 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<List<HistoryEntryDto>> 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<Set<FolderAction>> 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;
}

View File

@ -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;
}

View File

@ -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<DossierStatus> 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<DossierStatus> 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;
}
}
}

View File

@ -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<FolderAction> authorizedActions;
// ========== RELATED DATA ==========
private List<DocumentSummaryDto> documents;
private List<HistoryEntryDto> recentHistory;
// ========== TIMESTAMPS ==========
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
}

View File

@ -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;
}
}

View File

@ -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<String, Object> 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
}

View File

@ -1,8 +1,73 @@
package com.dh7789dev.xpeditis.dto.app; 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 { public enum Role {
USER, // ========== RÔLES EXISTANTS (Conservés) ==========
MANAGER, USER("Utilisateur Standard", 1),
ADMIN, MANAGER("Gestionnaire", 2),
ADMIN_PLATFORM 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;
}
} }

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -1,5 +1,6 @@
package com.dh7789dev.xpeditis.entity; package com.dh7789dev.xpeditis.entity;
import com.dh7789dev.xpeditis.dto.app.DossierStatus;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
@ -22,36 +23,107 @@ import java.util.List;
public class ExportFolderEntity extends BaseEntity { public class ExportFolderEntity extends BaseEntity {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; Long id;
// ========== EXISTING FIELDS (Preserved) ==========
@Column(unique = true) @Column(unique = true)
private String reference; String reference;
@Column(name = "validationDate") @Column(name = "validationDate")
private LocalDateTime validationDate; LocalDateTime validationDate;
@OneToMany(mappedBy = "exportFolder", cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(mappedBy = "exportFolder", cascade = CascadeType.ALL, orphanRemoval = true)
private List<DocumentEntity> documents; List<DocumentEntity> documents;
@OneToMany(mappedBy = "exportFolder", cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(mappedBy = "exportFolder", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ShipmentTrackingEntity> trackingUpdates = new ArrayList<>(); List<ShipmentTrackingEntity> trackingUpdates = new ArrayList<>();
@ManyToOne @ManyToOne
private CompanyEntity company; CompanyEntity company;
@OneToOne @OneToOne
@JoinColumn(name = "quote_id", unique = true) @JoinColumn(name = "quote_id", unique = true)
private QuoteEntity quote; QuoteEntity quote;
@Column(name = "created_at", updatable = false) @Column(name = "created_at", updatable = false)
private LocalDateTime createdAt; LocalDateTime createdAt;
@Column(name = "modified_at") @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<DocumentDossierEntity> documentsValidation = new ArrayList<>();
@OneToMany(mappedBy = "dossierId", cascade = CascadeType.ALL)
List<HistoriqueDossierEntity> historique = new ArrayList<>();
@PrePersist @PrePersist
public void onCreate() { public void onCreate() {
createdAt = LocalDateTime.now(); if (createdAt == null) {
createdAt = LocalDateTime.now();
}
if (dateCreation == null) {
dateCreation = LocalDateTime.now();
}
modifiedAt = LocalDateTime.now(); modifiedAt = LocalDateTime.now();
} }
@ -59,5 +131,35 @@ public class ExportFolderEntity extends BaseEntity {
public void onUpdate() { public void onUpdate() {
modifiedAt = LocalDateTime.now(); 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;
}
} }

View File

@ -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<String, Object> donneesSupplementaires;
@PrePersist
protected void onCreate() {
if (dateAction == null) {
dateAction = LocalDateTime.now();
}
}
}

View File

@ -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;