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}} + <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);