diff --git a/apps/red-ui/src/app/modules/dossier-overview/services/bulk-actions.service.ts b/apps/red-ui/src/app/modules/dossier-overview/services/bulk-actions.service.ts index 343e61cd6..6ba3e94ff 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/services/bulk-actions.service.ts +++ b/apps/red-ui/src/app/modules/dossier-overview/services/bulk-actions.service.ts @@ -29,7 +29,7 @@ export class BulkActionsService { this._assignFiles(files, WorkflowFileStatuses.UNDER_APPROVAL, true); } else { this._loadingService.start(); - await firstValueFrom(this._filesService.setUnderApprovalFor(files, dossier.id, dossier.approverIds[0])); + await this._filesService.setUnderApproval(files, dossier.approverIds[0]); this._loadingService.stop(); } } @@ -82,13 +82,13 @@ export class BulkActionsService { async backToUnderReview(files: File[]): Promise { this._loadingService.start(); - await firstValueFrom(this._filesService.setUnderReviewFor(files, files[0].dossierId)); + await this._filesService.setUnderReviewFor(files); this._loadingService.stop(); } async setToNew(files: File[]): Promise { this._loadingService.start(); - await firstValueFrom(this._filesService.setToNewFor(files, files[0].dossierId)); + await this._filesService.setToNew(files); this._loadingService.stop(); } @@ -113,13 +113,13 @@ export class BulkActionsService { }), async () => { this._loadingService.start(); - await firstValueFrom(this._filesService.setApprovedFor(files, files[0].dossierId)); + await this._filesService.setApproved(files); this._loadingService.stop(); }, ); } else { this._loadingService.start(); - await firstValueFrom(this._filesService.setApprovedFor(files, files[0].dossierId)); + await this._filesService.setApproved(files); this._loadingService.stop(); } } diff --git a/apps/red-ui/src/app/modules/file-preview/components/user-management/user-management.component.ts b/apps/red-ui/src/app/modules/file-preview/components/user-management/user-management.component.ts index e8ee23ee7..b4a1db772 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/user-management/user-management.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/user-management/user-management.component.ts @@ -7,7 +7,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { UserService } from '@users/user.service'; import { FilesService } from '@services/files/files.service'; import { TranslateService } from '@ngx-translate/core'; -import { combineLatest, combineLatestWith, firstValueFrom, Observable, switchMap } from 'rxjs'; +import { combineLatest, combineLatestWith, Observable, switchMap } from 'rxjs'; import { FilePreviewStateService } from '../../services/file-preview-state.service'; import { distinctUntilChanged, map } from 'rxjs/operators'; import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service'; @@ -99,22 +99,21 @@ export class UserManagementComponent { async assignReviewer(file: File, user: User | string) { const assigneeId = typeof user === 'string' ? user : user?.id; - const reviewerName = this.userService.getName(assigneeId); - const { dossierId, filename } = file; this.loadingService.start(); if (!assigneeId || file.isApproved) { - await firstValueFrom(this.filesService.setAssignee([file], dossierId, assigneeId)); + await this.filesService.setAssignee(file, assigneeId); } else if (file.isNew || file.isUnderReview) { - await firstValueFrom(this.filesService.setReviewerFor([file], dossierId, assigneeId)); + await this.filesService.setReviewer(file, assigneeId); } else { - await firstValueFrom(this.filesService.setUnderApprovalFor([file], dossierId, assigneeId)); + await this.filesService.setUnderApproval(file, assigneeId); } this.loadingService.stop(); - this.toaster.success(_('assignment.reviewer'), { params: { reviewerName, filename } }); + const translateParams = { reviewerName: this.userService.getName(assigneeId), filename: file.filename }; + this.toaster.success(_('assignment.reviewer'), { params: translateParams }); this.editingReviewer = false; } diff --git a/apps/red-ui/src/app/modules/pdf-viewer/utils/functions.ts b/apps/red-ui/src/app/modules/pdf-viewer/utils/functions.ts index 55273e8db..12091decb 100644 --- a/apps/red-ui/src/app/modules/pdf-viewer/utils/functions.ts +++ b/apps/red-ui/src/app/modules/pdf-viewer/utils/functions.ts @@ -1,5 +1,5 @@ import { List } from '@iqser/common-ui'; -import { AnnotationWrapper } from '../../../models/file/annotation.wrapper'; +import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { ALLOWED_KEYBOARD_SHORTCUTS } from './constants'; export function stopAndPrevent($event: T) { diff --git a/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts b/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts index b9d9fed90..e693b56d4 100644 --- a/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts +++ b/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts @@ -188,7 +188,7 @@ export class FileActionsComponent implements OnChanges { { id: 'set-file-to-new-btn-' + fileId, type: ActionTypes.circleBtn, - action: ($event: MouseEvent) => this._setToNew($event), + action: ($event: MouseEvent) => this.#setToNew($event), tooltip: _('dossier-overview.back-to-new'), icon: 'red:undo', show: this.showSetToNew, @@ -295,7 +295,7 @@ export class FileActionsComponent implements OnChanges { async setFileApproved($event: MouseEvent) { $event.stopPropagation(); if (!this.file.analysisRequired && !this.file.hasUpdates) { - await this._setFileApproved(); + await this.#setFileApproved(); return; } @@ -314,7 +314,7 @@ export class FileActionsComponent implements OnChanges { : null, denyText: this.file.analysisRequired ? _('confirmation-dialog.approve-file-without-analysis.denyText') : null, }), - () => this._setFileApproved(), + () => this.#setFileApproved(), ); } @@ -469,16 +469,16 @@ export class FileActionsComponent implements OnChanges { this._changeRef.markForCheck(); } - private async _setFileApproved() { + async #setFileApproved() { this._loadingService.start(); - await firstValueFrom(this._filesService.setApprovedFor([this.file], this.file.dossierId)); + await this._filesService.setApproved(this.file); this._loadingService.stop(); } - private async _setToNew($event: MouseEvent) { + async #setToNew($event: MouseEvent) { $event.stopPropagation(); this._loadingService.start(); - await firstValueFrom(this._filesService.setToNewFor([this.file], this.file.dossierId)); + await this._filesService.setToNew(this.file); this._loadingService.stop(); } } diff --git a/apps/red-ui/src/app/modules/shared-dossiers/dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component.ts b/apps/red-ui/src/app/modules/shared-dossiers/dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component.ts index 9870066b4..1263d9173 100644 --- a/apps/red-ui/src/app/modules/shared-dossiers/dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component.ts +++ b/apps/red-ui/src/app/modules/shared-dossiers/dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component.ts @@ -1,14 +1,13 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { UserService } from '@users/user.service'; -import { IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui'; -import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; -import { Dossier, File, WorkflowFileStatus, WorkflowFileStatuses } from '@red/domain'; +import { getCurrentUser, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui'; +import { FormBuilder, Validators } from '@angular/forms'; +import { File, User, WorkflowFileStatus, WorkflowFileStatuses } from '@red/domain'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { FilesService } from '@services/files/files.service'; import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service'; import { PermissionsService } from '@services/permissions.service'; -import { firstValueFrom } from 'rxjs'; import { moveElementInArray } from '@utils/functions'; class DialogData { @@ -24,59 +23,37 @@ class DialogData { }) export class AssignReviewerApproverDialogComponent { readonly iconButtonTypes = IconButtonTypes; - readonly form: UntypedFormGroup; - readonly mode: 'reviewer' | 'approver'; - dossier: Dossier; + readonly currentUser = getCurrentUser(); + readonly mode = this.#mode; + readonly dossier = this._activeDossiersService.find(this.data.files[0].dossierId); + readonly userOptions = this.#userOptions; + readonly form = this.#form; constructor( readonly userService: UserService, private readonly _toaster: Toaster, - private readonly _formBuilder: UntypedFormBuilder, + private readonly _formBuilder: FormBuilder, private readonly _activeDossiersService: ActiveDossiersService, private readonly _filesService: FilesService, private readonly _loadingService: LoadingService, readonly permissionsService: PermissionsService, private readonly _dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) readonly data: DialogData, - ) { - this.dossier = this._activeDossiersService.find(this.data.files[0].dossierId); - this.form = this._getForm(); - this.mode = - data.targetStatus === WorkflowFileStatuses.UNDER_APPROVAL || data.targetStatus === WorkflowFileStatuses.APPROVED - ? 'approver' - : 'reviewer'; - } + ) {} get selectedUser(): string { - const value = this.form.get('user').value; + const value = this.form.controls.user.value; return value === 'undefined' ? undefined : value; } - get userOptions() { - const unassignUser = this._canUnassignFiles && this.data.withUnassignedOption ? ['undefined'] : []; - const cannotAssignUser = !this.permissionsService.canAssignUser(this.data.files, this.dossier); - - if (this.mode === 'reviewer') { - if (this.dossier.hasReviewers && cannotAssignUser) { - return [...unassignUser]; - } - return this._customSort([...this.dossier.memberIds, ...unassignUser]); - } - - if (this.dossier.approverIds.length > 1 && cannotAssignUser) { - return [...unassignUser]; - } - - return this._customSort([...this.dossier.approverIds, ...unassignUser]); - } - get changed(): boolean { if (this.data.ignoreChanged) { return true; } + const selectedUser = this.selectedUser; for (const file of this.data.files) { - if (file.assignee !== this.selectedUser) { + if (file.assignee !== selectedUser) { return true; } } @@ -84,71 +61,100 @@ export class AssignReviewerApproverDialogComponent { return false; } - private get _canUnassignFiles() { + get #mode() { + const isUnderApproval = this.data.targetStatus === WorkflowFileStatuses.UNDER_APPROVAL; + const isApproved = this.data.targetStatus === WorkflowFileStatuses.APPROVED; + return isUnderApproval || isApproved ? 'approver' : 'reviewer'; + } + + get #userOptions() { + const unassignUser = this.#canUnassignUser && this.data.withUnassignedOption ? ['undefined'] : []; + const cannotAssignUser = !this.permissionsService.canAssignUser(this.data.files, this.dossier); + + if (this.mode === 'reviewer') { + if (this.dossier.hasReviewers && cannotAssignUser) { + return [...unassignUser]; + } + + return this.#customSort([...this.dossier.memberIds, ...unassignUser]); + } + + if (this.dossier.approverIds.length > 1 && cannotAssignUser) { + return [...unassignUser]; + } + + return this.#customSort([...this.dossier.approverIds, ...unassignUser]); + } + + get #canUnassignUser() { return this.permissionsService.canUnassignUser(this.data.files, this.dossier); } - /** Initialize the form with: - * the id of the current reviewer of the files list if there is only one reviewer for all of them; - * or the id of the current user - **/ - - private get _uniqueReviewers(): Set { + get #uniqueReviewers(): Set { const uniqueReviewers = new Set(); + for (const file of this.data.files) { if (file.assignee) { uniqueReviewers.add(file.assignee); } } + return uniqueReviewers; } - private get _user(): string { + get #user(): string { const userOptions = this.userOptions; - if (this.data.withCurrentUserAsDefault && userOptions.includes(this.userService.currentUser.id)) { - return this.userService.currentUser.id; + if (this.data.withCurrentUserAsDefault && userOptions.includes(this.currentUser.id)) { + return this.currentUser.id; } - const uniqueReviewers = [...this._uniqueReviewers.values()]; - const user = uniqueReviewers.length === 1 ? uniqueReviewers[0] : this.userService.currentUser.id; - + const uniqueReviewers = [...this.#uniqueReviewers.values()]; + const user = uniqueReviewers.length === 1 ? uniqueReviewers[0] : this.currentUser.id; return userOptions.indexOf(user) >= 0 ? userOptions[userOptions.indexOf(user)] : user; } + get #form() { + const user = this.#user; + return this._formBuilder.group({ + // Allow a null reviewer if a previous reviewer exists (= it's not the first assignment) & current user is allowed to unassign + user: [user, this.#canUnassignUser && !user ? Validators.required : null], + }); + } + async save() { this._loadingService.start(); + const selectedUser = this.selectedUser; + try { - if (!this.selectedUser || this.data.targetStatus === WorkflowFileStatuses.APPROVED) { - await firstValueFrom(this._filesService.setAssignee(this.data.files, this.dossier.id, this.selectedUser)); + if (!selectedUser || this.data.targetStatus === WorkflowFileStatuses.APPROVED) { + await this._filesService.setAssignee(this.data.files, selectedUser); } else if (this.mode === 'reviewer') { - await firstValueFrom(this._filesService.setReviewerFor(this.data.files, this.dossier.id, this.selectedUser)); + await this._filesService.setReviewer(this.data.files, selectedUser); } else { - await firstValueFrom(this._filesService.setUnderApprovalFor(this.data.files, this.dossier.id, this.selectedUser)); + await this._filesService.setUnderApproval(this.data.files, selectedUser); } } catch (error) { this._toaster.error(_('error.http.generic'), { params: error }); } - this._loadingService.stop(); + this._loadingService.stop(); this._dialogRef.close(true); } - private _getForm(): UntypedFormGroup { - return this._formBuilder.group({ - // Allow a null reviewer if a previous reviewer exists (= it's not the first assignment) & current user is allowed to unassign - user: [this._user, this._canUnassignFiles && !this._user ? Validators.required : null], - }); - } - - private _customSort(ids: string[]) { + #customSort(ids: string[]) { let sorted = ids.sort((a, b) => this.userService.getName(a).localeCompare(this.userService.getName(b))); - if (this.data.files.length === 1 && this.data.files[0].assignee) { + + const fileHasAssignee = this.data.files.length === 1 && this.data.files[0].assignee; + + if (fileHasAssignee) { sorted = moveElementInArray(sorted, this.data.files[0].assignee, 0); } - if (this.data.withUnassignedOption) { - sorted = moveElementInArray(sorted, 'undefined', this.data.files[0].assignee && this.data.files.length === 1 ? 1 : 0); + + if (sorted.includes('undefined')) { + sorted = moveElementInArray(sorted, 'undefined', fileHasAssignee ? 1 : 0); } + return sorted; } } diff --git a/apps/red-ui/src/app/modules/shared-dossiers/services/file-assign.service.ts b/apps/red-ui/src/app/modules/shared-dossiers/services/file-assign.service.ts index 046268d4e..c378a33a0 100644 --- a/apps/red-ui/src/app/modules/shared-dossiers/services/file-assign.service.ts +++ b/apps/red-ui/src/app/modules/shared-dossiers/services/file-assign.service.ts @@ -1,10 +1,9 @@ import { Injectable } from '@angular/core'; -import { UserService } from '@users/user.service'; import { Dossier, File, User, WorkflowFileStatus, WorkflowFileStatuses } from '@red/domain'; import { DossiersDialogService } from './dossiers-dialog.service'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { FilesService } from '@services/files/files.service'; -import { ConfirmationDialogInput, LoadingService, Toaster } from '@iqser/common-ui'; +import { ConfirmationDialogInput, getCurrentUser, LoadingService, Toaster } from '@iqser/common-ui'; import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service'; import { firstValueFrom } from 'rxjs'; @@ -12,18 +11,15 @@ const atLeastOneAssignee = (files: File[]) => files.reduce((acc, fs) => acc || ! @Injectable() export class FileAssignService { - readonly currentUser: User; + readonly currentUser = getCurrentUser(); constructor( - userService: UserService, private readonly _toaster: Toaster, private readonly _filesService: FilesService, private readonly _loadingService: LoadingService, private readonly _dialogService: DossiersDialogService, private readonly _activeDossiersService: ActiveDossiersService, - ) { - this.currentUser = userService.currentUser; - } + ) {} async assignToMe(files: File[]): Promise { const assignReq = async () => { @@ -31,10 +27,11 @@ export class FileAssignService { if (files[0].isNew) { await this._makeAssignFileRequest(this.currentUser.id, 'UNDER_REVIEW', files); } else { - await firstValueFrom(this._filesService.setAssignee(files, files[0].dossierId, this.currentUser.id)); + await this._filesService.setAssignee(files, this.currentUser.id); } this._loadingService.stop(); }; + if (atLeastOneAssignee(files)) { const dialogInput = new ConfirmationDialogInput({ title: _('confirmation-dialog.assign-file-to-me.title'), @@ -88,17 +85,19 @@ export class FileAssignService { private async _makeAssignFileRequest(userId: string, targetStatus: WorkflowFileStatus, files: File[]) { this._loadingService.start(); + try { if (!userId || targetStatus === WorkflowFileStatuses.APPROVED) { - await firstValueFrom(this._filesService.setAssignee(files, files[0].dossierId, userId)); + await this._filesService.setAssignee(files, userId); } else if (targetStatus === WorkflowFileStatuses.UNDER_REVIEW) { - await firstValueFrom(this._filesService.setReviewerFor(files, files[0].dossierId, userId)); + await this._filesService.setReviewer(files, userId); } else { - await firstValueFrom(this._filesService.setUnderApprovalFor(files, files[0].dossierId, userId)); + await this._filesService.setUnderApproval(files, userId); } } catch (error) { this._toaster.error(_('error.http.generic'), { params: error }); } + this._loadingService.stop(); } diff --git a/apps/red-ui/src/app/services/files/files.service.ts b/apps/red-ui/src/app/services/files/files.service.ts index 69ee46922..e4abe53db 100644 --- a/apps/red-ui/src/app/services/files/files.service.ts +++ b/apps/red-ui/src/app/services/files/files.service.ts @@ -1,13 +1,15 @@ import { Injectable } from '@angular/core'; -import { EntitiesService, List, mapEach, RequiredParam, Validate } from '@iqser/common-ui'; +import { EntitiesService, isArray, List, mapEach, QueryParam, RequiredParam, Validate } from '@iqser/common-ui'; import { File, IFile } from '@red/domain'; -import { Observable } from 'rxjs'; +import { firstValueFrom, Observable } from 'rxjs'; import { UserService } from '@users/user.service'; import { FilesMapService } from './files-map.service'; import { map, switchMap, tap } from 'rxjs/operators'; import { DossierStatsService } from '../dossiers/dossier-stats.service'; import { NGXLogger } from 'ngx-logger'; +const asList = (value: T | List): List => (isArray(value) ? value : [value]); + @Injectable({ providedIn: 'root', }) @@ -44,46 +46,47 @@ export class FilesService extends EntitiesService { } @Validate() - setAssignee(@RequiredParam() files: List, @RequiredParam() dossierId: string, assigneeId: string) { - const url = `${this._defaultModelPath}/set-assignee/${dossierId}/bulk`; - const fileIds = files.map(f => f.id); - return this._post(fileIds, url, [{ key: 'assigneeId', value: assigneeId }]).pipe(switchMap(() => this.loadAll(dossierId))); + async setAssignee(@RequiredParam() files: File | List, assigneeId: string) { + const _files = asList(files); + const url = `${this._defaultModelPath}/set-assignee/${_files[0].dossierId}/bulk`; + return this.#makePost(_files, url, [{ key: 'assigneeId', value: assigneeId }]); } @Validate() - setToNewFor(@RequiredParam() files: List, @RequiredParam() dossierId: string) { - const url = `${this._defaultModelPath}/new/${dossierId}/bulk`; - const fileIds = files.map(f => f.id); - return this._post(fileIds, url).pipe(switchMap(() => this.loadAll(dossierId))); + async setToNew(@RequiredParam() files: File | List) { + const _files = asList(files); + return this.#makePost(_files, `${this._defaultModelPath}/new/${_files[0].dossierId}/bulk`); } @Validate() - setUnderApprovalFor(@RequiredParam() files: List, @RequiredParam() dossierId: string, assigneeId: string) { - const url = `${this._defaultModelPath}/under-approval/${dossierId}/bulk`; - const fileIds = files.map(f => f.id); - return this._post(fileIds, url, [{ key: 'assigneeId', value: assigneeId }]).pipe(switchMap(() => this.loadAll(dossierId))); + async setUnderApproval(@RequiredParam() files: File | List, assigneeId: string) { + const _files = asList(files); + const url = `${this._defaultModelPath}/under-approval/${_files[0].dossierId}/bulk`; + return this.#makePost(_files, url, [{ key: 'assigneeId', value: assigneeId }]); } @Validate() - setReviewerFor(@RequiredParam() files: List, @RequiredParam() dossierId: string, assigneeId: string) { - const url = `${this._defaultModelPath}/under-review/${dossierId}/bulk`; - const fileIds = files.map(f => f.id); - return this._post(fileIds, url, [{ key: 'assigneeId', value: assigneeId }]).pipe(switchMap(() => this.loadAll(dossierId))); + async setReviewer(@RequiredParam() files: File | List, assigneeId: string) { + const _files = asList(files); + const url = `${this._defaultModelPath}/under-review/${_files[0].dossierId}/bulk`; + return this.#makePost(_files, url, [{ key: 'assigneeId', value: assigneeId }]); } @Validate() - setApprovedFor(@RequiredParam() files: List, @RequiredParam() dossierId: string) { - const fileIds = files.map(f => f.id); - return this._post(fileIds, `${this._defaultModelPath}/approved/${dossierId}/bulk`).pipe( - switchMap(() => this.loadAll(dossierId)), - ); + async setApproved(@RequiredParam() files: File | List) { + const _files = asList(files); + return this.#makePost(_files, `${this._defaultModelPath}/approved/${_files[0].dossierId}/bulk`); } @Validate() - setUnderReviewFor(@RequiredParam() files: List, @RequiredParam() dossierId: string) { + async setUnderReviewFor(@RequiredParam() files: File | List) { + const _files = asList(files); + return this.#makePost(_files, `${this._defaultModelPath}/under-review/${_files[0].dossierId}/bulk`); + } + + async #makePost(files: List, url: string, queryParams?: List) { const fileIds = files.map(f => f.id); - return this._post(fileIds, `${this._defaultModelPath}/under-review/${dossierId}/bulk`).pipe( - switchMap(() => this.loadAll(dossierId)), - ); + await firstValueFrom(this._post(fileIds, url, queryParams)); + return firstValueFrom(this.loadAll(files[0].dossierId)); } }