directly edit owner

This commit is contained in:
Dan Percic 2021-06-08 14:01:43 +03:00
parent 6f5b376fe7
commit 3887fab532
12 changed files with 270 additions and 197 deletions

View File

@ -1,33 +1,36 @@
<ng-container *ngIf="appStateService.activeDossier">
<div class="collapsed-wrapper">
<redaction-circle-button
(action)="toggleCollapse.emit()"
icon="red:expand"
tooltip="dossier-details.expand"
tooltipPosition="before"
></redaction-circle-button>
<ng-container *ngTemplateOutlet="collapsible; context: { action: 'expand' }"></ng-container>
<div class="all-caps-label" translate="dossier-details.title"></div>
</div>
<div class="header-wrapper mt-8">
<div class="heading-xl flex-1">{{ appStateService.activeDossier.dossier.dossierName }}</div>
<redaction-circle-button
(action)="toggleCollapse.emit()"
icon="red:collapse"
tooltip="dossier-details.collapse"
tooltipPosition="before"
></redaction-circle-button>
<ng-container
*ngTemplateOutlet="collapsible; context: { action: 'collapse' }"
></ng-container>
</div>
<div class="mt-24">
<div class="all-caps-label" translate="dossier-details.owner"></div>
<div class="mt-12">
<redaction-initials-avatar
[userId]="appStateService.activeDossier.dossier.ownerId"
[withName]="true"
color="gray"
size="large"
></redaction-initials-avatar>
<div class="mt-12 d-flex">
<ng-container *ngIf="!editingOwner; else editOwner">
<redaction-initials-avatar
[user]="owner"
[withName]="true"
color="gray"
size="large"
></redaction-initials-avatar>
<redaction-circle-button
class="ml-14"
(action)="editingOwner = true"
*ngIf="permissionsService.isManager()"
tooltip="dossier-overview.header-actions.edit"
icon="red:edit"
tooltipPosition="below"
></redaction-circle-button>
</ng-container>
</div>
</div>
@ -136,3 +139,21 @@
<div class="mt-8">{{ appStateService.activeDossier.dossier.description }}</div>
</div>
</ng-container>
<ng-template #editOwner>
<redaction-assign-user-dropdown
[value]="owner"
[options]="managers"
(save)="editingOwner = false; assignOwner($event)"
(cancel)="editingOwner = false"
></redaction-assign-user-dropdown>
</ng-template>
<ng-template #collapsible let-action="action">
<redaction-circle-button
(action)="toggleCollapse.emit()"
icon="{{ 'red:' + action }}"
tooltip="{{ 'dossier-details.' + action }}"
tooltipPosition="before"
></redaction-circle-button>
</ng-template>

View File

@ -2,12 +2,13 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } fro
import { AppStateService } from '@state/app-state.service';
import { groupBy } from '@utils/functions';
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
import { Router } from '@angular/router';
import { PermissionsService } from '@services/permissions.service';
import { TranslateChartService } from '@services/translate-chart.service';
import { StatusSorter } from '@utils/sorters/status-sorter';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
import { UserService } from '@services/user.service';
import { User } from '@redaction/red-ui-http';
import { NotificationService } from '@services/notification.service';
@Component({
selector: 'redaction-dossier-details',
@ -16,6 +17,8 @@ import { FilterModel } from '@shared/components/filters/popup-filter/model/filte
})
export class DossierDetailsComponent implements OnInit {
documentsChartData: DoughnutChartConfig[] = [];
owner: User;
editingOwner = false;
@Input() filters: { needsWorkFilters: FilterModel[]; statusFilters: FilterModel[] };
@Output() filtersChanged = new EventEmitter();
@Output() openAssignDossierMembersDialog = new EventEmitter();
@ -27,8 +30,8 @@ export class DossierDetailsComponent implements OnInit {
readonly translateChartService: TranslateChartService,
readonly permissionsService: PermissionsService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _dialogService: DossiersDialogService,
private readonly _router: Router
private readonly _userService: UserService,
private readonly _notificationService: NotificationService
) {}
get memberIds(): string[] {
@ -39,7 +42,12 @@ export class DossierDetailsComponent implements OnInit {
return this.appStateService.activeDossier.hasFiles;
}
get managers() {
return this._userService.managerUsers;
}
ngOnInit(): void {
this.owner = this._userService.getUserById(this.appStateService.activeDossier.ownerId);
this.calculateChartConfig();
this.appStateService.fileChanged.subscribe(() => {
this.calculateChartConfig();
@ -47,23 +55,25 @@ export class DossierDetailsComponent implements OnInit {
}
calculateChartConfig(): void {
if (this.appStateService.activeDossier) {
const groups = groupBy(this.appStateService.activeDossier?.files, 'status');
this.documentsChartData = [];
for (const key of Object.keys(groups)) {
this.documentsChartData.push({
value: groups[key].length,
color: key,
label: key,
key: key
});
}
this.documentsChartData.sort((a, b) => StatusSorter[a.key] - StatusSorter[b.key]);
this.documentsChartData = this.translateChartService.translateStatus(
this.documentsChartData
);
this._changeDetectorRef.detectChanges();
if (!this.appStateService.activeDossier) {
return;
}
const groups = groupBy(this.appStateService.activeDossier?.files, 'status');
this.documentsChartData = [];
for (const key of Object.keys(groups)) {
this.documentsChartData.push({
value: groups[key].length,
color: key,
label: key,
key: key
});
}
this.documentsChartData.sort((a, b) => StatusSorter[a.key] - StatusSorter[b.key]);
this.documentsChartData = this.translateChartService.translateStatus(
this.documentsChartData
);
this._changeDetectorRef.detectChanges();
}
toggleFilter(filterType: 'needsWorkFilters' | 'statusFilters', key: string): void {
@ -71,4 +81,16 @@ export class DossierDetailsComponent implements OnInit {
filter.checked = !filter.checked;
this.filtersChanged.emit(this.filters);
}
async assignOwner(user: User | string) {
this.owner = typeof user === 'string' ? this._userService.getUserById(user) : user;
const dw = Object.assign({}, this.appStateService.activeDossier);
dw.dossier.ownerId = this.owner.userId;
await this.appStateService.addOrUpdateDossier(dw.dossier);
const ownerName = this._userService.getNameForId(this.owner.userId);
const dossierName = this.appStateService.activeDossier.name;
const msg = 'Successfully assigned ' + ownerName + ' to dossier: ' + dossierName;
this._notificationService.showToastNotification(msg);
}
}

View File

@ -100,10 +100,6 @@ cdk-virtual-scroll-viewport {
margin-left: 6px;
}
.ml-14 {
margin-left: 14px;
}
.mr-4 {
margin-right: 4px;
}

View File

@ -39,121 +39,58 @@
<div *ngIf="viewReady" class="flex-1 actions-container">
<ng-container *ngIf="!appStateService.activeFile.isExcluded">
<redaction-status-bar
[config]="[
{
length: 1,
color: appStateService.activeFile.status
}
]"
[small]="true"
>
<redaction-status-bar [config]="statusBarConfig" [small]="true">
</redaction-status-bar>
<div class="all-caps-label mr-16 ml-8">
{{ appStateService.activeFile.status | translate }}
<span
*ngIf="
appStateService.activeFile.status === 'UNDER_REVIEW' ||
appStateService.activeFile.status === 'UNDER_APPROVAL'
"
>{{ 'by' | translate }}:</span
>
{{ status | translate }}
<span *ngIf="isUnderReviewOrApproval">{{ 'by' | translate }}:</span>
</div>
<ng-container *ngIf="!editingReviewer">
<redaction-initials-avatar
*ngIf="!editingReviewer"
[userId]="appStateService.activeFile.currentReviewer"
[withName]="!!appStateService.activeFile.currentReviewer"
tooltipPosition="below"
></redaction-initials-avatar>
<div
(click)="editingReviewer = true"
*ngIf="
!editingReviewer &&
!appStateService.activeFile.currentReviewer &&
permissionsService.canAssignUser() &&
appStateService.activeDossier.hasMoreThanOneReviewer
"
class="assign-reviewer pointer"
translate="file-preview.assign-reviewer"
></div>
</ng-container>
<redaction-initials-avatar
*ngIf="!editingReviewer"
[userId]="currentReviewer"
[withName]="!!currentReviewer"
tooltipPosition="below"
></redaction-initials-avatar>
<div
(click)="editingReviewer = true"
*ngIf="!editingReviewer && canAssignReviewer"
class="assign-reviewer pointer"
translate="file-preview.assign-reviewer"
></div>
</ng-container>
<ng-container *ngIf="editingReviewer">
<form [formGroup]="reviewerForm">
<div class="red-input-group w-250">
<mat-select formControlName="reviewer">
<mat-select-trigger>
<redaction-initials-avatar
[userId]="reviewerForm.get('reviewer').value"
[withName]="true"
size="small"
></redaction-initials-avatar>
</mat-select-trigger>
<mat-option
*ngFor="let userId of singleUsersSelectOptions"
[value]="userId"
>
<redaction-initials-avatar
[userId]="userId"
[withName]="true"
size="small"
></redaction-initials-avatar>
</mat-option>
</mat-select>
</div>
</form>
</ng-container>
<redaction-assign-user-dropdown
*ngIf="editingReviewer"
[value]="currentReviewer"
[options]="singleUsersSelectOptions"
(save)="assignReviewer($event)"
(cancel)="editingReviewer = false"
></redaction-assign-user-dropdown>
<div
*ngIf="permissionsService.canAssignUser() || permissionsService.canAssignToSelf()"
*ngIf="
!editingReviewer &&
(permissionsService.canAssignUser() || permissionsService.canAssignToSelf())
"
class="assign-actions-wrapper"
>
<redaction-circle-button
(action)="editingReviewer = true"
*ngIf="
!editingReviewer &&
permissionsService.canAssignUser() &&
appStateService.activeFile.currentReviewer
"
*ngIf="permissionsService.canAssignUser() && currentReviewer"
[tooltip]="assignTooltip"
icon="red:edit"
tooltipPosition="below"
>
</redaction-circle-button>
<redaction-circle-button
(action)="assignReviewer()"
*ngIf="editingReviewer"
icon="red:check"
tooltip="file-preview.save-assign-reviewer"
tooltipPosition="below"
>
</redaction-circle-button>
<redaction-circle-button
(action)="editingReviewer = false; resetReviewerForm()"
*ngIf="editingReviewer"
icon="red:close"
tooltip="file-preview.cancel-assign-reviewer"
tooltipPosition="below"
>
</redaction-circle-button>
></redaction-circle-button>
<redaction-circle-button
(action)="assignToMe()"
*ngIf="
!editingReviewer &&
permissionsService.canAssignToSelf() &&
!permissionsService.isFileReviewer()
"
*ngIf="permissionsService.canAssignToSelf()"
icon="red:assign-me"
tooltip="file-preview.assign-me"
tooltipPosition="below"
>
</redaction-circle-button>
></redaction-circle-button>
</div>
<ng-container

View File

@ -7,7 +7,7 @@ import {
OnInit,
ViewChild
} from '@angular/core';
import { ActivatedRoute, ActivatedRouteSnapshot, Router } from '@angular/router';
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationExtras, Router } from '@angular/router';
import { AppStateService } from '@state/app-state.service';
import { Annotations, WebViewerInstance } from '@pdftron/webviewer';
import { PdfViewerComponent } from '../../components/pdf-viewer/pdf-viewer.component';
@ -27,10 +27,11 @@ import { PermissionsService } from '@services/permissions.service';
import { Subscription, timer } from 'rxjs';
import { UserPreferenceService } from '@services/user-preference.service';
import { UserService } from '@services/user.service';
import { FormBuilder, FormGroup } from '@angular/forms';
import {
FileManagementControllerService,
FileStatus,
StatusControllerService,
User,
UserPreferenceControllerService
} from '@redaction/red-ui-http';
import { PdfViewerDataService } from '../../services/pdf-viewer-data.service';
@ -57,7 +58,6 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
viewMode: ViewMode = 'STANDARD';
fullScreen = false;
editingReviewer = false;
reviewerForm: FormGroup;
shouldDeselectAnnotationsOnPageChange = true;
analysisProgressInSeconds = 0;
analysisProgress: number;
@ -96,15 +96,10 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
private readonly _annotationDrawService: AnnotationDrawService,
private readonly _fileActionService: FileActionService,
private readonly _fileDownloadService: PdfViewerDataService,
private readonly _formBuilder: FormBuilder,
private readonly _statusControllerService: StatusControllerService,
private readonly _ngZone: NgZone,
private readonly _fileManagementControllerService: FileManagementControllerService
) {
this.reviewerForm = this._formBuilder.group({
reviewer: [this.appStateService.activeFile.currentReviewer]
});
document.documentElement.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) {
this.fullScreen = false;
@ -253,9 +248,9 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
rebuildFilters(deletePreviousAnnotations: boolean = false) {
const startTime = new Date().getTime();
if (deletePreviousAnnotations) {
const annotationsToDelete = this.activeViewer?.annotManager?.getAnnotationsList() || [];
const annotationsToDelete = this._instance?.annotManager?.getAnnotationsList() || [];
try {
this.activeViewer?.annotManager?.deleteAnnotations(annotationsToDelete, {
this._instance?.annotManager?.deleteAnnotations(annotationsToDelete, {
imported: true,
force: true
});
@ -339,10 +334,10 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
$event,
async (response: ManualAnnotationResponse) => {
if (response?.annotationId) {
const annotation = this.activeViewer.annotManager.getAnnotationById(
const annotation = this._instance.annotManager.getAnnotationById(
response.manualRedactionEntryWrapper.rectId
);
this.activeViewer.annotManager.deleteAnnotation(annotation);
this._instance.annotManager.deleteAnnotation(annotation);
this.fileData.fileStatus = await this.appStateService.reloadActiveFile();
const distinctPages = $event.manualRedactionEntry.positions
.map(p => p.page)
@ -396,21 +391,23 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
}
viewerPageChanged($event: any) {
if (typeof $event === 'number') {
this._scrollViews();
if (!this._workloadComponent?.multiSelectActive) {
this.shouldDeselectAnnotationsOnPageChange = true;
}
// Add current page in URL query params
this._router.navigate([], {
queryParams: {
page: $event
},
queryParamsHandling: 'merge'
});
this._changeDetectorRef.detectChanges();
if (typeof $event !== 'number') {
return;
}
this._scrollViews();
if (!this._workloadComponent.multiSelectActive) {
this.shouldDeselectAnnotationsOnPageChange = true;
}
// Add current page in URL query params
const extras: NavigationExtras = {
queryParams: { page: $event },
queryParamsHandling: 'merge'
};
this._router.navigate([], extras).then();
this._changeDetectorRef.detectChanges();
}
viewerReady($event: WebViewerInstance) {
@ -461,41 +458,29 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
async assignToMe() {
await this._fileActionService.assignToMe(this.fileData.fileStatus, async () => {
await this.appStateService.reloadActiveFile();
this.resetReviewerForm();
this._updateCanPerformActions();
});
}
assignReviewer() {
const reviewerId = this.reviewerForm.get('reviewer').value;
async assignReviewer(user: User | string) {
const reviewerId = typeof user === 'string' ? user : user.userId;
const reviewerName = this.userService.getNameForId(reviewerId);
this._statusControllerService
.setFileReviewer(
this.fileData.fileStatus.dossierId,
this.fileData.fileStatus.fileId,
reviewerId
)
.subscribe(async () => {
this._notificationService.showToastNotification(
'Successfully assigned ' +
this.userService.getNameForId(reviewerId) +
' to file: ' +
this.fileData.fileStatus.filename
);
await this.appStateService.reloadActiveFile();
this._updateCanPerformActions();
this.editingReviewer = false;
});
const { dossierId, fileId, filename } = this.fileData.fileStatus;
await this._statusControllerService
.setFileReviewer(dossierId, fileId, reviewerId)
.toPromise();
const msg = `Successfully assigned ${reviewerName} to file: ${filename}`;
this._notificationService.showToastNotification(msg);
await this.appStateService.reloadActiveFile();
this._updateCanPerformActions();
this.editingReviewer = false;
}
resetReviewerForm() {
this.reviewerForm.setValue({ reviewer: this.appStateService.activeFile.currentReviewer });
}
/* Close fullscreen */
closeFullScreen() {
if (!!document.fullscreenElement && document.exitFullscreen) {
document.exitFullscreen();
document.exitFullscreen().then();
}
}
@ -529,6 +514,30 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
return false;
}
get currentReviewer(): string {
return this.appStateService.activeFile.currentReviewer;
}
get status(): FileStatus.StatusEnum {
return this.appStateService.activeFile.status;
}
get statusBarConfig(): [{ length: number; color: FileStatus.StatusEnum }] {
return [{ length: 1, color: this.status }];
}
get isUnderReviewOrApproval(): boolean {
return this.status === 'UNDER_REVIEW' || this.status === 'UNDER_APPROVAL';
}
get canAssignReviewer(): boolean {
return (
!this.currentReviewer &&
this.permissionsService.canAssignUser() &&
this.appStateService.activeDossier.hasMoreThanOneReviewer
);
}
// <!-- Dev Mode Features-->
async openSSRFilePreview() {
window.open(`/pdf-preview/${this.dossierId}/${this.fileId}`, '_blank');
@ -664,9 +673,9 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
}
private _findAndDeleteAnnotation(id: string) {
const viewerAnnotation = this.activeViewer.annotManager.getAnnotationById(id);
const viewerAnnotation = this._instance.annotManager.getAnnotationById(id);
if (viewerAnnotation) {
this.activeViewer.annotManager.deleteAnnotation(viewerAnnotation, {
this._instance.annotManager.deleteAnnotation(viewerAnnotation, {
imported: true,
force: true
});

View File

@ -0,0 +1,36 @@
<div class="flex-align-items-center space-between">
<div class="red-input-group w-250">
<mat-select [(ngModel)]="value">
<mat-select-trigger>
<ng-container *ngTemplateOutlet="avatar; context: getContext(value)"></ng-container>
</mat-select-trigger>
<mat-option *ngFor="let user of options" [value]="user">
<ng-container *ngTemplateOutlet="avatar; context: getContext(user)"></ng-container>
</mat-option>
</mat-select>
</div>
<redaction-circle-button
(action)="save.emit(value)"
icon="red:check"
tooltip="assign-user.save"
tooltipPosition="below"
></redaction-circle-button>
<redaction-circle-button
(action)="value = oldUser; cancel.emit()"
icon="red:close"
tooltip="assign-user.cancel"
tooltipPosition="below"
></redaction-circle-button>
</div>
<ng-template #avatar let-user="user" let-userId="userId">
<redaction-initials-avatar
[user]="user"
[userId]="userId"
[withName]="true"
color="gray"
size="large"
></redaction-initials-avatar>
</ng-template>

View File

@ -0,0 +1,10 @@
.space-between {
justify-content: space-between;
}
.red-input-group mat-select {
display: flex;
flex-direction: column;
justify-content: center;
height: 40px;
}

View File

@ -0,0 +1,34 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { User } from '@redaction/red-ui-http';
import { UserService } from '@services/user.service';
@Component({
selector: 'redaction-assign-user-dropdown',
templateUrl: './assign-user-dropdown.component.html',
styleUrls: ['./assign-user-dropdown.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AssignUserDropdownComponent {
private _currentUser: User | string;
oldUser: User | string;
@Input() options: (User | string)[];
@Output() save = new EventEmitter<User | string>();
@Output() cancel = new EventEmitter<never>();
@Input()
set value(value: User | string) {
if (this.oldUser === undefined) this.oldUser = value;
this._currentUser = value;
}
get value(): User | string {
return this._currentUser;
}
constructor(private readonly _userService: UserService) {}
getContext(user: User | string) {
return typeof user === 'string' ? { userId: user } : { user: user };
}
}

View File

@ -36,6 +36,7 @@ import { SideNavComponent } from '@shared/components/side-nav/side-nav.component
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
import { QuickFiltersComponent } from './components/filters/quick-filters/quick-filters.component';
import { PopupFilterComponent } from '@shared/components/filters/popup-filter/popup-filter.component';
import { AssignUserDropdownComponent } from './components/assign-user-dropdown/assign-user-dropdown.component';
const buttons = [
ChevronButtonComponent,
@ -64,6 +65,8 @@ const components = [
SelectComponent,
SideNavComponent,
DictionaryManagerComponent,
QuickFiltersComponent,
AssignUserDropdownComponent,
...buttons
];
@ -85,9 +88,9 @@ const modules = [
];
@NgModule({
declarations: [...components, ...utils, QuickFiltersComponent],
declarations: [...components, ...utils],
imports: [CommonModule, ...modules, MonacoEditorModule],
exports: [...modules, ...components, ...utils, QuickFiltersComponent],
exports: [...modules, ...components, ...utils],
providers: [
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] },
{

View File

@ -121,12 +121,11 @@ export class PermissionsService {
!fileStatus.isError &&
!fileStatus.isApproved;
const isNotCurrentReviewer = fileStatus.currentReviewer !== this._userService.userId;
const isTheOnlyReviewer = !this._appStateService.activeDossier.hasMoreThanOneReviewer;
if (precondition) {
if (
(fileStatus.isUnassigned || (fileStatus.isUnderReview && isNotCurrentReviewer)) &&
(fileStatus.isUnassigned || (fileStatus.isUnderReview && !this.isFileReviewer())) &&
(this.isApprover() || isTheOnlyReviewer)
) {
return true;

View File

@ -386,8 +386,6 @@
"reviewer": "Assigned to",
"unassigned": "Unassigned",
"assign-reviewer": "Assign Reviewer",
"cancel-assign-reviewer": "Cancel",
"save-assign-reviewer": "Save",
"assign-me": "Assign to me",
"last-reviewer": "Last Reviewed by:",
"fullscreen": "Full Screen (F)",
@ -401,6 +399,10 @@
"jump-last": "Jump to last page"
}
},
"assign-user": {
"cancel": "Cancel",
"save": "Save"
},
"annotation-actions": {
"message": {
"manual-redaction": {

View File

@ -253,6 +253,10 @@ section.settings {
margin-bottom: 8px !important;
}
.ml-14 {
margin-left: 14px;
}
.pb-24 {
padding-bottom: 24px;
}