RED-4032: Updated assignment & workflow change permissions

This commit is contained in:
Adina Țeudan 2022-05-23 23:04:07 +03:00
parent 14193347fb
commit 20d27ab75e
11 changed files with 104 additions and 138 deletions

View File

@ -170,8 +170,9 @@ export class DossierOverviewBulkActionsComponent implements OnChanges {
true,
);
const allFilesAreUnderApproval = this.selectedFiles.reduce((acc, file) => acc && file.isUnderApproval, true);
const allFilesAreApproved = this.selectedFiles.reduce((acc, file) => acc && file.isApproved, true);
this.#allFilesAreExcluded = this.selectedFiles.reduce((acc, file) => acc && file.excluded, true);
this.#canMoveToSameState = allFilesAreUnderReviewOrUnassigned || allFilesAreUnderApproval;
this.#canMoveToSameState = allFilesAreUnderReviewOrUnassigned || allFilesAreUnderApproval || allFilesAreApproved;
this.#canAssign =
this.#canMoveToSameState &&

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { Dossier, File } from '@red/domain';
import { Dossier, File, WorkflowFileStatus, WorkflowFileStatuses } from '@red/domain';
import { DossiersDialogService } from '../../shared-dossiers/services/dossiers-dialog.service';
import { ConfirmationDialogInput, LoadingService } from '@iqser/common-ui';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
@ -26,7 +26,7 @@ export class BulkActionsService {
const dossier = this._getDossier(files);
// If more than 1 approver - show dialog and ask who to assign
if (dossier.approverIds.length > 1) {
this._assignFiles(files, 'approver', true);
this._assignFiles(files, WorkflowFileStatuses.UNDER_APPROVAL, true);
} else {
this._loadingService.start();
await firstValueFrom(this._filesService.setUnderApprovalFor(files, dossier.id, dossier.approverIds[0]));
@ -124,15 +124,15 @@ export class BulkActionsService {
}
}
assign(files: File[], mode: 'reviewer' | 'approver' = files[0].isUnderApproval ? 'approver' : 'reviewer') {
this._assignFiles(files, mode, false, true);
assign(files: File[]): void {
this._assignFiles(files, files[0].workflowStatus, false, true);
}
private _getDossier(files: File[]): Dossier {
return this._activeDossiersService.find(files[0].dossierId);
}
private _assignFiles(files: File[], mode: 'reviewer' | 'approver', ignoreChanged = false, withUnassignedOption = false) {
this._dialogService.openDialog('assignFile', null, { mode, files, ignoreChanged, withUnassignedOption });
private _assignFiles(files: File[], targetStatus: WorkflowFileStatus, ignoreChanged = false, withUnassignedOption = false): void {
this._dialogService.openDialog('assignFile', null, { targetStatus, files, ignoreChanged, withUnassignedOption });
}
}

View File

@ -102,11 +102,11 @@ export class UserManagementComponent {
const { dossierId, filename } = file;
this.loadingService.start();
if (!assigneeId) {
await firstValueFrom(this.filesService.setUnassigned([file], dossierId));
if (!assigneeId || file.isApproved) {
await firstValueFrom(this.filesService.setAssignee([file], dossierId, assigneeId));
} else if (file.isNew || file.isUnderReview) {
await firstValueFrom(this.filesService.setReviewerFor([file], dossierId, assigneeId));
} else if (file.isUnderApproval) {
} else {
await firstValueFrom(this.filesService.setUnderApprovalFor([file], dossierId, assigneeId));
}

View File

@ -319,11 +319,11 @@ export class FileActionsComponent implements OnChanges {
}
private _assign($event: MouseEvent) {
const mode = this.file.isUnderApproval ? 'approver' : 'reviewer';
const files = [this.file];
const targetStatus = this.file.workflowStatus;
const withCurrentUserAsDefault = true;
const withUnassignedOption = true;
this._dialogService.openDialog('assignFile', $event, { mode, files, withCurrentUserAsDefault, withUnassignedOption });
this._dialogService.openDialog('assignFile', $event, { targetStatus, files, withCurrentUserAsDefault, withUnassignedOption });
}
private async _assignToMe($event: MouseEvent) {

View File

@ -1,17 +1,11 @@
<section class="dialog">
<div
[translateParams]="{
type: data.mode
}"
[translate]="'assign-owner.dialog.title'"
class="dialog-header heading-l"
></div>
<div [translateParams]="{ type: mode }" [translate]="'assign-owner.dialog.title'" class="dialog-header heading-l"></div>
<form (submit)="save()" [formGroup]="form">
<div class="dialog-content">
<div class="iqser-input-group w-300 required">
<mat-form-field floatLabel="always">
<mat-label>{{ 'assign-owner.dialog.label' | translate: { type: data.mode } }}</mat-label>
<mat-label>{{ 'assign-owner.dialog.label' | translate: { type: mode } }}</mat-label>
<mat-select [placeholder]="'initials-avatar.unassigned' | translate" formControlName="user">
<mat-option *ngFor="let userId of userOptions" [value]="userId">
{{ userId | name: { defaultValue: 'initials-avatar.unassigned' | translate } }}

View File

@ -3,7 +3,7 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { UserService } from '@services/user.service';
import { LoadingService, Toaster } from '@iqser/common-ui';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Dossier, File } from '@red/domain';
import { Dossier, File, 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';
@ -11,7 +11,7 @@ import { PermissionsService } from '@services/permissions.service';
import { firstValueFrom } from 'rxjs';
class DialogData {
mode: 'approver' | 'reviewer';
targetStatus: WorkflowFileStatus;
files: File[];
ignoreChanged?: boolean;
withUnassignedOption?: boolean;
@ -24,6 +24,7 @@ class DialogData {
})
export class AssignReviewerApproverDialogComponent {
readonly form: FormGroup;
readonly mode: 'reviewer' | 'approver';
dossier: Dossier;
constructor(
@ -39,6 +40,10 @@ export class AssignReviewerApproverDialogComponent {
) {
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 {
@ -50,9 +55,7 @@ export class AssignReviewerApproverDialogComponent {
if (!this.permissionsService.canAssignUser(this.data.files, this.dossier)) {
return [...unassignUser];
}
return this.data.mode === 'approver'
? [...this.dossier.approverIds, ...unassignUser]
: [...this.dossier.memberIds, ...unassignUser];
return this.mode === 'reviewer' ? [...this.dossier.memberIds, ...unassignUser] : [...this.dossier.approverIds, ...unassignUser];
}
get changed(): boolean {
@ -100,9 +103,9 @@ export class AssignReviewerApproverDialogComponent {
async save() {
this._loadingService.start();
try {
if (!this.selectedUser) {
await firstValueFrom(this._filesService.setUnassigned(this.data.files, this.dossier.id));
} else if (this.data.mode === 'reviewer') {
if (!this.selectedUser || this.data.targetStatus === WorkflowFileStatuses.APPROVED) {
await firstValueFrom(this._filesService.setAssignee(this.data.files, this.dossier.id, this.selectedUser));
} else if (this.mode === 'reviewer') {
await firstValueFrom(this._filesService.setReviewerFor(this.data.files, this.dossier.id, this.selectedUser));
} else {
await firstValueFrom(this._filesService.setUnderApprovalFor(this.data.files, this.dossier.id, this.selectedUser));

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { UserService } from '@services/user.service';
import { Dossier, File, User } from '@red/domain';
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';
@ -8,16 +8,6 @@ import { ConfirmationDialogInput, LoadingService, Toaster } from '@iqser/common-
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { firstValueFrom } from 'rxjs';
const changeReviewerDialogInput = new ConfirmationDialogInput({
title: _('confirmation-dialog.assign-file-to-me.title'),
question: _('confirmation-dialog.assign-file-to-me.question'),
});
const changeApproverDialogInput = new ConfirmationDialogInput({
title: _('confirmation-dialog.assign-me-as-approver.title'),
question: _('confirmation-dialog.assign-me-as-approver.question'),
});
const atLeastOneAssignee = (files: File[]) => files.reduce((acc, fs) => acc || !!fs.assignee, false);
@Injectable()
@ -35,76 +25,66 @@ export class FileAssignService {
this.currentUser = userService.currentUser;
}
async assignToMe(files: File[]) {
const filesAreUnderApproval = files.reduce((acc, fs) => acc && fs.isUnderApproval, true);
async assignToMe(files: File[]): Promise<unknown> {
const assignReq = async () => {
this._loadingService.start();
await firstValueFrom(this._filesService.setAssignee(files, files[0].dossierId, this.currentUser.id));
this._loadingService.stop();
};
if (atLeastOneAssignee(files)) {
const dialogInput = new ConfirmationDialogInput({
title: _('confirmation-dialog.assign-file-to-me.title'),
question: _('confirmation-dialog.assign-file-to-me.question'),
});
const ref = this._dialogService.openDialog('confirm', null, dialogInput, assignReq);
return firstValueFrom(ref.afterClosed());
}
return filesAreUnderApproval ? this.#assignMeAsApprover(files) : this.#assignMeAsReviewer(files);
return assignReq();
}
async assignReviewer($event: MouseEvent, file: File, ignoreChanged = false): Promise<void> {
await this._assignFile('reviewer', $event, file, ignoreChanged);
await this._assignFile(WorkflowFileStatuses.UNDER_REVIEW, $event, file, ignoreChanged);
}
async assignApprover($event: MouseEvent, file: File, ignoreChanged = false): Promise<void> {
await this._assignFile('approver', $event, file, ignoreChanged);
await this._assignFile(WorkflowFileStatuses.UNDER_APPROVAL, $event, file, ignoreChanged);
}
#assignMeAsReviewer(files: File[]) {
if (atLeastOneAssignee(files)) {
const cb = () => this.#assignReviewerToCurrentUser(files);
const ref = this._dialogService.openDialog('confirm', null, changeReviewerDialogInput, cb);
return firstValueFrom(ref.afterClosed());
}
return this.#assignReviewerToCurrentUser(files);
}
#assignMeAsApprover(files: File[]) {
if (atLeastOneAssignee(files)) {
const cb = () => this.#assignApproverToCurrentUser(files);
const ref = this._dialogService.openDialog('confirm', null, changeApproverDialogInput, cb);
return firstValueFrom(ref.afterClosed());
}
return this.#assignApproverToCurrentUser(files);
}
private async _assignFile(mode: 'reviewer' | 'approver', $event: MouseEvent, file: File, ignoreChanged = false): Promise<void> {
private async _assignFile(targetStatus: WorkflowFileStatus, $event: MouseEvent, file: File, ignoreChanged = false): Promise<void> {
$event?.stopPropagation();
const currentUserId = this.currentUser.id;
const currentDossier = this._activeDossiersService.find(file.dossierId);
const eligibleUsersIds = this._getUserIds(mode, currentDossier);
const eligibleUsersIds = this._getUserIds(targetStatus, currentDossier);
if (file.isNew) {
await this._makeAssignFileRequest(currentUserId, mode, [file]);
await this._makeAssignFileRequest(currentUserId, targetStatus, [file]);
} else if (file.assignee === currentUserId) {
if (eligibleUsersIds.includes(currentUserId)) {
await this._makeAssignFileRequest(currentUserId, mode, [file]);
await this._makeAssignFileRequest(currentUserId, targetStatus, [file]);
} else if (eligibleUsersIds.length === 1) {
await this._makeAssignFileRequest(eligibleUsersIds[0], mode, [file]);
await this._makeAssignFileRequest(eligibleUsersIds[0], targetStatus, [file]);
} else {
const data = { mode, files: [file], ignoreChanged };
const data = { targetStatus, files: [file], ignoreChanged };
this._dialogService.openDialog('assignFile', null, data);
}
} else {
if (eligibleUsersIds.length === 1) {
await this._makeAssignFileRequest(eligibleUsersIds[0], mode, [file]);
await this._makeAssignFileRequest(eligibleUsersIds[0], targetStatus, [file]);
} else {
const data = { mode, files: [file], ignoreChanged, withCurrentUserAsDefault: true };
const data = { targetStatus, files: [file], ignoreChanged, withCurrentUserAsDefault: true };
this._dialogService.openDialog('assignFile', null, data);
}
}
}
private async _makeAssignFileRequest(userId: string, mode: 'reviewer' | 'approver', files: File[]) {
private async _makeAssignFileRequest(userId: string, targetStatus: WorkflowFileStatus, files: File[]) {
this._loadingService.start();
try {
if (!userId) {
await firstValueFrom(this._filesService.setUnassigned(files, files[0].dossierId));
} else if (mode === 'reviewer') {
if (!userId || targetStatus === WorkflowFileStatuses.APPROVED) {
await firstValueFrom(this._filesService.setAssignee(files, files[0].dossierId, userId));
} else if (targetStatus === WorkflowFileStatuses.UNDER_REVIEW) {
await firstValueFrom(this._filesService.setReviewerFor(files, files[0].dossierId, userId));
} else {
await firstValueFrom(this._filesService.setUnderApprovalFor(files, files[0].dossierId, userId));
@ -115,21 +95,9 @@ export class FileAssignService {
this._loadingService.stop();
}
private _getUserIds(mode: 'reviewer' | 'approver', dossier: Dossier) {
return mode === 'approver' ? dossier.approverIds : dossier.memberIds;
}
async #assignReviewerToCurrentUser(files: File[]) {
this._loadingService.start();
const reviewer$ = this._filesService.setReviewerFor(files, files[0].dossierId, this.currentUser.id);
await firstValueFrom(reviewer$);
this._loadingService.stop();
}
async #assignApproverToCurrentUser(files: File[]) {
this._loadingService.start();
const approver$ = this._filesService.setUnderApprovalFor(files, files[0].dossierId, this.currentUser.id);
await firstValueFrom(approver$);
this._loadingService.stop();
private _getUserIds(targetStatus: WorkflowFileStatus, dossier: Dossier) {
return targetStatus === WorkflowFileStatuses.UNDER_APPROVAL || targetStatus === WorkflowFileStatuses.APPROVED
? dossier.approverIds
: dossier.memberIds;
}
}

View File

@ -42,10 +42,10 @@ export class FilesService extends EntitiesService<File, IFile> {
}
@Validate()
setUnassigned(@RequiredParam() files: List<File>, @RequiredParam() dossierId: string) {
setAssignee(@RequiredParam() files: List<File>, @RequiredParam() dossierId: string, assigneeId: string) {
const url = `${this._defaultModelPath}/set-assignee/${dossierId}/bulk`;
const fileIds = files.map(f => f.id);
return this._post<unknown>(fileIds, url).pipe(switchMap(() => this.loadAll(dossierId)));
return this._post<unknown>(fileIds, url, [{ key: 'assigneeId', value: assigneeId }]).pipe(switchMap(() => this.loadAll(dossierId)));
}
@Validate()

View File

@ -29,7 +29,7 @@ export class PermissionsService {
return this.isAdmin();
}
isReviewerOrApprover(file: File, dossier: Dossier): boolean {
isAssigneeOrApprover(file: File, dossier: Dossier): boolean {
return this.isFileAssignee(file) || this.isApprover(dossier);
}
@ -121,7 +121,7 @@ export class PermissionsService {
canSetUnderReview(file: File | File[], dossier: Dossier): boolean {
const files = file instanceof File ? [file] : file;
return this.isApprover(dossier) && files.reduce((acc, _file) => this._canSetUnderReview(_file, dossier) && acc, true);
return files.reduce((acc, _file) => this._canSetUnderReview(_file, dossier) && acc, true);
}
canBeApproved(file: File | File[], dossier: Dossier): boolean {
@ -129,8 +129,9 @@ export class PermissionsService {
return files.reduce((acc, _file) => this._canBeApproved(_file, dossier) && acc, true);
}
isReadyForApproval(files: File | File[], dossier: Dossier): boolean {
return this.canSetUnderReview(files, dossier);
isReadyForApproval(file: File | File[], dossier: Dossier): boolean {
const files = file instanceof File ? [file] : file;
return files.reduce((acc, _file) => this._isReadyForApproval(_file, dossier) && acc, true);
}
canSetUnderApproval(file: File | File[], dossier: Dossier): boolean {
@ -232,7 +233,7 @@ export class PermissionsService {
}
canAddComment(file: File, dossier: Dossier): boolean {
return (this.isFileAssignee(file) || this.isApprover(dossier)) && !file.isApproved;
return this.isAssigneeOrApprover(file, dossier) && !file.isApproved;
}
canExcludePages(file: File, dossier: Dossier): boolean {
@ -260,21 +261,19 @@ export class PermissionsService {
}
private _canSoftDeleteFile(file: File, dossier: Dossier): boolean {
return (
dossier.isActive && (this.isApprover(dossier) || this.isFileAssignee(file) || (!file.assignee && this.isDossierMember(dossier)))
);
return dossier.isActive && (this.isAssigneeOrApprover(file, dossier) || (!file.assignee && this.isDossierMember(dossier)));
}
private _canRestoreFile(file: File, dossier: Dossier): boolean {
return this.isApprover(dossier) || this.isFileAssignee(file);
return this.isAssigneeOrApprover(file, dossier);
}
private _canHardDeleteFile(file: File, dossier: Dossier): boolean {
return this.isApprover(dossier) || this.isFileAssignee(file) || (!file.assignee && this.isDossierMember(dossier));
return this.isAssigneeOrApprover(file, dossier) || (!file.assignee && this.isDossierMember(dossier));
}
private _canReanalyseFile(file: File, dossier: Dossier): boolean {
return dossier.isActive && this.isReviewerOrApprover(file, dossier) && file.analysisRequired;
return dossier.isActive && this.isAssigneeOrApprover(file, dossier) && file.analysisRequired;
}
private _canEnableAutoAnalysis(file: File, dossier: Dossier): boolean {
@ -285,22 +284,39 @@ export class PermissionsService {
return dossier.isActive && !file.excludedFromAutomaticAnalysis && this.isFileAssignee(file) && !file.isApproved;
}
/** UNDER_REVIEW => NEW */
private _canSetToNew(file: File, dossier: Dossier): boolean {
return dossier.isActive && file.isUnderReview && this.isDossierMember(dossier);
}
/** UNDER_REVIEW => UNDER_APPROVAL */
private _canSetUnderApproval(file: File, dossier: Dossier): boolean {
return dossier.isActive && file.isUnderReview && this.isDossierMember(dossier);
}
/** UNDER_APPROVAL => UNDER_REVIEW */
private _canSetUnderReview(file: File, dossier: Dossier): boolean {
return dossier.isActive && file.isUnderApproval && this.isAssigneeOrApprover(file, dossier);
}
/** UNDER_APPROVAL => APPROVED */
private _canBeApproved(file: File, dossier: Dossier): boolean {
return this._isReadyForApproval(file, dossier) && file.canBeApproved;
}
private _isReadyForApproval(file: File, dossier: Dossier): boolean {
return dossier.isActive && file.isUnderApproval && this.isAssigneeOrApprover(file, dossier);
}
/** APPROVED => UNDER_APPROVAL */
private _canUndoApproval(file: File, dossier: Dossier): boolean {
return dossier.isActive && file.isApproved && this.isApprover(dossier);
}
private _assignmentPrecondition(file: File, dossier: Dossier): boolean {
return dossier.isActive && !file.isError && !file.isProcessing;
}
private _canSetUnderApproval(file: File, dossier: Dossier): boolean {
return dossier.isActive && file.isUnderReview && this.isFileAssignee(file);
}
private _canUndoApproval(file: File, dossier: Dossier): boolean {
return dossier.isActive && file.isApproved && this.isFileAssignee(file);
}
private _canBeApproved(file: File, dossier: Dossier): boolean {
return dossier.isActive && file.canBeApproved && this.isFileAssignee(file);
}
private _canAssignToSelf(file: File, dossier: Dossier): boolean {
const precondition = this._assignmentPrecondition(file, dossier) && !this.isFileAssignee(file);
return precondition && (this.isApprover(dossier) || (this.isDossierMember(dossier) && (file.isNew || file.isUnderReview)));
@ -313,7 +329,7 @@ export class PermissionsService {
if ((file.isNew || file.isUnderReview) && dossier.hasReviewers && this.isDossierMember(dossier)) {
return true;
}
if (file.isUnderApproval && dossier.approverIds.length > 1 && this.isApprover(dossier)) {
if ((file.isUnderApproval || file.isApproved) && dossier.approverIds.length > 1 && this.isApprover(dossier)) {
return true;
}
}
@ -321,14 +337,6 @@ export class PermissionsService {
}
private _canUnassignUser(file: File, dossier: Dossier) {
return this._assignmentPrecondition(file, dossier) && !!file.assignee && (this.isApprover(dossier) || this.isFileAssignee(file));
}
private _canSetToNew(file: File, dossier: Dossier): boolean {
return dossier.isActive && file.isUnderReview && this.isFileAssignee(file);
}
private _canSetUnderReview(file: File, dossier: Dossier): boolean {
return dossier.isActive && file.isUnderApproval && this.isFileAssignee(file);
return this._assignmentPrecondition(file, dossier) && !!file.assignee && this.isAssigneeOrApprover(file, dossier);
}
}

View File

@ -554,10 +554,6 @@
"question": "Dieses Dokument wird gerade von einer anderen Person geprüft. Möchten Sie Reviewer werden und sich selbst dem Dokument zuweisen?",
"title": "Neuen Reviewer zuweisen"
},
"assign-me-as-approver": {
"question": "",
"title": ""
},
"compare-file": {
"question": "<strong>Achtung!</strong> <br><br> Seitenzahl stimmt nicht überein, aktuelles Dokument hat <strong>{currentDocumentPageCount} Seite(n)</strong>. Das hochgeladene Dokument hat <strong>{compareDocumentPageCount} Seite(n)</strong>. <br><br> Möchten Sie fortfahren?",
"title": "Vergleichen mit: {fileName}"

View File

@ -551,13 +551,9 @@
"title": "Warning!"
},
"assign-file-to-me": {
"question": "This document is currently reviewed by someone else. Do you want to become the reviewer and assign yourself to this document?",
"question": "At least one document is currently assigned to someone else. Are you sure you want to replace them and assign yourself to these documents?",
"title": "Re-assign user"
},
"assign-me-as-approver": {
"question": "This document is currently under approval by someone else. Do you want to become the approver and assign yourself to this document?",
"title": "Re-assign approver"
},
"compare-file": {
"question": "<strong>Warning!</strong> <br><br> Number of pages does not match, current document has <strong>{currentDocumentPageCount} page(s)</strong>. Uploaded document has <strong>{compareDocumentPageCount} page(s)</strong>. <br><br> Do you wish to proceed?",
"title": "Compare with file: {fileName}"