diff --git a/angular.json b/angular.json
index 9bc188fb30e9b06e2c9669b4c4b69ec5b5b26174..92f7548269e91560e46bdf29ad63c16850d33c22 100755
--- a/angular.json
+++ b/angular.json
@@ -128,6 +128,7 @@
     }
   },
   "cli": {
+    "analytics": false,
     "defaultCollection": "@ionic/angular-toolkit"
   },
   "schematics": {
diff --git a/lang/en.json b/lang/en.json
index 57394fa28031c891a6154b74890f62f6bfea2fe8..c6a6a41890f64863f60acd5433a9040dec04dff1 100755
--- a/lang/en.json
+++ b/lang/en.json
@@ -40,6 +40,7 @@
         "documentRefusedAs": "Document refused as a:",
         "documentValidateAs": "Document validated as:",
         "documentViewed": "Document viewed",
+        "documentDeleted": "Document deleted",
         "documentProofViewed": "Document proof history viewed",
         "email": "Email",
         "emailAdded": "Email added",
@@ -652,6 +653,18 @@
         "emptyGroups": "No groups available to associate",
         "errorConvertingDocument": "Error converting document",
         "emptyGroupUsers": "No users associated with this group",
-        "emptyUsers": "No users available to associate"
+        "emptyUsers": "No users available to associate",
+        "can_purgeAdmin": "Mark and/or delete documents",
+        "purgeDocument": "Delete the document",
+        "downloadProofOnPurge": "You may not view the document after it is removed; its complete proof folder will be suggested for download afterwards.",
+		"manage_documents": "All-document access",
+		"indexation": "Workflow initiation",
+		"can_purge": "Document deletion",
+        "documentStateSearch": "Document status",
+        "deleted": "Deleted",
+        "softDeleted": "Marked as deleted",
+        "hardDelete": "Delete files",
+        "hardDeleted": "Deleted files",
+        "cannotAcces": "Unable to access document"
     }
 }
\ No newline at end of file
diff --git a/lang/fr.json b/lang/fr.json
index e830e51490c9811df7dde828149f116afa2f1a2c..12335c8f679bf5cb5e05d4aceb1ea0a1ce74829d 100755
--- a/lang/fr.json
+++ b/lang/fr.json
@@ -40,6 +40,7 @@
 		"documentRefusedAs"                  : "Document refusé en tant que :",
 		"documentValidateAs"                 : "Document validé en tant que :",
 		"documentViewed"                     : "Document consulté",
+		"documentDeleted"                    : "Document supprimé",
 		"documentProofViewed"                : "Faisceau de preuve du document consulté",
 		"email"                              : "Courriel",
 		"emailAdded"                         : "Courriel ajouté",
@@ -651,6 +652,18 @@
 		"emptyGroups": "Aucun groupe disponible à associer",
 		"errorConvertingDocument": "Erreur lors de la conversion du document",
 		"emptyGroupUsers": "Aucun utilisateur associé à ce groupe",
-		"emptyUsers": "Aucun utilisateur disponible à associer"
+		"emptyUsers": "Aucun utilisateur disponible à associer",
+		"can_purgeAdmin": "Marquer et/ou supprimer les documents",
+		"purgeDocument": "Supprimer le document",
+		"downloadProofOnPurge": "Après suppression, le document ne sera plus consultable ; son dossier complet vous sera ensuite proposé au téléchargement.",
+		"manage_documents": "Accès à tous les documents",
+		"indexation": "Initiation de circuit",
+		"can_purge": "Suppression de document",
+		"documentStateSearch": "Statuts du document",
+		"deleted": "Supprimé",
+		"hardDelete": "Supprimer les fichiers",
+		"cannotAcces": "Impossible d'accéder au document",
+		"softDeleted": "Marqué comme supprimé",
+		"hardDeleted":"Fichiers supprimés"
 	}
 }
diff --git a/rest/index.php b/rest/index.php
index 4584a4ffb8847a6940fcbaef0bb011156fb87a3e..51c418e3c10316f7195e59bcba0dcc3556cd0835 100755
--- a/rest/index.php
+++ b/rest/index.php
@@ -92,6 +92,7 @@ $app->get('/customization/watermark', \Configuration\controllers\ConfigurationCo
 $app->post('/documents', \Document\controllers\DocumentController::class . ':create');
 $app->get('/documents', \Document\controllers\DocumentController::class . ':get');
 $app->get('/documents/{id}', \Document\controllers\DocumentController::class . ':getById');
+$app->delete('/documents/{id}', \Document\controllers\DocumentController::class . ':delete');
 $app->get('/documents/{id}/content', \Document\controllers\DocumentController::class . ':getContent');
 $app->get('/documents/{id}/proof', \History\controllers\HistoryController::class . ':getHistoryProofByDocumentId');
 $app->get('/documents/{id}/history', \History\controllers\HistoryController::class . ':getByDocumentId');
diff --git a/src/app/docserver/models/DocserverModel.php b/src/app/docserver/models/DocserverModel.php
index 5b0bad0d203c546309f67bbdcd67105057f8a611..84847afb0393a6545bb97665914d87f88ea90771 100755
--- a/src/app/docserver/models/DocserverModel.php
+++ b/src/app/docserver/models/DocserverModel.php
@@ -18,17 +18,35 @@ use SrcCore\models\ValidatorModel;
 
 class DocserverModel
 {
-    public static function getByType(array $aArgs)
+    public static function get(array $args)
     {
-        ValidatorModel::notEmpty($aArgs, ['type']);
-        ValidatorModel::stringType($aArgs, ['type']);
-        ValidatorModel::arrayType($aArgs, ['select']);
+        ValidatorModel::notEmpty($args, ['select']);
+        ValidatorModel::arrayType($args, ['select', 'where', 'data']);
+
+        $docServers = DatabaseModel::select([
+            'select'    => $args['select'],
+            'table'     => ['docservers'],
+            'where'     => empty($args['where']) ? [] : $args['where'],
+            'data'      => empty($args['data']) ? [] : $args['data']
+        ]);
+
+        if (empty($docServers)) {
+            return [];
+        }
+        return $docServers;
+    }
+
+    public static function getByType(array $args)
+    {
+        ValidatorModel::notEmpty($args, ['type']);
+        ValidatorModel::stringType($args, ['type']);
+        ValidatorModel::arrayType($args, ['select']);
 
         $aDocserver = DatabaseModel::select([
-            'select'    => empty($aArgs['select']) ? ['*'] : $aArgs['select'],
+            'select'    => empty($args['select']) ? ['*'] : $args['select'],
             'table'     => ['docservers'],
             'where'     => ['type = ?'],
-            'data'      => [$aArgs['type']]
+            'data'      => [$args['type']]
         ]);
 
         if (empty($aDocserver[0])) {
@@ -38,16 +56,35 @@ class DocserverModel
         return $aDocserver[0];
     }
 
-    public static function update(array $aArgs)
+    public static function getByTypes(array $args)
+    {
+        ValidatorModel::notEmpty($args, ['types']);
+        ValidatorModel::arrayType($args, ['types']);
+        ValidatorModel::arrayType($args, ['select']);
+
+        $docServers = DatabaseModel::select([
+            'select'    => empty($args['select']) ? ['*'] : $args['select'],
+            'table'     => ['docservers'],
+            'where'     => ['type IN (?)'],
+            'data'      => [$args['types']]
+        ]);
+
+        if (empty($docServers)) {
+            return [];
+        }
+        return $docServers;
+    }
+
+    public static function update(array $args)
     {
-        ValidatorModel::notEmpty($aArgs, ['set', 'where', 'data']);
-        ValidatorModel::arrayType($aArgs, ['set', 'where', 'data']);
+        ValidatorModel::notEmpty($args, ['set', 'where', 'data']);
+        ValidatorModel::arrayType($args, ['set', 'where', 'data']);
 
         DatabaseModel::update([
             'table' => 'docservers',
-            'set'   => $aArgs['set'],
-            'where' => $aArgs['where'],
-            'data'  => $aArgs['data']
+            'set'   => $args['set'],
+            'where' => $args['where'],
+            'data'  => $args['data']
         ]);
 
         return true;
diff --git a/src/app/document/controllers/DocumentController.php b/src/app/document/controllers/DocumentController.php
index a4f05f673d49fe324163166874d7f0c7b03b0db8..61d48e6cc6a4a81bb8090c6d854cf7900a20d704 100755
--- a/src/app/document/controllers/DocumentController.php
+++ b/src/app/document/controllers/DocumentController.php
@@ -41,8 +41,8 @@ use Workflow\models\WorkflowModel;
 
 class DocumentController
 {
-    const ACTIONS   = [1 => 'VAL', 2 => 'REF'];
-    const MODES     = ['visa', 'sign', 'note'];
+    const ACTIONS = [1 => 'VAL', 2 => 'REF'];
+    const MODES   = ['visa', 'sign', 'note'];
 
     public function get(Request $request, Response $response)
     {
@@ -132,8 +132,7 @@ class DocumentController
 
     public function getById(Request $request, Response $response, array $args)
     {
-        $canManageDocuments = PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_documents']);
-        if (!$canManageDocuments && !DocumentController::hasRightById(['id' => $args['id'], 'userId' => $GLOBALS['id'], 'readOnly' => true])) {
+        if (!DocumentController::hasRightById(['id' => $args['id'], 'userId' => $GLOBALS['id'], 'readOnly' => true]) && !PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_documents'])) {
             return $response->withStatus(403)->withJson(['errors' => 'Document out of perimeter']);
         }
 
@@ -273,6 +272,116 @@ class DocumentController
         return $response->withJson(['document' => $formattedDocument]);
     }
 
+    public function delete(Request $request, Response $response, array $args)
+    {
+        if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'can_purge'])) {
+            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
+        }
+
+        $queryParams = $request->getQueryParams();
+
+        $lastStep = WorkflowModel::get([
+            'select'  => ['process_date'],
+            'where'   => ['main_document_id = ?'],
+            'data'    => [$args['id']],
+            'orderBy' => ['"order" desc'],
+            'limit'   => 1
+        ]);
+        if (!empty($lastStep[0]) && empty($lastStep[0]['process_date'])) {
+            return $response->withStatus(400)->withJson(['errors' => 'Document workflow is still ongoing']);
+        }
+        $document = DocumentModel::getById([
+            'select' => ['title'],
+            'id'     => $args['id']
+        ]);
+        if (empty($document['title'])) {
+            return $response->withStatus(500)->withJson(['errors' => 'No document associated with this workflow']);
+        }
+
+        $queryParams['physicalPurge'] = filter_var($queryParams['physicalPurge'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
+        if (!Validator::boolType()->validate($queryParams['physicalPurge'])) {
+            return $response->withStatus(400)->withJson(['errors' => 'physicalPurge is not a boolean']);
+        }
+
+        $historyInfo = '';
+        if($queryParams['physicalPurge']) {
+            $docServers = DocserverModel::get(['select' => ['type', 'path']]);
+            if (empty($docServers)) {
+                return $response->withStatus(500)->withJson(['errors' => 'No available Docserver']);
+            }
+
+            $filePaths = AdrModel::getDocumentsAdr([
+                'select'=> ['CONCAT(path, filename) as path', 'type'],
+                'where' => ['main_document_id = ?'],
+                'data'  => [$args['id']]
+            ]);
+            if (empty($filePaths)) {
+                return $response->withStatus(500)->withJson(['errors' => 'Document does not exist']);
+            }
+
+            $attachments = AttachmentModel::get([
+                'select'=> ['id'],
+                'where' => ['main_document_id = ?'],
+                'data'  => [$args['id']]
+            ]);
+            if (!empty($attachments)) {
+                $attachmentFilePaths = AdrModel::getAttachmentsAdr([
+                    'select'=> ['CONCAT(path, filename) as path', 'type'],
+                    'where' => ['attachment_id IN (?)'],
+                    'data'  => [array_column($attachments, 'id')]
+                ]);
+                $filePaths = array_merge($filePaths, $attachmentFilePaths);
+            }
+
+            $docServers = array_column($docServers, 'path', 'type');
+            foreach ($filePaths as $filePath) {
+                $docFilePath = $docServers[$filePath['type']] . $filePath['path'];
+
+                if (file_exists("$docFilePath")) {
+                    AdrModel::deleteDocumentAdr([
+                        'where' => ['id = ?'],
+                        'data'  => [$filePath['id']]
+                    ]);
+                    unlink($docFilePath);
+                }
+            }
+
+            if (!empty($attachments)) {
+                AdrModel::deleteAttachmentAdr([
+                    'where' => ['attachment_id IN (?)'],
+                    'data'  => [array_column($attachments, 'id')]
+                ]);
+            }
+            AdrModel::deleteDocumentAdr([
+                'where' => ['main_document_id = ?'],
+                'data'  => [$args['id']]
+            ]);
+            DocumentModel::update([
+                'set'   => ['status' => 'HARD_DEL'],
+                'where' => ['id = ?'],
+                'data'  => [$args['id']]
+            ]);
+            $historyInfo = "{documentDeleted} : {$document['title']}";
+        } else {
+            DocumentModel::update([
+                'set'   => ['status' => 'SOFT_DEL'],
+                'where' => ['id = ?'],
+                'data'  => [$args['id']]
+            ]);
+            $historyInfo = "{softDeleted} : {$document['title']}";
+        }
+
+        HistoryController::add([
+            'code'          => 'OK',
+            'objectType'    => 'main_documents',
+            'objectId'      => $args['id'],
+            'type'          => 'SUPPRESSION',
+            'message'       => $historyInfo
+        ]);
+
+        return $response->withStatus(204);
+    }
+
     public function getContent(Request $request, Response $response, array $args)
     {
         if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_documents'])
@@ -1193,6 +1302,7 @@ class DocumentController
         $args['userId']   = (int)$args['userId'];
 
         $document = DocumentModel::getById(['select' => ['typist'], 'id' => $args['id']]);
+
         if (!empty($document['typist']) && $document['typist'] == $GLOBALS['id']) {
             return true;
         }
diff --git a/src/app/document/models/DocumentModel.php b/src/app/document/models/DocumentModel.php
index 3f80904960c7fd561dd8c98f85167be0fdd91ed2..ddd371dfbf851d51c2a8e9dcb40aeb36f7c3301c 100755
--- a/src/app/document/models/DocumentModel.php
+++ b/src/app/document/models/DocumentModel.php
@@ -19,20 +19,23 @@ use SrcCore\models\DatabaseModel;
 
 class DocumentModel
 {
-    public static function get(array $aArgs)
+    public static function get(array $args)
     {
-        ValidatorModel::notEmpty($aArgs, ['select']);
-        ValidatorModel::arrayType($aArgs, ['select', 'where', 'data', 'orderBy']);
-        ValidatorModel::intType($aArgs, ['limit', 'offset']);
+        ValidatorModel::notEmpty($args, ['select']);
+        ValidatorModel::arrayType($args, ['select', 'where', 'data', 'orderBy']);
+        ValidatorModel::intType($args, ['limit', 'offset']);
+
+        $args['where'] = empty($args['where']) ? [] : $args['where'];
+        $args['data']  = empty($args['data']) ? [] : $args['data'];
 
         $aDocuments = DatabaseModel::select([
-            'select'    => $aArgs['select'],
+            'select'    => $args['select'],
             'table'     => ['main_documents'],
-            'where'     => empty($aArgs['where']) ? [] : $aArgs['where'],
-            'data'      => empty($aArgs['data']) ? [] : $aArgs['data'],
-            'orderBy'   => empty($aArgs['orderBy']) ? [] : $aArgs['orderBy'],
-            'offset'    => empty($aArgs['offset']) ? 0 : $aArgs['offset'],
-            'limit'     => empty($aArgs['limit']) ? 0 : $aArgs['limit'],
+            'where'     => $args['where'],
+            'data'      => $args['data'],
+            'orderBy'   => empty($args['orderBy']) ? [] : $args['orderBy'],
+            'offset'    => empty($args['offset']) ? 0 : $args['offset'],
+            'limit'     => empty($args['limit']) ? 0 : $args['limit'],
         ]);
 
         return $aDocuments;
@@ -43,11 +46,14 @@ class DocumentModel
         ValidatorModel::notEmpty($args, ['select', 'id']);
         ValidatorModel::arrayType($args, ['select']);
 
+        $where = ['id = ?'];
+        $data  = [$args['id']];
+
         $document = DatabaseModel::select([
             'select' => $args['select'],
             'table'  => ['main_documents'],
-            'where'  => ['id = ?'],
-            'data'   => [$args['id']]
+            'where'  => $where,
+            'data'   => $data
         ]);
 
         if (empty($document[0])) {
diff --git a/src/app/group/controllers/PrivilegeController.php b/src/app/group/controllers/PrivilegeController.php
index bc6f0bc9c2963e6c6bdd7e55b07fcfd37b9d7d62..ca2773ec5c5df691396cd51808f0955690ca9419 100755
--- a/src/app/group/controllers/PrivilegeController.php
+++ b/src/app/group/controllers/PrivilegeController.php
@@ -32,7 +32,8 @@ class PrivilegeController
         ['id' => 'manage_customization',        'type' => 'admin', 'icon' => 'color-wand-outline',  'route' => '/administration/customization'],
         ['id' => 'manage_notifications',        'type' => 'admin', 'icon' => 'notifications', 'route' => '/administration/notifications'],
         ['id' => 'manage_documents',            'type' => 'simple'],
-        ['id' => 'indexation',                  'type' => 'simple']
+        ['id' => 'indexation',                  'type' => 'simple'],
+        ['id' => 'can_purge',                   'type' => 'simple']
     ];
 
     public static function getPrivilegesByUserId(array $args)
diff --git a/src/app/history/controllers/HistoryController.php b/src/app/history/controllers/HistoryController.php
index d18d15f0f24656b46cb5ede255639a0b9a0c027d..2d0270b5472506a3745dad4f000935ab805e9804 100644
--- a/src/app/history/controllers/HistoryController.php
+++ b/src/app/history/controllers/HistoryController.php
@@ -584,7 +584,9 @@ class HistoryController
 
         if ($zip->open($zipFilename, \ZipArchive::CREATE) === true) {
             foreach ($aArgs['documents'] as $document) {
-                $zip->addFile($document['path'], $document['filename']);
+                if(file_exists($document['path']) && filesize($document['path']) > 0) {
+                    $zip->addFile($document['path'], $document['filename']);
+                }
             }
 
             $zip->close();
@@ -673,6 +675,9 @@ class HistoryController
 
     public function getByUserId(Request $request, Response $response, array $args)
     {
+        if (!Validator::intVal()->notEmpty()->validate($args['id'])) {
+            return $response->withStatus(400)->withJson(['errors' => 'Route id is not an integer']);
+        }
         if ($GLOBALS['id'] != $args['id'] && !PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
             return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
         }
@@ -680,10 +685,6 @@ class HistoryController
             return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
         }
 
-        if (!Validator::intVal()->notEmpty()->validate($args['id'])) {
-            return $response->withStatus(400)->withJson(['errors' => 'Route id is not an integer']);
-        }
-
         $user = UserModel::getById(['select' => [1], 'id' => $args['id']]);
         if (empty($user)) {
             return $response->withStatus(400)->withJson(['errors' => 'User does not exist']);
diff --git a/src/app/search/controllers/SearchController.php b/src/app/search/controllers/SearchController.php
index 3df92ff62f0085c53315bbe8a155a0f83f96b5f5..f2110ca5a50ad5928a9b7b546074c2e1673dbb2b 100755
--- a/src/app/search/controllers/SearchController.php
+++ b/src/app/search/controllers/SearchController.php
@@ -33,12 +33,16 @@ class SearchController
 
         $queryParams['offset'] = empty($queryParams['offset']) ? 0 : (int)$queryParams['offset'];
         $queryParams['limit']  = empty($queryParams['limit']) ? 0 : (int)$queryParams['limit'];
+        $queryParams['softDeleted'] = ($queryParams['softDeleted'] ?? 'true') == 'true';
+        $queryParams['hardDeleted'] = ($queryParams['hardDeleted'] ?? 'true') == 'true';
 
         $where = [];
         $data  = [];
         $hasFullRights = PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_documents']);
+        $baseRestriction = '';
+        $baseData = [];
         if (!$hasFullRights) {
-            $where = ['id IN (
+            $baseRestriction = 'id IN (
                 SELECT DISTINCT ws1.main_document_id
                 FROM workflows ws1
                 WHERE typist = ?
@@ -51,8 +55,8 @@ class SearchController
                         SELECT min(ws2."order") FROM workflows ws2 WHERE ws2.process_date IS NULL AND ws2.main_document_id = ws1.main_document_id
                     )
                 )
-            )'];
-            $data = [$GLOBALS['id'], $GLOBALS['id'], $GLOBALS['id'], $GLOBALS['id']];
+            )';
+            $baseData = [$GLOBALS['id'], $GLOBALS['id'], $GLOBALS['id'], $GLOBALS['id']];
         }
 
         $whereWorkflow = [];
@@ -116,14 +120,35 @@ class SearchController
             $where[] = 'id = ?';
             $data[]  = $body['documentId'];
         }
+        
+        if (Validator::arrayType()->each(Validator::in(['softDeleted', 'hardDeleted']))->notEmpty()->validate($body['documentState'] ?? null)) {
+            $docStatesForQuery = [];
+            if (in_array('softDeleted', $body['documentState'])) {
+                $docStatesForQuery[] = 'SOFT_DEL';
+            }
+            if (in_array('hardDeleted', $body['documentState'])) {
+                $docStatesForQuery[] = 'HARD_DEL';
+            }
+            if (!empty($docStatesForQuery)) {
+                $where[] = 'status IN (?)';
+                $data[]  = $docStatesForQuery;
+            }
+        }
+
+        $where = !empty($where) ? [implode(' OR ', $where)] : [];
+        if (!empty($baseRestriction)) {
+            array_unshift($where, $baseRestriction);
+            $data  = array_merge($baseData, $data);
+        }
 
+        $select = ['id', 'title', 'reference', 'typist', 'status', 'count(1) OVER()'];
         $documents = DocumentModel::get([
-            'select'  => ['id', 'title', 'reference', 'typist', 'count(1) OVER()'],
-            'where'   => $where,
-            'data'    => $data,
-            'limit'   => $queryParams['limit'],
-            'offset'  => $queryParams['offset'],
-            'orderBy' => ['creation_date desc']
+            'select'      => $select,
+            'where'       => $where,
+            'data'        => $data,
+            'limit'       => $queryParams['limit'],
+            'offset'      => $queryParams['offset'],
+            'orderBy'     => ['creation_date desc'],
         ]);
 
         $count = empty($documents[0]['count']) ? 0 : $documents[0]['count'];
diff --git a/src/app/user/controllers/UserController.php b/src/app/user/controllers/UserController.php
index ed9398ada46fd0c28a6943bd3b2d84a1aa0aa170..468e4753442a5eab6f20086ce3e1c21fa2807d1e 100755
--- a/src/app/user/controllers/UserController.php
+++ b/src/app/user/controllers/UserController.php
@@ -228,6 +228,10 @@ class UserController
 
     public function update(Request $request, Response $response, array $args)
     {
+        if (!Validator::intVal()->notEmpty()->validate($args['id'])) {
+            return $response->withStatus(400)->withJson(['errors' => 'Route id is not an integer']);
+        }
+        
         $connection = ConfigurationModel::getConnection();
         if (($GLOBALS['id'] != $args['id'] || $connection != 'default') && !UserController::hasRightByUserId(['activeUserId' => $GLOBALS['id'], 'targetUserId' => $args['id']])) {
             return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
@@ -245,7 +249,7 @@ class UserController
             return $response->withStatus(400)->withJson(['errors' => 'Body email is empty or not a valid email']);
         } elseif (!empty($body['x509Fingerprint']) && !Validator::stringType()->validate($body['x509Fingerprint'])) {
             return $response->withStatus(400)->withJson(['errors' => 'Body x509Fingerprint is not a string']);
-        } elseif (!Validator::arrayType()->each(Validator::intType())->validate($body['groups'])) {
+        } elseif (!empty($body['groups']) && !Validator::arrayType()->each(Validator::intType())->validate($body['groups'])) {
             return $response->withStatus(400)->withJson(['errors' => 'Body groups is not an array of integers']);
         }
 
@@ -404,14 +408,14 @@ class UserController
 
     public function delete(Request $request, Response $response, array $args)
     {
-        if ($GLOBALS['id'] == $args['id'] || !UserController::hasRightByUserId(['activeUserId' => $GLOBALS['id'], 'targetUserId' => $args['id']])) {
-            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
-        }
-
         if (!Validator::intVal()->notEmpty()->validate($args['id'])) {
             return $response->withStatus(400)->withJson(['errors' => 'Route id is not an integer']);
         }
 
+        if ($GLOBALS['id'] == $args['id'] || !UserController::hasRightByUserId(['activeUserId' => $GLOBALS['id'], 'targetUserId' => $args['id']])) {
+            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
+        }
+
         $user = UserModel::getById(['id' => $args['id'], 'select' => ['firstname', 'lastname']]);
         if (empty($user)) {
             return $response->withStatus(400)->withJson(['errors' => 'User does not exist']);
diff --git a/src/frontend/app/administration/group/group.component.scss b/src/frontend/app/administration/group/group.component.scss
index 44e48bc4dbd000d45168db2e46adade48de0b6df..9f5018a2f73760dd870104cd857c2a0979ceff7a 100644
--- a/src/frontend/app/administration/group/group.component.scss
+++ b/src/frontend/app/administration/group/group.component.scss
@@ -28,4 +28,8 @@ legend {
 
 .grid-3-col {
   grid-template-columns: repeat(3, 1fr);
+}
+
+::ng-deep.sc-ion-label-md-h.sc-ion-label-md-s.md {
+  white-space: pre-line;
 }
\ No newline at end of file
diff --git a/src/frontend/app/search/search.component.html b/src/frontend/app/search/search.component.html
index a4376e78d152658c44b4bbf70575508facbe8e22..662a300c84cc52c957253b04e39d4d72b51fc572 100644
--- a/src/frontend/app/search/search.component.html
+++ b/src/frontend/app/search/search.component.html
@@ -47,6 +47,15 @@
                             <ion-icon name="close-circle"></ion-icon>
                         </ion-chip>
                     </div>
+                    <div *ngIf="currentFilter.id === 'documentState' && currentFilter.val.length > 0">
+                        <ion-chip outline color="primary" *ngFor="let item of currentFilter.val"
+                            style="background: white;" [title]="'lang.documentStateSearch' | translate"
+                            (click)="removeFilter(currentFilter, item)">
+                            <ion-icon name="document-outline"></ion-icon>
+                            <ion-label>{{ 'lang.' + item | translate }}</ion-label>
+                            <ion-icon name="close-circle"></ion-icon>
+                        </ion-chip>
+                    </div>
                 </div>
             </div>
         </div>
@@ -102,10 +111,17 @@
                             <ion-icon *ngIf="element.state == 'REF'" color="danger" slot="start"
                                 name="thumbs-down-outline">
                             </ion-icon>
-                            <ion-label (click)="goTo(element.id)" [title]="'lang.accessDocument' | translate"
-                                style="cursor: pointer;">
-                                <p>{{element.reference}}</p>
-                                <h2>{{element.title}}</h2>
+                            <ion-label (click)="goTo(element)" [title]="(element.status !== 'HARD_DEL' ? 'lang.accessDocument': 'lang.cannotAcces') | translate"
+                                [ngStyle]="{'cursor': element.status !== 'HARD_DEL' ? 'pointer' : 'not-allowed'}">
+                                <p [class.unavailableDoc]="element.state === 'DEL'">
+                                    {{element.reference}}
+                                </p>
+                                <h2 [class.unavailableDoc]="element.state === 'DEL'" [ngStyle]="{'margin-top': ['HARD_DEL', 'SOFT_DEL'].indexOf(element.status) > -1 ? '-10px' : '', 'margin-bottom': ['HARD_DEL', 'SOFT_DEL'].indexOf(element.status) > -1 ? '-11px' : '-20px'}">{{element.title}} &nbsp;
+                                    <button  mat-icon-button class="deleted" [title]="(element.status !== 'HARD_DEL' ? 'lang.softDeleted': 'lang.hardDeleted') | translate">
+                                        <ion-icon *ngIf="element?.status === 'SOFT_DEL'" name="remove-circle-outline"></ion-icon>
+                                        <ion-icon *ngIf="element?.status === 'HARD_DEL'" name="close-circle-outline"></ion-icon>
+                                    </button>
+                                </h2>
                                 <p *ngIf="element.reason.length > 0" class="primary">
                                     <ng-container *ngFor="let note of element.reason">
                                         <ion-icon name="chatbox-outline"></ion-icon> {{note}}
@@ -129,7 +145,7 @@
                                 <ion-icon slot="bottom" name="ribbon-sharp"></ion-icon>
                                 {{'lang.download' | translate}}
                             </ion-item-option>
-                            <ion-item-option color="primary" (click)="openActions(element)">
+                            <ion-item-option *ngIf="element.status !== 'HARD_DEL'" color="primary" (click)="openActions(element)">
                                 <ion-icon slot="bottom" name="settings-sharp"></ion-icon>
                                 {{'lang.actions' | translate}}
                             </ion-item-option>
diff --git a/src/frontend/app/search/search.component.scss b/src/frontend/app/search/search.component.scss
index ddbfc736ea5e0557685e1726ab83a895ddcbd4d9..d885334bbaffe3a49a669be8f3e72331adbc26f0 100644
--- a/src/frontend/app/search/search.component.scss
+++ b/src/frontend/app/search/search.component.scss
@@ -74,4 +74,16 @@
     font-size: 12px;
     opacity: 0.7;
     font-weight: 500;
+}
+
+.unavailableDoc {
+    text-decoration: line-through 1px var(--ion-color-danger);
+}
+
+.deleted {
+    color: var(--ion-color-danger);
+    font-size: 2em;
+    margin-bottom: 15px;
+    opacity: 0.17;
+    cursor: help;
 }
\ No newline at end of file
diff --git a/src/frontend/app/search/search.component.ts b/src/frontend/app/search/search.component.ts
index 01369e214b8037d76eb13eab2324ebbb54cc55cf..d24afabc7520222cd6c49448b33f6b1f5b707478 100644
--- a/src/frontend/app/search/search.component.ts
+++ b/src/frontend/app/search/search.component.ts
@@ -5,7 +5,7 @@ import { ActivatedRoute, ParamMap, Router } from '@angular/router';
 import { ActionSheetController, AlertController, IonInfiniteScroll, LoadingController, MenuController } from '@ionic/angular';
 import { TranslateService } from '@ngx-translate/core';
 import { of } from 'rxjs';
-import { catchError, exhaustMap, tap } from 'rxjs/operators';
+import { catchError, tap } from 'rxjs/operators';
 import { VisaWorkflowComponent } from '../document/visa-workflow/visa-workflow.component';
 import { AuthService } from '../service/auth.service';
 import { FunctionsService } from '../service/functions.service';
@@ -47,6 +47,23 @@ export class SearchComponent implements OnInit {
             val: '',
             values: []
         },
+        {
+            id: 'documentState',
+            type: 'checkbox',
+            val: [],
+            values: [
+                {
+                    id: 'softDeleted',
+                    label: 'lang.softDeleted',
+                    selected: false
+                },
+                {
+                    id: 'hardDeleted',
+                    label: 'lang.hardDeleted',
+                    selected: false
+                }
+            ]
+        },
         {
             id: 'workflowStates',
             type: 'checkbox',
@@ -71,7 +88,7 @@ export class SearchComponent implements OnInit {
                     id: 'REF',
                     label: 'lang.refused',
                     selected: false
-                }
+                },
             ]
         },
         {
@@ -91,6 +108,10 @@ export class SearchComponent implements OnInit {
             icon: 'document-outline',
             id: 'newWorkflow'
         },
+        {
+            icon: 'trash-outline',
+            id: 'purgeDocument'
+        }
     ];
 
     ressources: any[] = [];
@@ -101,6 +122,10 @@ export class SearchComponent implements OnInit {
     openedLine = '';
     reActiveInfinite: any;
     documentId: any;
+    canPurge: boolean = false;
+    physicalPurge: boolean = true;
+    hasSoftDeletedDoc: boolean = false;
+    hasHardDeletedDoc: boolean = false;
 
     constructor(
         public http: HttpClient,
@@ -120,6 +145,7 @@ export class SearchComponent implements OnInit {
     ) { }
 
     ngOnInit(): void {
+        this.canPurge = this.authService.user.appPrivileges.find((privilege: any) => privilege.id === 'can_purge') !== undefined;
         this._activatedRoute.queryParamMap.subscribe((paramMap: ParamMap) => {
             if (!this.functionsService.empty(paramMap.get('documentId'))) {
                 this.documentId = paramMap.get('documentId');
@@ -174,6 +200,7 @@ export class SearchComponent implements OnInit {
             filter.val.splice(index, 1);
         } else {
             filter.val.push(item.id);
+            filter.val = [... new Set(filter.val)];
         }
     }
 
@@ -184,7 +211,7 @@ export class SearchComponent implements OnInit {
         tmpArr.forEach((filter: any) => {
             if (filter.id === 'workflowUsers') {
                 objToSend[filter.id] = filter.val.map((item: any) => item.id);
-            } else if (filter.id === 'workflowStates') {
+            } else if (filter.id === 'workflowStates' || filter.id === 'documentState') {
                 objToSend[filter.id] = filter.values.filter((item: any) => item.selected).map((item: any) => item.id);
             } else {
                 objToSend[filter.id] = filter.val;
@@ -237,7 +264,9 @@ export class SearchComponent implements OnInit {
     }
 
     canShowButton(id: string, item: any) {
-        if (id === 'interruptWorkflow' && item.canInterrupt) {
+        if (id === 'purgeDocument' && this.canPurge && ['STOP', 'VAL', 'REF'].indexOf(item.state) > -1 && item.status !== 'HARD_DEL') {
+            return true;
+        } else if (id === 'interruptWorkflow' && item.canInterrupt) {
             return true;
         } else if (id === 'newWorkflow' && item.canReaffect) {
             return true;
@@ -263,10 +292,13 @@ export class SearchComponent implements OnInit {
         this.refreshCurrentFilter();
 
         return new Promise((resolve) => {
-            this.http.post('../rest/search/documents?limit=10&offset=0', this.formatDatas())
+            const uri: string = '../rest/search/documents?limit=10&offset=0';
+            this.http.post(`${uri}`, this.formatDatas())
                 .pipe(
                     tap((data: any) => {
                         this.ressources = this.formatListDatas(data.documents);
+                        this.hasSoftDeletedDoc = this.ressources.filter((res: any) => res.status === 'SOFT_DEL').length > 0;
+                        this.hasHardDeletedDoc = this.ressources.filter((res: any) => res.status === 'HARD_DEL').length > 0;
                         this.count = data.count;
                         this.infiniteScroll.disabled = false;
                         resolve(true);
@@ -282,7 +314,6 @@ export class SearchComponent implements OnInit {
 
     refreshCurrentFilter() {
         this.currentFilters = JSON.parse(JSON.stringify(this.filters.filter((item: any) => !this.functionsService.empty(item.val))));
-
         if (this.currentFilters.filter((item: any) => item.id === 'workflowStates').length > 0) {
             this.currentFilters.filter((item: any) => item.id === 'workflowStates')[0].val = this.currentFilters.filter((item: any) => item.id === 'workflowStates')[0].values.filter((item: any) => item.selected);
             if (this.currentFilters.filter((item: any) => item.id === 'workflowStates')[0].val.length === 0) {
@@ -297,8 +328,8 @@ export class SearchComponent implements OnInit {
             event.target.disabled = true;
         } else {
             this.offset = this.offset + this.limit;
-
-            this.http.post('../rest/search/documents?limit=' + this.limit + '&offset=' + this.offset, this.formatDatas()).pipe(
+            const uri: string = `../rest/search/documents?limit=${this.limit}&offset=${this.offset}`;
+            this.http.post(uri, this.formatDatas()).pipe(
                 tap((data: any) => {
                     this.ressources = this.ressources.concat(this.formatListDatas(data.documents));
                     event.target.complete();
@@ -368,10 +399,19 @@ export class SearchComponent implements OnInit {
     }
 
     async openPromptProof(item: any) {
+        const proof: any[] = [
+            {
+                name: 'option1',
+                type: 'radio',
+                label: this.translate.instant('lang.proof'),
+                value: 'onlyProof',
+                checked: true
+            },
+        ];
         const alert = await this.alertController.create({
             cssClass: 'promptProof',
             header: this.translate.instant('lang.download'),
-            inputs: [
+            inputs: item.status !== 'HARD_DEL' ? [
                 {
                     name: 'option1',
                     type: 'radio',
@@ -386,7 +426,7 @@ export class SearchComponent implements OnInit {
                     value: 'all',
                 },
 
-            ],
+            ] : proof,
             buttons: [
                 {
                     text: this.translate.instant('lang.cancel'),
@@ -395,7 +435,7 @@ export class SearchComponent implements OnInit {
                     handler: () => { }
                 },
                 {
-                    text: this.translate.instant('lang.validate'),
+                    text: this.translate.instant('lang.download'),
                     handler: async (mode) => {
                         await this.downloadProof(item, mode);
                         alert.dismiss();
@@ -440,8 +480,10 @@ export class SearchComponent implements OnInit {
         return currentUserWorkflow.length > 0 ? currentUserWorkflow[0].userId : null;
     }
 
-    goTo(resId: number) {
-        this.router.navigate([`/documents/${resId}`]);
+    goTo(element: any) {
+        if (element.status !== 'HARD_DEL') {
+            this.router.navigate([`/documents/${element.id}`]);
+        }
     }
 
     clearFilters() {
@@ -470,6 +512,7 @@ export class SearchComponent implements OnInit {
             if (filter.id === 'workflowStates') {
                 this.filters.find((element: any) => element.id === filter.id).values.filter((element: any) => element.id === item)[0].selected = false;
             } else {
+                this.filters.find((element: any) => element.id === filter.id).values.filter((element: any) => element.id === item)[0].selected = false;
                 const index = filter.val.indexOf(item);
                 this.filters.filter((element: any) => element.id === filter.id)[0].val.splice(index, 1);
             }
@@ -484,10 +527,64 @@ export class SearchComponent implements OnInit {
 
     checkInput() {
         if ((this.filters.find((el: any) => el.id === 'title').val === '') && (this.filters.find((el: any) => el.id === 'reference').val === '') && (this.filters.find((el: any) => el.id === 'documentId').val === '')) {
-            if ((this.filters.find((el: any) => el.id === 'workflowStates').val.length === 0) && (this.filters.find((el: any) => el.id === 'workflowUsers').val.length === 0)) {
+            if ((this.filters.find((el: any) => el.id === 'workflowStates').val.length === 0) && (this.filters.find((el: any) => el.id === 'workflowUsers').val.length === 0) && (this.filters.find((el: any) => el.id === 'documentState').val.length === 0)) {
                 this.clearFilters();
                 this.currentFilters = [];
             }
         }
     }
+
+    async purgeDocument(item: any) {
+        const alert = await this.alertController.create({
+            header: this.translate.instant('lang.confirmMsg'),
+            // subHeader: this.translate.instant('lang.downloadProofOnPurge'),
+            backdropDismiss: false,
+            inputs: [
+                {
+                    name: 'physicalPurge',
+                    id: 'physicalPurge',
+                    type: 'checkbox',
+                    label: this.translate.instant('lang.hardDelete'),
+                    value: this.physicalPurge
+                },
+            ],
+            buttons: [
+                {
+                    text: this.translate.instant('lang.cancel'),
+                    role: 'cancel',
+                    cssClass: 'secondary',
+                    handler: () => { }
+                },
+                {
+                    text: this.translate.instant('lang.delete'),
+                    handler: async (res: any) => {
+                        this.physicalPurge = !this.functionsService.empty(res) ? true : false;
+                        await this.downloadProof(item, '');
+                        await this.http.delete(`../rest/documents/${item.id}?physicalPurge=${this.physicalPurge}`).pipe(
+                            tap(async (data: any) => {
+                                this.loadingController.create({
+                                    message: this.translate.instant('lang.processing'),
+                                    spinner: 'dots'
+                                }).then((load: HTMLIonLoadingElement) => {
+                                    load.present();
+                                    this.search();
+                                    load.dismiss();
+                                });
+                            }),
+                            catchError((err: any) => {
+                                this.notificationService.handleErrors(err);
+                                return of(false);
+                            })
+                        ).subscribe();
+                    }
+                }
+            ]
+        });
+        await alert.present();
+    }
+
+    isDeletedDocument(): boolean {
+        const documentState: any = this.currentFilters.find((filter: any) => filter.id === 'documentState');
+        return documentState?.val.indexOf('softDeleted') > -1 || documentState?.val.indexOf('hardDeleted') > -1;
+    }
 }
diff --git a/test/unitTests/app/user/UserControllerTest.php b/test/unitTests/app/user/UserControllerTest.php
index 6bd12b4a1bc05f96d3f7cab7e3df7a7f7ed8dde3..f33d62020acd89431bd69661744b2d38a4303888 100755
--- a/test/unitTests/app/user/UserControllerTest.php
+++ b/test/unitTests/app/user/UserControllerTest.php
@@ -27,7 +27,8 @@ class UserControllerTest extends TestCase
             'firstname'         => 'Prénom',
             'lastname'          => 'Nom',
             'email'             => 'email@test.fr',
-            'phone'             => '0701020304'
+            'phone'             => '0701020304',
+            'groups'            => [['id' => 1]]
         ];
 
         $fullRequest = \httpRequestCustom::addContentInBody($aArgs, $request);