diff --git a/lang/en.json b/lang/en.json index e38adf8cbf6e07a3b9097eb3f854c4da2ffaa6e4..3a63143bee4cf44b4954b0d8b292d816deb40534 100755 --- a/lang/en.json +++ b/lang/en.json @@ -13,7 +13,7 @@ "annotationAdded": "Annotation added", "annotationMode": "Annotation mode", "appleStylus": "Apple stylus", - "areYouSure": "Are you sure?", + "areYouSure": "Do you want to perform this action ?", "atRange": "at", "attachmentAdded": "Attachment added", "attachmentViewed": "Attachment viewed", @@ -651,6 +651,7 @@ "groupsToManage": "Choose the authorized assignment groups", "unlinkGroup": "Unlink group", "emptyGroups": "No groups available to associate", + "errorConvertingDocument": "Error converting document", "emptyGroupUsers": "No users associated with this group", "emptyUsers": "No users available to associate", "can_purgeAdmin": "Logically and physically remove interrupted or terminated signature processes", diff --git a/lang/fr.json b/lang/fr.json index 29f85953e3b4068573c38cd672e4b3c1d51cbaea..676c837c78eb23a44cd5a9d4033b28700991ce93 100755 --- a/lang/fr.json +++ b/lang/fr.json @@ -13,7 +13,7 @@ "annotationAdded" : "Annotation ajoutée", "annotationMode" : "Mode de l'annotation", "appleStylus" : "Stylet Apple", - "areYouSure" : "Êtes-vous sûr ?", + "areYouSure" : "Voulez-vous effectuer cette action ?", "atRange" : "à ", "attachmentAdded" : "Pièce jointe ajoutée", "attachmentViewed" : "Pièce jointe consulté", @@ -650,6 +650,7 @@ "groupsToManage": "Choisir les groupes d'affectations autorisés", "unlinkGroup": "Dissocier le groupe", "emptyGroups": "Aucun groupe disponible à associer", + "errorConvertingDocument": "Erreur lors de la conversion du document", "emptyGroupUsers": "Aucun utilisateur associé à ce groupe", "emptyUsers": "Aucun utilisateur disponible à associer", "can_purgeAdmin": "Supprimer logiquement les processus de signature interrompus ou terminés", diff --git a/src/app/document/controllers/DocumentController.php b/src/app/document/controllers/DocumentController.php index caaefab86d466e09085c95dfce038dc31e4009da..bf7a12956b938a04b5ba50b0a864071d8b80ef55 100755 --- a/src/app/document/controllers/DocumentController.php +++ b/src/app/document/controllers/DocumentController.php @@ -132,7 +132,7 @@ class DocumentController public function getById(Request $request, Response $response, array $args) { - if (!DocumentController::hasRightById(['id' => $args['id'], 'userId' => $GLOBALS['id'], 'withDeleted' => true]) && !PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_documents'])) { + if (!DocumentController::hasRightById(['id' => $args['id'], 'userId' => $GLOBALS['id'], 'withDeleted' => true, 'readOnly' => true]) && !PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_documents'])) { return $response->withStatus(403)->withJson(['errors' => 'Document out of perimeter']); } @@ -230,9 +230,15 @@ class DocumentController } } - if (!empty($currentId) && $currentId != $GLOBALS['id']) { - $substitute = UserModel::getById(['id' => $currentId, 'select' => ['substitute']]); - $formattedDocument['readOnly'] = $GLOBALS['id'] != $substitute['substitute'] && PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_documents']); + $formattedDocument['readOnly'] = !$canManageDocuments; + if ($formattedDocument['readOnly'] && !empty($currentId)) { + if ($currentId == $GLOBALS['id']) { + $formattedDocument['readOnly'] = false; + } else { + $substitute = UserModel::getById(['id' => $currentId, 'select' => ['substitute']]); + $substitute = $substitute['substitute'] ?? null; + $formattedDocument['readOnly'] = $GLOBALS['id'] != $substitute; + } } $formattedDocument['attachments'] = []; @@ -387,7 +393,7 @@ class DocumentController { $canPurge = PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'can_purge']); if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_documents']) - && !DocumentController::hasRightById(['id' => $args['id'], 'userId' => $GLOBALS['id'], 'withDeleted' => $canPurge])) { + && !DocumentController::hasRightById(['id' => $args['id'], 'userId' => $GLOBALS['id'], 'readOnly' => true, 'withDeleted' => $canPurge])) { return $response->withStatus(403)->withJson(['errors' => 'Document out of perimeter']); } @@ -1161,7 +1167,8 @@ class DocumentController public function getThumbnailContent(Request $request, Response $response, array $args) { - if (!DocumentController::hasRightById(['id' => $args['id'], 'userId' => $GLOBALS['id']]) && !PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_documents'])) { + if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_documents']) + && !DocumentController::hasRightById(['id' => $args['id'], 'userId' => $GLOBALS['id'], 'readOnly' => true])) { return $response->withStatus(403)->withJson(['errors' => 'Document out of perimeter']); } @@ -1296,28 +1303,51 @@ class DocumentController { ValidatorModel::notEmpty($args, ['id', 'userId']); ValidatorModel::intVal($args, ['id', 'userId']); + ValidatorModel::boolType($args, ['readOnly']); ValidatorModel::boolType($args, ['withDeleted']); + $args['readOnly'] = $args['readOnly'] ?? false; + $args['id'] = (int)$args['id']; + $args['userId'] = (int)$args['userId']; $args['withDeleted'] = $args['withDeleted'] ?? false; $document = DocumentModel::getById(['select' => ['typist'], 'id' => $args['id'], 'withDeleted' => $args['withDeleted']]); + if (!empty($document['typist']) && $document['typist'] == $GLOBALS['id']) { return true; } - $workflow = WorkflowModel::getCurrentStep(['select' => ['user_id'], 'documentId' => $args['id']]); - if (empty($workflow) || empty($workflow['user_id'])) { - return false; - } - - if ($workflow['user_id'] != $args['userId']) { - $user = UserModel::getById(['id' => $workflow['user_id'], 'select' => ['substitute']]); - if ($user['substitute'] != $args['userId']) { + if (!$args['readOnly']) { + $currentStep = WorkflowModel::getCurrentStep(['select' => ['user_id'], 'documentId' => $args['id']]); + if (empty($currentStep) || empty($currentStep['user_id'])) { return false; } + + if ($currentStep['user_id'] == $args['userId']) { + return true; + } else { + $user = UserModel::getById(['id' => $args['userId'], 'select' => ['substitute']]); + return $currentStep['user_id'] == $user['substitute']; + } } - return true; + $canReadOnly = WorkflowModel::get([ + 'select' => [1], + 'where' => [ + 'main_document_id = ?', + '(process_date IS NOT NULL AND user_id = ?) + OR ( + user_id IN ( + SELECT (SELECT ?::int) UNION (SELECT id FROM users WHERE substitute = ?) + ) + AND "order" = ( + SELECT min(ws2."order") FROM workflows ws2 WHERE ws2.process_date IS NULL AND ws2.main_document_id = main_document_id + ) + )' + ], + 'data' => [$args['id'], $args['userId'], $args['userId'], $args['userId']] + ]); + return !empty($canReadOnly); } public static function getEncodedDocumentFromEncodedZip(array $args) diff --git a/src/app/history/controllers/HistoryController.php b/src/app/history/controllers/HistoryController.php index f5e5f2cfee6b4a727355150f9533f2da1faba443..80d1a3abfe77ae89d23cd6db55048537429dfabf 100644 --- a/src/app/history/controllers/HistoryController.php +++ b/src/app/history/controllers/HistoryController.php @@ -392,7 +392,8 @@ class HistoryController return $response->withStatus(400)->withJson(['errors' => 'Route id is not an integer']); } - if (!DocumentController::hasRightById(['id' => $args['id'], 'userId' => $GLOBALS['id']]) && !PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_documents'])) { + if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_documents']) + && !DocumentController::hasRightById(['id' => $args['id'], 'userId' => $GLOBALS['id'], 'readOnly' => true])) { return $response->withStatus(403)->withJson(['errors' => 'Document out of perimeter']); } diff --git a/src/app/search/controllers/SearchController.php b/src/app/search/controllers/SearchController.php index dbcb1c03b42701d81511bdd0164b35516999dd0a..9a6d4878e2c26b245aeaf863185a7eb870ab0eee 100755 --- a/src/app/search/controllers/SearchController.php +++ b/src/app/search/controllers/SearchController.php @@ -44,16 +44,21 @@ class SearchController $data = []; $hasFullRights = PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_documents']); if (!$hasFullRights) { - $substitutedUsers = UserModel::get(['select' => ['id'], 'where' => ['substitute = ?'], 'data' => [$GLOBALS['id']]]); - $users = [$GLOBALS['id']]; - foreach ($substitutedUsers as $value) { - $users[] = $value['id']; - } - - $workflowSelect = "SELECT id FROM workflows ws WHERE workflows.main_document_id = main_document_id AND process_date IS NULL AND status IS NULL ORDER BY \"order\" LIMIT 1"; - $workflowSelect = "SELECT main_document_id FROM workflows WHERE user_id in (?) AND id in ({$workflowSelect})"; - $where = ["(id in ({$workflowSelect}) OR typist = ?)"]; - $data = [$users, $GLOBALS['id']]; + $where = ['id IN ( + SELECT DISTINCT ws1.main_document_id + FROM workflows ws1 + WHERE typist = ? + OR (ws1.process_date IS NOT NULL AND ws1.user_id = ?) + OR ( + ws1.user_id IN ( + SELECT (SELECT ?::int) UNION (SELECT id FROM users WHERE substitute = ?) + ) + AND ws1."order" = ( + 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']]; } $whereWorkflow = []; diff --git a/src/frontend/app/document/document.component.html b/src/frontend/app/document/document.component.html index 674e1f790288af2578b8780d2f70ff4aa533c1da..8af9bd6c26e8cc07a626a547ebf7ad0bdb9a73d8 100755 --- a/src/frontend/app/document/document.component.html +++ b/src/frontend/app/document/document.component.html @@ -44,10 +44,20 @@ *ngIf="authService.user.substitute !== null && docList[currentDoc]"> <ion-label style="font-size: 13px;">{{'lang.substitutionInfo' | translate}}</ion-label> </ion-toolbar> -<ion-toolbar class="ion-text-center" color="danger" *ngIf="mainDocument.id !== 0 && mainDocument.status !== 'READY'"> - <ion-label style="font-size: 13px;">{{'lang.convertingDocument' | translate}}</ion-label> +<ion-toolbar class="ion-text-center" color="danger" *ngIf="mainDocument.id !== 0 && mainDocument.status === 'CONVERTING'"> + <div class="loading" style="display:flex;height:100%;"> + <ion-label class="loadingMsg">{{'lang.convertingDocument' | translate}}</ion-label> + <ion-spinner name="dots" color="light" style="padding-top: 6.5%;"></ion-spinner> + </div> +</ion-toolbar> +<ion-toolbar class="ion-text-center" color="danger" *ngIf="mainDocument.id !== 0 && mainDocument.status === 'ERROR'"> + <ion-label style="font-size: 14px; font-weight: bold;">{{'lang.errorConvertingDocument' | translate}}</ion-label> </ion-toolbar> <ion-content *ngIf="!loadingdocument" #mainContent> + <!-- <div *ngIf="mainDocument.status === 'READY'" class="loading" style="display:flex;height:100%;"> + <ion-spinner name="lines" color="primary" style="margin: auto;"></ion-spinner> + <ion-label class="loadingMsg">{{'lang.convertingDocument' | translate}}</ion-label> + </div> --> <ng-container *ngIf="(mainDocument.notes !== undefined && mainDocument.notes !== null) || hasWorkflowNotes"> <ion-fab-button *ngIf="!expandedNote" ngDraggable [bounds]="myBounds" [inBounds]="true" (movingOffset)="signaturesService.dragging=true" (endOffset)="signaturesService.dragging=false" @@ -134,7 +144,7 @@ </ion-content> <ion-footer *ngIf="!loadingImage && currentDoc === 0" class="ion-no-border footer-buttons" [ngStyle]="{'grid-template-columns': actionsList.length === 2 ? 'repeat(2, 1fr)' : 'repeat(3, 1fr)'}"> - <ion-button [disabled]="mainDocument.status === 'CONVERTING'" *ngFor="let action of actionsList" [color]="action.color" shape="round" size="large" fill="outline" (click)="launchEvent(action)" style="width: auto;"> + <ion-button [disabled]="isNotReady()" *ngFor="let action of actionsList" [color]="action.color" shape="round" size="large" fill="outline" (click)="launchEvent(action)" style="width: auto;"> <ion-icon *ngIf="action.logo !== ''" [slot]="'start'" [name]="action.logo"></ion-icon> <ion-label style="font-size: 13px;">{{getLabel(action)}}</ion-label> </ion-button> diff --git a/src/frontend/app/document/document.component.scss b/src/frontend/app/document/document.component.scss index 5719a22075e6f30b415b521a8942d1bbe251ce66..b1fbe6af75fa406123c136ec8de8051d820f3e60 100644 --- a/src/frontend/app/document/document.component.scss +++ b/src/frontend/app/document/document.component.scss @@ -398,7 +398,7 @@ button.disabled { .popover-content{ height: 50%; top: 50px; - } + } } ::ng-deep .custom-popover-class { @@ -407,3 +407,26 @@ button.disabled { top: 50px; } } + +::ng-deep.popover-viewport.sc-ion-popover-md { + overflow: auto; +} + +.loading { + display: flex; + position: absolute; + justify-content: center; + top: 0; + left: 0px; + width: 100%; + height: 100%; + z-index: 2; + overflow: hidden; +} + +.loadingMsg { + padding: 2%; + color: var(--ion-color-light); + font-weight: bold; + margin-right: -5px; +} \ No newline at end of file diff --git a/src/frontend/app/document/document.component.ts b/src/frontend/app/document/document.component.ts index ffbda0c365058190e2c5b137684a86ecd4f63c4b..99d0490b9dc56d5ddebadf88ee7f3833a3cea0de 100755 --- a/src/frontend/app/document/document.component.ts +++ b/src/frontend/app/document/document.component.ts @@ -18,7 +18,7 @@ import { LocalStorageService } from '../service/local-storage.service'; import { ActionSheetController, AlertController, LoadingController, MenuController, ModalController, NavController } from '@ionic/angular'; import { NgxExtendedPdfViewerService } from 'ngx-extended-pdf-viewer'; import { catchError, exhaustMap, tap } from 'rxjs/operators'; -import { of, Subscription } from 'rxjs'; +import { of, Subscription, timer } from 'rxjs'; import { SignatureMethodService } from '../service/signature-method/signature-method.service'; import { FunctionsService } from '../service/functions.service'; import { ActionsService } from '../service/actions.service'; @@ -74,6 +74,7 @@ export class DocumentComponent implements OnInit, OnDestroy { }; subscription: Subscription; + timerSubscription: Subscription; signaturesContent: any = []; docList: any = []; @@ -135,17 +136,15 @@ export class DocumentComponent implements OnInit, OnDestroy { }); } - ngOnDestroy(): void { - this.subscription.unsubscribe(); - } - imageLoaded(ev: any) { this.userMode = this.mainDocument.workflow.find((item: any) => item.current)?.mode; if (this.userMode === 'note') { this.actionsList = this.actionsList.filter((item: any) => item.event !== 'openSignatures'); } this.getImageDimensions(true); - this.load.dismiss(); + if (this.mainDocument.status !== 'CONVERTING') { + this.load.dismiss(); + } this.menu.enable(true, 'right-menu'); this.loadingImage = false; document.getElementsByClassName('drag-scroll-content')[0].scrollTop = 0; @@ -310,6 +309,11 @@ export class DocumentComponent implements OnInit, OnDestroy { }); } + ngOnDestroy(): void { + this.timerSubscription?.unsubscribe(); + this.subscription.unsubscribe(); + } + initDocumentInfos(params: any) { this.http.get('../rest/documents/' + params['id']).pipe( tap((data: any) => { @@ -391,6 +395,29 @@ export class DocumentComponent implements OnInit, OnDestroy { // this.renderPdf(); this.renderImage(); this.loadingdocument = false; + this.load.dismiss(); + + if (this.mainDocument.status === 'CONVERTING') { + // timer(0, 10000) call the function immediately and every 10 seconds + this.timerSubscription = timer(0, 10000).pipe( + tap(() => { + this.http.get('../rest/documents/' + params['id']).pipe( + tap((res: any) => { + this.totalPages = res.document.pages; + if (res.document.status !== 'CONVERTING') { + this.mainDocument.status = res.document.status; + this.timerSubscription?.unsubscribe(); + } + }) + ).subscribe(); + }), + catchError((err: any) => { + this.load.dismiss(); + this.notificationService.handleErrors(err); + return of(false); + }) + ).subscribe(); + } }), catchError((err: any) => { console.log('error', err); @@ -769,4 +796,8 @@ export class DocumentComponent implements OnInit, OnDestroy { } } } + + isNotReady() { + return ['CONVERTING', 'ERROR'].indexOf(this.mainDocument.status) > -1; + } } diff --git a/src/frontend/app/indexation/indexation.component.ts b/src/frontend/app/indexation/indexation.component.ts index b9623e4795f80cae59591b3cb05dcab0e1683f5b..3273ee61fd0d942b82f4a7d2aa6d732b159e9001 100644 --- a/src/frontend/app/indexation/indexation.component.ts +++ b/src/frontend/app/indexation/indexation.component.ts @@ -301,35 +301,46 @@ export class IndexationComponent implements OnInit { } uploadTrigger(fileInput: any) { - if (fileInput.target.files && fileInput.target.files[0] && this.isExtensionAllowed(fileInput.target.files)) { - for (let index = 0; index < fileInput.target.files.length; index++) { - const filename = fileInput.target.files[index].name; - const file = { - title: filename.substr(0, filename.lastIndexOf('.')), - reference: filename.substr(0, filename.lastIndexOf('.')).substr(0, 53), - mainDocument: true, - content: '' - }; - const reader = new FileReader(); - reader.readAsArrayBuffer(fileInput.target.files[index]); - reader.onload = (value: any) => { - file.mainDocument = this.filesToUpload.length === 0; - file.reference = this.filesToUpload.length === 0 ? file.reference : ''; - file.content = this.getBase64Document(value.target.result); - this.filesToUpload.push(file); - if (this.filesToUpload.length === 1) { - setTimeout(() => { - this.menu.open('right-menu'); - }, 500); + this.loadingController.create({ + message: this.translate.instant('lang.loadingDocument'), + spinner: 'dots' + }).then(async (load: HTMLIonLoadingElement) => { + load.present(); + if (fileInput.target.files && fileInput.target.files[0] && this.isExtensionAllowed(fileInput.target.files)) { + for (let index = 0; index < fileInput.target.files.length; index++) { + const filename = fileInput.target.files[index].name; + const file = { + title: filename.substr(0, filename.lastIndexOf('.')), + reference: filename.substr(0, filename.lastIndexOf('.')).substr(0, 53), + mainDocument: true, + content: '' + }; + const reader = new FileReader(); + reader.readAsArrayBuffer(fileInput.target.files[index]); + reader.onload = (value: any) => { + file.mainDocument = this.filesToUpload.length === 0; + file.reference = this.filesToUpload.length === 0 ? file.reference : ''; + file.content = this.getBase64Document(value.target.result); + this.filesToUpload.push(file); + if (this.filesToUpload.length === 1) { + setTimeout(() => { + this.menu.open('right-menu'); + }, 500); + } + }; + if (index === fileInput.target.files.length - 1) { + load.dismiss(); } - }; + } + this.fileImport.nativeElement.value = ''; + } else { + this.loading = false; + load.dismiss(); } - this.fileImport.nativeElement.value = ''; - } else { - this.loading = false; - } + }); } + isExtensionAllowed(files: any[]) { for (let index = 0; index < files.length; index++) { if (files[index].name.toLowerCase().split('.').pop() !== 'pdf') {