Pull request #312: Stats
Merge in RED/ui from stats to master * commit 'f47b47a16f3b39009ea5f606901d192f48eb234d': (21 commits) fix dossiers chart use observable file instead of fileData Display processing status Added new file statuses fix team members assignment renamed selected single user fixed comments extract file stats and workflow item from dossier-overview component reload files while uploading every 2.5 seconds fix file actions issues, live update on dossier change remove dossier stats from dossier remove fileChanged and fileReanalyzed reduce use of active file remove files from dossier ctor add new files map service move chart computations to dossier listing details component remove file stats from dossier split needs work badge into separate components update brace style add dossier stats ...
This commit is contained in:
commit
d404d0bd41
@ -17,6 +17,7 @@ trim_trailing_whitespace = false
|
||||
[*.ts]
|
||||
ij_typescript_use_double_quotes = false
|
||||
ij_typescript_enforce_trailing_comma = keep
|
||||
ij_typescript_spaces_within_imports = true
|
||||
|
||||
[{*.json, .prettierrc, .eslintrc}]
|
||||
indent_size = 2
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
translate="top-bar.navigation-items.dossiers"
|
||||
></a>
|
||||
|
||||
<ng-container *ngIf="dossiersService.activeDossier$ | async as dossier">
|
||||
<ng-container *ngIf="activeDossier$ | async as dossier">
|
||||
<mat-icon svgIcon="iqser:arrow-right"></mat-icon>
|
||||
|
||||
<a
|
||||
|
||||
@ -8,7 +8,7 @@ import { FileDownloadService } from '@upload-download/services/file-download.ser
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { SpotlightSearchAction } from '@components/spotlight-search/spotlight-search-action';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { filter, map, startWith } from 'rxjs/operators';
|
||||
import { filter, map, startWith, switchMap } from 'rxjs/operators';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { shareDistinctLast } from '@iqser/common-ui';
|
||||
|
||||
@ -71,6 +71,7 @@ export class BaseScreenComponent {
|
||||
action: (query): void => this._search(query),
|
||||
},
|
||||
];
|
||||
readonly activeDossier$ = this.dossiersService.activeDossierId$.pipe(switchMap(id => this.dossiersService.getEntityChanged$(id)));
|
||||
private readonly _navigationStart$ = this._router.events.pipe(
|
||||
filter(isNavigationStart),
|
||||
map((event: NavigationStart) => event.url),
|
||||
|
||||
@ -6,6 +6,8 @@ import { AnnotationPermissions } from '@models/file/annotation.permissions';
|
||||
import { AnnotationActionsService } from '../../services/annotation-actions.service';
|
||||
import { WebViewerInstance } from '@pdftron/webviewer';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
|
||||
export const AnnotationButtonTypes = {
|
||||
dark: 'dark',
|
||||
@ -27,13 +29,18 @@ export class AnnotationActionsComponent implements OnInit {
|
||||
@Input() alwaysVisible: boolean;
|
||||
@Output() annotationsChanged = new EventEmitter<AnnotationWrapper>();
|
||||
annotationPermissions: AnnotationPermissions;
|
||||
readonly dossierId: string;
|
||||
|
||||
constructor(
|
||||
readonly appStateService: AppStateService,
|
||||
readonly annotationActionsService: AnnotationActionsService,
|
||||
private readonly _permissionsService: PermissionsService,
|
||||
private readonly _userService: UserService,
|
||||
) {}
|
||||
private readonly _dossiersService: DossiersService,
|
||||
activatedRoute: ActivatedRoute,
|
||||
) {
|
||||
this.dossierId = activatedRoute.snapshot.paramMap.get('dossierId');
|
||||
}
|
||||
|
||||
private _annotations: AnnotationWrapper[];
|
||||
|
||||
@ -94,14 +101,6 @@ export class AnnotationActionsComponent implements OnInit {
|
||||
this.annotationActionsService.updateHiddenAnnotation(this.annotations, this.viewerAnnotations, false);
|
||||
}
|
||||
|
||||
private _setPermissions() {
|
||||
this.annotationPermissions = AnnotationPermissions.forUser(
|
||||
this._permissionsService.isApprover(),
|
||||
this._userService.currentUser,
|
||||
this.annotations,
|
||||
);
|
||||
}
|
||||
|
||||
resize($event: MouseEvent) {
|
||||
this.annotationActionsService.resize($event, this.viewer, this.annotations[0]);
|
||||
}
|
||||
@ -113,4 +112,13 @@ export class AnnotationActionsComponent implements OnInit {
|
||||
cancelResize($event: MouseEvent) {
|
||||
this.annotationActionsService.cancelResize($event, this.viewer, this.annotations[0], this.annotationsChanged);
|
||||
}
|
||||
|
||||
private _setPermissions() {
|
||||
const dossier = this._dossiersService.find(this.dossierId);
|
||||
this.annotationPermissions = AnnotationPermissions.forUser(
|
||||
this._permissionsService.isApprover(dossier),
|
||||
this._userService.currentUser,
|
||||
this.annotations,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,32 +1,34 @@
|
||||
<div *ngFor="let comment of annotation.comments; trackBy: trackBy" class="comment">
|
||||
<div class="comment-details-wrapper">
|
||||
<div [matTooltipPosition]="'above'" [matTooltip]="comment.date | date: 'exactDate'" class="small-label">
|
||||
<strong> {{ comment.user | name }} </strong>
|
||||
{{ comment.date | date: 'sophisticatedDate' }}
|
||||
<ng-container *ngIf="file$ | async as file">
|
||||
<div *ngFor="let comment of annotation.comments; trackBy: trackBy" class="comment">
|
||||
<div class="comment-details-wrapper">
|
||||
<div [matTooltipPosition]="'above'" [matTooltip]="comment.date | date: 'exactDate'" class="small-label">
|
||||
<strong> {{ comment.user | name }} </strong>
|
||||
{{ comment.date | date: 'sophisticatedDate' }}
|
||||
</div>
|
||||
|
||||
<div class="comment-actions">
|
||||
<iqser-circle-button
|
||||
(action)="deleteComment(comment)"
|
||||
*ngIf="permissionsService.canDeleteComment(comment, file)"
|
||||
[iconSize]="10"
|
||||
[size]="20"
|
||||
class="pointer"
|
||||
icon="iqser:trash"
|
||||
></iqser-circle-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comment-actions">
|
||||
<iqser-circle-button
|
||||
(action)="deleteComment(comment)"
|
||||
*ngIf="permissionsService.canDeleteComment(comment)"
|
||||
[iconSize]="10"
|
||||
[size]="20"
|
||||
class="pointer"
|
||||
icon="iqser:trash"
|
||||
></iqser-circle-button>
|
||||
</div>
|
||||
<div>{{ comment.text }}</div>
|
||||
</div>
|
||||
|
||||
<div>{{ comment.text }}</div>
|
||||
</div>
|
||||
|
||||
<iqser-input-with-action
|
||||
(action)="addComment($event)"
|
||||
*ngIf="permissionsService.canAddComment()"
|
||||
[placeholder]="'comments.add-comment' | translate"
|
||||
autocomplete="off"
|
||||
icon="iqser:collapse"
|
||||
width="full"
|
||||
></iqser-input-with-action>
|
||||
<iqser-input-with-action
|
||||
(action)="addComment($event)"
|
||||
*ngIf="permissionsService.canAddComment(file)"
|
||||
[placeholder]="'comments.add-comment' | translate"
|
||||
autocomplete="off"
|
||||
icon="iqser:collapse"
|
||||
width="full"
|
||||
></iqser-input-with-action>
|
||||
</ng-container>
|
||||
|
||||
<div (click)="toggleExpandComments($event)" class="all-caps-label pointer hide-comments" translate="comments.hide-comments"></div>
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, ViewChild } from '@angular/core';
|
||||
import { IComment } from '@red/domain';
|
||||
import { File, IComment } from '@red/domain';
|
||||
import { ManualAnnotationService } from '../../services/manual-annotation.service';
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { InputWithActionComponent, trackBy } from '@iqser/common-ui';
|
||||
import { FilesMapService } from '@services/entity-services/files-map.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-comments',
|
||||
@ -15,6 +18,7 @@ import { InputWithActionComponent, trackBy } from '@iqser/common-ui';
|
||||
export class CommentsComponent {
|
||||
@Input() annotation: AnnotationWrapper;
|
||||
readonly trackBy = trackBy();
|
||||
readonly file$: Observable<File>;
|
||||
@HostBinding('class.hidden') private _hidden = true;
|
||||
@ViewChild(InputWithActionComponent) private readonly _input: InputWithActionComponent;
|
||||
|
||||
@ -23,7 +27,13 @@ export class CommentsComponent {
|
||||
private readonly _userService: UserService,
|
||||
private readonly _manualAnnotationService: ManualAnnotationService,
|
||||
private readonly _changeDetectorRef: ChangeDetectorRef,
|
||||
) {}
|
||||
readonly filesMapService: FilesMapService,
|
||||
activatedRoute: ActivatedRoute,
|
||||
) {
|
||||
const fileId = activatedRoute.snapshot.paramMap.get('fileId');
|
||||
const dossierId = activatedRoute.snapshot.paramMap.get('dossierId');
|
||||
this.file$ = filesMapService.watch$(dossierId, fileId);
|
||||
}
|
||||
|
||||
addComment(value: string): void {
|
||||
if (!value) {
|
||||
|
||||
@ -26,8 +26,8 @@
|
||||
</div>
|
||||
|
||||
<div class="right-content">
|
||||
<div *ngIf="isReadOnly" [class.justify-center]="!isProcessing" class="read-only d-flex">
|
||||
<div *ngIf="isProcessing" class="flex-align-items-center">
|
||||
<div *ngIf="isReadOnly" [class.justify-center]="!file.isProcessing" class="read-only d-flex">
|
||||
<div *ngIf="file.isProcessing" class="flex-align-items-center">
|
||||
<span [translate]="'file-status.processing'" class="read-only-text"></span>
|
||||
<mat-progress-bar [mode]="'indeterminate'" class="w-100"></mat-progress-bar>
|
||||
</div>
|
||||
@ -87,14 +87,15 @@
|
||||
*ngFor="let pageNumber of displayedPages"
|
||||
[activeSelection]="pageHasSelection(pageNumber)"
|
||||
[active]="pageNumber === activeViewerPage"
|
||||
[file]="file"
|
||||
[number]="pageNumber"
|
||||
[showDottedIcon]="hasOnlyManualRedactionsAndNotExcluded(pageNumber)"
|
||||
[viewedPages]="fileData?.viewedPages"
|
||||
[viewedPages]="viewedPages"
|
||||
></redaction-page-indicator>
|
||||
</div>
|
||||
<div
|
||||
(click)="scrollQuickNavLast()"
|
||||
[class.disabled]="activeViewerPage === fileData?.file?.numberOfPages"
|
||||
[class.disabled]="activeViewerPage === file?.numberOfPages"
|
||||
[matTooltip]="'file-preview.quick-nav.jump-last' | translate"
|
||||
class="jump"
|
||||
matTooltipPosition="above"
|
||||
@ -194,7 +195,7 @@
|
||||
<redaction-page-exclusion
|
||||
(actionPerformed)="actionPerformed.emit($event)"
|
||||
*ngIf="excludePages"
|
||||
[file]="fileData.file"
|
||||
[file]="file"
|
||||
></redaction-page-exclusion>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,14 +1,25 @@
|
||||
import { ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Input, Output, TemplateRef, ViewChild } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Input,
|
||||
Output,
|
||||
TemplateRef,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
import { AnnotationProcessingService } from '../../services/annotation-processing.service';
|
||||
import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
|
||||
import scrollIntoView from 'scroll-into-view-if-needed';
|
||||
import { CircleButtonTypes, Debounce, FilterService, IconButtonTypes, INestedFilter, IqserEventTarget } from '@iqser/common-ui';
|
||||
import { FileDataModel } from '@models/file/file-data.model';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { WebViewerInstance } from '@pdftron/webviewer';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { File, IViewedPage } from '@red/domain';
|
||||
|
||||
const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape'];
|
||||
const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
|
||||
@ -17,6 +28,7 @@ const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
|
||||
selector: 'redaction-file-workload',
|
||||
templateUrl: './file-workload.component.html',
|
||||
styleUrls: ['./file-workload.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FileWorkloadComponent {
|
||||
readonly iconButtonTypes = IconButtonTypes;
|
||||
@ -27,7 +39,8 @@ export class FileWorkloadComponent {
|
||||
@Input() activeViewerPage: number;
|
||||
@Input() shouldDeselectAnnotationsOnPageChange: boolean;
|
||||
@Input() dialogRef: MatDialogRef<unknown>;
|
||||
@Input() fileData: FileDataModel;
|
||||
@Input() viewedPages: IViewedPage[];
|
||||
@Input() file: File;
|
||||
@Input() hideSkipped: boolean;
|
||||
@Input() excludePages: boolean;
|
||||
@Input() annotationActionsTemplate: TemplateRef<unknown>;
|
||||
@ -78,20 +91,16 @@ export class FileWorkloadComponent {
|
||||
}
|
||||
}
|
||||
|
||||
get isProcessing(): boolean {
|
||||
return this.fileData?.file?.isProcessing;
|
||||
}
|
||||
|
||||
get activeAnnotations(): AnnotationWrapper[] | undefined {
|
||||
return this.displayedAnnotations.get(this.activeViewerPage);
|
||||
}
|
||||
|
||||
get isReadOnly(): boolean {
|
||||
return !this._permissionsService.canPerformAnnotationActions();
|
||||
return !this._permissionsService.canPerformAnnotationActions(this.file);
|
||||
}
|
||||
|
||||
get currentPageIsExcluded(): boolean {
|
||||
return this.fileData?.file?.excludedPages?.includes(this.activeViewerPage);
|
||||
return this.file?.excludedPages?.includes(this.activeViewerPage);
|
||||
}
|
||||
|
||||
private get _firstSelectedAnnotation() {
|
||||
@ -120,7 +129,7 @@ export class FileWorkloadComponent {
|
||||
|
||||
hasOnlyManualRedactionsAndNotExcluded(pageNumber: number): boolean {
|
||||
const hasOnlyManualRedactions = this.displayedAnnotations.get(pageNumber).every(annotation => annotation.manual);
|
||||
return hasOnlyManualRedactions && this.fileData.file.excludedPages.includes(pageNumber);
|
||||
return hasOnlyManualRedactions && this.file.excludedPages.includes(pageNumber);
|
||||
}
|
||||
|
||||
pageHasSelection(page: number) {
|
||||
@ -171,7 +180,7 @@ export class FileWorkloadComponent {
|
||||
this._navigatePages($event);
|
||||
}
|
||||
|
||||
this._changeDetectorRef.detectChanges();
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
scrollAnnotations(): void {
|
||||
@ -212,7 +221,7 @@ export class FileWorkloadComponent {
|
||||
}
|
||||
|
||||
scrollQuickNavLast(): void {
|
||||
this.selectPage.emit(this.fileData.file.numberOfPages);
|
||||
this.selectPage.emit(this.file.numberOfPages);
|
||||
}
|
||||
|
||||
pageSelectedByClick($event: number): void {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div *ngIf="permissionsService.canExcludePages()" class="exclude-pages-input-container">
|
||||
<div *ngIf="permissionsService.canExcludePages(file)" class="exclude-pages-input-container">
|
||||
<iqser-input-with-action
|
||||
(action)="excludePagesRange($event)"
|
||||
[hint]="'file-preview.tabs.exclude-pages.hint' | translate"
|
||||
@ -21,7 +21,7 @@
|
||||
<ng-container *ngIf="range.startPage !== range.endPage"> {{ range.startPage }} -{{ range.endPage }} </ng-container>
|
||||
<iqser-circle-button
|
||||
(action)="includePagesRange(range)"
|
||||
*ngIf="permissionsService.canExcludePages()"
|
||||
*ngIf="permissionsService.canExcludePages(file)"
|
||||
[tooltip]="'file-preview.tabs.exclude-pages.put-back' | translate"
|
||||
icon="red:undo"
|
||||
></iqser-circle-button>
|
||||
@ -29,7 +29,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="!permissionsService.canExcludePages() && !excludedPagesRanges.length"
|
||||
*ngIf="!permissionsService.canExcludePages(file) && !excludedPagesRanges.length"
|
||||
class="no-excluded heading-l"
|
||||
translate="file-preview.tabs.exclude-pages.no-excluded"
|
||||
></div>
|
||||
|
||||
@ -1,21 +1,11 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnDestroy, Output } from '@angular/core';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { ViewedPagesService } from '../../shared/services/viewed-pages.service';
|
||||
import { IViewedPage } from '@red/domain';
|
||||
import { File, IViewedPage } from '@red/domain';
|
||||
import { AutoUnsubscribe } from '@iqser/common-ui';
|
||||
import { FilesMapService } from '@services/entity-services/files-map.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-page-indicator',
|
||||
@ -23,8 +13,13 @@ import { AutoUnsubscribe } from '@iqser/common-ui';
|
||||
styleUrls: ['./page-indicator.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PageIndicatorComponent extends AutoUnsubscribe implements OnChanges, OnInit, OnDestroy {
|
||||
@Input() active: boolean;
|
||||
export class PageIndicatorComponent extends AutoUnsubscribe implements OnDestroy, OnChanges {
|
||||
@Input()
|
||||
file: File;
|
||||
|
||||
@Input()
|
||||
active = false;
|
||||
|
||||
@Input() showDottedIcon = false;
|
||||
@Input() number: number;
|
||||
@Input() viewedPages: IViewedPage[];
|
||||
@ -33,11 +28,10 @@ export class PageIndicatorComponent extends AutoUnsubscribe implements OnChanges
|
||||
@Output() readonly pageSelected = new EventEmitter<number>();
|
||||
|
||||
pageReadTimeout: number = null;
|
||||
canMarkPagesAsViewed: boolean;
|
||||
|
||||
constructor(
|
||||
private readonly _viewedPagesService: ViewedPagesService,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _filesMapService: FilesMapService,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
private readonly _configService: ConfigService,
|
||||
private readonly _permissionService: PermissionsService,
|
||||
@ -58,23 +52,12 @@ export class PageIndicatorComponent extends AutoUnsubscribe implements OnChanges
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.addSubscription = this._appStateService.fileChanged$.subscribe(() => {
|
||||
if (this.canMarkPagesAsViewed !== this._permissionService.canMarkPagesAsViewed()) {
|
||||
this.canMarkPagesAsViewed = this._permissionService.canMarkPagesAsViewed();
|
||||
this._handlePageRead();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.active) {
|
||||
this._handlePageRead();
|
||||
}
|
||||
ngOnChanges() {
|
||||
this.handlePageRead();
|
||||
}
|
||||
|
||||
async toggleReadState() {
|
||||
if (this.canMarkPagesAsViewed) {
|
||||
if (this._permissionService.canMarkPagesAsViewed(this.file)) {
|
||||
if (this.read) {
|
||||
await this._markPageUnread();
|
||||
} else {
|
||||
@ -83,8 +66,8 @@ export class PageIndicatorComponent extends AutoUnsubscribe implements OnChanges
|
||||
}
|
||||
}
|
||||
|
||||
private _handlePageRead() {
|
||||
if (!this.canMarkPagesAsViewed) {
|
||||
handlePageRead() {
|
||||
if (!this._permissionService.canMarkPagesAsViewed(this.file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -119,20 +102,16 @@ export class PageIndicatorComponent extends AutoUnsubscribe implements OnChanges
|
||||
// }
|
||||
|
||||
private async _markPageRead() {
|
||||
await this._viewedPagesService
|
||||
.addPage({ page: this.number }, this._dossiersService.activeDossierId, this._appStateService.activeFileId)
|
||||
.toPromise();
|
||||
await this._viewedPagesService.addPage({ page: this.number }, this.file.dossierId, this.file.fileId).toPromise();
|
||||
if (this.activePage) {
|
||||
this.activePage.hasChanges = false;
|
||||
} else {
|
||||
this.viewedPages?.push({ page: this.number, fileId: this._appStateService.activeFileId });
|
||||
this.viewedPages?.push({ page: this.number, fileId: this.file.fileId });
|
||||
}
|
||||
}
|
||||
|
||||
private async _markPageUnread() {
|
||||
await this._viewedPagesService
|
||||
.removePage(this._dossiersService.activeDossierId, this._appStateService.activeFileId, this.number)
|
||||
.toPromise();
|
||||
await this._viewedPagesService.removePage(this.file.dossierId, this.file.fileId, this.number).toPromise();
|
||||
this.viewedPages?.splice(
|
||||
this.viewedPages?.findIndex(p => p.page === this.number),
|
||||
1,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<form (submit)="saveMembers()" [formGroup]="teamForm">
|
||||
<form (submit)="save()" [formGroup]="form">
|
||||
<div class="iqser-input-group w-300">
|
||||
<mat-form-field floatLabel="always">
|
||||
<mat-label>{{ 'assign-dossier-owner.dialog.single-user' | translate }}</mat-label>
|
||||
@ -28,15 +28,19 @@
|
||||
[canAdd]="false"
|
||||
[canRemove]="true"
|
||||
[largeSpacing]="true"
|
||||
[memberIds]="selectedReviewersList"
|
||||
[memberIds]="selectedReviewers$ | async"
|
||||
[perLine]="13"
|
||||
[unremovableMembers]="[selectedOwnerId]"
|
||||
></redaction-team-members>
|
||||
|
||||
<pre *ngIf="selectedReviewersList.length === 0" [innerHTML]="'assign-dossier-owner.dialog.no-reviewers' | translate" class="info"></pre>
|
||||
<pre
|
||||
*ngIf="(selectedReviewers$ | async).length === 0"
|
||||
[innerHTML]="'assign-dossier-owner.dialog.no-reviewers' | translate"
|
||||
class="info"
|
||||
></pre>
|
||||
|
||||
<iqser-input-with-action
|
||||
(valueChange)="setMembersSelectOptions()"
|
||||
(valueChange)="setMembersSelectOptions($event)"
|
||||
[(value)]="searchQuery"
|
||||
[placeholder]="'assign-dossier-owner.dialog.search' | translate"
|
||||
[width]="560"
|
||||
|
||||
@ -1,55 +1,75 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { Toaster } from '@iqser/common-ui';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { Dossier, IDossier, IDossierRequest } from '@red/domain';
|
||||
import { AutoUnsubscribe } from '@iqser/common-ui';
|
||||
import { EditDossierSectionInterface } from '../../dialogs/edit-dossier-dialog/edit-dossier-section.interface';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-team-members-manager',
|
||||
templateUrl: './team-members-manager.component.html',
|
||||
styleUrls: ['./team-members-manager.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TeamMembersManagerComponent implements OnInit {
|
||||
teamForm: FormGroup;
|
||||
export class TeamMembersManagerComponent extends AutoUnsubscribe implements EditDossierSectionInterface, OnInit, OnDestroy {
|
||||
form: FormGroup;
|
||||
searchQuery = '';
|
||||
|
||||
@Input() dossier: Dossier;
|
||||
@Output() readonly save = new EventEmitter<IDossier>();
|
||||
@Output() readonly updateDossier = new EventEmitter<IDossier>();
|
||||
|
||||
readonly ownersSelectOptions = this.userService.managerUsers.map(m => m.id);
|
||||
selectedReviewersList: string[] = [];
|
||||
membersSelectOptions: string[] = [];
|
||||
changed = false;
|
||||
readonly selectedReviewers$ = new BehaviorSubject<string[]>([]);
|
||||
|
||||
constructor(
|
||||
readonly userService: UserService,
|
||||
private readonly _toaster: Toaster,
|
||||
private readonly _formBuilder: FormBuilder,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
) {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
get selectedOwnerId(): string {
|
||||
return this.teamForm.get('owner').value;
|
||||
return this.form.get('owner').value;
|
||||
}
|
||||
|
||||
get selectedApproversList(): string[] {
|
||||
return this.teamForm.get('approvers').value;
|
||||
return this.form.get('approvers').value;
|
||||
}
|
||||
|
||||
get selectedMembersList(): string[] {
|
||||
return this.teamForm.get('members').value;
|
||||
return this.form.get('members').value;
|
||||
}
|
||||
|
||||
get valid(): boolean {
|
||||
return this.teamForm.valid;
|
||||
return this.form.valid;
|
||||
}
|
||||
|
||||
get disabled() {
|
||||
return !this.userService.currentUser.isManager;
|
||||
}
|
||||
|
||||
get changed() {
|
||||
if (this.dossier.ownerId !== this.selectedOwnerId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const initialMembers = [...this.dossier.memberIds].sort();
|
||||
const currentMembers = this.selectedMembersList.sort();
|
||||
|
||||
const initialApprovers = [...this.dossier.approverIds].sort();
|
||||
const currentApprovers = this.selectedApproversList.sort();
|
||||
return this._compareLists(initialMembers, currentMembers) || this._compareLists(initialApprovers, currentApprovers);
|
||||
}
|
||||
|
||||
isOwner(userId: string): boolean {
|
||||
return userId === this.selectedOwnerId;
|
||||
}
|
||||
|
||||
async saveMembers() {
|
||||
async save() {
|
||||
const dossier = {
|
||||
...this.dossier,
|
||||
memberIds: this.selectedMembersList,
|
||||
@ -59,8 +79,7 @@ export class TeamMembersManagerComponent implements OnInit {
|
||||
|
||||
const result = await this._dossiersService.createOrUpdate(dossier).toPromise();
|
||||
if (result) {
|
||||
this.save.emit(result);
|
||||
this._updateChanged();
|
||||
this.updateDossier.emit(result);
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,39 +131,25 @@ export class TeamMembersManagerComponent implements OnInit {
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
setMembersSelectOptions(): void {
|
||||
setMembersSelectOptions(value = this.searchQuery): void {
|
||||
this.membersSelectOptions = this.userService.eligibleUsers
|
||||
.filter(user => this.userService.getNameForId(user.id).toLowerCase().includes(this.searchQuery.toLowerCase()))
|
||||
.filter(user => this.userService.getNameForId(user.id).toLowerCase().includes(value.toLowerCase()))
|
||||
.filter(user => this.selectedOwnerId !== user.id)
|
||||
.map(user => user.id);
|
||||
}
|
||||
|
||||
private _updateChanged() {
|
||||
if (this.dossier.ownerId !== this.selectedOwnerId) {
|
||||
this.changed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const initialMembers = [...this.dossier.memberIds].sort();
|
||||
const currentMembers = this.selectedMembersList.sort();
|
||||
|
||||
const initialApprovers = [...this.dossier.approverIds].sort();
|
||||
const currentApprovers = this.selectedApproversList.sort();
|
||||
|
||||
this.changed = this._compareLists(initialMembers, currentMembers) || this._compareLists(initialApprovers, currentApprovers);
|
||||
}
|
||||
|
||||
private _setSelectedReviewersList() {
|
||||
this.selectedReviewersList = this.selectedMembersList.filter(m => this.selectedApproversList.indexOf(m) === -1);
|
||||
const selectedReviewers = this.selectedMembersList.filter(m => this.selectedApproversList.indexOf(m) === -1);
|
||||
this.selectedReviewers$.next(selectedReviewers);
|
||||
}
|
||||
|
||||
private _loadData() {
|
||||
this.teamForm = this._formBuilder.group({
|
||||
this.form = this._formBuilder.group({
|
||||
owner: [this.dossier?.ownerId, Validators.required],
|
||||
approvers: [[...this.dossier?.approverIds]],
|
||||
members: [[...this.dossier?.memberIds]],
|
||||
});
|
||||
this.teamForm.get('owner').valueChanges.subscribe(owner => {
|
||||
this.addSubscription = this.form.get('owner').valueChanges.subscribe(owner => {
|
||||
if (!this.isApprover(owner)) {
|
||||
this.toggleApprover(owner);
|
||||
}
|
||||
@ -157,7 +162,6 @@ export class TeamMembersManagerComponent implements OnInit {
|
||||
private _updateLists() {
|
||||
this._setSelectedReviewersList();
|
||||
this.setMembersSelectOptions();
|
||||
this._updateChanged();
|
||||
}
|
||||
|
||||
private _compareLists(l1: string[], l2: string[]) {
|
||||
|
||||
@ -7,13 +7,13 @@
|
||||
class="dialog-header heading-l"
|
||||
></div>
|
||||
|
||||
<form (submit)="save()" [formGroup]="usersForm">
|
||||
<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-select [placeholder]="'initials-avatar.unassigned' | translate" formControlName="singleUser">
|
||||
<mat-option *ngFor="let userId of singleUsersSelectOptions" [value]="userId">
|
||||
<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 } }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
@ -22,7 +22,7 @@
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button [disabled]="!usersForm.valid || !changed" color="primary" mat-flat-button type="submit">
|
||||
<button [disabled]="!form.valid || !changed" color="primary" mat-flat-button type="submit">
|
||||
{{ 'assign-owner.dialog.save' | translate }}
|
||||
</button>
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import { AppStateService } from '@state/app-state.service';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { Toaster } from '@iqser/common-ui';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { Dossier, File } from '@red/domain';
|
||||
import { File } from '@red/domain';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { FilesService } from '@services/entity-services/files.service';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
@ -12,7 +12,6 @@ import { PermissionsService } from '@services/permissions.service';
|
||||
|
||||
class DialogData {
|
||||
mode: 'approver' | 'reviewer';
|
||||
dossier?: Dossier;
|
||||
files?: File[];
|
||||
ignoreChanged?: boolean;
|
||||
}
|
||||
@ -22,8 +21,7 @@ class DialogData {
|
||||
styleUrls: ['./assign-reviewer-approver-dialog.component.scss'],
|
||||
})
|
||||
export class AssignReviewerApproverDialogComponent {
|
||||
usersForm: FormGroup;
|
||||
searchForm: FormGroup;
|
||||
form: FormGroup;
|
||||
|
||||
constructor(
|
||||
readonly userService: UserService,
|
||||
@ -39,15 +37,14 @@ export class AssignReviewerApproverDialogComponent {
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
get selectedSingleUser(): string {
|
||||
return this.usersForm.get('singleUser').value;
|
||||
get selectedUser(): string {
|
||||
return this.form.get('user').value;
|
||||
}
|
||||
|
||||
get singleUsersSelectOptions() {
|
||||
get userOptions() {
|
||||
const unassignUser = this._canUnassignFiles ? [undefined] : [];
|
||||
return this.data.mode === 'approver'
|
||||
? [...this._dossiersService.activeDossier.approverIds, ...unassignUser]
|
||||
: [...this._dossiersService.activeDossier.memberIds, ...unassignUser];
|
||||
const dossier = this._dossiersService.activeDossier;
|
||||
return this.data.mode === 'approver' ? [...dossier.approverIds, ...unassignUser] : [...dossier.memberIds, ...unassignUser];
|
||||
}
|
||||
|
||||
get changed(): boolean {
|
||||
@ -56,7 +53,7 @@ export class AssignReviewerApproverDialogComponent {
|
||||
}
|
||||
|
||||
for (const file of this.data.files) {
|
||||
if (file.currentReviewer !== this.selectedSingleUser) {
|
||||
if (file.currentReviewer !== this.selectedUser) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -69,19 +66,17 @@ export class AssignReviewerApproverDialogComponent {
|
||||
}
|
||||
|
||||
isOwner(userId: string): boolean {
|
||||
return userId === this.selectedSingleUser;
|
||||
return userId === this.selectedUser;
|
||||
}
|
||||
|
||||
async save() {
|
||||
try {
|
||||
const selectedUser = this.selectedSingleUser;
|
||||
|
||||
if (this.data.mode === 'reviewer') {
|
||||
await this._filesService
|
||||
.setReviewerFor(
|
||||
this.data.files.map(f => f.fileId),
|
||||
this._dossiersService.activeDossierId,
|
||||
selectedUser,
|
||||
this.selectedUser,
|
||||
)
|
||||
.toPromise();
|
||||
} else {
|
||||
@ -89,7 +84,7 @@ export class AssignReviewerApproverDialogComponent {
|
||||
.setUnderApprovalFor(
|
||||
this.data.files.map(f => f.fileId),
|
||||
this._dossiersService.activeDossierId,
|
||||
selectedUser,
|
||||
this.selectedUser,
|
||||
)
|
||||
.toPromise();
|
||||
}
|
||||
@ -111,13 +106,13 @@ export class AssignReviewerApproverDialogComponent {
|
||||
uniqueReviewers.add(file.currentReviewer);
|
||||
}
|
||||
}
|
||||
let singleUser: string = uniqueReviewers.size === 1 ? uniqueReviewers.values().next().value : this.userService.currentUser.id;
|
||||
let user: string = uniqueReviewers.size === 1 ? uniqueReviewers.values().next().value : this.userService.currentUser.id;
|
||||
|
||||
singleUser = this.singleUsersSelectOptions.indexOf(singleUser) >= 0 ? singleUser : this.singleUsersSelectOptions[0];
|
||||
user = this.userOptions.indexOf(user) >= 0 ? user : this.userOptions[0];
|
||||
|
||||
this.usersForm = this._formBuilder.group({
|
||||
this.form = this._formBuilder.group({
|
||||
// Allow a null reviewer if a previous reviewer exists (= it's not the first assignment) & current user is allowed to unassign
|
||||
singleUser: [singleUser, this._canUnassignFiles && !singleUser ? Validators.required : null],
|
||||
user: [user, this._canUnassignFiles && !user ? Validators.required : null],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { JustificationsService } from '@services/entity-services/justifications.service';
|
||||
@ -15,7 +13,6 @@ export interface LegalBasisOption {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-change-legal-basis-dialog',
|
||||
templateUrl: './change-legal-basis-dialog.component.html',
|
||||
styleUrls: ['./change-legal-basis-dialog.component.scss'],
|
||||
})
|
||||
@ -25,14 +22,12 @@ export class ChangeLegalBasisDialogComponent implements OnInit {
|
||||
legalOptions: LegalBasisOption[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly _translateService: TranslateService,
|
||||
private readonly _justificationsService: JustificationsService,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
private readonly _permissionsService: PermissionsService,
|
||||
private readonly _formBuilder: FormBuilder,
|
||||
public dialogRef: MatDialogRef<ChangeLegalBasisDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public annotations: AnnotationWrapper[],
|
||||
readonly dialogRef: MatDialogRef<ChangeLegalBasisDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) readonly annotations: AnnotationWrapper[],
|
||||
) {}
|
||||
|
||||
get changed(): boolean {
|
||||
@ -40,19 +35,18 @@ export class ChangeLegalBasisDialogComponent implements OnInit {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.isDocumentAdmin = this._permissionsService.isApprover();
|
||||
const dossier = this._dossiersService.activeDossier;
|
||||
this.isDocumentAdmin = this._permissionsService.isApprover(dossier);
|
||||
|
||||
this.legalBasisForm = this._formBuilder.group({
|
||||
reason: [null, Validators.required],
|
||||
comment: this.isDocumentAdmin ? [null] : [null, Validators.required],
|
||||
});
|
||||
|
||||
const data = await this._justificationsService
|
||||
.getForDossierTemplate(this._dossiersService.activeDossier.dossierTemplateId)
|
||||
.toPromise();
|
||||
const data = await this._justificationsService.getForDossierTemplate(dossier.dossierTemplateId).toPromise();
|
||||
|
||||
this.legalOptions = data
|
||||
.map(lbm => ({
|
||||
.map<LegalBasisOption>(lbm => ({
|
||||
legalBasis: lbm.reason,
|
||||
description: lbm.description,
|
||||
label: lbm.name,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<section class="dialog">
|
||||
<section *ngIf="dossier$ | async as dossier" class="dialog">
|
||||
<div class="dialog-header heading-l">
|
||||
{{ 'edit-dossier-dialog.header' | translate: { dossierName: dossier.dossierName } }}
|
||||
</div>
|
||||
@ -13,6 +13,7 @@
|
||||
class="item"
|
||||
></div>
|
||||
</iqser-side-nav>
|
||||
|
||||
<div>
|
||||
<div [class.no-actions]="!showActionButtons" [class.no-padding]="noPaddingTab" class="content">
|
||||
<div *ngIf="showHeading" class="heading">
|
||||
@ -20,37 +21,37 @@
|
||||
</div>
|
||||
|
||||
<redaction-edit-dossier-general-info
|
||||
(updateDossier)="updatedDossier()"
|
||||
(updateDossier)="updatedDossier(dossier)"
|
||||
*ngIf="activeNav === 'dossierInfo'"
|
||||
[dossier]="dossier"
|
||||
></redaction-edit-dossier-general-info>
|
||||
|
||||
<redaction-edit-dossier-download-package
|
||||
(updateDossier)="updatedDossier()"
|
||||
(updateDossier)="updatedDossier(dossier)"
|
||||
*ngIf="activeNav === 'downloadPackage'"
|
||||
[dossier]="dossier"
|
||||
></redaction-edit-dossier-download-package>
|
||||
|
||||
<redaction-edit-dossier-dictionary
|
||||
(updateDossier)="updatedDossier()"
|
||||
(updateDossier)="updatedDossier(dossier)"
|
||||
*ngIf="activeNav === 'dossierDictionary'"
|
||||
[dossier]="dossier"
|
||||
></redaction-edit-dossier-dictionary>
|
||||
|
||||
<redaction-edit-dossier-team-members
|
||||
(updateDossier)="updatedDossier()"
|
||||
<redaction-team-members-manager
|
||||
(updateDossier)="updatedDossier(dossier)"
|
||||
*ngIf="activeNav === 'members'"
|
||||
[dossier]="dossier"
|
||||
></redaction-edit-dossier-team-members>
|
||||
></redaction-team-members-manager>
|
||||
|
||||
<redaction-edit-dossier-attributes
|
||||
(updateDossier)="updatedDossier()"
|
||||
(updateDossier)="updatedDossier(dossier)"
|
||||
*ngIf="activeNav === 'dossierAttributes'"
|
||||
[dossier]="dossier"
|
||||
></redaction-edit-dossier-attributes>
|
||||
|
||||
<redaction-edit-dossier-deleted-documents
|
||||
(updateDossier)="updatedDossier()"
|
||||
(updateDossier)="updatedDossier(dossier)"
|
||||
*ngIf="activeNav === 'deletedDocuments'"
|
||||
[dossier]="dossier"
|
||||
></redaction-edit-dossier-deleted-documents>
|
||||
@ -69,7 +70,8 @@
|
||||
(click)="save(true)"
|
||||
[disabled]="activeComponent?.disabled || !activeComponent?.changed"
|
||||
[label]="'edit-dossier-dialog.actions.save-and-close' | translate"
|
||||
[type]="iconButtonTypes.dark">
|
||||
[type]="iconButtonTypes.dark"
|
||||
>
|
||||
</iqser-icon-button>
|
||||
|
||||
<div (click)="revert()" class="all-caps-label cancel" translate="edit-dossier-dialog.actions.revert"></div>
|
||||
|
||||
@ -6,13 +6,14 @@ import { EditDossierDownloadPackageComponent } from './download-package/edit-dos
|
||||
import { EditDossierSectionInterface } from './edit-dossier-section.interface';
|
||||
import { IconButtonTypes, Toaster } from '@iqser/common-ui';
|
||||
import { EditDossierDictionaryComponent } from './dictionary/edit-dossier-dictionary.component';
|
||||
import { EditDossierTeamMembersComponent } from './team-members/edit-dossier-team-members.component';
|
||||
import { EditDossierAttributesComponent } from './attributes/edit-dossier-attributes.component';
|
||||
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { EditDossierDeletedDocumentsComponent } from './deleted-documents/edit-dossier-deleted-documents.component';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { TeamMembersManagerComponent } from '../../components/team-members-manager/team-members-manager.component';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
type Section = 'dossierInfo' | 'downloadPackage' | 'dossierDictionary' | 'members' | 'dossierAttributes' | 'deletedDocuments';
|
||||
|
||||
@ -24,12 +25,12 @@ export class EditDossierDialogComponent {
|
||||
readonly navItems: { key: Section; title?: string; sideNavTitle?: string }[];
|
||||
readonly iconButtonTypes = IconButtonTypes;
|
||||
activeNav: Section;
|
||||
dossier: Dossier;
|
||||
readonly dossier$: Observable<Dossier>;
|
||||
|
||||
@ViewChild(EditDossierGeneralInfoComponent) generalInfoComponent: EditDossierGeneralInfoComponent;
|
||||
@ViewChild(EditDossierDownloadPackageComponent) downloadPackageComponent: EditDossierDownloadPackageComponent;
|
||||
@ViewChild(EditDossierDictionaryComponent) dictionaryComponent: EditDossierDictionaryComponent;
|
||||
@ViewChild(EditDossierTeamMembersComponent) membersComponent: EditDossierTeamMembersComponent;
|
||||
@ViewChild(TeamMembersManagerComponent) membersComponent: TeamMembersManagerComponent;
|
||||
@ViewChild(EditDossierAttributesComponent) attributesComponent: EditDossierAttributesComponent;
|
||||
@ViewChild(EditDossierDeletedDocumentsComponent) deletedDocumentsComponent: EditDossierDeletedDocumentsComponent;
|
||||
|
||||
@ -41,7 +42,7 @@ export class EditDossierDialogComponent {
|
||||
private readonly _dialogRef: MatDialogRef<EditDossierDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA)
|
||||
private readonly _data: {
|
||||
dossier: Dossier;
|
||||
dossierId: string;
|
||||
afterSave: Function;
|
||||
section?: Section;
|
||||
},
|
||||
@ -77,7 +78,7 @@ export class EditDossierDialogComponent {
|
||||
},
|
||||
];
|
||||
|
||||
this.dossier = _data.dossier;
|
||||
this.dossier$ = this._dossiersService.getEntityChanged$(_data.dossierId);
|
||||
this.activeNav = _data.section || 'dossierInfo';
|
||||
}
|
||||
|
||||
@ -108,10 +109,8 @@ export class EditDossierDialogComponent {
|
||||
return !['deletedDocuments'].includes(this.activeNav);
|
||||
}
|
||||
|
||||
updatedDossier() {
|
||||
this._toaster.success(_('edit-dossier-dialog.change-successful'), { params: { dossierName: this.dossier.dossierName } });
|
||||
this.dossier = this._dossiersService.find(this.dossier.id);
|
||||
this._changeRef.detectChanges();
|
||||
updatedDossier(dossier: Dossier) {
|
||||
this._toaster.success(_('edit-dossier-dialog.change-successful'), { params: { dossierName: dossier.dossierName } });
|
||||
this.afterSave();
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import { ConfirmationDialogInput, IconButtonTypes, TitleColors, Toaster } from '
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
|
||||
import { DossierStatsService } from '@services/entity-services/dossier-stats.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-edit-dossier-general-info',
|
||||
@ -32,6 +33,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
|
||||
readonly permissionsService: PermissionsService,
|
||||
private readonly _dossierTemplatesService: DossierTemplatesService,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
private readonly _dossierStatsService: DossierStatsService,
|
||||
private readonly _formBuilder: FormBuilder,
|
||||
private readonly _dialogService: DossiersDialogService,
|
||||
private readonly _router: Router,
|
||||
@ -71,7 +73,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
|
||||
dossierTemplateId: [
|
||||
{
|
||||
value: this.dossier.dossierTemplateId,
|
||||
disabled: this.dossier.hasFiles,
|
||||
disabled: this._dossierStatsService.get(this.dossier.dossierId).hasFiles,
|
||||
},
|
||||
Validators.required,
|
||||
],
|
||||
|
||||
@ -1 +0,0 @@
|
||||
<redaction-team-members-manager (save)="updateDossier.emit()" [dossier]="dossier"></redaction-team-members-manager>
|
||||
@ -1,37 +0,0 @@
|
||||
import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
|
||||
import { Dossier } from '@red/domain';
|
||||
import { EditDossierSectionInterface } from '../edit-dossier-section.interface';
|
||||
import { TeamMembersManagerComponent } from '../../../components/team-members-manager/team-members-manager.component';
|
||||
import { UserService } from '@services/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-edit-dossier-team-members',
|
||||
templateUrl: './edit-dossier-team-members.component.html',
|
||||
styleUrls: ['./edit-dossier-team-members.component.scss'],
|
||||
})
|
||||
export class EditDossierTeamMembersComponent implements EditDossierSectionInterface {
|
||||
readonly currentUser = this._userService.currentUser;
|
||||
|
||||
@Input() dossier: Dossier;
|
||||
@Output() readonly updateDossier = new EventEmitter();
|
||||
|
||||
@ViewChild(TeamMembersManagerComponent) managerComponent: TeamMembersManagerComponent;
|
||||
|
||||
constructor(private readonly _userService: UserService) {}
|
||||
|
||||
get changed() {
|
||||
return this.managerComponent.changed;
|
||||
}
|
||||
|
||||
get disabled() {
|
||||
return !this.currentUser.isManager;
|
||||
}
|
||||
|
||||
async save() {
|
||||
await this.managerComponent.saveMembers();
|
||||
}
|
||||
|
||||
revert() {
|
||||
this.managerComponent.revert();
|
||||
}
|
||||
}
|
||||
@ -27,7 +27,6 @@ import { EditDossierDialogComponent } from './dialogs/edit-dossier-dialog/edit-d
|
||||
import { EditDossierGeneralInfoComponent } from './dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component';
|
||||
import { EditDossierDownloadPackageComponent } from './dialogs/edit-dossier-dialog/download-package/edit-dossier-download-package.component';
|
||||
import { EditDossierDictionaryComponent } from './dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component';
|
||||
import { EditDossierTeamMembersComponent } from './dialogs/edit-dossier-dialog/team-members/edit-dossier-team-members.component';
|
||||
import { TeamMembersManagerComponent } from './components/team-members-manager/team-members-manager.component';
|
||||
import { ChangeLegalBasisDialogComponent } from './dialogs/change-legal-basis-dialog/change-legal-basis-dialog.component';
|
||||
import { PageExclusionComponent } from './components/page-exclusion/page-exclusion.component';
|
||||
@ -68,7 +67,6 @@ const components = [
|
||||
EditDossierGeneralInfoComponent,
|
||||
EditDossierDownloadPackageComponent,
|
||||
EditDossierDictionaryComponent,
|
||||
EditDossierTeamMembersComponent,
|
||||
EditDossierAttributesComponent,
|
||||
TeamMembersManagerComponent,
|
||||
PageExclusionComponent,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<ng-container *ngIf="areSomeFilesSelected" (longPress)="forceReanalysisAction($event)" redactionLongPress>
|
||||
<ng-container (longPress)="forceReanalysisAction($event)" *ngIf="listingService.selectedLength$ | async" redactionLongPress>
|
||||
<iqser-circle-button
|
||||
(action)="delete()"
|
||||
*ngIf="canDelete"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { Dossier, File } from '@red/domain';
|
||||
@ -17,6 +17,7 @@ import { ReanalysisService } from '@services/reanalysis.service';
|
||||
selector: 'redaction-dossier-overview-bulk-actions',
|
||||
templateUrl: './dossier-overview-bulk-actions.component.html',
|
||||
styleUrls: ['./dossier-overview-bulk-actions.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DossierOverviewBulkActionsComponent {
|
||||
readonly circleButtonTypes = CircleButtonTypes;
|
||||
@ -35,32 +36,21 @@ export class DossierOverviewBulkActionsComponent {
|
||||
private readonly _fileActionService: FileActionService,
|
||||
private readonly _loadingService: LoadingService,
|
||||
private readonly _translateService: TranslateService,
|
||||
private readonly _listingService: ListingService<File>,
|
||||
readonly listingService: ListingService<File>,
|
||||
private readonly _userPreferenceService: UserPreferenceService,
|
||||
) {}
|
||||
|
||||
get selectedFiles(): File[] {
|
||||
return this._listingService.selected;
|
||||
}
|
||||
|
||||
get areAllFilesSelected() {
|
||||
return this.dossier.files.length !== 0 && this.selectedFiles.length === this.dossier.files.length;
|
||||
}
|
||||
|
||||
get areSomeFilesSelected() {
|
||||
return this.selectedFiles.length > 0;
|
||||
return this.listingService.selected;
|
||||
}
|
||||
|
||||
get allSelectedFilesCanBeAssignedIntoSameState() {
|
||||
if (this.areSomeFilesSelected) {
|
||||
const allFilesAreUnderReviewOrUnassigned = this.selectedFiles.reduce(
|
||||
(acc, file) => acc && (file.isUnderReview || file.isUnassigned),
|
||||
true,
|
||||
);
|
||||
const allFilesAreUnderApproval = this.selectedFiles.reduce((acc, file) => acc && file.isUnderApproval, true);
|
||||
return allFilesAreUnderReviewOrUnassigned || allFilesAreUnderApproval;
|
||||
}
|
||||
return false;
|
||||
const allFilesAreUnderReviewOrUnassigned = this.selectedFiles.reduce(
|
||||
(acc, file) => acc && (file.isUnderReview || file.isUnassigned),
|
||||
true,
|
||||
);
|
||||
const allFilesAreUnderApproval = this.selectedFiles.reduce((acc, file) => acc && file.isUnderApproval, true);
|
||||
return allFilesAreUnderReviewOrUnassigned || allFilesAreUnderApproval;
|
||||
}
|
||||
|
||||
get canAssignToSelf() {
|
||||
@ -81,31 +71,27 @@ export class DossierOverviewBulkActionsComponent {
|
||||
}
|
||||
|
||||
get canDelete() {
|
||||
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canDeleteFile(file), true);
|
||||
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canDeleteFile(file, this.dossier), true);
|
||||
}
|
||||
|
||||
get canReanalyse() {
|
||||
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canReanalyseFile(file), true);
|
||||
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canReanalyseFile(file, this.dossier), true);
|
||||
}
|
||||
|
||||
get canOcr() {
|
||||
return this.selectedFiles.reduce((acc, file) => acc && file.canBeOCRed, true);
|
||||
}
|
||||
|
||||
get files() {
|
||||
return this.selectedFiles.map(file => file.status);
|
||||
}
|
||||
|
||||
get canSetToUnderReview() {
|
||||
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canSetUnderReview(file), true);
|
||||
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canSetUnderReview(file, this.dossier), true);
|
||||
}
|
||||
|
||||
get canSetToUnderApproval() {
|
||||
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canSetUnderApproval(file), true);
|
||||
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canSetUnderApproval(file, this.dossier), true);
|
||||
}
|
||||
|
||||
get isReadyForApproval() {
|
||||
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.isReadyForApproval(file), true);
|
||||
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.isReadyForApproval(file, this.dossier), true);
|
||||
}
|
||||
|
||||
get canApprove() {
|
||||
|
||||
@ -1,76 +1,72 @@
|
||||
<ng-container *ngIf="dossiersService.activeDossier$ | async as dossier">
|
||||
<ng-container *ngIf="dossierStats$ | async as stats">
|
||||
<div>
|
||||
<mat-icon svgIcon="iqser:document"></mat-icon>
|
||||
<span>{{ 'dossier-overview.dossier-details.stats.documents' | translate: { count: dossier.files.length } }}</span>
|
||||
<span>{{ 'dossier-overview.dossier-details.stats.documents' | translate: { count: stats.numberOfFiles } }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<mat-icon svgIcon="red:reanalyse"></mat-icon>
|
||||
<span>{{ 'dossier-overview.dossier-details.stats.processing-documents' | translate: { count: stats.numberOfProcessingFiles } }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<mat-icon svgIcon="red:user"></mat-icon>
|
||||
<span>{{ 'dossier-overview.dossier-details.stats.people' | translate: { count: dossier.memberIds.length } }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<mat-icon svgIcon="iqser:pages"></mat-icon>
|
||||
<span>{{
|
||||
'dossier-overview.dossier-details.stats.analysed-pages' | translate: { count: dossier.totalNumberOfPages | number }
|
||||
}}</span>
|
||||
<span>{{ 'dossier-overview.dossier-details.stats.analysed-pages' | translate: { count: stats.numberOfPages | number } }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<mat-icon svgIcon="red:calendar"></mat-icon>
|
||||
<span
|
||||
>{{
|
||||
'dossier-overview.dossier-details.stats.created-on'
|
||||
| translate
|
||||
: {
|
||||
date: dossier.date | date: 'd MMM. yyyy'
|
||||
}
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div *ngIf="dossier.dueDate">
|
||||
<mat-icon svgIcon="red:lightning"></mat-icon>
|
||||
<span>{{
|
||||
'dossier-overview.dossier-details.stats.due-date'
|
||||
| translate
|
||||
: {
|
||||
date: dossier.dueDate | date: 'd MMM. yyyy'
|
||||
}
|
||||
}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<mat-icon svgIcon="red:template"></mat-icon>
|
||||
<span>{{ dossierTemplate(dossier).name }} </span>
|
||||
</div>
|
||||
<div (click)="openDossierDictionaryDialog.emit()" *ngIf="dossier.type" class="link-property">
|
||||
<mat-icon svgIcon="red:dictionary"></mat-icon>
|
||||
<span>{{ 'dossier-overview.dossier-details.dictionary' | translate }} </span>
|
||||
</div>
|
||||
<div (click)="openEditDossierAttributesDialog(dossier, 'deletedDocuments')" class="link-property">
|
||||
<mat-icon svgIcon="iqser:trash"></mat-icon>
|
||||
<span>{{ 'dossier-overview.dossier-details.stats.deleted' | translate: { count: deletedFilesCount$ | async } }}</span>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="dossier.date | date: 'd MMM. yyyy' as date">
|
||||
<mat-icon svgIcon="red:calendar"></mat-icon>
|
||||
<span>{{ 'dossier-overview.dossier-details.stats.created-on' | translate: { date: date } }}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="dossier.dueDate | date: 'd MMM. yyyy' as dueDate">
|
||||
<mat-icon svgIcon="red:lightning"></mat-icon>
|
||||
<span>{{ 'dossier-overview.dossier-details.stats.due-date' | translate: { date: dueDate } }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<mat-icon svgIcon="red:template"></mat-icon>
|
||||
<span>{{ dossierTemplate(dossier).name }} </span>
|
||||
</div>
|
||||
|
||||
<div (click)="openDossierDictionaryDialog.emit()" *ngIf="dossier.type" class="link-property">
|
||||
<mat-icon svgIcon="red:dictionary"></mat-icon>
|
||||
<span>{{ 'dossier-overview.dossier-details.dictionary' | translate }} </span>
|
||||
</div>
|
||||
|
||||
<div (click)="openEditDossierAttributesDialog(dossier.dossierId, 'deletedDocuments')" class="link-property">
|
||||
<mat-icon svgIcon="iqser:trash"></mat-icon>
|
||||
<span>{{ 'dossier-overview.dossier-details.stats.deleted' | translate: { count: deletedFilesCount$ | async } }}</span>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="dossierAttributes?.length">
|
||||
<div (click)="attributesExpanded = true" *ngIf="!attributesExpanded" class="all-caps-label show-attributes">
|
||||
{{ 'dossier-overview.dossier-details.attributes.expand' | translate: { count: dossierAttributes.length } }}
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="dossierAttributes?.length">
|
||||
<div (click)="attributesExpanded = true" *ngIf="!attributesExpanded" class="all-caps-label show-attributes">
|
||||
{{ 'dossier-overview.dossier-details.attributes.expand' | translate: { count: dossierAttributes.length } }}
|
||||
<ng-container *ngIf="attributesExpanded">
|
||||
<div
|
||||
(click)="openEditDossierAttributesDialog(dossier.dossierId, 'dossierAttributes')"
|
||||
*ngFor="let attr of dossierAttributes"
|
||||
class="link-property"
|
||||
>
|
||||
<mat-icon svgIcon="red:attribute"></mat-icon>
|
||||
<span *ngIf="!attr.value"> {{ attr.label + ': -' }}</span>
|
||||
<span *ngIf="attr.value && attr.type === 'DATE'"> {{ attr.label + ': ' + (attr.value | date: 'd MMM. yyyy') }}</span>
|
||||
<span *ngIf="attr.value && attr.type === 'IMAGE'">
|
||||
{{ attr.label + ': ' + ('dossier-overview.dossier-details.attributes.image-uploaded' | translate) }}</span
|
||||
>
|
||||
<span *ngIf="attr.value && (attr.type === 'TEXT' || attr.type === 'NUMBER')"> {{ attr.label + ': ' + attr.value }}</span>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="attributesExpanded">
|
||||
<div
|
||||
(click)="openEditDossierAttributesDialog(dossier, 'dossierAttributes')"
|
||||
*ngFor="let attr of dossierAttributes"
|
||||
class="link-property"
|
||||
>
|
||||
<mat-icon svgIcon="red:attribute"></mat-icon>
|
||||
<span *ngIf="!attr.value"> {{ attr.label + ': -' }}</span>
|
||||
<span *ngIf="attr.value && attr.type === 'DATE'"> {{ attr.label + ': ' + (attr.value | date: 'd MMM. yyyy') }}</span>
|
||||
<span *ngIf="attr.value && attr.type === 'IMAGE'">
|
||||
{{ attr.label + ': ' + ('dossier-overview.dossier-details.attributes.image-uploaded' | translate) }}</span
|
||||
>
|
||||
<span *ngIf="attr.value && (attr.type === 'TEXT' || attr.type === 'NUMBER')"> {{ attr.label + ': ' + attr.value }}</span>
|
||||
</div>
|
||||
|
||||
<div (click)="attributesExpanded = false" class="all-caps-label hide-attributes">
|
||||
{{ 'dossier-overview.dossier-details.attributes.show-less' | translate }}
|
||||
</div>
|
||||
</ng-container>
|
||||
<div (click)="attributesExpanded = false" class="all-caps-label hide-attributes">
|
||||
{{ 'dossier-overview.dossier-details.attributes.show-less' | translate }}
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
@ -1,33 +1,40 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { Dossier, DossierAttributeWithValue, DossierTemplate } from '@red/domain';
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Dossier, DossierAttributeWithValue, DossierStats, DossierTemplate } from '@red/domain';
|
||||
import { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
|
||||
import { FilesService } from '@services/entity-services/files.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators';
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||
import { DossierStatsService } from '@services/entity-services/dossier-stats.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-dossier-details-stats',
|
||||
templateUrl: './dossier-details-stats.component.html',
|
||||
styleUrls: ['./dossier-details-stats.component.scss'],
|
||||
})
|
||||
export class DossierDetailsStatsComponent {
|
||||
export class DossierDetailsStatsComponent implements OnInit {
|
||||
attributesExpanded = false;
|
||||
deletedFilesCount$: Observable<number>;
|
||||
@Input() dossierAttributes: DossierAttributeWithValue[];
|
||||
@Output() readonly openDossierDictionaryDialog = new EventEmitter();
|
||||
dossierStats$: Observable<DossierStats>;
|
||||
@Input()
|
||||
dossierAttributes: DossierAttributeWithValue[];
|
||||
@Input()
|
||||
dossier: Dossier;
|
||||
@Output()
|
||||
readonly openDossierDictionaryDialog = new EventEmitter();
|
||||
|
||||
constructor(
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _dossierTemplatesService: DossierTemplatesService,
|
||||
private readonly _dialogService: DossiersDialogService,
|
||||
private readonly _filesService: FilesService,
|
||||
private readonly _dossierStatsService: DossierStatsService,
|
||||
readonly dossiersService: DossiersService,
|
||||
) {
|
||||
this.deletedFilesCount$ = dossiersService.activeDossier$.pipe(
|
||||
switchMap(dossier => _filesService.getDeletedFilesFor(dossier.id)),
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.dossierStats$ = this._dossierStatsService.watch$(this.dossier.dossierId);
|
||||
this.deletedFilesCount$ = this._filesService.getDeletedFilesFor(this.dossier.id).pipe(
|
||||
map(files => files.length),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
@ -37,9 +44,9 @@ export class DossierDetailsStatsComponent {
|
||||
return this._dossierTemplatesService.find(dossier.dossierTemplateId);
|
||||
}
|
||||
|
||||
openEditDossierAttributesDialog(dossier: Dossier, section: string) {
|
||||
openEditDossierAttributesDialog(dossierId: string, section: string) {
|
||||
this._dialogService.openDialog('editDossier', null, {
|
||||
dossier,
|
||||
dossierId,
|
||||
section: section,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<ng-container *ngIf="dossiersService.activeDossier$ | async as dossier">
|
||||
<ng-container *ngIf="dossier$ | async as dossier">
|
||||
<div class="collapsed-wrapper">
|
||||
<ng-container *ngTemplateOutlet="collapsible; context: { action: 'expand', tooltip: (expandTooltip | translate) }"></ng-container>
|
||||
<div class="all-caps-label" translate="dossier-details.title"></div>
|
||||
@ -15,7 +15,7 @@
|
||||
<div class="all-caps-label" translate="dossier-details.owner"></div>
|
||||
<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-initials-avatar [user]="dossier.ownerId" [withName]="true" color="gray" size="large"></redaction-initials-avatar>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="editingOwner = true"
|
||||
@ -38,47 +38,50 @@
|
||||
></redaction-team-members>
|
||||
</div>
|
||||
|
||||
<div *ngIf="dossier.hasFiles" class="mt-24">
|
||||
<redaction-simple-doughnut-chart
|
||||
[config]="documentsChartData"
|
||||
[radius]="63"
|
||||
[strokeWidth]="15"
|
||||
[subtitle]="'dossier-overview.dossier-details.charts.documents-in-dossier' | translate"
|
||||
direction="row"
|
||||
></redaction-simple-doughnut-chart>
|
||||
</div>
|
||||
|
||||
<div *ngIf="dossier.hasFiles && needsWorkFilters$ | async as filters" class="mt-24 legend pb-32">
|
||||
<div
|
||||
(click)="filterService.toggleFilter('needsWorkFilters', filter.id)"
|
||||
*ngFor="let filter of filters"
|
||||
[class.active]="filter.checked"
|
||||
>
|
||||
<redaction-type-filter [filter]="filter"></redaction-type-filter>
|
||||
<ng-container *ngIf="dossierStats$ | async as stats">
|
||||
<div *ngIf="stats.hasFiles" class="mt-24">
|
||||
<redaction-simple-doughnut-chart
|
||||
[config]="calculateChartConfig(stats)"
|
||||
[radius]="63"
|
||||
[strokeWidth]="15"
|
||||
[subtitle]="'dossier-overview.dossier-details.charts.documents-in-dossier' | translate"
|
||||
direction="row"
|
||||
></redaction-simple-doughnut-chart>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div [class.mt-24]="!dossier.hasFiles" class="pb-32">
|
||||
<redaction-dossier-details-stats
|
||||
(openDossierDictionaryDialog)="openDossierDictionaryDialog.emit()"
|
||||
[dossierAttributes]="dossierAttributes"
|
||||
></redaction-dossier-details-stats>
|
||||
</div>
|
||||
<div *ngIf="stats.hasFiles && needsWorkFilters$ | async as filters" class="mt-24 legend pb-32">
|
||||
<div
|
||||
(click)="filterService.toggleFilter('needsWorkFilters', filter.id)"
|
||||
*ngFor="let filter of filters"
|
||||
[class.active]="filter.checked"
|
||||
>
|
||||
<redaction-type-filter [filter]="filter"></redaction-type-filter>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div [class.mt-24]="!stats.hasFiles" class="pb-32">
|
||||
<redaction-dossier-details-stats
|
||||
(openDossierDictionaryDialog)="openDossierDictionaryDialog.emit()"
|
||||
[dossierAttributes]="dossierAttributes"
|
||||
[dossier]="dossier"
|
||||
></redaction-dossier-details-stats>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="dossier.description as description" class="pb-32">
|
||||
<div class="heading" translate="dossier-overview.dossier-details.description"></div>
|
||||
<div class="mt-8">{{ description }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #editOwner>
|
||||
<redaction-assign-user-dropdown
|
||||
(cancel)="editingOwner = false"
|
||||
(save)="editingOwner = false; assignOwner($event)"
|
||||
[options]="managers"
|
||||
[value]="owner"
|
||||
></redaction-assign-user-dropdown>
|
||||
</ng-template>
|
||||
<ng-template #editOwner>
|
||||
<redaction-assign-user-dropdown
|
||||
(cancel)="editingOwner = false"
|
||||
(save)="editingOwner = false; assignOwner($event, dossier)"
|
||||
[options]="managers"
|
||||
[value]="dossier.ownerId"
|
||||
></redaction-assign-user-dropdown>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #collapsible let-action="action" let-tooltip="tooltip">
|
||||
<iqser-circle-button
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { groupBy } from '@utils/index';
|
||||
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
|
||||
import { TranslateChartService } from '@services/translate-chart.service';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { FilterService, Toaster } from '@iqser/common-ui';
|
||||
import { fileStatusTranslations } from '../../../../translations/file-status-translations';
|
||||
import { FilterService, shareLast, Toaster } from '@iqser/common-ui';
|
||||
import { workflowFileStatusTranslations } from '../../../../translations/file-status-translations';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { DossierAttributeWithValue, IDossierRequest, StatusSorter, User } from '@red/domain';
|
||||
import { Dossier, DossierAttributeWithValue, DossierStats, IDossierRequest, StatusSorter, User } from '@red/domain';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { DossierStatsService } from '@services/entity-services/dossier-stats.service';
|
||||
import { pluck, switchMap } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-dossier-details',
|
||||
@ -16,9 +19,7 @@ import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
styleUrls: ['./dossier-details.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DossierDetailsComponent implements OnInit {
|
||||
documentsChartData: DoughnutChartConfig[] = [];
|
||||
owner: User;
|
||||
export class DossierDetailsComponent {
|
||||
editingOwner = false;
|
||||
@Input() dossierAttributes: DossierAttributeWithValue[];
|
||||
@Output() readonly openAssignDossierMembersDialog = new EventEmitter();
|
||||
@ -29,6 +30,9 @@ export class DossierDetailsComponent implements OnInit {
|
||||
|
||||
readonly needsWorkFilters$ = this.filterService.getFilterModels$('needsWorkFilters');
|
||||
readonly currentUser = this._userService.currentUser;
|
||||
readonly dossierId: string;
|
||||
readonly dossier$: Observable<Dossier>;
|
||||
readonly dossierStats$: Observable<DossierStats>;
|
||||
|
||||
constructor(
|
||||
readonly appStateService: AppStateService,
|
||||
@ -37,44 +41,40 @@ export class DossierDetailsComponent implements OnInit {
|
||||
readonly filterService: FilterService,
|
||||
private readonly _changeDetectorRef: ChangeDetectorRef,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _dossierStatsService: DossierStatsService,
|
||||
private readonly _toaster: Toaster,
|
||||
) {}
|
||||
activatedRoute: ActivatedRoute,
|
||||
) {
|
||||
this.dossierId = activatedRoute.snapshot.paramMap.get('dossierId');
|
||||
this.dossier$ = this.dossiersService.getEntityChanged$(this.dossierId).pipe(shareLast());
|
||||
this.dossierStats$ = this.dossier$.pipe(
|
||||
pluck('dossierId'),
|
||||
switchMap(dossierId => this._dossierStatsService.watch$(dossierId)),
|
||||
);
|
||||
}
|
||||
|
||||
get managers() {
|
||||
return this._userService.managerUsers;
|
||||
return this._userService.managerUsers.map(manager => manager.id);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.owner = this._userService.find(this.dossiersService.activeDossier.ownerId);
|
||||
}
|
||||
|
||||
calculateChartConfig(): void {
|
||||
const activeDossier = this.dossiersService.activeDossier;
|
||||
if (!activeDossier) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groups = groupBy(activeDossier?.files, 'status');
|
||||
let documentsChartData: DoughnutChartConfig[] = Object.keys(groups).map(status => ({
|
||||
value: groups[status].length,
|
||||
calculateChartConfig(stats: DossierStats): DoughnutChartConfig[] {
|
||||
const documentsChartData: DoughnutChartConfig[] = Object.keys(stats.fileCountPerWorkflowStatus).map(status => ({
|
||||
value: stats.fileCountPerWorkflowStatus[status],
|
||||
color: status,
|
||||
label: fileStatusTranslations[status],
|
||||
label: workflowFileStatusTranslations[status],
|
||||
key: status,
|
||||
}));
|
||||
documentsChartData.sort((a, b) => StatusSorter.byStatus(a.key, b.key));
|
||||
documentsChartData = this.translateChartService.translateStatus(documentsChartData);
|
||||
this.documentsChartData = documentsChartData;
|
||||
this._changeDetectorRef.detectChanges();
|
||||
return this.translateChartService.translateStatus(documentsChartData);
|
||||
}
|
||||
|
||||
async assignOwner(user: User | string) {
|
||||
this.owner = typeof user === 'string' ? this._userService.find(user) : user;
|
||||
const activeDossier = this.dossiersService.activeDossier;
|
||||
const dossierRequest: IDossierRequest = { ...activeDossier, dossierId: activeDossier.dossierId, ownerId: this.owner.id };
|
||||
async assignOwner(user: User | string, dossier: Dossier) {
|
||||
const owner = typeof user === 'string' ? this._userService.find(user) : user;
|
||||
const dossierRequest: IDossierRequest = { ...dossier, ownerId: owner.id };
|
||||
await this.dossiersService.createOrUpdate(dossierRequest).toPromise();
|
||||
|
||||
const ownerName = this._userService.getNameForId(this.owner.id);
|
||||
const dossierName = activeDossier.dossierName;
|
||||
const ownerName = this._userService.getNameForId(owner.id);
|
||||
const dossierName = dossier.dossierName;
|
||||
this._toaster.info(_('assignment.owner'), { params: { ownerName, dossierName } });
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
<div class="small-label stats-subtitle">
|
||||
<div>
|
||||
<mat-icon svgIcon="iqser:pages"></mat-icon>
|
||||
{{ file.numberOfPages }}
|
||||
</div>
|
||||
<div>
|
||||
<mat-icon svgIcon="red:exclude-pages"></mat-icon>
|
||||
{{ file.excludedPages.length }}
|
||||
</div>
|
||||
<div *ngIf="file.lastOCRTime" [matTooltipPosition]="'above'" [matTooltip]="'dossier-overview.ocr-performed' | translate">
|
||||
<mat-icon svgIcon="iqser:ocr"></mat-icon>
|
||||
{{ file.lastOCRTime | date: 'mediumDate' }}
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { File } from '@red/domain';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-file-stats',
|
||||
templateUrl: './file-stats.component.html',
|
||||
styleUrls: ['./file-stats.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FileStatsComponent {
|
||||
@Input() file: File;
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
<div class="needs-work">
|
||||
<redaction-annotation-icon *ngIf="file.analysisRequired" [color]="analysisColor" label="A" type="square"></redaction-annotation-icon>
|
||||
<redaction-annotation-icon *ngIf="file.hasUpdates" [color]="updatedColor" label="U" type="square"></redaction-annotation-icon>
|
||||
<redaction-annotation-icon *ngIf="file.hasRedactions" [color]="redactionColor" label="R" type="square"></redaction-annotation-icon>
|
||||
<redaction-annotation-icon *ngIf="file.hasImages" [color]="imageColor" label="I" type="square"></redaction-annotation-icon>
|
||||
<redaction-annotation-icon *ngIf="file.hintsOnly" [color]="hintColor" label="H" type="circle"></redaction-annotation-icon>
|
||||
<redaction-annotation-icon *ngIf="file.hasSuggestions" [color]="suggestionColor" label="S" type="rhombus"></redaction-annotation-icon>
|
||||
<mat-icon *ngIf="file.hasAnnotationComments" svgIcon="red:comment"></mat-icon>
|
||||
</div>
|
||||
@ -0,0 +1,43 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { File } from '@red/domain';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-file-workload-column',
|
||||
templateUrl: './file-workload-column.component.html',
|
||||
styleUrls: ['./file-workload-column.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FileWorkloadColumnComponent {
|
||||
@Input() file: File;
|
||||
|
||||
constructor(private readonly _appStateService: AppStateService) {}
|
||||
|
||||
get suggestionColor() {
|
||||
return this._getDictionaryColor('suggestion');
|
||||
}
|
||||
|
||||
get imageColor() {
|
||||
return this._getDictionaryColor('image');
|
||||
}
|
||||
|
||||
get updatedColor() {
|
||||
return this._getDictionaryColor('updated');
|
||||
}
|
||||
|
||||
get analysisColor() {
|
||||
return this._getDictionaryColor('analysis');
|
||||
}
|
||||
|
||||
get hintColor() {
|
||||
return this._getDictionaryColor('hint');
|
||||
}
|
||||
|
||||
get redactionColor() {
|
||||
return this._getDictionaryColor('redaction');
|
||||
}
|
||||
|
||||
private _getDictionaryColor(type: string) {
|
||||
return this._appStateService.getDictionaryColor(type);
|
||||
}
|
||||
}
|
||||
@ -30,7 +30,7 @@
|
||||
</div>
|
||||
|
||||
<div *ngIf="!file.isError" class="cell">
|
||||
<redaction-needs-work-badge [needsWorkInput]="file"></redaction-needs-work-badge>
|
||||
<redaction-file-workload-column [file]="file"></redaction-file-workload-column>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!file.isError" class="user-column cell">
|
||||
@ -49,17 +49,22 @@
|
||||
<div [class.extend-cols]="file.isError" class="status-container cell">
|
||||
<div *ngIf="file.isError" class="small-label error" translate="dossier-overview.file-listing.file-entry.file-error"></div>
|
||||
<div *ngIf="file.isPending" class="small-label" translate="dossier-overview.file-listing.file-entry.file-pending"></div>
|
||||
<div *ngIf="file.isProcessing" class="small-label loading" translate="dossier-overview.file-listing.file-entry.file-processing"></div>
|
||||
<iqser-status-bar
|
||||
*ngIf="file.isWorkable"
|
||||
[configs]="[
|
||||
|
||||
<div *ngIf="file.isProcessing || file.canBeOpened" class="status-wrapper">
|
||||
<div *ngIf="file.isProcessing" [matTooltip]="'file-status.processing' | translate" class="spinning" matTooltipPosition="above">
|
||||
<mat-icon svgIcon="red:reanalyse"></mat-icon>
|
||||
</div>
|
||||
|
||||
<iqser-status-bar
|
||||
*ngIf="!file.isError && !file.isPending"
|
||||
[configs]="[
|
||||
{
|
||||
color: file.status,
|
||||
color: file.workflowStatus,
|
||||
length: 1
|
||||
}
|
||||
]"
|
||||
></iqser-status-bar>
|
||||
|
||||
></iqser-status-bar>
|
||||
</div>
|
||||
<redaction-file-actions
|
||||
(actionPerformed)="calculateData.emit($event)"
|
||||
*ngIf="!file.isProcessing"
|
||||
|
||||
@ -22,5 +22,20 @@
|
||||
|
||||
&.status-container {
|
||||
align-items: flex-end;
|
||||
|
||||
.status-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.spinning > mat-icon {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
<div class="workflow-item">
|
||||
<div>
|
||||
<div class="details">
|
||||
<div [matTooltip]="file.filename" class="filename" matTooltipPosition="above">
|
||||
{{ file.filename }}
|
||||
</div>
|
||||
|
||||
<ng-container *ngTemplateOutlet="statsTemplate; context: { entity: file }"></ng-container>
|
||||
</div>
|
||||
|
||||
<div class="user">
|
||||
<redaction-initials-avatar [user]="file.currentReviewer"></redaction-initials-avatar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<redaction-file-actions
|
||||
(actionPerformed)="actionPerformed.emit($event)"
|
||||
*ngIf="!file.isProcessing"
|
||||
[file]="file"
|
||||
type="dossier-overview-workflow"
|
||||
></redaction-file-actions>
|
||||
</div>
|
||||
@ -0,0 +1,34 @@
|
||||
@use 'common-mixins';
|
||||
|
||||
.workflow-item {
|
||||
padding: 10px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.details {
|
||||
max-width: calc(100% - 28px);
|
||||
|
||||
.filename {
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
@include common-mixins.line-clamp(1);
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
redaction-file-actions {
|
||||
margin-top: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover redaction-file-actions {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, TemplateRef } from '@angular/core';
|
||||
import { File } from '@red/domain';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-workflow-item',
|
||||
templateUrl: './workflow-item.component.html',
|
||||
styleUrls: ['./workflow-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class WorkflowItemComponent {
|
||||
@Input() file: File;
|
||||
@Input() statsTemplate: TemplateRef<unknown>;
|
||||
@Output() readonly actionPerformed = new EventEmitter<string>();
|
||||
}
|
||||
@ -1,8 +1,10 @@
|
||||
import { Injectable, TemplateRef } from '@angular/core';
|
||||
import {
|
||||
ActionConfig,
|
||||
IFilterGroup,
|
||||
INestedFilter,
|
||||
keyChecker,
|
||||
List,
|
||||
ListingMode,
|
||||
ListingModes,
|
||||
LoadingService,
|
||||
@ -10,8 +12,8 @@ import {
|
||||
TableColumnConfig,
|
||||
WorkflowConfig,
|
||||
} from '@iqser/common-ui';
|
||||
import { File, FileStatus, FileStatuses, IFileAttributeConfig, StatusSorter } from '@red/domain';
|
||||
import { fileStatusTranslations } from '../../translations/file-status-translations';
|
||||
import { File, IFileAttributeConfig, StatusSorter, WorkflowFileStatus, WorkflowFileStatuses } from '@red/domain';
|
||||
import { workflowFileStatusTranslations } from '../../translations/file-status-translations';
|
||||
import { FileActionService } from '../../shared/services/file-action.service';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
@ -41,11 +43,11 @@ export class ConfigService {
|
||||
private readonly _appConfigService: AppConfigService,
|
||||
) {}
|
||||
|
||||
get actionConfig() {
|
||||
actionConfig(dossierId: string): List<ActionConfig> {
|
||||
return [
|
||||
{
|
||||
label: this._translateService.instant('dossier-overview.header-actions.edit'),
|
||||
action: $event => this._openEditDossierDialog($event),
|
||||
action: $event => this._openEditDossierDialog($event, dossierId),
|
||||
icon: 'iqser:edit',
|
||||
hide: !this._userService.currentUser.isManager,
|
||||
},
|
||||
@ -90,41 +92,41 @@ export class ConfigService {
|
||||
];
|
||||
}
|
||||
|
||||
workflowConfig(reloadDossiers: () => Promise<void>): WorkflowConfig<File, FileStatus> {
|
||||
workflowConfig(reloadDossiers: () => Promise<void>): WorkflowConfig<File, WorkflowFileStatus> {
|
||||
return {
|
||||
columnIdentifierFn: entity => entity.status,
|
||||
columnIdentifierFn: entity => entity.workflowStatus,
|
||||
itemVersionFn: (entity: File) => `${entity.lastUpdated}-${entity.numberOfAnalyses}`,
|
||||
columns: [
|
||||
{
|
||||
label: fileStatusTranslations[FileStatuses.UNASSIGNED],
|
||||
key: FileStatuses.UNASSIGNED,
|
||||
label: workflowFileStatusTranslations[WorkflowFileStatuses.UNASSIGNED],
|
||||
key: WorkflowFileStatuses.UNASSIGNED,
|
||||
enterFn: this._unassignFn(reloadDossiers),
|
||||
enterPredicate: (file: File) => this._permissionsService.canUnassignUser(file),
|
||||
color: '#D3D5DA',
|
||||
},
|
||||
{
|
||||
label: fileStatusTranslations[FileStatuses.UNDER_REVIEW],
|
||||
label: workflowFileStatusTranslations[WorkflowFileStatuses.UNDER_REVIEW],
|
||||
enterFn: this._underReviewFn(reloadDossiers),
|
||||
enterPredicate: (file: File) =>
|
||||
this._permissionsService.canSetUnderReview(file) ||
|
||||
this._permissionsService.canAssignToSelf(file) ||
|
||||
this._permissionsService.canAssignUser(file),
|
||||
key: FileStatuses.UNDER_REVIEW,
|
||||
key: WorkflowFileStatuses.UNDER_REVIEW,
|
||||
color: '#FDBD00',
|
||||
},
|
||||
{
|
||||
label: fileStatusTranslations[FileStatuses.UNDER_APPROVAL],
|
||||
label: workflowFileStatusTranslations[WorkflowFileStatuses.UNDER_APPROVAL],
|
||||
enterFn: this._underApprovalFn(reloadDossiers),
|
||||
enterPredicate: (file: File) =>
|
||||
this._permissionsService.canSetUnderApproval(file) || this._permissionsService.canUndoApproval(file),
|
||||
key: FileStatuses.UNDER_APPROVAL,
|
||||
key: WorkflowFileStatuses.UNDER_APPROVAL,
|
||||
color: '#374C81',
|
||||
},
|
||||
{
|
||||
label: fileStatusTranslations[FileStatuses.APPROVED],
|
||||
label: workflowFileStatusTranslations[WorkflowFileStatuses.APPROVED],
|
||||
enterFn: this._approveFn(reloadDossiers),
|
||||
enterPredicate: (file: File) => this._permissionsService.isReadyForApproval(file) && file.canBeApproved,
|
||||
key: FileStatuses.APPROVED,
|
||||
key: WorkflowFileStatuses.APPROVED,
|
||||
color: '#48C9F7',
|
||||
},
|
||||
],
|
||||
@ -139,7 +141,7 @@ export class ConfigService {
|
||||
checkedRequiredFilters: () => NestedFilter[],
|
||||
checkedNotRequiredFilters: () => NestedFilter[],
|
||||
) {
|
||||
const allDistinctFileStatuses = new Set<string>();
|
||||
const allDistinctWorkflowFileStatuses = new Set<string>();
|
||||
const allDistinctPeople = new Set<string>();
|
||||
const allDistinctAddedDates = new Set<string>();
|
||||
const allDistinctNeedsWork = new Set<string>();
|
||||
@ -150,7 +152,7 @@ export class ConfigService {
|
||||
|
||||
entities.forEach(file => {
|
||||
allDistinctPeople.add(file.currentReviewer);
|
||||
allDistinctFileStatuses.add(file.status);
|
||||
allDistinctWorkflowFileStatuses.add(file.workflowStatus);
|
||||
allDistinctAddedDates.add(moment(file.added).format('DD/MM/YYYY'));
|
||||
|
||||
if (file.analysisRequired) {
|
||||
@ -194,11 +196,11 @@ export class ConfigService {
|
||||
});
|
||||
|
||||
if (listingMode === ListingModes.table) {
|
||||
const statusFilters = [...allDistinctFileStatuses].map(
|
||||
const statusFilters = [...allDistinctWorkflowFileStatuses].map(
|
||||
status =>
|
||||
new NestedFilter({
|
||||
id: status,
|
||||
label: this._translateService.instant(fileStatusTranslations[status]),
|
||||
label: this._translateService.instant(workflowFileStatusTranslations[status]),
|
||||
}),
|
||||
);
|
||||
|
||||
@ -207,7 +209,7 @@ export class ConfigService {
|
||||
label: this._translateService.instant('filters.status'),
|
||||
icon: 'red:status',
|
||||
filters: statusFilters.sort((a, b) => StatusSorter[a.id] - StatusSorter[b.id]),
|
||||
checker: keyChecker('status'),
|
||||
checker: keyChecker('workflowStatus'),
|
||||
});
|
||||
}
|
||||
|
||||
@ -331,20 +333,18 @@ export class ConfigService {
|
||||
{
|
||||
id: 'unassigned',
|
||||
label: this._translateService.instant('dossier-overview.quick-filters.unassigned'),
|
||||
checker: (file: File) => !file.currentReviewer,
|
||||
checker: (file: File) => file.isUnassigned,
|
||||
},
|
||||
{
|
||||
id: 'assigned-to-others',
|
||||
label: this._translateService.instant('dossier-overview.quick-filters.assigned-to-others'),
|
||||
checker: (file: File) => !!file.currentReviewer && file.currentReviewer !== this._userService.currentUser.id,
|
||||
checker: (file: File) => !file.isUnassigned && file.currentReviewer !== this._userService.currentUser.id,
|
||||
},
|
||||
].map(filter => new NestedFilter(filter));
|
||||
}
|
||||
|
||||
private _openEditDossierDialog($event: MouseEvent) {
|
||||
this._dialogService.openDialog('editDossier', $event, {
|
||||
dossier: this._dossiersService.activeDossier,
|
||||
});
|
||||
private _openEditDossierDialog($event: MouseEvent, dossierId: string) {
|
||||
this._dialogService.openDialog('editDossier', $event, { dossierId });
|
||||
}
|
||||
|
||||
private _unassignFn = (reloadDossiers: () => Promise<void>) => async (file: File) => {
|
||||
@ -362,7 +362,8 @@ export class ConfigService {
|
||||
};
|
||||
|
||||
private _underApprovalFn = (reloadDossiers: () => Promise<void>) => async (file: File) => {
|
||||
if (this._dossiersService.activeDossier.approverIds.length > 1) {
|
||||
const dossier = this._dossiersService.find(file.dossierId);
|
||||
if (dossier.approverIds.length > 1) {
|
||||
this._fileActionService.assignFile('approver', null, file, () => this._loadingService.loadWhile(reloadDossiers()), true);
|
||||
} else {
|
||||
this._loadingService.start();
|
||||
|
||||
@ -11,6 +11,9 @@ import { DossierDetailsStatsComponent } from './components/dossier-details-stats
|
||||
import { TableItemComponent } from './components/table-item/table-item.component';
|
||||
import { ConfigService } from './config.service';
|
||||
import { SharedDossiersModule } from '../../shared/shared-dossiers.module';
|
||||
import { FileWorkloadColumnComponent } from './components/file-workload-column/file-workload-column.component';
|
||||
import { FileStatsComponent } from './components/file-stats/file-stats.component';
|
||||
import { WorkflowItemComponent } from './components/workflow-item/workflow-item.component';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@ -26,7 +29,10 @@ const routes = [
|
||||
DossierOverviewBulkActionsComponent,
|
||||
DossierDetailsComponent,
|
||||
DossierDetailsStatsComponent,
|
||||
FileWorkloadColumnComponent,
|
||||
TableItemComponent,
|
||||
FileStatsComponent,
|
||||
WorkflowItemComponent,
|
||||
],
|
||||
providers: [ConfigService],
|
||||
imports: [RouterModule.forChild(routes), CommonModule, SharedModule, SharedDossiersModule, IqserIconsModule, TranslateModule],
|
||||
|
||||
@ -1,91 +1,100 @@
|
||||
<section (longPress)="forceReanalysisAction($event)" *ngIf="!!currentDossier" redactionLongPress>
|
||||
<iqser-page-header
|
||||
(closeAction)="routerHistoryService.navigateToLastDossiersScreen()"
|
||||
[actionConfigs]="actionConfigs"
|
||||
[showCloseButton]="true"
|
||||
[viewModeSelection]="viewModeSelection"
|
||||
>
|
||||
<redaction-file-download-btn
|
||||
[dossier]="currentDossier"
|
||||
[files]="entitiesService.all$ | async"
|
||||
tooltipPosition="below"
|
||||
></redaction-file-download-btn>
|
||||
<ng-container *ngIf="dossier$ | async as dossier">
|
||||
<section (longPress)="forceReanalysisAction($event)" redactionLongPress>
|
||||
<iqser-page-header
|
||||
(closeAction)="routerHistoryService.navigateToLastDossiersScreen()"
|
||||
[actionConfigs]="actionConfigs"
|
||||
[showCloseButton]="true"
|
||||
[viewModeSelection]="viewModeSelection"
|
||||
>
|
||||
<redaction-file-download-btn
|
||||
[dossier]="dossier"
|
||||
[files]="entitiesService.all$ | async"
|
||||
tooltipPosition="below"
|
||||
></redaction-file-download-btn>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="exportFilesAsCSV()"
|
||||
[tooltip]="'dossier-overview.header-actions.download-csv' | translate"
|
||||
icon="red:csv"
|
||||
tooltipPosition="below"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="exportFilesAsCSV()"
|
||||
[tooltip]="'dossier-overview.header-actions.download-csv' | translate"
|
||||
icon="red:csv"
|
||||
tooltipPosition="below"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="reanalyseDossier()"
|
||||
*ngIf="permissionsService.displayReanalyseBtn(currentDossier) && analysisForced"
|
||||
[tooltipClass]="'small ' + ((listingService.areSomeSelected$ | async) ? '' : 'warn')"
|
||||
[tooltip]="'dossier-overview.new-rule.toast.actions.reanalyse-all' | translate"
|
||||
[type]="circleButtonTypes.warn"
|
||||
icon="iqser:refresh"
|
||||
tooltipPosition="below"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="reanalyseDossier()"
|
||||
*ngIf="permissionsService.displayReanalyseBtn(dossier) && analysisForced"
|
||||
[tooltipClass]="'small ' + ((listingService.areSomeSelected$ | async) ? '' : 'warn')"
|
||||
[tooltip]="'dossier-overview.new-rule.toast.actions.reanalyse-all' | translate"
|
||||
[type]="circleButtonTypes.warn"
|
||||
icon="iqser:refresh"
|
||||
tooltipPosition="below"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="fileInput.click()"
|
||||
[tooltip]="'dossier-overview.header-actions.upload-document' | translate"
|
||||
[type]="circleButtonTypes.primary"
|
||||
class="ml-14"
|
||||
icon="iqser:upload"
|
||||
tooltipPosition="below"
|
||||
></iqser-circle-button>
|
||||
</iqser-page-header>
|
||||
<iqser-circle-button
|
||||
(action)="fileInput.click()"
|
||||
[tooltip]="'dossier-overview.header-actions.upload-document' | translate"
|
||||
[type]="circleButtonTypes.primary"
|
||||
class="ml-14"
|
||||
icon="iqser:upload"
|
||||
tooltipPosition="below"
|
||||
></iqser-circle-button>
|
||||
</iqser-page-header>
|
||||
|
||||
<div class="overlay-shadow"></div>
|
||||
<div class="overlay-shadow"></div>
|
||||
|
||||
<div class="content-inner">
|
||||
<div *ngIf="listingMode$ | async as mode" [class.extended]="collapsedDetails" class="content-container">
|
||||
<iqser-table
|
||||
(noDataAction)="fileInput.click()"
|
||||
*ngIf="mode === listingModes.table"
|
||||
[bulkActions]="bulkActions"
|
||||
[hasScrollButton]="true"
|
||||
[itemSize]="80"
|
||||
[noDataButtonLabel]="'dossier-overview.no-data.action' | translate"
|
||||
[noDataText]="'dossier-overview.no-data.title' | translate"
|
||||
[noMatchText]="'dossier-overview.no-match.title' | translate"
|
||||
[selectionEnabled]="true"
|
||||
[showNoDataButton]="true"
|
||||
[tableItemClasses]="{ disabled: disabledFn, 'last-opened': lastOpenedFn }"
|
||||
noDataButtonIcon="iqser:upload"
|
||||
noDataIcon="iqser:document"
|
||||
></iqser-table>
|
||||
<div class="content-inner">
|
||||
<div *ngIf="listingMode$ | async as mode" [class.extended]="collapsedDetails" class="content-container">
|
||||
<iqser-table
|
||||
(noDataAction)="fileInput.click()"
|
||||
*ngIf="mode === listingModes.table"
|
||||
[bulkActions]="bulkActions"
|
||||
[hasScrollButton]="true"
|
||||
[itemSize]="80"
|
||||
[noDataButtonLabel]="'dossier-overview.no-data.action' | translate"
|
||||
[noDataText]="'dossier-overview.no-data.title' | translate"
|
||||
[noMatchText]="'dossier-overview.no-match.title' | translate"
|
||||
[selectionEnabled]="true"
|
||||
[showNoDataButton]="true"
|
||||
[tableItemClasses]="{ disabled: disabledFn, 'last-opened': lastOpenedFn }"
|
||||
noDataButtonIcon="iqser:upload"
|
||||
noDataIcon="iqser:document"
|
||||
></iqser-table>
|
||||
|
||||
<iqser-workflow
|
||||
(addElement)="fileInput.click()"
|
||||
(noDataAction)="fileInput.click()"
|
||||
*ngIf="mode === listingModes.workflow"
|
||||
[config]="workflowConfig"
|
||||
[itemClasses]="{ disabled: disabledFn }"
|
||||
[itemTemplate]="workflowItemTemplate"
|
||||
[noDataButtonLabel]="'dossier-overview.no-data.action' | translate"
|
||||
[noDataText]="'dossier-overview.no-data.title' | translate"
|
||||
[showNoDataButton]="true"
|
||||
addElementColumn="UNASSIGNED"
|
||||
addElementIcon="iqser:upload"
|
||||
itemHeight="56px"
|
||||
noDataButtonIcon="iqser:upload"
|
||||
noDataIcon="iqser:document"
|
||||
></iqser-workflow>
|
||||
<iqser-workflow
|
||||
(addElement)="fileInput.click()"
|
||||
(noDataAction)="fileInput.click()"
|
||||
*ngIf="mode === listingModes.workflow"
|
||||
[config]="workflowConfig"
|
||||
[itemClasses]="{ disabled: disabledFn }"
|
||||
[itemTemplate]="workflowItemTemplate"
|
||||
[noDataButtonLabel]="'dossier-overview.no-data.action' | translate"
|
||||
[noDataText]="'dossier-overview.no-data.title' | translate"
|
||||
[showNoDataButton]="true"
|
||||
addElementColumn="UNASSIGNED"
|
||||
addElementIcon="iqser:upload"
|
||||
itemHeight="56px"
|
||||
noDataButtonIcon="iqser:upload"
|
||||
noDataIcon="iqser:document"
|
||||
></iqser-workflow>
|
||||
</div>
|
||||
|
||||
<div [class.collapsed]="collapsedDetails" class="right-container" iqserHasScrollbar>
|
||||
<redaction-dossier-details
|
||||
(openAssignDossierMembersDialog)="openAssignDossierMembersDialog()"
|
||||
(openDossierDictionaryDialog)="openDossierDictionaryDialog()"
|
||||
(toggleCollapse)="toggleCollapsedDetails()"
|
||||
[dossierAttributes]="dossierAttributes"
|
||||
></redaction-dossier-details>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div [class.collapsed]="collapsedDetails" class="right-container" iqserHasScrollbar>
|
||||
<redaction-dossier-details
|
||||
(openAssignDossierMembersDialog)="openAssignDossierMembersDialog()"
|
||||
(openDossierDictionaryDialog)="openDossierDictionaryDialog()"
|
||||
(toggleCollapse)="toggleCollapsedDetails()"
|
||||
[dossierAttributes]="dossierAttributes"
|
||||
></redaction-dossier-details>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<ng-template #bulkActions>
|
||||
<redaction-dossier-overview-bulk-actions
|
||||
(reload)="bulkActionPerformed()"
|
||||
[dossier]="dossier"
|
||||
></redaction-dossier-overview-bulk-actions>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #needsWorkFilterTemplate let-filter="filter">
|
||||
<redaction-type-filter [filter]="filter"></redaction-type-filter>
|
||||
@ -93,13 +102,6 @@
|
||||
|
||||
<input #fileInput (change)="uploadFiles($event.target['files'])" class="file-upload-input" multiple="true" type="file" />
|
||||
|
||||
<ng-template #bulkActions>
|
||||
<redaction-dossier-overview-bulk-actions
|
||||
(reload)="bulkActionPerformed()"
|
||||
[dossier]="currentDossier"
|
||||
></redaction-dossier-overview-bulk-actions>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #viewModeSelection>
|
||||
<div *ngIf="listingMode$ | async as mode" class="view-mode-selection">
|
||||
<div class="all-caps-label" translate="view-mode.view-as"></div>
|
||||
@ -129,40 +131,13 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #workflowItemTemplate let-entity="entity">
|
||||
<div *ngIf="cast(entity) as file" class="workflow-item">
|
||||
<div>
|
||||
<div class="details">
|
||||
<div [matTooltip]="file.filename" class="filename" matTooltipPosition="above">
|
||||
{{ file.filename }}
|
||||
</div>
|
||||
<ng-container *ngTemplateOutlet="statsTemplate; context: { entity: file }"></ng-container>
|
||||
</div>
|
||||
<div class="user">
|
||||
<redaction-initials-avatar [user]="file.currentReviewer"></redaction-initials-avatar>
|
||||
</div>
|
||||
</div>
|
||||
<redaction-file-actions
|
||||
(actionPerformed)="actionPerformed($event, file)"
|
||||
*ngIf="!file.isProcessing"
|
||||
[file]="file"
|
||||
type="dossier-overview-workflow"
|
||||
></redaction-file-actions>
|
||||
</div>
|
||||
<redaction-workflow-item
|
||||
(actionPerformed)="actionPerformed($event, entity)"
|
||||
[file]="entity"
|
||||
[statsTemplate]="statsTemplate"
|
||||
></redaction-workflow-item>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #statsTemplate let-file="entity">
|
||||
<div class="small-label stats-subtitle">
|
||||
<div>
|
||||
<mat-icon svgIcon="iqser:pages"></mat-icon>
|
||||
{{ file.numberOfPages }}
|
||||
</div>
|
||||
<div>
|
||||
<mat-icon svgIcon="red:exclude-pages"></mat-icon>
|
||||
{{ file.excludedPages.length }}
|
||||
</div>
|
||||
<div *ngIf="file.lastOCRTime" [matTooltipPosition]="'above'" [matTooltip]="'dossier-overview.ocr-performed' | translate">
|
||||
<mat-icon svgIcon="iqser:ocr"></mat-icon>
|
||||
{{ file.lastOCRTime | date: 'mediumDate' }}
|
||||
</div>
|
||||
</div>
|
||||
<redaction-file-stats [file]="file"></redaction-file-stats>
|
||||
</ng-template>
|
||||
|
||||
@ -59,36 +59,3 @@
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.workflow-item {
|
||||
padding: 10px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.details {
|
||||
max-width: calc(100% - 28px);
|
||||
|
||||
.filename {
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
@include common-mixins.line-clamp(1);
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
redaction-file-actions {
|
||||
margin-top: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover redaction-file-actions {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,18 +11,16 @@ import {
|
||||
TemplateRef,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { Dossier, DossierAttributeWithValue, File, FileStatus, IFileAttributeConfig } from '@red/domain';
|
||||
import { Dossier, DossierAttributeWithValue, File, IFileAttributeConfig, WorkflowFileStatus } from '@red/domain';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { FileDropOverlayService } from '@upload-download/services/file-drop-overlay.service';
|
||||
import { FileUploadModel } from '@upload-download/model/file-upload.model';
|
||||
import { FileUploadService } from '@upload-download/services/file-upload.service';
|
||||
import { StatusOverlayService } from '@upload-download/services/status-overlay.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import * as moment from 'moment';
|
||||
import { DossierDetailsComponent } from '../components/dossier-details/dossier-details.component';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { timer } from 'rxjs';
|
||||
import { take, tap } from 'rxjs/operators';
|
||||
import { Observable, timer } from 'rxjs';
|
||||
import { take, tap, withLatestFrom } from 'rxjs/operators';
|
||||
import { convertFiles, Files, handleFileDrop } from '@utils/index';
|
||||
import { DossiersDialogService } from '../../../services/dossiers-dialog.service';
|
||||
import {
|
||||
@ -44,7 +42,7 @@ import { DossierAttributesService } from '@shared/services/controller-wrappers/d
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { RouterHistoryService } from '@services/router-history.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
|
||||
import { ConfigService as AppConfigService } from '@services/config.service';
|
||||
import { ConfigService } from '../config.service';
|
||||
@ -53,6 +51,10 @@ import { DossierTemplatesService } from '@services/entity-services/dossier-templ
|
||||
import { LongPressEvent } from '@shared/directives/long-press.directive';
|
||||
import { UserPreferenceService } from '@services/user-preference.service';
|
||||
import { saveAsCSV } from '@utils/csv-utils';
|
||||
import { FilesService } from '@services/entity-services/files.service';
|
||||
import { FilesMapService } from '@services/entity-services/files-map.service';
|
||||
import { ReanalysisService } from '@services/reanalysis.service';
|
||||
import { DossierStatsService } from '@services/entity-services/dossier-stats.service';
|
||||
|
||||
@Component({
|
||||
templateUrl: './dossier-overview-screen.component.html',
|
||||
@ -66,16 +68,17 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
|
||||
readonly currentUser = this._userService.currentUser;
|
||||
readonly tableHeaderLabel = _('dossier-overview.table-header.title');
|
||||
|
||||
currentDossier = this._dossiersService.activeDossier;
|
||||
collapsedDetails = false;
|
||||
dossierAttributes: DossierAttributeWithValue[] = [];
|
||||
tableColumnConfigs: readonly TableColumnConfig<File>[];
|
||||
analysisForced: boolean;
|
||||
displayedInFileListAttributes: IFileAttributeConfig[] = [];
|
||||
displayedAttributes: IFileAttributeConfig[] = [];
|
||||
readonly workflowConfig: WorkflowConfig<File, FileStatus> = this._configService.workflowConfig(() => this.reloadDossiers());
|
||||
readonly actionConfigs: readonly ActionConfig[] = this._configService.actionConfig;
|
||||
@ViewChild(DossierDetailsComponent, { static: false }) private readonly _dossierDetailsComponent: DossierDetailsComponent;
|
||||
readonly workflowConfig: WorkflowConfig<File, WorkflowFileStatus> = this._configService.workflowConfig(() => this.reloadFiles());
|
||||
readonly actionConfigs: readonly ActionConfig[];
|
||||
readonly dossier$: Observable<Dossier>;
|
||||
readonly dossierId: string;
|
||||
currentDossier: Dossier;
|
||||
private _lastScrolledIndex: number;
|
||||
@ViewChild('needsWorkFilterTemplate', { read: TemplateRef, static: true })
|
||||
private readonly _needsWorkFilterTemplate: TemplateRef<unknown>;
|
||||
@ -90,11 +93,11 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
|
||||
readonly permissionsService: PermissionsService,
|
||||
private readonly _loadingService: LoadingService,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _reanalysisService: ReanalysisService,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
private readonly _dossierTemplatesService: DossierTemplatesService,
|
||||
readonly routerHistoryService: RouterHistoryService,
|
||||
private readonly _appConfigService: AppConfigService,
|
||||
private readonly _translateService: TranslateService,
|
||||
private readonly _dialogService: DossiersDialogService,
|
||||
private readonly _changeDetectorRef: ChangeDetectorRef,
|
||||
private readonly _fileUploadService: FileUploadService,
|
||||
@ -104,8 +107,22 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
|
||||
private readonly _fileAttributesService: FileAttributesService,
|
||||
private readonly _configService: ConfigService,
|
||||
private readonly _userPreferenceService: UserPreferenceService,
|
||||
private readonly _filesService: FilesService,
|
||||
private readonly _fileMapService: FilesMapService,
|
||||
private readonly _dossierStatsService: DossierStatsService,
|
||||
activatedRoute: ActivatedRoute,
|
||||
) {
|
||||
super(_injector);
|
||||
this._appStateService.reset();
|
||||
this.dossierId = activatedRoute.snapshot.paramMap.get('dossierId');
|
||||
this.actionConfigs = this._configService.actionConfig(this.dossierId);
|
||||
this.dossier$ = this._dossiersService.getEntityChanged$(this.dossierId);
|
||||
this.currentDossier = this._dossiersService.find(this.dossierId);
|
||||
|
||||
this.fileAttributeConfigs = this._fileAttributesService.getFileAttributeConfig(
|
||||
this.currentDossier.dossierTemplateId,
|
||||
)?.fileAttributeConfigs;
|
||||
this.tableColumnConfigs = this._configService.tableConfig(this.displayedAttributes);
|
||||
}
|
||||
|
||||
private _fileAttributeConfigs: IFileAttributeConfig[];
|
||||
@ -126,10 +143,10 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
|
||||
|
||||
async actionPerformed(action?: string, file?: File) {
|
||||
if (action === 'assign-reviewer') {
|
||||
return this.reloadDossiers();
|
||||
return this.reloadFiles();
|
||||
}
|
||||
|
||||
this.calculateData();
|
||||
await this.calculateData();
|
||||
|
||||
if (action === 'navigate') {
|
||||
await this._router.navigate([file.routerLink]);
|
||||
@ -141,30 +158,26 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
|
||||
lastOpenedFn = (fileStatus: File) => fileStatus.lastOpened;
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this._loadingService.start();
|
||||
this._loadEntitiesFromState();
|
||||
this.fileAttributeConfigs = this._fileAttributesService.getFileAttributeConfig(
|
||||
this.currentDossier.dossierTemplateId,
|
||||
)?.fileAttributeConfigs;
|
||||
this.tableColumnConfigs = this._configService.tableConfig(this.displayedAttributes);
|
||||
await this._loadEntitiesFromState();
|
||||
|
||||
this.addSubscription = this._fileMapService
|
||||
.get$(this.dossierId)
|
||||
.pipe(tap(files => this.entitiesService.setEntities(files)))
|
||||
.subscribe();
|
||||
|
||||
try {
|
||||
this._fileDropOverlayService.initFileDropHandling();
|
||||
|
||||
this.calculateData();
|
||||
await this.calculateData();
|
||||
|
||||
this.addSubscription = timer(0, 20 * 1000).subscribe(async () => {
|
||||
await this._appStateService.reloadActiveDossierFilesIfNecessary();
|
||||
this.calculateData();
|
||||
await this.reloadFiles();
|
||||
});
|
||||
|
||||
this.addSubscription = this.listingMode$.subscribe(() => {
|
||||
this._computeAllFilters();
|
||||
});
|
||||
|
||||
// this.addSubscription = this._appStateService.fileChanged$.subscribe(() => {
|
||||
// this.calculateData();
|
||||
// });
|
||||
|
||||
this.addSubscription = this._dossierTemplatesService.entityChanged$.subscribe(() => {
|
||||
this.fileAttributeConfigs = this._fileAttributesService.getFileAttributeConfig(
|
||||
this.currentDossier.dossierTemplateId,
|
||||
@ -189,8 +202,8 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
|
||||
}
|
||||
|
||||
async ngOnAttach() {
|
||||
await this._appStateService.reloadActiveDossierFiles();
|
||||
this._loadEntitiesFromState();
|
||||
// await this._appStateService.reloadActiveDossierFiles();
|
||||
// await this._loadEntitiesFromState();
|
||||
await this.ngOnInit();
|
||||
this._tableComponent.scrollViewport.scrollToIndex(this._lastScrolledIndex, 'smooth');
|
||||
}
|
||||
@ -205,34 +218,32 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
|
||||
|
||||
async reanalyseDossier() {
|
||||
try {
|
||||
await this._appStateService.reanalyzeDossier();
|
||||
await this.reloadDossiers();
|
||||
await this._reanalysisService.reanalyzeDossier(this.dossierId, true).toPromise();
|
||||
await this.reloadFiles();
|
||||
this._toaster.success(_('dossier-overview.reanalyse-dossier.success'));
|
||||
} catch (e) {
|
||||
this._toaster.error(_('dossier-overview.reanalyse-dossier.error'));
|
||||
}
|
||||
}
|
||||
|
||||
async reloadDossiers() {
|
||||
await this._appStateService.getFiles(this.currentDossier, false);
|
||||
this.calculateData();
|
||||
async reloadFiles() {
|
||||
const files = await this._appStateService.getFiles(this.currentDossier);
|
||||
await this._dossierStatsService.getFor([this.dossierId]).toPromise();
|
||||
this.entitiesService.setEntities(files);
|
||||
await this.calculateData();
|
||||
}
|
||||
|
||||
calculateData(): void {
|
||||
if (!this._dossiersService.activeDossierId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._loadEntitiesFromState();
|
||||
async calculateData(): Promise<void> {
|
||||
await this._loadEntitiesFromState();
|
||||
this._computeAllFilters();
|
||||
|
||||
this._dossierDetailsComponent?.calculateChartConfig();
|
||||
this._changeDetectorRef.detectChanges();
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
@HostListener('drop', ['$event'])
|
||||
onDrop(event: DragEvent): void {
|
||||
handleFileDrop(event, this.currentDossier, this._uploadFiles.bind(this));
|
||||
const currentDossier = this._dossiersService.find(this.dossierId);
|
||||
handleFileDrop(event, currentDossier, this._uploadFiles.bind(this));
|
||||
}
|
||||
|
||||
@HostListener('dragover', ['$event'])
|
||||
@ -242,8 +253,8 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
|
||||
}
|
||||
|
||||
exportFilesAsCSV() {
|
||||
this.sortedDisplayedEntities$.pipe(take(1)).subscribe(entities => {
|
||||
const fileName = this._dossiersService.activeDossier.dossierName + '.export.csv';
|
||||
this.sortedDisplayedEntities$.pipe(take(1), withLatestFrom(this.dossier$)).subscribe(([entities, dossier]) => {
|
||||
const fileName = dossier.dossierName + '.export.csv';
|
||||
saveAsCSV(
|
||||
fileName,
|
||||
entities,
|
||||
@ -254,7 +265,8 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
|
||||
'primaryAttribute',
|
||||
'numberOfPages',
|
||||
'assignee',
|
||||
'status',
|
||||
'workflowStatus',
|
||||
'processingStatus',
|
||||
'lastUpdated',
|
||||
'lastUploaded',
|
||||
'lastProcessed',
|
||||
@ -275,18 +287,18 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
|
||||
}
|
||||
|
||||
async bulkActionPerformed(): Promise<void> {
|
||||
await this.reloadDossiers();
|
||||
await this.reloadFiles();
|
||||
}
|
||||
|
||||
openAssignDossierMembersDialog(): void {
|
||||
const data = { dossier: this.currentDossier, section: 'members' };
|
||||
this._dialogService.openDialog('editDossier', null, data, async () => this.reloadDossiers());
|
||||
const data = { dossierId: this.dossierId, section: 'members' };
|
||||
this._dialogService.openDialog('editDossier', null, data, async () => this.reloadFiles());
|
||||
}
|
||||
|
||||
openDossierDictionaryDialog() {
|
||||
const data = { dossier: this.currentDossier, section: 'dossierDictionary' };
|
||||
const data = { dossierId: this.dossierId, section: 'dossierDictionary' };
|
||||
this._dialogService.openDialog('editDossier', null, data, async () => {
|
||||
await this.reloadDossiers();
|
||||
await this.reloadFiles();
|
||||
});
|
||||
}
|
||||
|
||||
@ -297,25 +309,22 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
|
||||
recentlyModifiedChecker = (file: File) =>
|
||||
moment(file.lastUpdated).add(this._appConfigService.values.RECENT_PERIOD_IN_HOURS, 'hours').isAfter(moment());
|
||||
|
||||
private _loadEntitiesFromState() {
|
||||
this.currentDossier = this._dossiersService.activeDossier;
|
||||
if (this.currentDossier) {
|
||||
this.entitiesService.setEntities([...this.currentDossier.files]);
|
||||
private async _loadEntitiesFromState() {
|
||||
this.currentDossier = this._dossiersService.find(this.dossierId);
|
||||
if (!this._fileMapService.has(this.dossierId)) {
|
||||
this._loadingService.start();
|
||||
await this._appStateService.getFiles(this.currentDossier);
|
||||
}
|
||||
}
|
||||
|
||||
private async _uploadFiles(files: FileUploadModel[]) {
|
||||
const fileCount = await this._fileUploadService.uploadFiles(files);
|
||||
const fileCount = await this._fileUploadService.uploadFiles(files, this.dossierId);
|
||||
if (fileCount) {
|
||||
this._statusOverlayService.openUploadStatusOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
private _computeAllFilters() {
|
||||
if (!this.currentDossier) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filterGroups = this._configService.filterGroups(
|
||||
this.entitiesService.all,
|
||||
this.listingMode,
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
<div class="needs-work">
|
||||
<redaction-annotation-icon
|
||||
*ngIf="dossierStats.hasRedactionsFilePresent"
|
||||
[color]="redactionColor"
|
||||
label="R"
|
||||
type="square"
|
||||
></redaction-annotation-icon>
|
||||
|
||||
<redaction-annotation-icon
|
||||
*ngIf="dossierStats.hasHintsNoRedactionsFilePresent"
|
||||
[color]="hintColor"
|
||||
label="H"
|
||||
type="circle"
|
||||
></redaction-annotation-icon>
|
||||
|
||||
<redaction-annotation-icon
|
||||
*ngIf="dossierStats.hasSuggestionsFilePresent"
|
||||
[color]="suggestionColor"
|
||||
label="S"
|
||||
type="rhombus"
|
||||
></redaction-annotation-icon>
|
||||
</div>
|
||||
@ -0,0 +1,17 @@
|
||||
.needs-work {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
min-width: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill-opacity: 0.6;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { Dossier, DossierStats } from '@red/domain';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-dossier-workload-column',
|
||||
templateUrl: './dossier-workload-column.component.html',
|
||||
styleUrls: ['./dossier-workload-column.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DossierWorkloadColumnComponent {
|
||||
@Input() dossier: Dossier;
|
||||
@Input() dossierStats: DossierStats;
|
||||
|
||||
constructor(private readonly _appStateService: AppStateService) {}
|
||||
|
||||
get suggestionColor() {
|
||||
return this._appStateService.getDictionaryColor('suggestion');
|
||||
}
|
||||
|
||||
get hintColor() {
|
||||
return this._appStateService.getDictionaryColor('hint');
|
||||
}
|
||||
|
||||
get redactionColor() {
|
||||
return this._appStateService.getDictionaryColor('redaction');
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
<iqser-status-bar [configs]="statusConfig"></iqser-status-bar>
|
||||
<div class="action-buttons" (longPress)="forceReanalysisAction($event)" redactionLongPress>
|
||||
<iqser-status-bar *ngIf="dossierStats$ | async as stats" [configs]="statusConfig(stats)"></iqser-status-bar>
|
||||
|
||||
<div (longPress)="forceReanalysisAction($event)" class="action-buttons" redactionLongPress>
|
||||
<iqser-circle-button
|
||||
(action)="openEditDossierDialog($event, dossier)"
|
||||
(action)="openEditDossierDialog($event, dossier.dossierId)"
|
||||
*ngIf="currentUser.isManager"
|
||||
[tooltip]="'dossier-listing.edit.action' | translate"
|
||||
[type]="circleButtonTypes.dark"
|
||||
@ -9,12 +10,16 @@
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="reanalyseDossier($event, dossier)"
|
||||
(action)="reanalyseDossier($event, dossier.dossierId)"
|
||||
*ngIf="permissionsService.displayReanalyseBtn(dossier) && analysisForced"
|
||||
[tooltip]="'dossier-listing.reanalyse.action' | translate"
|
||||
[type]="circleButtonTypes.dark"
|
||||
icon="iqser:refresh"
|
||||
></iqser-circle-button>
|
||||
|
||||
<redaction-file-download-btn [dossier]="dossier" [files]="dossier.files" [type]="circleButtonTypes.dark"></redaction-file-download-btn>
|
||||
<redaction-file-download-btn
|
||||
[dossier]="dossier"
|
||||
[files]="filesMapService.get(dossier.id)"
|
||||
[type]="circleButtonTypes.dark"
|
||||
></redaction-file-download-btn>
|
||||
</div>
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { CircleButtonTypes, StatusBarConfig } from '@iqser/common-ui';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { Dossier, StatusSorter } from '@red/domain';
|
||||
import { Dossier, DossierStats, StatusSorter } from '@red/domain';
|
||||
import { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
|
||||
import { LongPressEvent } from '@shared/directives/long-press.directive';
|
||||
import { UserPreferenceService } from '@services/user-preference.service';
|
||||
import { FilesMapService } from '@services/entity-services/files-map.service';
|
||||
import { ReanalysisService } from '@services/reanalysis.service';
|
||||
import { switchMapTo, tap } from 'rxjs/operators';
|
||||
import { DossierStatsService } from '@services/entity-services/dossier-stats.service';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-dossiers-listing-actions',
|
||||
@ -14,7 +19,7 @@ import { UserPreferenceService } from '@services/user-preference.service';
|
||||
styleUrls: ['./dossiers-listing-actions.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DossiersListingActionsComponent {
|
||||
export class DossiersListingActionsComponent implements OnInit {
|
||||
readonly circleButtonTypes = CircleButtonTypes;
|
||||
readonly currentUser = this._userService.currentUser;
|
||||
|
||||
@ -22,50 +27,43 @@ export class DossiersListingActionsComponent {
|
||||
|
||||
@Input() dossier: Dossier;
|
||||
@Output() readonly actionPerformed = new EventEmitter<Dossier | undefined>();
|
||||
dossierStats$: Observable<DossierStats>;
|
||||
|
||||
constructor(
|
||||
readonly permissionsService: PermissionsService,
|
||||
readonly appStateService: AppStateService,
|
||||
private readonly _dialogService: DossiersDialogService,
|
||||
private readonly _reanalysisService: ReanalysisService,
|
||||
private readonly _userService: UserService,
|
||||
readonly permissionsService: PermissionsService,
|
||||
readonly filesMapService: FilesMapService,
|
||||
private readonly _dialogService: DossiersDialogService,
|
||||
private readonly _dossierStatsService: DossierStatsService,
|
||||
private readonly _userPreferenceService: UserPreferenceService,
|
||||
) {}
|
||||
|
||||
get statusConfig(): readonly StatusBarConfig<string>[] {
|
||||
if (!this.dossier) {
|
||||
return [];
|
||||
}
|
||||
ngOnInit() {
|
||||
this.dossierStats$ = this._dossierStatsService.watch$(this.dossier.dossierId);
|
||||
}
|
||||
|
||||
const obj = this.dossier.files.reduce((acc, file) => {
|
||||
const status = file.status;
|
||||
if (!acc[status]) {
|
||||
acc[status] = 1;
|
||||
} else {
|
||||
acc[status]++;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return Object.keys(obj)
|
||||
statusConfig(stats: DossierStats): readonly StatusBarConfig<string>[] {
|
||||
return Object.keys(stats.fileCountPerWorkflowStatus)
|
||||
.sort(StatusSorter.byStatus)
|
||||
.map(status => ({ length: obj[status], color: status }));
|
||||
.map(status => ({ length: stats.fileCountPerWorkflowStatus[status], color: status }));
|
||||
}
|
||||
|
||||
forceReanalysisAction($event: LongPressEvent) {
|
||||
this.analysisForced = !$event.touchEnd && this._userPreferenceService.areDevFeaturesEnabled;
|
||||
}
|
||||
|
||||
openEditDossierDialog($event: MouseEvent, dossier: Dossier): void {
|
||||
openEditDossierDialog($event: MouseEvent, dossierId: string): void {
|
||||
this._dialogService.openDialog('editDossier', $event, {
|
||||
dossier,
|
||||
dossierId,
|
||||
afterSave: () => this.actionPerformed.emit(),
|
||||
});
|
||||
}
|
||||
|
||||
reanalyseDossier($event: MouseEvent, dossier: Dossier): void {
|
||||
async reanalyseDossier($event: MouseEvent, id: string): Promise<void> {
|
||||
$event.stopPropagation();
|
||||
this.appStateService.reanalyzeDossier(dossier).then(() => {
|
||||
this.appStateService.loadAllDossiers().then(() => this.actionPerformed.emit());
|
||||
});
|
||||
const reanalysis$ = this._reanalysisService.reanalyzeDossier(id).pipe(switchMapTo(this.appStateService.loadAllDossiers()));
|
||||
await reanalysis$.pipe(tap(() => this.actionPerformed.emit())).toPromise();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
<div>
|
||||
<redaction-simple-doughnut-chart
|
||||
[config]="dossiersChartData"
|
||||
[config]="dossiersChartData$ | async"
|
||||
[radius]="80"
|
||||
[strokeWidth]="15"
|
||||
[subtitle]="'dossier-listing.stats.charts.dossiers' | translate"
|
||||
></redaction-simple-doughnut-chart>
|
||||
|
||||
<div *ngIf="dossiersService.stats$ | async as stats" class="dossier-stats-container">
|
||||
<div *ngIf="dossiersService.generalStats$ | async as stats" class="dossier-stats-container">
|
||||
<div class="dossier-stats-item">
|
||||
<mat-icon svgIcon="red:needs-work"></mat-icon>
|
||||
<div>
|
||||
@ -24,9 +24,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<redaction-simple-doughnut-chart
|
||||
[config]="documentsChartData"
|
||||
*ngIf="documentsChartData$ | async as config"
|
||||
[config]="config"
|
||||
[radius]="80"
|
||||
[strokeWidth]="15"
|
||||
[subtitle]="'dossier-listing.stats.charts.total-documents' | translate"
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
|
||||
import { FilterService } from '@iqser/common-ui';
|
||||
import { FilterService, mapEach } from '@iqser/common-ui';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { Dossier, DossierStats, DossierStatuses, FileCountPerWorkflowStatus, StatusSorter } from '@red/domain';
|
||||
import { workflowFileStatusTranslations } from '../../../../translations/file-status-translations';
|
||||
import { TranslateChartService } from '@services/translate-chart.service';
|
||||
import { filter, map, switchMap } from 'rxjs/operators';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { DossierStatsService } from '@services/entity-services/dossier-stats.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-dossiers-listing-details',
|
||||
@ -10,8 +17,54 @@ import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DossiersListingDetailsComponent {
|
||||
@Input() dossiersChartData: DoughnutChartConfig[];
|
||||
@Input() documentsChartData: DoughnutChartConfig[];
|
||||
readonly documentsChartData$: Observable<DoughnutChartConfig[]>;
|
||||
readonly dossiersChartData$: Observable<DoughnutChartConfig[]>;
|
||||
|
||||
constructor(readonly filterService: FilterService, readonly dossiersService: DossiersService) {}
|
||||
constructor(
|
||||
readonly filterService: FilterService,
|
||||
readonly dossiersService: DossiersService,
|
||||
private readonly _dossierStatsMap: DossierStatsService,
|
||||
private readonly _translateChartService: TranslateChartService,
|
||||
) {
|
||||
this.documentsChartData$ = this.dossiersService.all$.pipe(
|
||||
mapEach(dossier => _dossierStatsMap.watch$(dossier.dossierId)),
|
||||
switchMap(stats$ => combineLatest(stats$)),
|
||||
filter(stats => !stats.some(s => s === undefined)),
|
||||
map(stats => this._toChartData(stats)),
|
||||
);
|
||||
|
||||
this.dossiersChartData$ = this.dossiersService.all$.pipe(map(dossiers => this._toDossierChartData(dossiers)));
|
||||
}
|
||||
|
||||
private _toDossierChartData(dossiers: Dossier[]): DoughnutChartConfig[] {
|
||||
const activeDossiersCount = dossiers.filter(p => p.status === DossierStatuses.ACTIVE).length;
|
||||
const inactiveDossiersCount = dossiers.length - activeDossiersCount;
|
||||
|
||||
return [
|
||||
{ value: activeDossiersCount, color: 'ACTIVE', label: _('active') },
|
||||
{ value: inactiveDossiersCount, color: 'DELETED', label: _('archived') },
|
||||
];
|
||||
}
|
||||
|
||||
private _toChartData(stats: DossierStats[]) {
|
||||
const chartData: FileCountPerWorkflowStatus = {};
|
||||
stats.forEach(stat => {
|
||||
const statuses: FileCountPerWorkflowStatus = stat.fileCountPerWorkflowStatus;
|
||||
Object.keys(statuses).forEach(status => {
|
||||
chartData[status] = chartData[status] ? (chartData[status] as number) + (statuses[status] as number) : statuses[status];
|
||||
});
|
||||
});
|
||||
|
||||
const documentsChartData = Object.keys(chartData).map(
|
||||
status =>
|
||||
({
|
||||
value: chartData[status],
|
||||
color: status,
|
||||
label: workflowFileStatusTranslations[status],
|
||||
key: status,
|
||||
} as DoughnutChartConfig),
|
||||
);
|
||||
documentsChartData.sort((a, b) => StatusSorter.byStatus(a.key, b.key));
|
||||
return this._translateChartService.translateStatus(documentsChartData);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,29 +1,35 @@
|
||||
<div [matTooltip]="dossier.dossierName" class="table-item-title heading mb-6" matTooltipPosition="above">
|
||||
{{ dossier.dossierName }}
|
||||
</div>
|
||||
|
||||
<div class="small-label stats-subtitle mb-6">
|
||||
<div>
|
||||
<mat-icon svgIcon="red:template"></mat-icon>
|
||||
{{ getDossierTemplateNameFor(dossier.dossierTemplateId) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="small-label stats-subtitle">
|
||||
<div>
|
||||
<mat-icon svgIcon="iqser:document"></mat-icon>
|
||||
{{ dossier.filesLength }}
|
||||
{{ dossierStats.numberOfFiles }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<mat-icon svgIcon="iqser:pages"></mat-icon>
|
||||
{{ dossier.totalNumberOfPages }}
|
||||
{{ dossierStats.numberOfPages }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<mat-icon svgIcon="red:user"></mat-icon>
|
||||
{{ dossier.memberIds.length }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<mat-icon svgIcon="red:calendar"></mat-icon>
|
||||
{{ dossier.date | date: 'mediumDate' }}
|
||||
</div>
|
||||
|
||||
<div *ngIf="dossier.dueDate">
|
||||
<mat-icon svgIcon="red:lightning"></mat-icon>
|
||||
{{ dossier.dueDate | date: 'mediumDate' }}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { Dossier } from '@red/domain';
|
||||
import { Dossier, DossierStats } from '@red/domain';
|
||||
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
|
||||
import { DossierStatsService } from '@services/entity-services/dossier-stats.service';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-dossiers-listing-dossier-name',
|
||||
@ -10,8 +12,13 @@ import { DossierTemplatesService } from '@services/entity-services/dossier-templ
|
||||
})
|
||||
export class DossiersListingDossierNameComponent {
|
||||
@Input() dossier: Dossier;
|
||||
@Input() dossierStats: DossierStats;
|
||||
|
||||
constructor(private readonly _dossierTemplatesService: DossierTemplatesService) {}
|
||||
constructor(
|
||||
private readonly _dossierTemplatesService: DossierTemplatesService,
|
||||
private readonly _dossierStatsService: DossierStatsService,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
) {}
|
||||
|
||||
getDossierTemplateNameFor(dossierTemplateId: string): string {
|
||||
return this._dossierTemplatesService.find(dossierTemplateId).name;
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
<div class="cell">
|
||||
<redaction-dossiers-listing-dossier-name [dossier]="dossier"></redaction-dossiers-listing-dossier-name>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<redaction-needs-work-badge [needsWorkInput]="dossier"></redaction-needs-work-badge>
|
||||
</div>
|
||||
<ng-container *ngIf="dossierStatsService.watch$(dossier.dossierId) | async as stats">
|
||||
<div class="cell">
|
||||
<redaction-dossiers-listing-dossier-name [dossierStats]="stats" [dossier]="dossier"></redaction-dossiers-listing-dossier-name>
|
||||
</div>
|
||||
|
||||
<div class="cell">
|
||||
<redaction-dossier-workload-column [dossierStats]="stats" [dossier]="dossier"></redaction-dossier-workload-column>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div class="cell user-column">
|
||||
<redaction-initials-avatar [user]="dossier.ownerId" [withName]="true"></redaction-initials-avatar>
|
||||
</div>
|
||||
|
||||
<div class="cell status-container">
|
||||
<redaction-dossiers-listing-actions (actionPerformed)="calculateData.emit()" [dossier]="dossier"></redaction-dossiers-listing-actions>
|
||||
</div>
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { Dossier } from '@red/domain';
|
||||
import { Required } from '@iqser/common-ui';
|
||||
import { DossierStatsService } from '@services/entity-services/dossier-stats.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-table-item',
|
||||
templateUrl: './table-item.component.html',
|
||||
styleUrls: ['./table-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TableItemComponent {
|
||||
@Input() @Required() dossier!: Dossier;
|
||||
@Output() readonly calculateData = new EventEmitter();
|
||||
|
||||
constructor(readonly dossierStatsService: DossierStatsService) {}
|
||||
}
|
||||
|
||||
@ -1,21 +1,16 @@
|
||||
import { Injectable, TemplateRef } from '@angular/core';
|
||||
import { ButtonConfig, IFilterGroup, keyChecker, NestedFilter, TableColumnConfig } from '@iqser/common-ui';
|
||||
import { ButtonConfig, IFilterGroup, INestedFilter, keyChecker, NestedFilter, TableColumnConfig } from '@iqser/common-ui';
|
||||
import { Dossier, StatusSorter, User } from '@red/domain';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { UserPreferenceService } from '@services/user-preference.service';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { fileStatusTranslations } from '../../translations/file-status-translations';
|
||||
import {
|
||||
annotationFilterChecker,
|
||||
dossierMemberChecker,
|
||||
dossierStatusChecker,
|
||||
dossierTemplateChecker,
|
||||
RedactionFilterSorter,
|
||||
} from '@utils/index';
|
||||
import { workflowFileStatusTranslations } from '../../translations/file-status-translations';
|
||||
import { dossierMemberChecker, dossierTemplateChecker, RedactionFilterSorter } from '@utils/index';
|
||||
import { workloadTranslations } from '../../translations/workload-translations';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
|
||||
import { DossierStatsService } from '@services/entity-services/dossier-stats.service';
|
||||
|
||||
@Injectable()
|
||||
export class ConfigService {
|
||||
@ -25,6 +20,7 @@ export class ConfigService {
|
||||
private readonly _userService: UserService,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _dossierTemplatesService: DossierTemplatesService,
|
||||
private readonly _dossierStatsService: DossierStatsService,
|
||||
) {}
|
||||
|
||||
get tableConfig(): TableColumnConfig<Dossier>[] {
|
||||
@ -89,36 +85,35 @@ export class ConfigService {
|
||||
const filterGroups: IFilterGroup[] = [];
|
||||
|
||||
entities?.forEach(entry => {
|
||||
// all people
|
||||
entry.memberIds.forEach(f => allDistinctPeople.add(f));
|
||||
// Needs work
|
||||
entry.files.forEach(file => {
|
||||
allDistinctFileStatus.add(file.status);
|
||||
if (file.analysisRequired) {
|
||||
allDistinctNeedsWork.add('analysis');
|
||||
}
|
||||
if (entry.hintsOnly) {
|
||||
allDistinctNeedsWork.add('hint');
|
||||
}
|
||||
if (entry.hasRedactions) {
|
||||
allDistinctNeedsWork.add('redaction');
|
||||
}
|
||||
if (entry.hasSuggestions) {
|
||||
allDistinctNeedsWork.add('suggestion');
|
||||
}
|
||||
if (entry.hasNone) {
|
||||
allDistinctNeedsWork.add('none');
|
||||
}
|
||||
});
|
||||
|
||||
allDistinctDossierTemplates.add(entry.dossierTemplateId);
|
||||
const stats = this._dossierStatsService.get(entry.dossierId);
|
||||
|
||||
if (!stats) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(stats?.fileCountPerWorkflowStatus).forEach(status => allDistinctFileStatus.add(status));
|
||||
|
||||
if (stats.hasHintsNoRedactionsFilePresent) {
|
||||
allDistinctNeedsWork.add('hint');
|
||||
}
|
||||
if (stats.hasRedactionsFilePresent) {
|
||||
allDistinctNeedsWork.add('redaction');
|
||||
}
|
||||
if (stats.hasSuggestionsFilePresent) {
|
||||
allDistinctNeedsWork.add('suggestion');
|
||||
}
|
||||
if (stats.hasNoFlagsFilePresent) {
|
||||
allDistinctNeedsWork.add('none');
|
||||
}
|
||||
});
|
||||
|
||||
const statusFilters = [...allDistinctFileStatus].map(
|
||||
status =>
|
||||
new NestedFilter({
|
||||
id: status,
|
||||
label: this._translateService.instant(fileStatusTranslations[status]),
|
||||
label: this._translateService.instant(workflowFileStatusTranslations[status]),
|
||||
}),
|
||||
);
|
||||
|
||||
@ -127,7 +122,7 @@ export class ConfigService {
|
||||
label: this._translateService.instant('filters.status'),
|
||||
icon: 'red:status',
|
||||
filters: statusFilters.sort((a, b) => StatusSorter[a.id] - StatusSorter[b.id]),
|
||||
checker: dossierStatusChecker,
|
||||
checker: (dossier: Dossier, filter: INestedFilter) => this._dossierStatusChecker(dossier, filter),
|
||||
});
|
||||
|
||||
const peopleFilters = [...allDistinctPeople].map(
|
||||
@ -160,7 +155,7 @@ export class ConfigService {
|
||||
icon: 'red:needs-work',
|
||||
filterTemplate: needsWorkFilterTemplate,
|
||||
filters: needsWorkFilters.sort((a, b) => RedactionFilterSorter[a.id] - RedactionFilterSorter[b.id]),
|
||||
checker: annotationFilterChecker,
|
||||
checker: (dossier: Dossier, filter: INestedFilter) => this._annotationFilterChecker(dossier, filter),
|
||||
matchAll: true,
|
||||
});
|
||||
|
||||
@ -206,4 +201,30 @@ export class ConfigService {
|
||||
|
||||
return filterGroups;
|
||||
}
|
||||
|
||||
private _dossierStatusChecker = (dossier: Dossier, filter: INestedFilter) => {
|
||||
const stats = this._dossierStatsService.get(dossier.dossierId);
|
||||
return stats?.fileCountPerWorkflowStatus[filter.id];
|
||||
};
|
||||
|
||||
private _annotationFilterChecker = (dossier: Dossier, filter: INestedFilter) => {
|
||||
const stats = this._dossierStatsService.get(dossier.dossierId);
|
||||
switch (filter.id) {
|
||||
// case 'analysis': {
|
||||
// return stats.reanalysisRequired;
|
||||
// }
|
||||
case 'suggestion': {
|
||||
return stats.hasSuggestionsFilePresent;
|
||||
}
|
||||
case 'redaction': {
|
||||
return stats.hasRedactionsFilePresent;
|
||||
}
|
||||
case 'hint': {
|
||||
return stats.hasHintsNoRedactionsFilePresent;
|
||||
}
|
||||
case 'none': {
|
||||
return stats.hasNoFlagsFilePresent;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import { DossiersListingDossierNameComponent } from './components/dossiers-listi
|
||||
import { ConfigService } from './config.service';
|
||||
import { TableItemComponent } from './components/table-item/table-item.component';
|
||||
import { SharedDossiersModule } from '../../shared/shared-dossiers.module';
|
||||
import { DossierWorkloadColumnComponent } from './components/dossier-workload-column/dossier-workload-column.component';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@ -26,6 +27,7 @@ const routes = [
|
||||
DossiersListingActionsComponent,
|
||||
DossiersListingDetailsComponent,
|
||||
DossiersListingDossierNameComponent,
|
||||
DossierWorkloadColumnComponent,
|
||||
TableItemComponent,
|
||||
],
|
||||
providers: [ConfigService],
|
||||
|
||||
@ -18,11 +18,7 @@
|
||||
</div>
|
||||
|
||||
<div class="right-container" iqserHasScrollbar>
|
||||
<redaction-dossiers-listing-details
|
||||
*ngIf="(entitiesService.noData$ | async) === false"
|
||||
[documentsChartData]="documentsChartData"
|
||||
[dossiersChartData]="dossiersChartData"
|
||||
></redaction-dossiers-listing-details>
|
||||
<redaction-dossiers-listing-details *ngIf="(entitiesService.noData$ | async) === false"></redaction-dossiers-listing-details>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -32,5 +28,5 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #tableItemTemplate let-dossier="entity">
|
||||
<redaction-table-item (calculateData)="calculateData()" [dossier]="dossier"></redaction-table-item>
|
||||
<redaction-table-item (calculateData)="computeAllFilters()" [dossier]="dossier"></redaction-table-item>
|
||||
</ng-template>
|
||||
|
||||
@ -10,21 +10,19 @@ import {
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { Dossier, DossierStatuses, StatusSorter } from '@red/domain';
|
||||
import { Dossier } from '@red/domain';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { TranslateChartService } from '@services/translate-chart.service';
|
||||
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
|
||||
import { timer } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { Router } from '@angular/router';
|
||||
import { DossiersDialogService } from '../../../services/dossiers-dialog.service';
|
||||
import { groupBy } from '@utils/index';
|
||||
import { DefaultListingServicesTmp, EntitiesService, ListingComponent, OnAttach, OnDetach, TableComponent } from '@iqser/common-ui';
|
||||
import { fileStatusTranslations } from '../../../translations/file-status-translations';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { ConfigService } from '../config.service';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { FilesService } from '@services/entity-services/files.service';
|
||||
|
||||
@Component({
|
||||
templateUrl: './dossiers-listing-screen.component.html',
|
||||
@ -38,13 +36,12 @@ import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
})
|
||||
export class DossiersListingScreenComponent
|
||||
extends ListingComponent<Dossier>
|
||||
implements OnInit, AfterViewInit, OnDestroy, OnAttach, OnDetach {
|
||||
implements OnInit, AfterViewInit, OnDestroy, OnAttach, OnDetach
|
||||
{
|
||||
readonly currentUser = this._userService.currentUser;
|
||||
readonly tableColumnConfigs = this._configService.tableConfig;
|
||||
readonly tableHeaderLabel = _('dossier-listing.table-header.title');
|
||||
readonly buttonConfigs = this._configService.buttonsConfig(() => this.openAddDossierDialog());
|
||||
dossiersChartData: DoughnutChartConfig[] = [];
|
||||
documentsChartData: DoughnutChartConfig[] = [];
|
||||
private _lastScrolledIndex: number;
|
||||
@ViewChild('needsWorkFilterTemplate', {
|
||||
read: TemplateRef,
|
||||
@ -63,29 +60,17 @@ export class DossiersListingScreenComponent
|
||||
private readonly _dialogService: DossiersDialogService,
|
||||
private readonly _translateChartService: TranslateChartService,
|
||||
private readonly _configService: ConfigService,
|
||||
private readonly _filesService: FilesService,
|
||||
) {
|
||||
super(_injector);
|
||||
this._appStateService.reset();
|
||||
}
|
||||
|
||||
private get _activeDossiersCount(): number {
|
||||
return this.entitiesService.all.filter(p => p.status === DossierStatuses.ACTIVE).length;
|
||||
}
|
||||
|
||||
private get _inactiveDossiersCount(): number {
|
||||
return this.entitiesService.all.length - this._activeDossiersCount;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.calculateData();
|
||||
this.computeAllFilters();
|
||||
|
||||
this.addSubscription = timer(0, 20000).subscribe(async () => {
|
||||
await this._appStateService.loadAllDossiers();
|
||||
this.calculateData();
|
||||
});
|
||||
|
||||
this.addSubscription = this._appStateService.fileChanged$.subscribe(() => {
|
||||
this.calculateData();
|
||||
this.computeAllFilters();
|
||||
});
|
||||
}
|
||||
|
||||
@ -96,7 +81,6 @@ export class DossiersListingScreenComponent
|
||||
}
|
||||
|
||||
ngOnAttach(): void {
|
||||
this._appStateService.reset();
|
||||
this.ngOnInit();
|
||||
this.ngAfterViewInit();
|
||||
this._tableComponent.scrollViewport.scrollToIndex(this._lastScrolledIndex, 'smooth');
|
||||
@ -111,36 +95,14 @@ export class DossiersListingScreenComponent
|
||||
await this._router.navigate([`/main/dossiers/${addResponse.dossier.id}`]);
|
||||
if (addResponse.addMembers) {
|
||||
this._dialogService.openDialog('editDossier', null, {
|
||||
dossier: addResponse.dossier,
|
||||
dossierId: addResponse.dossier.dossierId,
|
||||
section: 'members',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
calculateData(): void {
|
||||
this._computeAllFilters();
|
||||
|
||||
this.dossiersChartData = [
|
||||
{ value: this._activeDossiersCount, color: 'ACTIVE', label: _('active') },
|
||||
{ value: this._inactiveDossiersCount, color: 'DELETED', label: _('archived') },
|
||||
];
|
||||
const groups = groupBy(this._dossiersService.allFiles, 'status');
|
||||
this.documentsChartData = [];
|
||||
|
||||
for (const status of Object.keys(groups)) {
|
||||
this.documentsChartData.push({
|
||||
value: groups[status].length,
|
||||
color: status,
|
||||
label: fileStatusTranslations[status],
|
||||
key: status,
|
||||
});
|
||||
}
|
||||
this.documentsChartData.sort((a, b) => StatusSorter.byStatus(a.key, b.key));
|
||||
this.documentsChartData = this._translateChartService.translateStatus(this.documentsChartData);
|
||||
}
|
||||
|
||||
private _computeAllFilters() {
|
||||
computeAllFilters() {
|
||||
const filterGroups = this._configService.filterGroups(this.entitiesService.all, this._needsWorkFilterTemplate);
|
||||
this.filterService.addFilterGroups(filterGroups);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<section *ngIf="appStateService.activeFile as file" [class.fullscreen]="fullScreen">
|
||||
<section *ngIf="file$ | async as file" [class.fullscreen]="fullScreen">
|
||||
<div class="page-header">
|
||||
<div class="flex flex-1">
|
||||
<div
|
||||
@ -19,9 +19,9 @@
|
||||
{{ 'file-preview.delta' | translate }}
|
||||
</div>
|
||||
<div
|
||||
(click)="canSwitchToRedactedView && switchView('REDACTED')"
|
||||
(click)="canSwitchToRedactedView(file) && switchView('REDACTED')"
|
||||
[class.active]="viewMode === 'REDACTED'"
|
||||
[class.disabled]="!canSwitchToRedactedView"
|
||||
[class.disabled]="!canSwitchToRedactedView(file)"
|
||||
[matTooltip]="'file-preview.redacted-tooltip' | translate"
|
||||
class="red-tab"
|
||||
>
|
||||
@ -29,26 +29,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="dossiersService.activeDossier$ | async as dossier" class="flex-1 actions-container">
|
||||
<div *ngIf="dossier$ | async as dossier" class="flex-1 actions-container">
|
||||
<ng-container *ngIf="!file.excluded">
|
||||
<ng-container *ngIf="!file.isProcessing">
|
||||
<iqser-status-bar [configs]="statusBarConfig" [small]="true"></iqser-status-bar>
|
||||
<iqser-status-bar [configs]="statusBarConfig(file)" [small]="true"></iqser-status-bar>
|
||||
|
||||
<div class="all-caps-label mr-16 ml-8">
|
||||
{{ translations[status] | translate }}
|
||||
<span *ngIf="isUnderReviewOrApproval">{{ 'by' | translate }}:</span>
|
||||
{{ translations[file.workflowStatus] | translate }}
|
||||
<span *ngIf="isUnderReviewOrApproval(file)">{{ 'by' | translate }}:</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<redaction-initials-avatar
|
||||
*ngIf="!editingReviewer"
|
||||
[user]="currentReviewer"
|
||||
[withName]="!!currentReviewer"
|
||||
[user]="file.currentReviewer"
|
||||
[withName]="!!file.currentReviewer"
|
||||
tooltipPosition="below"
|
||||
></redaction-initials-avatar>
|
||||
<div
|
||||
(click)="editingReviewer = true"
|
||||
*ngIf="!editingReviewer && canAssignReviewer(dossier)"
|
||||
*ngIf="!editingReviewer && canAssignReviewer(file, dossier)"
|
||||
class="assign-reviewer pointer"
|
||||
translate="file-preview.assign-reviewer"
|
||||
></div>
|
||||
@ -56,34 +56,34 @@
|
||||
|
||||
<redaction-assign-user-dropdown
|
||||
(cancel)="editingReviewer = false"
|
||||
(save)="assignReviewer($event)"
|
||||
(save)="assignReviewer(file, $event)"
|
||||
*ngIf="editingReviewer"
|
||||
[options]="singleUsersSelectOptions(dossier)"
|
||||
[value]="currentReviewer"
|
||||
[options]="usersOptions(file, dossier)"
|
||||
[value]="file.currentReviewer"
|
||||
></redaction-assign-user-dropdown>
|
||||
|
||||
<div *ngIf="canAssign" class="assign-actions-wrapper">
|
||||
<div *ngIf="canAssign(file)" class="assign-actions-wrapper">
|
||||
<iqser-circle-button
|
||||
(action)="editingReviewer = true"
|
||||
*ngIf="(permissionsService.canAssignUser() || permissionsService.canUnassignUser()) && !!currentReviewer"
|
||||
[tooltip]="assignTooltip"
|
||||
*ngIf="(permissionsService.canAssignUser(file) || permissionsService.canUnassignUser(file)) && !!file.currentReviewer"
|
||||
[tooltip]="assignTooltip(file)"
|
||||
icon="iqser:edit"
|
||||
tooltipPosition="below"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="assignToMe()"
|
||||
*ngIf="permissionsService.canAssignToSelf()"
|
||||
(action)="assignToMe(file)"
|
||||
*ngIf="permissionsService.canAssignToSelf(file)"
|
||||
[tooltip]="'file-preview.assign-me' | translate"
|
||||
icon="red:assign-me"
|
||||
tooltipPosition="below"
|
||||
></iqser-circle-button>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="permissionsService.isApprover() && !!lastReviewer">
|
||||
<ng-container *ngIf="permissionsService.isApprover(dossier) && !!file.lastReviewer">
|
||||
<div class="vertical-line"></div>
|
||||
<div class="all-caps-label mr-16 ml-8" translate="file-preview.last-reviewer"></div>
|
||||
<redaction-initials-avatar [user]="lastReviewer" [withName]="true"></redaction-initials-avatar>
|
||||
<redaction-initials-avatar [user]="file.lastReviewer" [withName]="true"></redaction-initials-avatar>
|
||||
</ng-container>
|
||||
<div class="vertical-line"></div>
|
||||
|
||||
@ -92,6 +92,7 @@
|
||||
(actionPerformed)="fileActionPerformed($event)"
|
||||
[activeDocumentInfo]="viewDocumentInfo"
|
||||
[activeExcludePages]="excludePages"
|
||||
[file]="file"
|
||||
type="file-preview"
|
||||
></redaction-file-actions>
|
||||
|
||||
@ -105,7 +106,7 @@
|
||||
|
||||
<!-- Dev Mode Features-->
|
||||
<iqser-circle-button
|
||||
(action)="downloadOriginalFile()"
|
||||
(action)="downloadOriginalFile(file)"
|
||||
*ngIf="userPreferenceService.areDevFeaturesEnabled"
|
||||
[tooltip]="'file-preview.download-original-file' | translate"
|
||||
[type]="circleButtonTypes.primary"
|
||||
@ -177,9 +178,10 @@
|
||||
[annotations]="annotations"
|
||||
[dialogRef]="dialogRef"
|
||||
[excludePages]="excludePages"
|
||||
[fileData]="fileData"
|
||||
[file]="file"
|
||||
[hideSkipped]="hideSkipped"
|
||||
[selectedAnnotations]="selectedAnnotations"
|
||||
[viewedPages]="fileData?.viewedPages"
|
||||
[viewer]="activeViewer"
|
||||
></redaction-file-workload>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,14 @@
|
||||
import { ChangeDetectorRef, Component, HostListener, NgZone, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
HostListener,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
TemplateRef,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationExtras, Router } from '@angular/router';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { Core, WebViewerInstance } from '@pdftron/webviewer';
|
||||
@ -23,9 +33,9 @@ import { AnnotationData, FileDataModel } from '@models/file/file-data.model';
|
||||
import { FileActionService } from '../../shared/services/file-action.service';
|
||||
import { AnnotationDrawService } from '../../services/annotation-draw.service';
|
||||
import { AnnotationProcessingService } from '../../services/annotation-processing.service';
|
||||
import { Dossier, FileStatus, User, ViewMode } from '@red/domain';
|
||||
import { Dossier, File, User, ViewMode, WorkflowFileStatus } from '@red/domain';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { timer } from 'rxjs';
|
||||
import { Observable, timer } from 'rxjs';
|
||||
import { UserPreferenceService } from '@services/user-preference.service';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { PdfViewerDataService } from '../../services/pdf-viewer-data.service';
|
||||
@ -34,7 +44,7 @@ import { FileWorkloadComponent } from '../../components/file-workload/file-workl
|
||||
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
|
||||
import { clearStamps, stampPDFPage } from '@utils/page-stamper';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { fileStatusTranslations } from '../../translations/file-status-translations';
|
||||
import { workflowFileStatusTranslations } from '../../translations/file-status-translations';
|
||||
import { handleFilterDelta } from '@utils/filter-utils';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { FileActionsComponent } from '../../shared/components/file-actions/file-actions.component';
|
||||
@ -42,6 +52,7 @@ import { FilesService } from '@services/entity-services/files.service';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { FileManagementService } from '../../shared/services/file-management.service';
|
||||
import { filter, switchMapTo, tap } from 'rxjs/operators';
|
||||
import { FilesMapService } from '@services/entity-services/files-map.service';
|
||||
import Annotation = Core.Annotations.Annotation;
|
||||
|
||||
const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f'];
|
||||
@ -50,10 +61,11 @@ const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f'];
|
||||
templateUrl: './file-preview-screen.component.html',
|
||||
styleUrls: ['./file-preview-screen.component.scss'],
|
||||
providers: [FilterService],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnInit, OnDestroy, OnAttach, OnDetach {
|
||||
readonly circleButtonTypes = CircleButtonTypes;
|
||||
readonly translations = fileStatusTranslations;
|
||||
readonly translations = workflowFileStatusTranslations;
|
||||
|
||||
dialogRef: MatDialogRef<unknown>;
|
||||
viewMode: ViewMode = 'STANDARD';
|
||||
@ -70,6 +82,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
excludePages = false;
|
||||
@ViewChild(PdfViewerComponent) readonly viewerComponent: PdfViewerComponent;
|
||||
@ViewChild('fileActions') fileActions: FileActionsComponent;
|
||||
readonly dossierId: string;
|
||||
readonly dossier$: Observable<Dossier>;
|
||||
readonly file$: Observable<File>;
|
||||
readonly fileId: string;
|
||||
private _instance: WebViewerInstance;
|
||||
private _lastPage: string;
|
||||
private _reloadFileOnReanalysis = false;
|
||||
@ -101,9 +117,15 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
private readonly _loadingService: LoadingService,
|
||||
private readonly _filterService: FilterService,
|
||||
private readonly _translateService: TranslateService,
|
||||
private readonly _filesMapService: FilesMapService,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
) {
|
||||
super();
|
||||
this._loadingService.start();
|
||||
this.dossierId = _activatedRoute.snapshot.paramMap.get('dossierId');
|
||||
this.dossier$ = this._dossiersService.getEntityChanged$(this.dossierId);
|
||||
this.fileId = _activatedRoute.snapshot.paramMap.get('fileId');
|
||||
this.file$ = _filesMapService.watch$(this.dossierId, this.fileId);
|
||||
document.documentElement.addEventListener('fullscreenchange', () => {
|
||||
if (!document.fullscreenElement) {
|
||||
this.fullScreen = false;
|
||||
@ -111,12 +133,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
});
|
||||
}
|
||||
|
||||
get assignTooltip(): string {
|
||||
return this.appStateService.activeFile.isUnderApproval
|
||||
? this._translateService.instant('dossier-overview.assign-approver')
|
||||
: this.assignOrChangeReviewerTooltip;
|
||||
}
|
||||
|
||||
get annotations(): AnnotationWrapper[] {
|
||||
return this.annotationData ? this.annotationData.visibleAnnotations : [];
|
||||
}
|
||||
@ -137,70 +153,58 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
: (currentPage + 1) / 2;
|
||||
}
|
||||
|
||||
get canSwitchToRedactedView(): boolean {
|
||||
return this.fileData && !this.fileData.file.analysisRequired && !this.fileData.file.excluded;
|
||||
}
|
||||
|
||||
get canSwitchToDeltaView(): boolean {
|
||||
return this.fileData?.hasChangeLog;
|
||||
}
|
||||
|
||||
get canAssign(): boolean {
|
||||
return (
|
||||
!this.editingReviewer &&
|
||||
(this.permissionsService.canAssignUser() ||
|
||||
this.permissionsService.canAssignToSelf() ||
|
||||
this.permissionsService.canUnassignUser())
|
||||
);
|
||||
}
|
||||
|
||||
get displayData(): Blob {
|
||||
return this.fileData?.fileData;
|
||||
}
|
||||
|
||||
get fileId(): string {
|
||||
return this.appStateService.activeFileId;
|
||||
}
|
||||
|
||||
get multiSelectActive(): boolean {
|
||||
return !!this._workloadComponent?.multiSelectActive;
|
||||
}
|
||||
|
||||
get lastReviewer(): string | undefined {
|
||||
return this.appStateService.activeFile.lastReviewer;
|
||||
assignTooltip(file: File): string {
|
||||
return file.isUnderApproval
|
||||
? this._translateService.instant('dossier-overview.assign-approver')
|
||||
: this.assignOrChangeReviewerTooltip(file);
|
||||
}
|
||||
|
||||
get assignOrChangeReviewerTooltip(): string {
|
||||
return this.currentReviewer
|
||||
canSwitchToRedactedView(file: File): boolean {
|
||||
return this.fileData && !file.analysisRequired && !file.excluded;
|
||||
}
|
||||
|
||||
assignOrChangeReviewerTooltip(file: File): string {
|
||||
return file.currentReviewer
|
||||
? this._translateService.instant('file-preview.change-reviewer')
|
||||
: this._translateService.instant('file-preview.assign-reviewer');
|
||||
}
|
||||
|
||||
get currentReviewer(): string {
|
||||
return this.appStateService.activeFile.currentReviewer;
|
||||
statusBarConfig(file: File): [{ length: number; color: WorkflowFileStatus }] {
|
||||
return [{ length: 1, color: file.workflowStatus }];
|
||||
}
|
||||
|
||||
get status(): FileStatus {
|
||||
return this.appStateService.activeFile.status;
|
||||
isUnderReviewOrApproval(file: File): boolean {
|
||||
return file.isUnderReview || file.isUnderApproval;
|
||||
}
|
||||
|
||||
get statusBarConfig(): [{ length: number; color: FileStatus }] {
|
||||
return [{ length: 1, color: this.status }];
|
||||
canAssign(file: File): boolean {
|
||||
return (
|
||||
!this.editingReviewer &&
|
||||
(this.permissionsService.canAssignUser(file) ||
|
||||
this.permissionsService.canAssignToSelf(file) ||
|
||||
this.permissionsService.canUnassignUser(file))
|
||||
);
|
||||
}
|
||||
|
||||
get isUnderReviewOrApproval(): boolean {
|
||||
return this.status === 'UNDER_REVIEW' || this.status === 'UNDER_APPROVAL';
|
||||
usersOptions(file: File, dossier: Dossier): List {
|
||||
const unassignUser = this.permissionsService.canUnassignUser(file) ? [undefined] : [];
|
||||
return file.isUnderApproval ? [...dossier.approverIds, ...unassignUser] : [...dossier.memberIds, ...unassignUser];
|
||||
}
|
||||
|
||||
singleUsersSelectOptions(dossier: Dossier): List {
|
||||
const unassignUser = this.permissionsService.canUnassignUser() ? [undefined] : [];
|
||||
return this.appStateService.activeFile?.isUnderApproval
|
||||
? [...dossier.approverIds, ...unassignUser]
|
||||
: [...dossier.memberIds, ...unassignUser];
|
||||
}
|
||||
|
||||
canAssignReviewer(dossier: Dossier): boolean {
|
||||
return !this.currentReviewer && this.permissionsService.canAssignUser() && dossier.hasReviewers;
|
||||
canAssignReviewer(file: File, dossier: Dossier): boolean {
|
||||
return !file.currentReviewer && this.permissionsService.canAssignUser(file) && dossier.hasReviewers;
|
||||
}
|
||||
|
||||
updateViewMode(): void {
|
||||
@ -243,9 +247,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
super.ngOnDestroy();
|
||||
}
|
||||
|
||||
async ngOnAttach(previousRoute: ActivatedRouteSnapshot): Promise<void> {
|
||||
if (!this.appStateService.activeFile.canBeOpened) {
|
||||
return this.dossiersService.goToActiveDossier();
|
||||
async ngOnAttach(previousRoute: ActivatedRouteSnapshot): Promise<boolean> {
|
||||
const file = this._filesMapService.get(this.dossierId, this.fileId);
|
||||
if (!file.canBeOpened) {
|
||||
return this._router.navigate([this.dossiersService.find(this.dossierId)?.routerLink]);
|
||||
}
|
||||
|
||||
await this.ngOnInit();
|
||||
@ -258,7 +263,8 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
this._updateCanPerformActions();
|
||||
this._subscribeToFileUpdates();
|
||||
|
||||
if (this.fileData?.file?.analysisRequired) {
|
||||
const file = this._filesMapService.get(this.dossierId, this.fileId);
|
||||
if (file?.analysisRequired) {
|
||||
this.fileActions.reanalyseFile();
|
||||
}
|
||||
}
|
||||
@ -278,8 +284,9 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
}
|
||||
console.log(`[REDACTION] Delete previous annotations time: ${new Date().getTime() - startTime} ms`);
|
||||
const processStartTime = new Date().getTime();
|
||||
const dossier = this._dossiersService.find(this.dossierId);
|
||||
const newAnnotationsData = this.fileData.getAnnotations(
|
||||
this.appStateService.dictionaryData[this.dossiersService.activeDossier.dossierTemplateId],
|
||||
this.appStateService.dictionaryData[dossier.dossierTemplateId],
|
||||
this.userService.currentUser,
|
||||
this.viewMode,
|
||||
this.userPreferenceService.areDevFeaturesEnabled,
|
||||
@ -351,7 +358,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
response.manualRedactionEntryWrapper.rectId,
|
||||
);
|
||||
this._instance.Core.annotationManager.deleteAnnotation(annotation);
|
||||
this.fileData.file = await this.appStateService.reloadActiveFile();
|
||||
await this.appStateService.reloadFile(this.dossierId, this.fileId);
|
||||
const distinctPages = entryWrapper.manualRedactionEntry.positions
|
||||
.map(p => p.page)
|
||||
.filter((item, pos, self) => self.indexOf(item) === pos);
|
||||
@ -454,7 +461,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
return this.dossiersService.goToActiveDossier();
|
||||
return this._router.navigate([this.dossiersService.find(this.dossierId).routerLink]);
|
||||
|
||||
case 'reanalyse':
|
||||
await this._loadFileData(true);
|
||||
@ -490,22 +497,22 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
}
|
||||
}
|
||||
|
||||
async assignToMe() {
|
||||
await this._fileActionService.assignToMe([this.fileData.file], async () => {
|
||||
await this.appStateService.reloadActiveFile();
|
||||
async assignToMe(file: File) {
|
||||
await this._fileActionService.assignToMe([file], async () => {
|
||||
await this.appStateService.reloadFile(this.dossierId, this.fileId);
|
||||
this._updateCanPerformActions();
|
||||
});
|
||||
}
|
||||
|
||||
async assignReviewer(user: User | string) {
|
||||
async assignReviewer(file: File, user: User | string) {
|
||||
const reviewerId = typeof user === 'string' ? user : user?.id;
|
||||
const reviewerName = this.userService.getNameForId(reviewerId);
|
||||
|
||||
const { dossierId, fileId, filename } = this.fileData.file;
|
||||
const { dossierId, fileId, filename } = file;
|
||||
await this._filesService.setReviewerFor([fileId], dossierId, reviewerId).toPromise();
|
||||
|
||||
this._toaster.info(_('assignment.reviewer'), { params: { reviewerName, filename } });
|
||||
await this.appStateService.reloadActiveFile();
|
||||
await this.appStateService.reloadFile(this.dossierId, this.fileId);
|
||||
this._updateCanPerformActions();
|
||||
this.editingReviewer = false;
|
||||
}
|
||||
@ -522,11 +529,11 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
this._scrollViews();
|
||||
}
|
||||
|
||||
async downloadOriginalFile() {
|
||||
async downloadOriginalFile(file: File) {
|
||||
const data = await this._fileManagementService
|
||||
.downloadOriginalFile(this.fileData.file.dossierId, this.fileId, 'response', true, this.fileData.file.cacheIdentifier)
|
||||
.downloadOriginalFile(this.dossierId, this.fileId, 'response', true, file.cacheIdentifier)
|
||||
.toPromise();
|
||||
download(data, this.fileData.file.filename);
|
||||
download(data, file.filename);
|
||||
}
|
||||
|
||||
toggleSkipped($event) {
|
||||
@ -548,14 +555,15 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
});
|
||||
}
|
||||
|
||||
private async _doStampExcludedPages(excludedPages: number[]) {
|
||||
private async _doStampExcludedPages() {
|
||||
const pdfNet = this._instance.Core.PDFNet;
|
||||
const document = await this._instance.Core.documentViewer.getDocument().getPDFDoc();
|
||||
const allPages = [...Array(this.fileData.file.numberOfPages).keys()].map(page => page + 1);
|
||||
const file = this._filesMapService.get(this.dossierId, this.fileId);
|
||||
const allPages = [...Array(file.numberOfPages).keys()].map(page => page + 1);
|
||||
await clearStamps(document, pdfNet, allPages);
|
||||
|
||||
if (excludedPages && excludedPages.length > 0) {
|
||||
this.viewerComponent.utils.excludedPages = excludedPages;
|
||||
if (file.excludedPages && file.excludedPages.length > 0) {
|
||||
this.viewerComponent.utils.excludedPages = file.excludedPages;
|
||||
await stampPDFPage(
|
||||
document,
|
||||
pdfNet,
|
||||
@ -565,21 +573,23 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
'DIAGONAL',
|
||||
33,
|
||||
'#283241',
|
||||
excludedPages,
|
||||
file.excludedPages,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async _stampExcludedPages() {
|
||||
await this._doStampExcludedPages(this.fileData.file.excludedPages);
|
||||
await this._doStampExcludedPages();
|
||||
this._instance.Core.documentViewer.refreshAll();
|
||||
this._instance.Core.documentViewer.updateView([this.activeViewerPage], this.activeViewerPage);
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
private _subscribeToFileUpdates(): void {
|
||||
this.addSubscription = timer(0, 10000).pipe(switchMapTo(this.appStateService.reloadActiveFile())).subscribe();
|
||||
this.addSubscription = this.appStateService.fileReanalysed$
|
||||
this.addSubscription = timer(0, 10000)
|
||||
.pipe(switchMapTo(this.appStateService.reloadFile(this.dossierId, this.fileId)))
|
||||
.subscribe();
|
||||
this.addSubscription = this._filesMapService.fileReanalysed$
|
||||
.pipe(filter(file => file.fileId === this.fileId))
|
||||
.subscribe(async () => {
|
||||
await this._loadFileData(!this._reloadFileOnReanalysis);
|
||||
@ -591,20 +601,21 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
}
|
||||
|
||||
private _updateCanPerformActions() {
|
||||
const file = this._filesMapService.get(this.dossierId, this.fileId);
|
||||
this.canPerformAnnotationActions =
|
||||
this.permissionsService.canPerformAnnotationActions() &&
|
||||
this.permissionsService.canPerformAnnotationActions(file) &&
|
||||
this.viewMode === 'STANDARD' &&
|
||||
!this.viewerComponent?.utils.isCompareMode;
|
||||
}
|
||||
|
||||
private async _loadFileData(performUpdate = false): Promise<void> {
|
||||
const fileData = await this._fileDownloadService.loadActiveFileData().toPromise();
|
||||
const file = this._filesMapService.get(this.dossierId, this.fileId);
|
||||
const fileData = await this._fileDownloadService.loadDataFor(file).toPromise();
|
||||
|
||||
if (!fileData.file?.isPending && !fileData.file?.isError) {
|
||||
if (!file?.isPending && !file?.isError) {
|
||||
if (performUpdate) {
|
||||
this.fileData.redactionLog = fileData.redactionLog;
|
||||
this.fileData.viewedPages = fileData.viewedPages;
|
||||
this.fileData.file = fileData.file;
|
||||
this.rebuildFilters(true);
|
||||
} else {
|
||||
this.fileData = fileData;
|
||||
@ -614,8 +625,8 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileData.file.isError) {
|
||||
await this.dossiersService.goToActiveDossier();
|
||||
if (file.isError) {
|
||||
await this._router.navigate([this.dossiersService.find(this.dossierId).routerLink]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -628,7 +639,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
/* Get the documentElement (<html>) to display the page in fullscreen */
|
||||
|
||||
private _cleanupAndRedrawManualAnnotations$() {
|
||||
return this._fileDownloadService.loadActiveFileRedactionLog().pipe(
|
||||
return this._fileDownloadService.loadRedactionLogFor(this.dossierId, this.fileId).pipe(
|
||||
tap(redactionLog => (this.fileData.redactionLog = redactionLog)),
|
||||
switchMapTo(this._redrawAnnotations()),
|
||||
);
|
||||
@ -638,8 +649,8 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
const currentPageAnnotations = this.annotations.filter(a => a.pageNumber === page);
|
||||
const currentPageAnnotationIds = currentPageAnnotations.map(a => a.id);
|
||||
|
||||
this.fileData.file = await this.appStateService.reloadActiveFile();
|
||||
this.fileData.redactionLog = await this._fileDownloadService.loadActiveFileRedactionLog().toPromise();
|
||||
await this.appStateService.reloadFile(this.dossierId, this.fileId);
|
||||
this.fileData.redactionLog = await this._fileDownloadService.loadRedactionLogFor(this.dossierId, this.fileId).toPromise();
|
||||
|
||||
this.rebuildFilters();
|
||||
|
||||
|
||||
@ -15,12 +15,13 @@ import { merge, Observable } from 'rxjs';
|
||||
import { debounceTime, map, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { fileStatusTranslations } from '../../translations/file-status-translations';
|
||||
import { workflowFileStatusTranslations } from '../../translations/file-status-translations';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { RouterHistoryService } from '@services/router-history.service';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { PlatformSearchService } from '../../shared/services/platform-search.service';
|
||||
import { Dossier, IMatchedDocument, ISearchInput, ISearchListItem, ISearchResponse } from '@red/domain';
|
||||
import { FilesMapService } from '@services/entity-services/files-map.service';
|
||||
|
||||
function toSearchInput(query: string, dossierIds: List | string): ISearchInput {
|
||||
return {
|
||||
@ -36,7 +37,7 @@ function toSearchInput(query: string, dossierIds: List | string): ISearchInput {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SearchScreenComponent extends ListingComponent<ISearchListItem> implements OnDestroy {
|
||||
readonly fileStatusTranslations = fileStatusTranslations;
|
||||
readonly fileStatusTranslations = workflowFileStatusTranslations;
|
||||
readonly searchPositions = SearchPositions;
|
||||
|
||||
readonly tableHeaderLabel = _('search-screen.table-header');
|
||||
@ -66,6 +67,7 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
|
||||
private readonly _dossiersService: DossiersService,
|
||||
readonly routerHistoryService: RouterHistoryService,
|
||||
private readonly _translateService: TranslateService,
|
||||
private readonly _filesMapService: FilesMapService,
|
||||
private readonly _platformSearchService: PlatformSearchService,
|
||||
) {
|
||||
super(_injector);
|
||||
@ -127,7 +129,7 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
|
||||
}
|
||||
|
||||
private _toListItem({ dossierId, fileId, unmatchedTerms, highlights, score }: IMatchedDocument): ISearchListItem {
|
||||
const file = this._dossiersService.find(dossierId, fileId);
|
||||
const file = this._filesMapService.get(dossierId, fileId);
|
||||
if (!file) {
|
||||
return undefined;
|
||||
}
|
||||
@ -137,7 +139,7 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
|
||||
dossierId,
|
||||
unmatched: unmatchedTerms || null,
|
||||
highlights,
|
||||
status: file.status,
|
||||
status: file.workflowStatus,
|
||||
numberOfPages: file.numberOfPages,
|
||||
dossierName: this._dossiersService.find(dossierId).dossierName,
|
||||
filename: file.filename,
|
||||
|
||||
@ -2,10 +2,8 @@ import { Injectable } from '@angular/core';
|
||||
import { forkJoin, Observable, of } from 'rxjs';
|
||||
import { catchError, map, tap } from 'rxjs/operators';
|
||||
import { FileDataModel } from '@models/file/file-data.model';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { File } from '@red/domain';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { FileManagementService } from '../shared/services/file-management.service';
|
||||
import { RedactionLogService } from './redaction-log.service';
|
||||
import { ViewedPagesService } from '../shared/services/viewed-pages.service';
|
||||
@ -13,36 +11,30 @@ import { ViewedPagesService } from '../shared/services/viewed-pages.service';
|
||||
@Injectable()
|
||||
export class PdfViewerDataService {
|
||||
constructor(
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
private readonly _permissionsService: PermissionsService,
|
||||
private readonly _fileManagementService: FileManagementService,
|
||||
private readonly _redactionLogService: RedactionLogService,
|
||||
private readonly _viewedPagesService: ViewedPagesService,
|
||||
) {}
|
||||
|
||||
loadActiveFileRedactionLog() {
|
||||
return this._redactionLogService.getRedactionLog(this._dossiersService.activeDossierId, this._appStateService.activeFileId).pipe(
|
||||
loadRedactionLogFor(dossierId: string, fileId: string) {
|
||||
return this._redactionLogService.getRedactionLog(dossierId, fileId).pipe(
|
||||
tap(redactionLog => redactionLog.redactionLogEntry.sort((a, b) => a.positions[0].page - b.positions[0].page)),
|
||||
catchError(() => of({})),
|
||||
);
|
||||
}
|
||||
|
||||
loadActiveFileData(): Observable<FileDataModel> {
|
||||
const file$ = this.downloadOriginalFile(this._appStateService.activeFile);
|
||||
const reactionLog$ = this.loadActiveFileRedactionLog();
|
||||
const viewedPages$ = this.getViewedPagesForActiveFile();
|
||||
loadDataFor(file: File): Observable<FileDataModel> {
|
||||
const file$ = this.downloadOriginalFile(file);
|
||||
const reactionLog$ = this.loadRedactionLogFor(file.dossierId, file.fileId);
|
||||
const viewedPages$ = this.getViewedPagesFor(file);
|
||||
|
||||
return forkJoin([file$, reactionLog$, viewedPages$]).pipe(
|
||||
map(data => new FileDataModel(this._appStateService.activeFile, ...data)),
|
||||
);
|
||||
return forkJoin([file$, reactionLog$, viewedPages$]).pipe(map(data => new FileDataModel(file, ...data)));
|
||||
}
|
||||
|
||||
getViewedPagesForActiveFile() {
|
||||
if (this._permissionsService.canMarkPagesAsViewed()) {
|
||||
return this._viewedPagesService
|
||||
.getViewedPages(this._dossiersService.activeDossierId, this._appStateService.activeFileId)
|
||||
.pipe(catchError(() => of([])));
|
||||
getViewedPagesFor(file: File) {
|
||||
if (this._permissionsService.canMarkPagesAsViewed(file)) {
|
||||
return this._viewedPagesService.getViewedPages(file.dossierId, file.fileId).pipe(catchError(() => of([])));
|
||||
}
|
||||
return of([]);
|
||||
}
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
<div *ngIf="isDossierOverviewList" class="action-buttons">
|
||||
<ng-container *ngTemplateOutlet="actions"></ng-container>
|
||||
<div *ngIf="showStatusBar && file.isProcessing" [matTooltip]="'file-status.processing' | translate" class="spinning"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon svgIcon="red:reanalyse"></mat-icon>
|
||||
</div>
|
||||
<iqser-status-bar *ngIf="showStatusBar" [configs]="statusBarConfig"></iqser-status-bar>
|
||||
</div>
|
||||
|
||||
@ -47,7 +51,7 @@
|
||||
|
||||
<!-- download redacted file-->
|
||||
<redaction-file-download-btn
|
||||
[dossier]="dossiersService.activeDossier$ | async"
|
||||
[dossier]="dossier$ | async"
|
||||
[files]="[file]"
|
||||
[tooltipClass]="'small'"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
|
||||
@ -31,9 +31,16 @@ mat-slide-toggle {
|
||||
height: 34px;
|
||||
width: 34px;
|
||||
line-height: 33px;
|
||||
}
|
||||
|
||||
mat-slide-toggle {
|
||||
margin-left: 8px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.spinning {
|
||||
margin: 0 12px 0 11px;
|
||||
|
||||
> mat-icon {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { File, FileStatus } from '@red/domain';
|
||||
import { Dossier, File, WorkflowFileStatus } from '@red/domain';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { DossiersDialogService } from '../../../services/dossiers-dialog.service';
|
||||
import {
|
||||
@ -8,36 +8,44 @@ import {
|
||||
CircleButtonType,
|
||||
CircleButtonTypes,
|
||||
ConfirmationDialogInput,
|
||||
List,
|
||||
LoadingService,
|
||||
OnChange,
|
||||
Required,
|
||||
StatusBarConfig,
|
||||
Toaster,
|
||||
} from '@iqser/common-ui';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { UserPreferenceService } from '@services/user-preference.service';
|
||||
import { LongPressEvent } from '@shared/directives/long-press.directive';
|
||||
import { FileActionService } from '../../services/file-action.service';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { FileManagementService } from '../../services/file-management.service';
|
||||
import { FilesMapService } from '@services/entity-services/files-map.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-file-actions',
|
||||
templateUrl: './file-actions.component.html',
|
||||
styleUrls: ['./file-actions.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FileActionsComponent extends AutoUnsubscribe implements OnInit, OnDestroy, OnChanges {
|
||||
export class FileActionsComponent extends AutoUnsubscribe implements OnInit, OnDestroy {
|
||||
readonly circleButtonTypes = CircleButtonTypes;
|
||||
readonly currentUser = this._userService.currentUser;
|
||||
|
||||
@Input() file: File;
|
||||
@Input()
|
||||
@OnChange<File, FileActionsComponent>('setup')
|
||||
file: File;
|
||||
@Input() activeDocumentInfo: boolean;
|
||||
@Input() activeExcludePages: boolean;
|
||||
@Input() @Required() type: 'file-preview' | 'dossier-overview-list' | 'dossier-overview-workflow';
|
||||
@Output() readonly actionPerformed = new EventEmitter<string>();
|
||||
|
||||
statusBarConfig?: readonly StatusBarConfig<FileStatus>[];
|
||||
dossier$: Observable<Dossier>;
|
||||
statusBarConfig?: List<StatusBarConfig<WorkflowFileStatus>>;
|
||||
tooltipPosition?: 'below' | 'above';
|
||||
toggleTooltip?: string;
|
||||
assignTooltip?: string;
|
||||
@ -67,6 +75,7 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnInit, OnD
|
||||
private readonly _fileActionService: FileActionService,
|
||||
private readonly _loadingService: LoadingService,
|
||||
private readonly _fileManagementService: FileManagementService,
|
||||
private readonly _filesMapService: FilesMapService,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _toaster: Toaster,
|
||||
private readonly _userPreferenceService: UserPreferenceService,
|
||||
@ -99,17 +108,11 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnInit, OnD
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.file) {
|
||||
this.file = this.appStateService.activeFile;
|
||||
}
|
||||
this._setup();
|
||||
this.addSubscription = this.appStateService.fileChanged$.pipe(filter(file => file.fileId === this.file?.fileId)).subscribe(file => {
|
||||
this.setup();
|
||||
this.dossier$ = this.dossiersService.getEntityChanged$(this.file.dossierId).pipe(tap(() => this.setup()));
|
||||
this.addSubscription = this._filesMapService.watch$(this.file.dossierId, this.file.fileId).subscribe(file => {
|
||||
this.file = file;
|
||||
this._setup();
|
||||
});
|
||||
|
||||
this.addSubscription = this.dossiersService.entityChanged$.subscribe(() => {
|
||||
this._setup();
|
||||
this.setup();
|
||||
});
|
||||
}
|
||||
|
||||
@ -160,7 +163,7 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnInit, OnD
|
||||
$event.stopPropagation();
|
||||
|
||||
await this._fileActionService.assignToMe([this.file], () => {
|
||||
this.reloadDossiers('reanalyse');
|
||||
this.reloadFiles('reanalyse');
|
||||
});
|
||||
}
|
||||
|
||||
@ -169,103 +172,86 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnInit, OnD
|
||||
$event.stopPropagation();
|
||||
}
|
||||
this.addSubscription = this._fileActionService.reanalyseFile(this.file).subscribe(() => {
|
||||
this.reloadDossiers('reanalyse');
|
||||
this.reloadFiles('reanalyse');
|
||||
});
|
||||
}
|
||||
|
||||
setFileUnderApproval($event: MouseEvent) {
|
||||
async setFileUnderApproval($event: MouseEvent) {
|
||||
$event.stopPropagation();
|
||||
if (this.dossiersService.activeDossier.approverIds.length > 1) {
|
||||
this._fileActionService.assignFile('approver', $event, this.file, () => this.reloadDossiers('assign-reviewer'), true);
|
||||
const dossier = this.dossiersService.find(this.file.dossierId);
|
||||
if (dossier.approverIds.length > 1) {
|
||||
this._fileActionService.assignFile('approver', $event, this.file, () => this.reloadFiles('assign-reviewer'), true);
|
||||
} else {
|
||||
this.addSubscription = this._fileActionService.setFilesUnderApproval([this.file]).subscribe(() => {
|
||||
this.reloadDossiers('set-under-approval');
|
||||
});
|
||||
await this._fileActionService.setFilesUnderApproval([this.file]).toPromise();
|
||||
this.reloadFiles('set-under-approval');
|
||||
}
|
||||
}
|
||||
|
||||
setFileApproved($event: MouseEvent) {
|
||||
async setFileApproved($event: MouseEvent) {
|
||||
$event.stopPropagation();
|
||||
if (this.file.hasUpdates) {
|
||||
this._dialogService.openDialog(
|
||||
'confirm',
|
||||
$event,
|
||||
new ConfirmationDialogInput({
|
||||
title: _('confirmation-dialog.approve-file.title'),
|
||||
question: _('confirmation-dialog.approve-file.question'),
|
||||
}),
|
||||
() => {
|
||||
this._setFileApproved();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
this._setFileApproved();
|
||||
if (!this.file.hasUpdates) {
|
||||
await this._setFileApproved();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ocrFile($event: MouseEvent) {
|
||||
$event.stopPropagation();
|
||||
this.addSubscription = this._fileActionService.ocrFiles([this.file]).subscribe(() => {
|
||||
this.reloadDossiers('ocr-file');
|
||||
});
|
||||
}
|
||||
|
||||
setFileUnderReview($event: MouseEvent, ignoreDialogChanges = false) {
|
||||
this._fileActionService.assignFile(
|
||||
'reviewer',
|
||||
this._dialogService.openDialog(
|
||||
'confirm',
|
||||
$event,
|
||||
this.file,
|
||||
() => this.reloadDossiers('assign-reviewer'),
|
||||
ignoreDialogChanges,
|
||||
new ConfirmationDialogInput({
|
||||
title: _('confirmation-dialog.approve-file.title'),
|
||||
question: _('confirmation-dialog.approve-file.question'),
|
||||
}),
|
||||
async () => {
|
||||
await this._setFileApproved();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
reloadDossiers(action: string) {
|
||||
this.appStateService.getFiles().then(() => {
|
||||
async ocrFile($event: MouseEvent) {
|
||||
$event.stopPropagation();
|
||||
await this._fileActionService.ocrFiles([this.file]).toPromise();
|
||||
this.reloadFiles('ocr-file');
|
||||
}
|
||||
|
||||
setFileUnderReview($event: MouseEvent, ignoreDialogChanges = false) {
|
||||
this._fileActionService.assignFile('reviewer', $event, this.file, () => this.reloadFiles('assign-reviewer'), ignoreDialogChanges);
|
||||
}
|
||||
|
||||
reloadFiles(action: string) {
|
||||
this.appStateService.getFiles(this.dossiersService.find(this.file.dossierId)).then(() => {
|
||||
this.actionPerformed.emit(action);
|
||||
});
|
||||
}
|
||||
|
||||
async toggleAnalysis() {
|
||||
await this._fileActionService.toggleAnalysis(this.file).toPromise();
|
||||
await this.appStateService.getFiles();
|
||||
await this.appStateService.getFiles(this.dossiersService.find(this.file.dossierId));
|
||||
this.actionPerformed.emit(this.file?.excluded ? 'enable-analysis' : 'disable-analysis');
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.file) {
|
||||
this._setup();
|
||||
}
|
||||
}
|
||||
|
||||
forceReanalysisAction($event: LongPressEvent) {
|
||||
this.analysisForced = !$event.touchEnd && this._userPreferenceService.areDevFeaturesEnabled;
|
||||
}
|
||||
|
||||
private _setFileApproved() {
|
||||
this.addSubscription = this._fileActionService.setFilesApproved([this.file]).subscribe(() => {
|
||||
this.reloadDossiers('set-approved');
|
||||
});
|
||||
}
|
||||
|
||||
private _setup() {
|
||||
this.statusBarConfig = [{ color: this.file.status, length: 1 }];
|
||||
setup() {
|
||||
this.statusBarConfig = [{ color: this.file.workflowStatus, length: 1 }];
|
||||
this.tooltipPosition = this.isFilePreview ? 'below' : 'above';
|
||||
this.assignTooltip = this.file.isUnderApproval ? _('dossier-overview.assign-approver') : _('dossier-overview.assign-reviewer');
|
||||
this.buttonType = this.isFilePreview ? CircleButtonTypes.default : CircleButtonTypes.dark;
|
||||
this.toggleTooltip = this._toggleTooltip;
|
||||
const dossier = this.dossiersService.find(this.file.dossierId);
|
||||
|
||||
this.showUndoApproval = this.permissionsService.canUndoApproval(this.file) && !this.isDossierOverviewWorkflow;
|
||||
this.showUnderReview = this.permissionsService.canSetUnderReview(this.file) && !this.isDossierOverviewWorkflow;
|
||||
this.showUnderReview = this.permissionsService.canSetUnderReview(this.file, dossier) && !this.isDossierOverviewWorkflow;
|
||||
this.showUnderApproval = this.permissionsService.canSetUnderApproval(this.file) && !this.isDossierOverviewWorkflow;
|
||||
this.showApprove = this.permissionsService.isReadyForApproval(this.file) && !this.isDossierOverviewWorkflow;
|
||||
this.showApprove = this.permissionsService.isReadyForApproval(this.file, dossier) && !this.isDossierOverviewWorkflow;
|
||||
|
||||
this.canToggleAnalysis = this.permissionsService.canToggleAnalysis(this.file);
|
||||
this.showDelete = this.permissionsService.canDeleteFile(this.file);
|
||||
this.canToggleAnalysis = this.permissionsService.canToggleAnalysis(this.file, dossier);
|
||||
this.showDelete = this.permissionsService.canDeleteFile(this.file, dossier);
|
||||
this.showOCR = this.file.canBeOCRed;
|
||||
this.canReanalyse = this.permissionsService.canReanalyseFile(this.file);
|
||||
this.canReanalyse = this.permissionsService.canReanalyseFile(this.file, dossier);
|
||||
|
||||
this.showStatusBar = this.file.isWorkable && this.isDossierOverviewList;
|
||||
this.showStatusBar = !this.file.isError && !this.file.isPending && this.isDossierOverviewList;
|
||||
|
||||
this.showAssignToSelf = this.permissionsService.canAssignToSelf(this.file) && this.isDossierOverview;
|
||||
this.showAssign =
|
||||
@ -277,4 +263,13 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnInit, OnD
|
||||
this.showExcludePages = this.isFilePreview;
|
||||
this.showDocumentInfo = this.isFilePreview;
|
||||
}
|
||||
|
||||
private async _setFileApproved() {
|
||||
await this._fileActionService
|
||||
.setFilesApproved([this.file])
|
||||
.toPromise()
|
||||
.then(() => {
|
||||
this.reloadFiles('set-approved');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
<div class="needs-work">
|
||||
<redaction-annotation-icon *ngIf="reanalysisRequired()" [color]="analysisColor" label="A" type="square"></redaction-annotation-icon>
|
||||
<redaction-annotation-icon *ngIf="hasUpdates" [color]="updatedColor" label="U" type="square"></redaction-annotation-icon>
|
||||
<redaction-annotation-icon
|
||||
*ngIf="needsWorkInput.hasRedactions"
|
||||
[color]="redactionColor"
|
||||
label="R"
|
||||
type="square"
|
||||
></redaction-annotation-icon>
|
||||
<redaction-annotation-icon *ngIf="hasImages" [color]="imageColor" label="I" type="square"></redaction-annotation-icon>
|
||||
<redaction-annotation-icon *ngIf="needsWorkInput.hintsOnly" [color]="hintColor" label="H" type="circle"></redaction-annotation-icon>
|
||||
<redaction-annotation-icon
|
||||
*ngIf="needsWorkInput.hasSuggestions"
|
||||
[color]="suggestionColor"
|
||||
label="S"
|
||||
type="rhombus"
|
||||
></redaction-annotation-icon>
|
||||
<mat-icon *ngIf="hasAnnotationComments" svgIcon="red:comment"></mat-icon>
|
||||
</div>
|
||||
@ -1,62 +0,0 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { Dossier, File } from '@red/domain';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-needs-work-badge',
|
||||
templateUrl: './needs-work-badge.component.html',
|
||||
styleUrls: ['./needs-work-badge.component.scss'],
|
||||
})
|
||||
export class NeedsWorkBadgeComponent {
|
||||
@Input() needsWorkInput: File | Dossier;
|
||||
|
||||
constructor(private readonly _appStateService: AppStateService) {}
|
||||
|
||||
get suggestionColor() {
|
||||
return this._getDictionaryColor('suggestion');
|
||||
}
|
||||
|
||||
get imageColor() {
|
||||
return this._getDictionaryColor('image');
|
||||
}
|
||||
|
||||
get updatedColor() {
|
||||
return this._getDictionaryColor('updated');
|
||||
}
|
||||
|
||||
get analysisColor() {
|
||||
return this._getDictionaryColor('analysis');
|
||||
}
|
||||
|
||||
get hintColor() {
|
||||
return this._getDictionaryColor('hint');
|
||||
}
|
||||
|
||||
get redactionColor() {
|
||||
return this._getDictionaryColor('redaction');
|
||||
}
|
||||
|
||||
get hasImages() {
|
||||
return this.needsWorkInput instanceof File && this.needsWorkInput.hasImages;
|
||||
}
|
||||
|
||||
get hasUpdates() {
|
||||
return this.needsWorkInput instanceof File && this.needsWorkInput.hasUpdates;
|
||||
}
|
||||
|
||||
get hasAnnotationComments(): boolean {
|
||||
return this.needsWorkInput instanceof File && (<any> this.needsWorkInput).hasAnnotationComments;
|
||||
}
|
||||
|
||||
reanalysisRequired() {
|
||||
if (this.needsWorkInput instanceof Dossier) {
|
||||
return this.needsWorkInput.reanalysisRequired;
|
||||
} else {
|
||||
return this.needsWorkInput.analysisRequired;
|
||||
}
|
||||
}
|
||||
|
||||
private _getDictionaryColor(type: string) {
|
||||
return this._appStateService.getDictionaryColor(type);
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { File } from '@red/domain';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { FilesService } from '@services/entity-services/files.service';
|
||||
@ -14,21 +12,18 @@ import { ReanalysisService } from '@services/reanalysis.service';
|
||||
export class FileActionService {
|
||||
constructor(
|
||||
private readonly _dialogService: DossiersDialogService,
|
||||
private readonly _permissionsService: PermissionsService,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _fileService: FilesService,
|
||||
private readonly _reanalysisService: ReanalysisService,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
private readonly _filesService: FilesService,
|
||||
private readonly _reanalysisService: ReanalysisService,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
private readonly _toaster: Toaster,
|
||||
) {}
|
||||
|
||||
reanalyseFile(file = this._appStateService.activeFile) {
|
||||
reanalyseFile(file: File) {
|
||||
return this._reanalysisService.reanalyzeFilesForDossier([file.fileId], this._dossiersService.activeDossier.id, true);
|
||||
}
|
||||
|
||||
toggleAnalysis(file = this._appStateService.activeFile) {
|
||||
toggleAnalysis(file: File) {
|
||||
return this._reanalysisService.toggleAnalysis(file.dossierId, file.fileId, !file.excluded);
|
||||
}
|
||||
|
||||
@ -58,7 +53,7 @@ export class FileActionService {
|
||||
approverId = this._dossiersService.activeDossier.approverIds[0];
|
||||
}
|
||||
|
||||
return this._fileService.setUnderApprovalFor(
|
||||
return this._filesService.setUnderApprovalFor(
|
||||
files.map(f => f.fileId),
|
||||
this._dossiersService.activeDossierId,
|
||||
approverId,
|
||||
@ -66,14 +61,14 @@ export class FileActionService {
|
||||
}
|
||||
|
||||
setFilesApproved(files: File[]) {
|
||||
return this._fileService.setApprovedFor(
|
||||
return this._filesService.setApprovedFor(
|
||||
files.map(f => f.fileId),
|
||||
this._dossiersService.activeDossierId,
|
||||
);
|
||||
}
|
||||
|
||||
setFilesUnderReview(files: File[]) {
|
||||
return this._fileService.setUnderReviewFor(
|
||||
return this._filesService.setUnderReviewFor(
|
||||
files.map(f => f.fileId),
|
||||
this._dossiersService.activeDossierId,
|
||||
);
|
||||
@ -86,13 +81,7 @@ export class FileActionService {
|
||||
);
|
||||
}
|
||||
|
||||
assignFile(
|
||||
mode: 'reviewer' | 'approver',
|
||||
$event: MouseEvent,
|
||||
file = this._appStateService.activeFile,
|
||||
callback?: Function,
|
||||
ignoreChanged = false,
|
||||
) {
|
||||
assignFile(mode: 'reviewer' | 'approver', $event: MouseEvent, file: File, callback?: Function, ignoreChanged = false) {
|
||||
const userIds = this._getUserIds(mode);
|
||||
if (userIds.length === 1 || userIds.includes(this._userService.currentUser.id)) {
|
||||
$event?.stopPropagation(); // event$ is null when called from workflow view
|
||||
@ -141,7 +130,7 @@ export class FileActionService {
|
||||
}
|
||||
|
||||
private async _assignReviewerToCurrentUser(files: File[], callback?: Function) {
|
||||
await this._fileService
|
||||
await this._filesService
|
||||
.setReviewerFor(
|
||||
files.map(f => f.fileId),
|
||||
this._dossiersService.activeDossierId,
|
||||
|
||||
@ -2,11 +2,10 @@ import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FileActionService } from './services/file-action.service';
|
||||
import { FileActionsComponent } from './components/file-actions/file-actions.component';
|
||||
import { NeedsWorkBadgeComponent } from './components/needs-work-badge/needs-work-badge.component';
|
||||
import { IqserIconsModule } from '@iqser/common-ui';
|
||||
import { SharedModule } from '@shared/shared.module';
|
||||
|
||||
const components = [FileActionsComponent, NeedsWorkBadgeComponent];
|
||||
const components = [FileActionsComponent];
|
||||
|
||||
@NgModule({
|
||||
declarations: [...components],
|
||||
|
||||
@ -1,18 +1,21 @@
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { FileStatus } from '@red/domain';
|
||||
import { ProcessingFileStatus, WorkflowFileStatus } from '@red/domain';
|
||||
|
||||
export const fileStatusTranslations: { [key in FileStatus]: string } = {
|
||||
export const workflowFileStatusTranslations: { [key in WorkflowFileStatus]: string } = {
|
||||
APPROVED: _('file-status.approved'),
|
||||
UNASSIGNED: _('file-status.unassigned'),
|
||||
UNDER_APPROVAL: _('file-status.under-approval'),
|
||||
UNDER_REVIEW: _('file-status.under-review'),
|
||||
};
|
||||
|
||||
export const processingFileStatusTranslations: { [key in ProcessingFileStatus]: string } = {
|
||||
PROCESSED: _('file-status.processed'),
|
||||
DELETED: _('file-status.deleted'),
|
||||
ERROR: _('file-status.error'),
|
||||
EXCLUDED: _('file-status.excluded'),
|
||||
FULLREPROCESS: _('file-status.full-reprocess'),
|
||||
INDEXING: _('file-status.indexing'),
|
||||
OCR_PROCESSING: _('file-status.ocr-processing'),
|
||||
PROCESSING: _('file-status.processing'),
|
||||
REPROCESS: _('file-status.reprocess'),
|
||||
UNASSIGNED: _('file-status.unassigned'),
|
||||
UNDER_APPROVAL: _('file-status.under-approval'),
|
||||
UNDER_REVIEW: _('file-status.under-review'),
|
||||
UNPROCESSED: _('file-status.unprocessed'),
|
||||
};
|
||||
|
||||
@ -12,7 +12,6 @@ export class IconsModule {
|
||||
constructor(private readonly _iconRegistry: MatIconRegistry, private readonly _sanitizer: DomSanitizer) {
|
||||
const icons = [
|
||||
'ai',
|
||||
'analyse',
|
||||
'approved',
|
||||
'arrow-up',
|
||||
'assign',
|
||||
@ -49,6 +48,7 @@ export class IconsModule {
|
||||
'put-back',
|
||||
'read-only',
|
||||
'ready-for-approval',
|
||||
'reanalyse',
|
||||
'reason',
|
||||
'remove-from-dict',
|
||||
'report',
|
||||
|
||||
@ -5,6 +5,7 @@ import { OverlayRef } from '@angular/cdk/overlay';
|
||||
import { StatusOverlayService } from '../services/status-overlay.service';
|
||||
import { handleFileDrop } from '@utils/file-drop-utils';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-file-drop',
|
||||
@ -18,6 +19,7 @@ export class FileDropComponent {
|
||||
private readonly _dossiersService: DossiersService,
|
||||
private readonly _changeDetectorRef: ChangeDetectorRef,
|
||||
private readonly _statusOverlayService: StatusOverlayService,
|
||||
private readonly _activatedRoute: ActivatedRoute,
|
||||
) {}
|
||||
|
||||
close() {
|
||||
@ -33,7 +35,8 @@ export class FileDropComponent {
|
||||
|
||||
@HostListener('drop', ['$event'])
|
||||
onDrop(event: DragEvent) {
|
||||
handleFileDrop(event, this._dossiersService.activeDossier, this.uploadFiles.bind(this));
|
||||
const dossier = this._dossiersService.find(this._dossiersService.activeDossierId);
|
||||
handleFileDrop(event, dossier, this.uploadFiles.bind(this));
|
||||
}
|
||||
|
||||
@HostListener('dragover', ['$event'])
|
||||
@ -43,11 +46,11 @@ export class FileDropComponent {
|
||||
}
|
||||
|
||||
async uploadFiles(files: FileUploadModel[]) {
|
||||
const fileCount = await this._fileUploadService.uploadFiles(files);
|
||||
const fileCount = await this._fileUploadService.uploadFiles(files, this._dossiersService.activeDossierId);
|
||||
if (fileCount) {
|
||||
this._statusOverlayService.openUploadStatusOverlay();
|
||||
}
|
||||
this._dialogRef.detach();
|
||||
this._changeDetectorRef.detectChanges();
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import { UploadDownloadDialogService } from './upload-download-dialog.service';
|
||||
import { IFileUploadResult } from '@red/domain';
|
||||
import { isCsv } from '@utils/file-drop-utils';
|
||||
import { ErrorMessageService, GenericService, HeadersConfiguration, RequiredParam, Validate } from '@iqser/common-ui';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { FilesMapService } from '@services/entity-services/files-map.service';
|
||||
|
||||
export interface ActiveUpload {
|
||||
subscription: Subscription;
|
||||
@ -28,7 +28,7 @@ export class FileUploadService extends GenericService<IFileUploadResult> {
|
||||
|
||||
constructor(
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
private readonly _filesMapService: FilesMapService,
|
||||
private readonly _applicationRef: ApplicationRef,
|
||||
private readonly _translateService: TranslateService,
|
||||
private readonly _configService: ConfigService,
|
||||
@ -37,7 +37,10 @@ export class FileUploadService extends GenericService<IFileUploadResult> {
|
||||
protected readonly _injector: Injector,
|
||||
) {
|
||||
super(_injector, 'upload');
|
||||
interval(2500).subscribe(() => {
|
||||
interval(2500).subscribe(async () => {
|
||||
if (this.activeUploads > 0) {
|
||||
await this._appStateService.reloadActiveDossierFiles();
|
||||
}
|
||||
this._handleUploads();
|
||||
});
|
||||
}
|
||||
@ -55,10 +58,10 @@ export class FileUploadService extends GenericService<IFileUploadResult> {
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFiles(files: FileUploadModel[]): Promise<number> {
|
||||
async uploadFiles(files: FileUploadModel[], dossierId?: string): Promise<number> {
|
||||
const maxSizeMB = this._configService.values.MAX_FILE_SIZE_MB;
|
||||
const maxSizeBytes = maxSizeMB * 1024 * 1024;
|
||||
const dossierFiles = this._dossiersService.activeDossier.files;
|
||||
const dossierFiles = this._filesMapService.get(dossierId);
|
||||
let option: 'overwrite' | 'skip';
|
||||
for (let idx = 0; idx < files.length; ++idx) {
|
||||
const file = files[idx];
|
||||
@ -174,7 +177,7 @@ export class FileUploadService extends GenericService<IFileUploadResult> {
|
||||
this.activeUploads++;
|
||||
const obs = this.uploadFileForm(uploadFile.dossierId, uploadFile.file);
|
||||
return obs.subscribe(
|
||||
async event => {
|
||||
event => {
|
||||
if (event.type === HttpEventType.UploadProgress) {
|
||||
uploadFile.progress = Math.round((event.loaded / (event.total || event.loaded)) * 100);
|
||||
this._applicationRef.tick();
|
||||
@ -190,7 +193,6 @@ export class FileUploadService extends GenericService<IFileUploadResult> {
|
||||
};
|
||||
}
|
||||
this._removeUpload(uploadFile);
|
||||
await this._appStateService.reloadActiveDossierFiles();
|
||||
}
|
||||
},
|
||||
(err: HttpErrorResponse) => {
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HeadersConfiguration, mapEach, RequiredParam, Validate } from '@iqser/common-ui';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { DossierStats, IDossierStats } from '@red/domain';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DossierStatsService {
|
||||
private readonly _map = new Map<string, BehaviorSubject<DossierStats>>();
|
||||
|
||||
constructor(private readonly _http: HttpClient) {}
|
||||
|
||||
@Validate()
|
||||
getFor(@RequiredParam() dossierIds: string[]): Observable<DossierStats[]> {
|
||||
const request = this._http.post<IDossierStats[]>(`/${encodeURI('dossier-stats')}`, dossierIds, {
|
||||
headers: HeadersConfiguration.getHeaders(),
|
||||
observe: 'body',
|
||||
});
|
||||
|
||||
return request.pipe(
|
||||
mapEach(entity => new DossierStats(entity)),
|
||||
tap(entities => entities.forEach(entity => this.set(entity))),
|
||||
);
|
||||
}
|
||||
|
||||
get(key: string): DossierStats {
|
||||
return this._map.get(key)?.value;
|
||||
}
|
||||
|
||||
set(stats: DossierStats): void {
|
||||
if (!this._map.has(stats.dossierId)) {
|
||||
this._map.set(stats.dossierId, new BehaviorSubject<DossierStats>(stats));
|
||||
} else {
|
||||
this._map.get(stats.dossierId).next(stats);
|
||||
}
|
||||
}
|
||||
|
||||
watch$(key: string): Observable<DossierStats> {
|
||||
return this._map.get(key)?.asObservable();
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,14 @@
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { EntitiesService, List, QueryParam, RequiredParam, Toaster, Validate } from '@iqser/common-ui';
|
||||
import { Dossier, File, IDossier, IDossierRequest } from '@red/domain';
|
||||
import { catchError, map, tap } from 'rxjs/operators';
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||
import { EntitiesService, List, QueryParam, RequiredParam, shareLast, Toaster, Validate } from '@iqser/common-ui';
|
||||
import { Dossier, IDossier, IDossierRequest } from '@red/domain';
|
||||
import { catchError, filter, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { BehaviorSubject, combineLatest, Observable, of, throwError } from 'rxjs';
|
||||
import { ActivationEnd, Router } from '@angular/router';
|
||||
import { DictionaryService } from '@shared/services/dictionary.service';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { currentComponentRoute } from '@utils/functions';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { DossierStatsService } from '@services/entity-services/dossier-stats.service';
|
||||
|
||||
export interface IDossiersStats {
|
||||
totalPeople: number;
|
||||
@ -20,69 +22,49 @@ const GENERIC_MGS = _('add-dossier-dialog.errors.generic');
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DossiersService extends EntitiesService<Dossier, IDossier> {
|
||||
readonly stats$ = this.all$.pipe(map(entities => this._computeStats(entities)));
|
||||
readonly activeDossier$: Observable<Dossier | undefined>;
|
||||
private readonly _activeDossier$ = new BehaviorSubject<Dossier | undefined>(undefined);
|
||||
readonly generalStats$ = this.all$.pipe(switchMap(entities => this._generalStats$(entities)));
|
||||
readonly activeDossierId$: Observable<string | undefined>;
|
||||
private readonly _activeDossierId$ = new BehaviorSubject<string | undefined>(undefined);
|
||||
|
||||
constructor(
|
||||
protected readonly _injector: Injector,
|
||||
private readonly _router: Router,
|
||||
private readonly _dictionaryService: DictionaryService,
|
||||
private readonly _toaster: Toaster,
|
||||
protected readonly _injector: Injector,
|
||||
private readonly _dictionaryService: DictionaryService,
|
||||
private readonly _dossierStatsService: DossierStatsService,
|
||||
) {
|
||||
super(_injector, Dossier, 'dossier');
|
||||
this.activeDossier$ = this._activeDossier$.asObservable();
|
||||
this.activeDossierId$ = this._activeDossierId$.asObservable();
|
||||
|
||||
_router.events.pipe(currentComponentRoute).subscribe((event: ActivationEnd) => {
|
||||
const dossierId = event.snapshot.paramMap.get('dossierId');
|
||||
const sameIdAsCurrentActive = dossierId === this._activeDossier$.getValue()?.dossierId;
|
||||
const sameIdAsCurrentActive = dossierId === this._activeDossierId$.getValue();
|
||||
|
||||
if (sameIdAsCurrentActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dossierId === null || dossierId === undefined) {
|
||||
return this._activeDossier$.next(undefined);
|
||||
return this._activeDossierId$.next(undefined);
|
||||
}
|
||||
|
||||
if (!this.has(dossierId)) {
|
||||
this._activeDossier$.next(undefined);
|
||||
this._activeDossierId$.next(undefined);
|
||||
return this._router.navigate(['/main/dossiers']).then();
|
||||
}
|
||||
|
||||
this._activeDossier$.next(this.find(dossierId));
|
||||
this.updateDossierDictionary(this.activeDossier.dossierTemplateId, dossierId).then();
|
||||
this._activeDossierId$.next(dossierId);
|
||||
const dossier = this.find(dossierId);
|
||||
this.updateDossierDictionary(dossier.dossierTemplateId, dossierId).then();
|
||||
});
|
||||
}
|
||||
|
||||
get allFiles(): File[] {
|
||||
return this.all.reduce((acc: File[], { files }) => [...acc, ...files], []);
|
||||
}
|
||||
|
||||
get activeDossier(): Dossier | undefined {
|
||||
return this._activeDossier$.value;
|
||||
return this.find(this.activeDossierId);
|
||||
}
|
||||
|
||||
get activeDossierId(): string | undefined {
|
||||
return this._activeDossier$.value?.dossierId;
|
||||
}
|
||||
|
||||
goToActiveDossier(): Promise<void> {
|
||||
return this._router.navigate([this.activeDossier?.routerLink]).then();
|
||||
}
|
||||
|
||||
find(dossierId: string): Dossier | undefined;
|
||||
find(dossierId: string, fileId: string): File | undefined;
|
||||
find(dossierId: string, fileId?: string): Dossier | File | undefined {
|
||||
const dossier = super.find(dossierId);
|
||||
return fileId ? dossier?.files.find(file => file.fileId === fileId) : dossier;
|
||||
}
|
||||
|
||||
replace(newDossier: Dossier) {
|
||||
super.replace(newDossier);
|
||||
if (newDossier.dossierId === this.activeDossierId) {
|
||||
this._activeDossier$.next(newDossier);
|
||||
}
|
||||
return this._activeDossierId$.value;
|
||||
}
|
||||
|
||||
async updateDossierDictionary(dossierTemplateId: string, dossierId: string) {
|
||||
@ -93,17 +75,23 @@ export class DossiersService extends EntitiesService<Dossier, IDossier> {
|
||||
} catch (e) {
|
||||
dossier.type = null;
|
||||
}
|
||||
this.replace(dossier);
|
||||
}
|
||||
|
||||
@Validate()
|
||||
createOrUpdate(@RequiredParam() dossier: IDossierRequest): Observable<Dossier | undefined> {
|
||||
return this._post(dossier).pipe(
|
||||
map(updatedDossier => new Dossier(updatedDossier, this.find(updatedDossier.dossierId)?.files ?? [])),
|
||||
const showToast = (error: HttpErrorResponse) => {
|
||||
this._toaster.error(error.status === 409 ? DOSSIER_EXISTS_MSG : GENERIC_MGS);
|
||||
return throwError(error);
|
||||
};
|
||||
|
||||
const dossier$ = this._post(dossier).pipe(shareLast());
|
||||
const stats$ = dossier$.pipe(switchMap(updatedDossier => this._dossierStatsService.getFor([updatedDossier.dossierId])));
|
||||
|
||||
return combineLatest([dossier$, stats$]).pipe(
|
||||
map(([updatedDossier]) => new Dossier(updatedDossier)),
|
||||
tap(newDossier => this.replace(newDossier)),
|
||||
catchError(error => {
|
||||
this._toaster.error(error.status === 409 ? DOSSIER_EXISTS_MSG : GENERIC_MGS);
|
||||
return of(undefined);
|
||||
}),
|
||||
catchError(showToast),
|
||||
);
|
||||
}
|
||||
|
||||
@ -139,7 +127,7 @@ export class DossiersService extends EntitiesService<Dossier, IDossier> {
|
||||
|
||||
entities.forEach(dossier => {
|
||||
dossier.memberIds?.forEach(m => totalPeople.add(m));
|
||||
totalAnalyzedPages += dossier.totalNumberOfPages;
|
||||
totalAnalyzedPages += this._dossierStatsService.get(dossier.dossierId).numberOfPages;
|
||||
});
|
||||
|
||||
return {
|
||||
@ -147,4 +135,13 @@ export class DossiersService extends EntitiesService<Dossier, IDossier> {
|
||||
totalAnalyzedPages,
|
||||
};
|
||||
}
|
||||
|
||||
private _generalStats$(entities: List<Dossier>): Observable<IDossiersStats> {
|
||||
const stats$ = entities.map(entity => this._dossierStatsService.watch$(entity.dossierId));
|
||||
return combineLatest(stats$).pipe(
|
||||
filter(stats => stats.every(s => !!s)),
|
||||
map(() => this._computeStats(entities)),
|
||||
shareLast(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,69 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FilesService } from '@services/entity-services/files.service';
|
||||
import { BehaviorSubject, Observable, Subject } from 'rxjs';
|
||||
import { File } from '@red/domain';
|
||||
import { filter, startWith } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FilesMapService {
|
||||
readonly fileReanalysed$ = new Subject<File>();
|
||||
private readonly _entityChanged$ = new Subject<File>();
|
||||
private readonly _map = new Map<string, BehaviorSubject<File[]>>();
|
||||
|
||||
constructor(private readonly _filesService: FilesService) {}
|
||||
|
||||
get$(dossierId: string) {
|
||||
return this._map.get(dossierId).asObservable();
|
||||
}
|
||||
|
||||
has(dossierId: string) {
|
||||
return this._map.has(dossierId);
|
||||
}
|
||||
|
||||
get(key: string): File[];
|
||||
get(key: string, id: string): File | undefined;
|
||||
get(key: string, id?: string): File | File[] | undefined {
|
||||
const value = this._map.get(key)?.value;
|
||||
if (!id) {
|
||||
return value ?? [];
|
||||
}
|
||||
return value?.find(item => item.id === id);
|
||||
}
|
||||
|
||||
set(key: string, entities: File[]): void {
|
||||
if (!this._map.has(key)) {
|
||||
this._map.set(key, new BehaviorSubject<File[]>(entities));
|
||||
return entities.forEach(entity => this._entityChanged$.next(entity));
|
||||
}
|
||||
|
||||
// Keep old object references for unchanged entities
|
||||
const newEntities = entities.map(newEntity => {
|
||||
const oldEntity = this.get(key, newEntity.id);
|
||||
|
||||
if (oldEntity?.lastProcessed !== newEntity.lastProcessed) {
|
||||
this.fileReanalysed$.next(newEntity);
|
||||
}
|
||||
|
||||
if (newEntity.isEqual(oldEntity)) {
|
||||
return oldEntity;
|
||||
}
|
||||
|
||||
this._entityChanged$.next(newEntity);
|
||||
return newEntity;
|
||||
});
|
||||
|
||||
this._map.get(key).next(newEntities);
|
||||
}
|
||||
|
||||
replace(entity: File) {
|
||||
const all = this.get(entity.dossierId).filter(file => file.fileId !== entity.fileId);
|
||||
this.set(entity.dossierId, [...all, entity]);
|
||||
}
|
||||
|
||||
watch$(key: string, entityId: string): Observable<File> {
|
||||
return this._entityChanged$.pipe(
|
||||
filter(entity => entity.id === entityId),
|
||||
startWith(this.get(key, entityId)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -13,10 +13,6 @@ export class FilesService extends EntitiesService<File, IFile> {
|
||||
super(_injector, File, 'status');
|
||||
}
|
||||
|
||||
getExistingFilesFor(dossierId: string): List<File> {
|
||||
return this.all.filter(file => file.dossierId === dossierId);
|
||||
}
|
||||
|
||||
fetch() {
|
||||
this.get().pipe(map(files => files.map(file => new File(file, this._userService.getNameForId(file.currentReviewer)))));
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import { notificationsTranslations } from '../translations/notifications-transla
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { FilesMapService } from '@services/entity-services/files-map.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@ -19,6 +20,7 @@ export class NotificationsService extends GenericService<unknown> {
|
||||
private readonly _translateService: TranslateService,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _filesMapService: FilesMapService,
|
||||
) {
|
||||
super(_injector, 'notification');
|
||||
}
|
||||
@ -61,7 +63,8 @@ export class NotificationsService extends GenericService<unknown> {
|
||||
const fileId = notification.target.fileId;
|
||||
const dossierId = notification.target.dossierId;
|
||||
const dossier = this._dossiersService.find(dossierId);
|
||||
const file = dossier?.files?.find(f => f.fileId === fileId);
|
||||
const files = this._filesMapService.get(dossierId);
|
||||
const file = files?.find(f => f.fileId === fileId);
|
||||
|
||||
return this._translateService.instant(translation, {
|
||||
fileHref: file?.routerLink,
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { UserService } from './user.service';
|
||||
import { Dossier, File, IComment } from '@red/domain';
|
||||
import { DossiersService } from './entity-services/dossiers.service';
|
||||
@ -8,53 +7,46 @@ import { DossiersService } from './entity-services/dossiers.service';
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PermissionsService {
|
||||
constructor(
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
) {}
|
||||
|
||||
private get _activeFile(): File | undefined {
|
||||
return this._appStateService.activeFile;
|
||||
}
|
||||
constructor(private readonly _userService: UserService, private readonly _dossiersService: DossiersService) {}
|
||||
|
||||
private get _activeDossier(): Dossier | undefined {
|
||||
return this._dossiersService.activeDossier;
|
||||
}
|
||||
|
||||
isReviewerOrApprover(file?: File): boolean {
|
||||
return this.isFileReviewer(file) || this.isApprover();
|
||||
isReviewerOrApprover(file: File, dossier: Dossier): boolean {
|
||||
return this.isFileReviewer(file) || this.isApprover(dossier);
|
||||
}
|
||||
|
||||
displayReanalyseBtn(dossier: Dossier): boolean {
|
||||
return this.isApprover(dossier);
|
||||
}
|
||||
|
||||
canToggleAnalysis(file: File): boolean {
|
||||
return this.isReviewerOrApprover(file) && ['UNASSIGNED', 'UNDER_REVIEW', 'UNDER_APPROVAL'].includes(file.status);
|
||||
canToggleAnalysis(file: File, dossier: Dossier): boolean {
|
||||
return this.isReviewerOrApprover(file, dossier) && (file.isUnassigned || file.isUnderReview || file.isUnderApproval);
|
||||
}
|
||||
|
||||
canReanalyseFile(file = this._activeFile): boolean {
|
||||
return this.isReviewerOrApprover(file) || file.isUnassigned || (file.isError && file.isUnassigned);
|
||||
canReanalyseFile(file: File, dossier: Dossier): boolean {
|
||||
return this.isReviewerOrApprover(file, dossier) || file.isUnassigned || (file.isError && file.isUnassigned);
|
||||
}
|
||||
|
||||
isFileReviewer(file = this._activeFile): boolean {
|
||||
isFileReviewer(file: File): boolean {
|
||||
return file.currentReviewer === this._userService.currentUser.id;
|
||||
}
|
||||
|
||||
canDeleteFile(file = this._activeFile, dossier?: Dossier): boolean {
|
||||
canDeleteFile(file: File, dossier?: Dossier): boolean {
|
||||
return (this.isOwner(dossier) && !file.isApproved) || file.isUnassigned;
|
||||
}
|
||||
|
||||
canAssignToSelf(file = this._activeFile): boolean {
|
||||
const precondition = this.isDossierMember() && !file.isProcessing && !file.isError && !file.isApproved;
|
||||
canAssignToSelf(file: File): boolean {
|
||||
const dossier = this._getDossier(file);
|
||||
const precondition = this.isDossierMember(dossier) && !file.isProcessing && !file.isError && !file.isApproved;
|
||||
|
||||
const isTheOnlyReviewer = !this._activeDossier?.hasReviewers;
|
||||
const isTheOnlyReviewer = !dossier?.hasReviewers;
|
||||
|
||||
if (precondition) {
|
||||
if (
|
||||
(file.isUnassigned || (file.isUnderReview && !this.isFileReviewer(file))) &&
|
||||
(this.isApprover() || isTheOnlyReviewer || (this.isDossierReviewer() && file.isUnassigned))
|
||||
(this.isApprover(dossier) || isTheOnlyReviewer || (this.isDossierReviewer(dossier) && file.isUnassigned))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@ -62,37 +54,39 @@ export class PermissionsService {
|
||||
return false;
|
||||
}
|
||||
|
||||
canAssignUser(file = this._activeFile): boolean {
|
||||
const precondition = !file.isProcessing && !file.isError && !file.isApproved && this.isApprover();
|
||||
canAssignUser(file: File): boolean {
|
||||
const dossier = this._getDossier(file);
|
||||
const precondition = !file.isProcessing && !file.isError && !file.isApproved && this.isApprover(dossier);
|
||||
|
||||
if (precondition) {
|
||||
if ((file.isUnassigned || file.isUnderReview) && this._activeDossier.hasReviewers) {
|
||||
if ((file.isUnassigned || file.isUnderReview) && dossier.hasReviewers) {
|
||||
return true;
|
||||
}
|
||||
if (file.isUnderApproval && this._activeDossier.approverIds.length > 1) {
|
||||
if (file.isUnderApproval && dossier.approverIds.length > 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
canUnassignUser(file = this._activeFile): boolean {
|
||||
return (file.isUnderReview || file.isUnderApproval) && (this.isFileReviewer(file) || this.isApprover());
|
||||
canUnassignUser(file: File): boolean {
|
||||
const dossier = this._getDossier(file);
|
||||
return (file.isUnderReview || file.isUnderApproval) && (this.isFileReviewer(file) || this.isApprover(dossier));
|
||||
}
|
||||
|
||||
canSetUnderReview(file = this._activeFile): boolean {
|
||||
return file?.isUnderApproval && this.isApprover();
|
||||
canSetUnderReview(file: File, dossier: Dossier = this._dossiersService.activeDossier): boolean {
|
||||
return file?.isUnderApproval && this.isApprover(dossier);
|
||||
}
|
||||
|
||||
isReadyForApproval(file = this._activeFile): boolean {
|
||||
return this.canSetUnderReview(file);
|
||||
isReadyForApproval(file: File, dossier: Dossier = this._dossiersService.activeDossier): boolean {
|
||||
return this.canSetUnderReview(file, dossier);
|
||||
}
|
||||
|
||||
canSetUnderApproval(file = this._activeFile): boolean {
|
||||
return file?.isUnderReview && this.isReviewerOrApprover(file);
|
||||
canSetUnderApproval(file: File, dossier: Dossier = this._dossiersService.activeDossier): boolean {
|
||||
return file?.isUnderReview && this.isReviewerOrApprover(file, dossier);
|
||||
}
|
||||
|
||||
isOwner(dossier = this._activeDossier, user = this._userService.currentUser): boolean {
|
||||
isOwner(dossier: Dossier, user = this._userService.currentUser): boolean {
|
||||
return dossier?.ownerId === user.id;
|
||||
}
|
||||
|
||||
@ -100,28 +94,29 @@ export class PermissionsService {
|
||||
return dossier?.approverIds.indexOf(user.id) >= 0;
|
||||
}
|
||||
|
||||
isDossierReviewer(dossier = this._activeDossier, user = this._userService.currentUser): boolean {
|
||||
isDossierReviewer(dossier: Dossier, user = this._userService.currentUser): boolean {
|
||||
return this.isDossierMember(dossier, user) && !this.isApprover(dossier, user);
|
||||
}
|
||||
|
||||
isDossierMember(dossier = this._activeDossier, user = this._userService.currentUser): boolean {
|
||||
isDossierMember(dossier: Dossier, user = this._userService.currentUser): boolean {
|
||||
return dossier?.memberIds.includes(user.id);
|
||||
}
|
||||
|
||||
canPerformAnnotationActions(file = this._activeFile): boolean {
|
||||
return ['UNDER_REVIEW', 'UNDER_APPROVAL'].includes(file?.status) && this.isFileReviewer(file);
|
||||
// TODO: Remove '?', after we make sure file is loaded before page
|
||||
canPerformAnnotationActions(file: File): boolean {
|
||||
return (file?.isUnderReview || file?.isUnderApproval) && this.isFileReviewer(file);
|
||||
}
|
||||
|
||||
canUndoApproval(file = this._activeFile): boolean {
|
||||
return file?.isApproved && this.isApprover();
|
||||
canUndoApproval(file: File): boolean {
|
||||
return file?.isApproved && this.isApprover(this._getDossier(file));
|
||||
}
|
||||
|
||||
canMarkPagesAsViewed(file = this._activeFile): boolean {
|
||||
return ['UNDER_REVIEW', 'UNDER_APPROVAL'].includes(file?.status) && this.isFileReviewer(file);
|
||||
canMarkPagesAsViewed(file: File): boolean {
|
||||
return (file.isUnderReview || file.isUnderApproval) && this.isFileReviewer(file);
|
||||
}
|
||||
|
||||
canDownloadFiles(file: File): boolean {
|
||||
const dossier = this._dossiersService.find(file?.dossierId);
|
||||
const dossier = this._getDossier(file);
|
||||
if (!dossier) {
|
||||
return false;
|
||||
}
|
||||
@ -129,7 +124,7 @@ export class PermissionsService {
|
||||
return file.isApproved && this.isApprover(dossier);
|
||||
}
|
||||
|
||||
canDeleteDossier(dossier = this._activeDossier): boolean {
|
||||
canDeleteDossier(dossier: Dossier): boolean {
|
||||
return dossier.ownerId === this._userService.currentUser.id;
|
||||
}
|
||||
|
||||
@ -137,15 +132,21 @@ export class PermissionsService {
|
||||
return user.isAdmin;
|
||||
}
|
||||
|
||||
canAddComment(file = this._activeFile): boolean {
|
||||
return (this.isFileReviewer(file) || this.isApprover()) && !file.isApproved;
|
||||
canAddComment(file: File): boolean {
|
||||
return (this.isFileReviewer(file) || this.isApprover(this._getDossier(file))) && !file.isApproved;
|
||||
}
|
||||
|
||||
canExcludePages(file = this._activeFile): boolean {
|
||||
return ['UNDER_REVIEW', 'UNDER_APPROVAL'].includes(file.status) && (this.isFileReviewer(file) || this.isApprover());
|
||||
canExcludePages(file: File): boolean {
|
||||
const dossier = this._getDossier(file);
|
||||
return (file.isUnderReview || file.isUnderApproval) && (this.isFileReviewer(file) || this.isApprover(dossier));
|
||||
}
|
||||
|
||||
canDeleteComment(comment: IComment, file = this._activeFile) {
|
||||
return (comment.user === this._userService.currentUser.id || this.isApprover()) && !file.isApproved;
|
||||
canDeleteComment(comment: IComment, file: File) {
|
||||
const dossier = this._getDossier(file);
|
||||
return (comment.user === this._userService.currentUser.id || this.isApprover(dossier)) && !file.isApproved;
|
||||
}
|
||||
|
||||
private _getDossier(file: File): Dossier {
|
||||
return this._dossiersService.find(file.dossierId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { AppStateService } from './app-state.service';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
|
||||
import { FilesMapService } from '@services/entity-services/files-map.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@ -12,6 +13,7 @@ export class AppStateGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
private readonly _filesMapService: FilesMapService,
|
||||
private readonly _dossierTemplatesService: DossierTemplatesService,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _router: Router,
|
||||
@ -34,12 +36,17 @@ export class AppStateGuard implements CanActivate {
|
||||
|
||||
const { dossierId, fileId, dossierTemplateId, type } = route.params;
|
||||
|
||||
if (dossierId && !this._dossiersService.find(dossierId)) {
|
||||
const dossier = this._dossiersService.find(dossierId);
|
||||
if (dossierId && !dossier) {
|
||||
await this._router.navigate(['main', 'dossiers']);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fileId && !this._dossiersService.find(dossierId, fileId)) {
|
||||
if (fileId && this._filesMapService.get(dossierId).length === 0) {
|
||||
await this._appStateService.getFiles(dossier);
|
||||
}
|
||||
|
||||
if (fileId && !this._filesMapService.get(dossierId, fileId)) {
|
||||
await this._router.navigate(['main', 'dossiers', dossierId]);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Dictionary, Dossier, DossierTemplate, File, IColors, IDossier, IFile } from '@red/domain';
|
||||
import { Dictionary, Dossier, DossierTemplate, File, IColors, IDossier } from '@red/domain';
|
||||
import { ActivationEnd, Router } from '@angular/router';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { forkJoin, Observable, of, Subject } from 'rxjs';
|
||||
import { catchError, filter, first, map, tap } from 'rxjs/operators';
|
||||
import { forkJoin, Observable, of } from 'rxjs';
|
||||
import { catchError, first, map, tap } from 'rxjs/operators';
|
||||
import { currentComponentRoute, FALLBACK_COLOR, hexToRgb } from '@utils/functions';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { UserPreferenceService } from '@services/user-preference.service';
|
||||
@ -11,7 +11,8 @@ import { FilesService } from '@services/entity-services/files.service';
|
||||
import { DictionaryService } from '@shared/services/dictionary.service';
|
||||
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
|
||||
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
|
||||
import { ReanalysisService } from '@services/reanalysis.service';
|
||||
import { DossierStatsService } from '@services/entity-services/dossier-stats.service';
|
||||
import { FilesMapService } from '@services/entity-services/files-map.service';
|
||||
|
||||
export interface AppState {
|
||||
activeFileId?: string;
|
||||
@ -22,9 +23,6 @@ export interface AppState {
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AppStateService {
|
||||
readonly fileChanged$ = new Subject<File>();
|
||||
readonly fileReanalysed$ = new Subject<File>();
|
||||
|
||||
private _appState: AppState = {};
|
||||
|
||||
constructor(
|
||||
@ -32,10 +30,11 @@ export class AppStateService {
|
||||
private readonly _userService: UserService,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
private readonly _filesService: FilesService,
|
||||
private readonly _reanalysisService: ReanalysisService,
|
||||
private readonly _dictionaryService: DictionaryService,
|
||||
private readonly _dossierTemplatesService: DossierTemplatesService,
|
||||
private readonly _dossierStatsService: DossierStatsService,
|
||||
private readonly _fileAttributesService: FileAttributesService,
|
||||
private readonly _filesMapService: FilesMapService,
|
||||
private readonly _userPreferenceService: UserPreferenceService,
|
||||
) {
|
||||
_router.events.pipe(currentComponentRoute).subscribe(async (event: ActivationEnd) => {
|
||||
@ -50,12 +49,6 @@ export class AppStateService {
|
||||
return (this._appState.activeFileId = undefined);
|
||||
}
|
||||
|
||||
await _dossiersService.activeDossier$
|
||||
.pipe(
|
||||
filter(dossier => !!dossier),
|
||||
first(),
|
||||
)
|
||||
.toPromise();
|
||||
const dossierId = event.snapshot.paramMap.get('dossierId');
|
||||
return this.activateFile(dossierId, fileId);
|
||||
});
|
||||
@ -83,17 +76,16 @@ export class AppStateService {
|
||||
}
|
||||
|
||||
get activeFile(): File | undefined {
|
||||
return this._dossiersService.activeDossier?.files.find(f => f.fileId === this.activeFileId);
|
||||
if (!this.activeFileId) {
|
||||
return;
|
||||
}
|
||||
return this._filesMapService.get(this._dossiersService.activeDossierId, this.activeFileId);
|
||||
}
|
||||
|
||||
get activeFileId(): string | undefined {
|
||||
return this._appState.activeFileId;
|
||||
}
|
||||
|
||||
async reloadActiveDossierFilesIfNecessary() {
|
||||
await this.reloadActiveDossierFiles();
|
||||
}
|
||||
|
||||
getDictionaryColor(type?: string, dossierTemplateId = this._dossiersService.activeDossier?.dossierTemplateId) {
|
||||
if (!dossierTemplateId) {
|
||||
dossierTemplateId = this.dossierTemplates[0]?.dossierTemplateId;
|
||||
@ -122,74 +114,65 @@ export class AppStateService {
|
||||
return data ? data : this._dictionaryData[dossierTemplateId]['default'];
|
||||
}
|
||||
|
||||
async loadAllDossiers(emitEvents = true) {
|
||||
async loadAllDossiers() {
|
||||
const dossiers = await this._dossiersService.get().toPromise();
|
||||
if (!dossiers) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dossierIds = dossiers.map(dossier => dossier.dossierId);
|
||||
await this._dossierStatsService.getFor(dossierIds).toPromise();
|
||||
|
||||
const mappedDossiers$ = dossiers.map(async p => {
|
||||
const oldDossier = this._dossiersService.find(p.dossierId);
|
||||
const type = oldDossier?.type ?? (await this._getDictionaryFor(p));
|
||||
return new Dossier(p, oldDossier?.files ?? [], type);
|
||||
this._dossiersService.replace(new Dossier(p, type));
|
||||
});
|
||||
const mappedDossiers = await Promise.all(mappedDossiers$);
|
||||
const fileData = await this._filesService.getFor(mappedDossiers.map(p => p.id)).toPromise();
|
||||
|
||||
for (const dossierId of Object.keys(fileData)) {
|
||||
const dossier = mappedDossiers.find(p => p.id === dossierId);
|
||||
if (dossier) {
|
||||
this._processFiles(dossier, fileData[dossierId], emitEvents);
|
||||
}
|
||||
}
|
||||
return Promise.all(mappedDossiers$);
|
||||
}
|
||||
|
||||
async reloadActiveFile() {
|
||||
const activeDossier = this._dossiersService.activeDossier;
|
||||
async reloadFile(dossierId: string, fileId: string) {
|
||||
const dossier = this._dossiersService.find(dossierId);
|
||||
const oldFile = this._filesMapService.get(dossierId, fileId);
|
||||
|
||||
if (!this.activeFile || !activeDossier) {
|
||||
if (!oldFile || !dossier) {
|
||||
return null;
|
||||
}
|
||||
const oldProcessedDate = this.activeFile.lastProcessed;
|
||||
const iFile = await this._filesService.get(activeDossier.dossierId, this.activeFileId).toPromise();
|
||||
|
||||
const activeFile = new File(
|
||||
const iFile = await this._filesService.get(dossierId, fileId).toPromise();
|
||||
|
||||
const newFile = new File(
|
||||
iFile,
|
||||
this._userService.getNameForId(iFile.currentReviewer),
|
||||
this._fileAttributesService.getFileAttributeConfig(activeDossier.dossierTemplateId),
|
||||
this._fileAttributesService.getFileAttributeConfig(dossier.dossierTemplateId),
|
||||
);
|
||||
const files = activeDossier.files.filter(file => file.fileId !== activeFile.fileId);
|
||||
files.push(activeFile);
|
||||
const newDossier = new Dossier(activeDossier, files, activeDossier.type);
|
||||
this._dossiersService.replace(newDossier);
|
||||
|
||||
if (activeFile.lastProcessed !== oldProcessedDate) {
|
||||
this.fileReanalysed$.next(activeFile);
|
||||
}
|
||||
this.fileChanged$.next(activeFile);
|
||||
return activeFile;
|
||||
this._filesMapService.replace(newFile);
|
||||
return newFile;
|
||||
}
|
||||
|
||||
async getFiles(dossier = this._dossiersService.activeDossier, emitEvents = true) {
|
||||
async getFiles(dossier = this._dossiersService.activeDossier) {
|
||||
const files = await this._filesService.getFor(dossier.id).toPromise();
|
||||
await this._dossierStatsService.getFor([dossier.id]).toPromise();
|
||||
|
||||
return this._processFiles(dossier, files, emitEvents);
|
||||
}
|
||||
const fileAttributes = this._fileAttributesService.getFileAttributeConfig(dossier.dossierTemplateId);
|
||||
const newFiles = files.map(iFile => new File(iFile, this._userService.getNameForId(iFile.currentReviewer), fileAttributes));
|
||||
|
||||
async reanalyzeDossier({ id } = this._dossiersService.activeDossier) {
|
||||
await this._reanalysisService.reanalyzeDossier(id, true).toPromise();
|
||||
const lastOpenedFileId = this._userPreferenceService.getLastOpenedFileForDossier(dossier.id);
|
||||
newFiles.forEach(file => (file.lastOpened = file.fileId === lastOpenedFileId));
|
||||
this._filesMapService.set(dossier.dossierId, newFiles);
|
||||
|
||||
return newFiles;
|
||||
}
|
||||
|
||||
async activateFile(dossierId: string, fileId: string) {
|
||||
if (this._dossiersService.activeDossierId === dossierId && this.activeFileId === fileId) {
|
||||
const activeDossierId = await this._dossiersService.activeDossierId$.pipe(first()).toPromise();
|
||||
if (activeDossierId === dossierId && this.activeFileId === fileId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._dossiersService.activeDossier) {
|
||||
this._appState.activeFileId = fileId;
|
||||
if (!this.activeFile) {
|
||||
this._appState.activeFileId = null;
|
||||
await this._dossiersService.goToActiveDossier();
|
||||
}
|
||||
}
|
||||
await this._updateLastActiveFileForDossier(dossierId, fileId);
|
||||
}
|
||||
@ -211,7 +194,7 @@ export class AppStateService {
|
||||
|
||||
async reloadActiveDossierFiles() {
|
||||
if (this._dossiersService.activeDossierId) {
|
||||
await this.getFiles();
|
||||
return this.getFiles();
|
||||
}
|
||||
}
|
||||
|
||||
@ -475,46 +458,10 @@ export class AppStateService {
|
||||
}
|
||||
|
||||
private async _updateLastActiveFileForDossier(dossierId: string, fileId: string) {
|
||||
this._dossiersService.activeDossier.files.forEach(f => {
|
||||
this._filesMapService.get(dossierId).forEach(f => {
|
||||
f.lastOpened = f.fileId === fileId;
|
||||
});
|
||||
|
||||
await this._userPreferenceService.saveLastOpenedFileForDossier(dossierId, fileId);
|
||||
}
|
||||
|
||||
private _processFiles(dossier: Dossier, iFiles: IFile[], emitEvents = true) {
|
||||
const oldFiles = dossier.files;
|
||||
const fileAttributes = this._fileAttributesService.getFileAttributeConfig(dossier.dossierTemplateId);
|
||||
const newFiles = iFiles.map(iFile => new File(iFile, this._userService.getNameForId(iFile.currentReviewer), fileAttributes));
|
||||
|
||||
const lastOpenedFileId = this._userPreferenceService.getLastOpenedFileForDossier(dossier.id);
|
||||
newFiles.forEach(file => (file.lastOpened = file.fileId === lastOpenedFileId));
|
||||
|
||||
for (const newFile of newFiles) {
|
||||
let found = false;
|
||||
|
||||
for (const oldFile of oldFiles) {
|
||||
if (oldFile.fileId === newFile.fileId) {
|
||||
// emit when analysis count changed
|
||||
if (JSON.stringify(oldFile) !== JSON.stringify(newFile) && emitEvents) {
|
||||
this.fileChanged$.next(newFile);
|
||||
}
|
||||
if (oldFile.lastProcessed !== newFile.lastProcessed && emitEvents) {
|
||||
this.fileReanalysed$.next(newFile);
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// emit for new file
|
||||
if (!found && emitEvents) {
|
||||
this.fileChanged$.next(newFile);
|
||||
}
|
||||
}
|
||||
|
||||
const newDossier = new Dossier(dossier, newFiles, dossier.type);
|
||||
this._dossiersService.replace(newDossier);
|
||||
|
||||
return newFiles;
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,41 +44,35 @@ export function handleFilterDelta(oldFilters: INestedFilter[], newFilters: INest
|
||||
});
|
||||
}
|
||||
|
||||
export const annotationFilterChecker = (input: File | Dossier, filter: INestedFilter) => {
|
||||
export const annotationFilterChecker = (file: File, filter: INestedFilter) => {
|
||||
switch (filter.id) {
|
||||
case 'analysis': {
|
||||
if (input instanceof Dossier) {
|
||||
return input.reanalysisRequired;
|
||||
} else {
|
||||
return input.analysisRequired;
|
||||
}
|
||||
return file.analysisRequired;
|
||||
}
|
||||
case 'suggestion': {
|
||||
return input.hasSuggestions;
|
||||
return file.hasSuggestions;
|
||||
}
|
||||
case 'redaction': {
|
||||
return input.hasRedactions;
|
||||
return file.hasRedactions;
|
||||
}
|
||||
case 'hint': {
|
||||
return input.hintsOnly;
|
||||
return file.hintsOnly;
|
||||
}
|
||||
case 'none': {
|
||||
return input.hasNone;
|
||||
return file.hasNone;
|
||||
}
|
||||
case 'updated': {
|
||||
return input instanceof File && input.hasUpdates;
|
||||
return file.hasUpdates;
|
||||
}
|
||||
case 'image': {
|
||||
return input instanceof File && input.hasImages;
|
||||
return file.hasImages;
|
||||
}
|
||||
case 'comment': {
|
||||
return input instanceof File && input.hasAnnotationComments;
|
||||
return file.hasAnnotationComments;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const dossierStatusChecker = (dw: Dossier, filter: INestedFilter) => dw.hasStatus(filter.id);
|
||||
|
||||
export const dossierMemberChecker = (dw: Dossier, filter: INestedFilter) => dw.hasMember(filter.id);
|
||||
|
||||
export const dossierTemplateChecker = (dw: Dossier, filter: INestedFilter) => dw.dossierTemplateId === filter.id;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -690,7 +690,8 @@
|
||||
"deleted": "{count} deleted files",
|
||||
"documents": "{count} {count, plural, one{document} other{documents}}",
|
||||
"due-date": "Due {date}",
|
||||
"people": "{count} {count, plural, one{user} other{users}}"
|
||||
"people": "{count} {count, plural, one{user} other{users}}",
|
||||
"processing-documents": "{count} processing {count, plural, one{document} other{documents}}"
|
||||
}
|
||||
},
|
||||
"download-file": "Download",
|
||||
@ -698,8 +699,7 @@
|
||||
"file-listing": {
|
||||
"file-entry": {
|
||||
"file-error": "Re-processing required",
|
||||
"file-pending": "Pending...",
|
||||
"file-processing": "Processing"
|
||||
"file-pending": "Pending..."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
@ -1081,11 +1081,11 @@
|
||||
"approved": "Approved",
|
||||
"deleted": "Deleted",
|
||||
"error": "Re-processing required",
|
||||
"excluded": "Excluded",
|
||||
"full-reprocess": "Processing",
|
||||
"indexing": "Processing",
|
||||
"ocr-processing": "OCR Processing",
|
||||
"processing": "Processing",
|
||||
"processed": "Processed",
|
||||
"processing": "Processing...",
|
||||
"reprocess": "Processing",
|
||||
"unassigned": "Unassigned",
|
||||
"under-approval": "Under Approval",
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg height="100px" version="1.1" viewBox="0 0 100 100" width="100px"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd" id="analyse" stroke="none" stroke-width="1">
|
||||
<path
|
||||
d="M16.5,89 L16.5,100 L0,100 L0,89 L16.5,89 Z M44.5,89 L44.5,100 L28,100 L28,89 L44.5,89 Z M72,89 L72,100 L55.5,100 L55.5,89 L72,89 Z M100,89 L100,100 L83.5,100 L83.5,89 L100,89 Z M16.5,66.5 L16.5,77.5 L0,77.5 L0,66.5 L16.5,66.5 Z M44.5,66.5 L44.5,77.5 L28,77.5 L28,66.5 L44.5,66.5 Z M72,66.5 L72,77.5 L55.5,77.5 L55.5,66.5 L72,66.5 Z M100,66.5 L100,77.5 L83.5,77.5 L83.5,66.5 L100,66.5 Z M16.5,44.5 L16.5,55.5 L0,55.5 L0,44.5 L16.5,44.5 Z M44.5,44.5 L44.5,55.5 L28,55.5 L28,44.5 L44.5,44.5 Z M100,44.5 L100,55.5 L83.5,55.5 L83.5,44.5 L100,44.5 Z M44.5,22 L44.5,33 L28,33 L28,22 L44.5,22 Z M100,22 L100,33 L83.5,33 L83.5,22 L100,22 Z M44.5,0 L44.5,11 L28,11 L28,0 L44.5,0 Z"
|
||||
fill="currentColor" fill-rule="nonzero" id="Combined-Shape"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
16
apps/red-ui/src/assets/icons/general/reanalyse.svg
Normal file
16
apps/red-ui/src/assets/icons/general/reanalyse.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg height="10px" version="1.1" viewBox="0 0 10 10" width="10px" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd" id="Processing" stroke="none" stroke-width="1">
|
||||
<g id="01.Processing" transform="translate(-1024.000000, -214.000000)">
|
||||
<rect height="705" width="1440" x="0" y="0"></rect>
|
||||
<polygon id="Rectangle" points="0 194 1086 194 1086 244 0 244"></polygon>
|
||||
<g fill="currentColor" fill-rule="nonzero" id="status" transform="translate(1024.000000, 214.000000)">
|
||||
<g id="refresh">
|
||||
<path
|
||||
d="M1.25,6.35 C1.8,7.95 3.3,9 5,9 C6.2,9 7.35,8.45 8.1,7.5 L8.1,7.5 L6.5,7.5 L6.5,6.5 L9.5,6.5 L9.5,9.5 L8.5,9.5 L8.5,8.55 C7.6,9.45 6.35,10 5,10 C2.9,10 1,8.65 0.3,6.7 L0.3,6.7 Z M5,0 C7.1,0 9,1.35 9.7,3.3 L9.7,3.3 L8.75,3.65 C8.2,2.05 6.7,1 5,1 C3.8,1 2.65,1.55 1.9,2.5 L1.9,2.5 L3.5,2.5 L3.5,3.5 L0.5,3.5 L0.5,0.5 L1.5,0.5 L1.5,1.45 C2.45,0.55 3.7,0 5,0 Z"
|
||||
id="Combined-Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -1,25 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>status</title>
|
||||
<g id="Styleguide" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Styleguide-Actions" transform="translate(-979.000000, -630.000000)" fill="currentColor">
|
||||
<svg height="14px" version="1.1" viewBox="0 0 14 14" width="14px" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd" id="Styleguide" stroke="none" stroke-width="1">
|
||||
<g fill="currentColor" id="Styleguide-Actions" transform="translate(-979.000000, -630.000000)">
|
||||
<g id="reference" transform="translate(969.000000, 620.000000)">
|
||||
<g id="status" transform="translate(10.000000, 10.000000)">
|
||||
<path
|
||||
d="M1.4,9.8 L1.4,12.6 L4.2,12.6 L4.2,14 L0,14 L0,9.8 L1.4,9.8 Z M14,9.8 L14,14 L9.8,14 L9.8,12.6 L12.6,12.6 L12.6,9.8 L14,9.8 Z M4.2,0 L4.2,1.4 L1.4,1.4 L1.4,4.2 L0,4.2 L0,0 L4.2,0 Z M14,0 L14,4.2 L12.6,4.2 L12.6,1.4 L9.8,1.4 L9.8,0 L14,0 Z"
|
||||
id="OCR" fill-rule="nonzero"></path>
|
||||
<path d="M4.2,0 L0,0 L0,4.2 L4.2,4.2 L4.2,0 Z M2.8,1.4 L2.8,2.8 L1.4,2.8 L1.4,1.4 L2.8,1.4 Z" id="Path"
|
||||
fill-rule="nonzero"></path>
|
||||
<path d="M4.2,9.8 L0,9.8 L0,14 L4.2,14 L4.2,9.8 Z M2.8,11.2 L2.8,12.6 L1.4,12.6 L1.4,11.2 L2.8,11.2 Z" id="Path"
|
||||
fill-rule="nonzero"></path>
|
||||
<path d="M14,0 L9.8,0 L9.8,4.2 L14,4.2 L14,0 Z M12.6,1.4 L12.6,2.8 L11.2,2.8 L11.2,1.4 L12.6,1.4 Z" id="Path"
|
||||
fill-rule="nonzero"></path>
|
||||
<path d="M14,9.8 L9.8,9.8 L9.8,14 L14,14 L14,9.8 Z M12.6,11.2 L12.6,12.6 L11.2,12.6 L11.2,11.2 L12.6,11.2 Z" id="Path"
|
||||
fill-rule="nonzero"></path>
|
||||
<rect id="Rectangle" x="4.2" y="1.4" width="5.6" height="1.4"></rect>
|
||||
<rect id="Rectangle" x="4.2" y="11.2" width="5.6" height="1.4"></rect>
|
||||
<rect id="Rectangle" x="11.2" y="4.2" width="1.4" height="5.6"></rect>
|
||||
<rect id="Rectangle" x="1.4" y="4.2" width="1.4" height="5.6"></rect>
|
||||
fill-rule="nonzero" id="OCR"></path>
|
||||
<path d="M4.2,0 L0,0 L0,4.2 L4.2,4.2 L4.2,0 Z M2.8,1.4 L2.8,2.8 L1.4,2.8 L1.4,1.4 L2.8,1.4 Z" fill-rule="nonzero"
|
||||
id="Path"></path>
|
||||
<path d="M4.2,9.8 L0,9.8 L0,14 L4.2,14 L4.2,9.8 Z M2.8,11.2 L2.8,12.6 L1.4,12.6 L1.4,11.2 L2.8,11.2 Z" fill-rule="nonzero"
|
||||
id="Path"></path>
|
||||
<path d="M14,0 L9.8,0 L9.8,4.2 L14,4.2 L14,0 Z M12.6,1.4 L12.6,2.8 L11.2,2.8 L11.2,1.4 L12.6,1.4 Z" fill-rule="nonzero"
|
||||
id="Path"></path>
|
||||
<path d="M14,9.8 L9.8,9.8 L9.8,14 L14,14 L14,9.8 Z M12.6,11.2 L12.6,12.6 L11.2,12.6 L11.2,11.2 L12.6,11.2 Z" fill-rule="nonzero"
|
||||
id="Path"></path>
|
||||
<rect height="1.4" id="Rectangle" width="5.6" x="4.2" y="1.4"></rect>
|
||||
<rect height="1.4" id="Rectangle" width="5.6" x="4.2" y="11.2"></rect>
|
||||
<rect height="5.6" id="Rectangle" width="1.4" x="11.2" y="4.2"></rect>
|
||||
<rect height="5.6" id="Rectangle" width="1.4" x="1.4" y="4.2"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
@ -1 +1 @@
|
||||
Subproject commit 8beb712c5492dd77e81f46cb67752615f582ecf4
|
||||
Subproject commit 643df41d09d3a1d159adbb1f2cf3cefa26002bc8
|
||||
@ -18,3 +18,4 @@ export * from './lib/reports';
|
||||
export * from './lib/configuration';
|
||||
export * from './lib/signature';
|
||||
export * from './lib/legal-basis';
|
||||
export * from './lib/dossier-stats';
|
||||
|
||||
37
libs/red-domain/src/lib/dossier-stats/dossier-stats.model.ts
Normal file
37
libs/red-domain/src/lib/dossier-stats/dossier-stats.model.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { IDossierStats } from './dossier-stats';
|
||||
import { FileCountPerProcessingStatus, FileCountPerWorkflowStatus } from './types';
|
||||
import { isProcessingStatuses, ProcessingFileStatus } from '../files';
|
||||
|
||||
export class DossierStats implements IDossierStats {
|
||||
readonly dossierId: string;
|
||||
readonly fileCountPerProcessingStatus: FileCountPerProcessingStatus;
|
||||
readonly fileCountPerWorkflowStatus: FileCountPerWorkflowStatus;
|
||||
readonly hasHintsNoRedactionsFilePresent: boolean;
|
||||
readonly hasNoFlagsFilePresent: boolean;
|
||||
readonly hasRedactionsFilePresent: boolean;
|
||||
readonly hasSuggestionsFilePresent: boolean;
|
||||
readonly hasUpdatesFilePresent: boolean;
|
||||
readonly numberOfPages: number;
|
||||
readonly numberOfFiles: number;
|
||||
readonly numberOfProcessingFiles: number;
|
||||
|
||||
readonly hasFiles: boolean;
|
||||
|
||||
constructor(stats: IDossierStats) {
|
||||
this.dossierId = stats.dossierId;
|
||||
this.fileCountPerProcessingStatus = stats.fileCountPerProcessingStatus;
|
||||
this.fileCountPerWorkflowStatus = stats.fileCountPerWorkflowStatus;
|
||||
this.hasHintsNoRedactionsFilePresent = stats.hasHintsNoRedactionsFilePresent;
|
||||
this.hasNoFlagsFilePresent = stats.hasNoFlagsFilePresent;
|
||||
this.hasRedactionsFilePresent = stats.hasRedactionsFilePresent;
|
||||
this.hasSuggestionsFilePresent = stats.hasSuggestionsFilePresent;
|
||||
this.hasUpdatesFilePresent = stats.hasUpdatesFilePresent;
|
||||
this.numberOfPages = stats.numberOfPages;
|
||||
this.numberOfFiles = stats.numberOfFiles;
|
||||
this.numberOfProcessingFiles = Object.entries<number>(this.fileCountPerProcessingStatus)
|
||||
.filter(([key, _]) => isProcessingStatuses.includes(key as ProcessingFileStatus))
|
||||
.reduce((count, [_, value]) => count + value, 0);
|
||||
|
||||
this.hasFiles = this.numberOfFiles > 0;
|
||||
}
|
||||
}
|
||||
14
libs/red-domain/src/lib/dossier-stats/dossier-stats.ts
Normal file
14
libs/red-domain/src/lib/dossier-stats/dossier-stats.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { FileCountPerProcessingStatus, FileCountPerWorkflowStatus } from './types';
|
||||
|
||||
export interface IDossierStats {
|
||||
dossierId: string;
|
||||
fileCountPerProcessingStatus: FileCountPerProcessingStatus;
|
||||
fileCountPerWorkflowStatus: FileCountPerWorkflowStatus;
|
||||
hasHintsNoRedactionsFilePresent: boolean;
|
||||
hasNoFlagsFilePresent: boolean;
|
||||
hasRedactionsFilePresent: boolean;
|
||||
hasSuggestionsFilePresent: boolean;
|
||||
hasUpdatesFilePresent: boolean;
|
||||
numberOfPages: number;
|
||||
numberOfFiles: number;
|
||||
}
|
||||
3
libs/red-domain/src/lib/dossier-stats/index.ts
Normal file
3
libs/red-domain/src/lib/dossier-stats/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './dossier-stats';
|
||||
export * from './dossier-stats.model';
|
||||
export * from './types';
|
||||
4
libs/red-domain/src/lib/dossier-stats/types.ts
Normal file
4
libs/red-domain/src/lib/dossier-stats/types.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { ProcessingFileStatus, WorkflowFileStatus } from '../files';
|
||||
|
||||
export type FileCountPerWorkflowStatus = { [key in WorkflowFileStatus]?: number };
|
||||
export type FileCountPerProcessingStatus = { [key in ProcessingFileStatus]?: number };
|
||||
@ -1,4 +1,3 @@
|
||||
import { File } from '../files';
|
||||
import { IListable, List } from '@iqser/common-ui';
|
||||
import { IDossier } from './dossier';
|
||||
import { DossierStatus } from './types';
|
||||
@ -24,18 +23,7 @@ export class Dossier implements IDossier, IListable {
|
||||
readonly watermarkEnabled: boolean;
|
||||
readonly hasReviewers: boolean;
|
||||
|
||||
readonly reanalysisRequired = this.files.some(file => file.analysisRequired);
|
||||
readonly hasFiles = this.files.length > 0;
|
||||
readonly filesLength = this.files.length;
|
||||
|
||||
readonly totalNumberOfPages: number;
|
||||
readonly hintsOnly: boolean;
|
||||
readonly hasRedactions: boolean;
|
||||
readonly hasSuggestions: boolean;
|
||||
readonly hasNone: boolean;
|
||||
readonly hasPendingOrProcessing: boolean;
|
||||
|
||||
constructor(dossier: IDossier, readonly files: List<File> = [], public type?: IDictionary) {
|
||||
constructor(dossier: IDossier, public type?: IDictionary) {
|
||||
this.dossierId = dossier.dossierId;
|
||||
this.approverIds = dossier.approverIds;
|
||||
this.date = dossier.date;
|
||||
@ -53,27 +41,6 @@ export class Dossier implements IDossier, IListable {
|
||||
this.status = dossier.status;
|
||||
this.watermarkEnabled = dossier.watermarkEnabled;
|
||||
this.hasReviewers = !!this.memberIds && this.memberIds.length > 1;
|
||||
|
||||
let hintsOnly = false;
|
||||
let hasRedactions = false;
|
||||
let hasSuggestions = false;
|
||||
let totalNumberOfPages = 0;
|
||||
let hasPendingOrProcessing = false;
|
||||
|
||||
this.files.forEach(f => {
|
||||
hintsOnly = hintsOnly || f.hintsOnly;
|
||||
hasRedactions = hasRedactions || f.hasRedactions;
|
||||
hasSuggestions = hasSuggestions || f.hasSuggestions;
|
||||
totalNumberOfPages += f.numberOfPages ?? 0;
|
||||
hasPendingOrProcessing = hasPendingOrProcessing || f.isPending || f.isProcessing;
|
||||
});
|
||||
|
||||
this.hintsOnly = hintsOnly;
|
||||
this.hasRedactions = hasRedactions;
|
||||
this.hasSuggestions = hasSuggestions;
|
||||
this.totalNumberOfPages = totalNumberOfPages;
|
||||
this.hasPendingOrProcessing = hasPendingOrProcessing;
|
||||
this.hasNone = !this.hasSuggestions && !this.hasRedactions && !this.hintsOnly;
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
@ -88,10 +55,6 @@ export class Dossier implements IDossier, IListable {
|
||||
return this.dossierName;
|
||||
}
|
||||
|
||||
hasStatus(status: string): boolean {
|
||||
return !!this.files.find(f => f.status === status);
|
||||
}
|
||||
|
||||
hasMember(memberId: string): boolean {
|
||||
return !!this.memberIds && this.memberIds.indexOf(memberId) >= 0;
|
||||
}
|
||||
|
||||
@ -1,18 +1,10 @@
|
||||
import { IListable, List } from '@iqser/common-ui';
|
||||
import { Entity } from '@iqser/common-ui';
|
||||
import { StatusSorter } from '../shared';
|
||||
import { FileStatus, FileStatuses } from './types';
|
||||
import { isProcessingStatuses, ProcessingFileStatus, ProcessingFileStatuses, WorkflowFileStatus, WorkflowFileStatuses } from './types';
|
||||
import { IFile } from './file';
|
||||
import { FileAttributes, IFileAttributesConfig } from '../file-attributes';
|
||||
|
||||
const processingStatuses: List<FileStatus> = [
|
||||
FileStatuses.REPROCESS,
|
||||
FileStatuses.FULLREPROCESS,
|
||||
FileStatuses.OCR_PROCESSING,
|
||||
FileStatuses.INDEXING,
|
||||
FileStatuses.PROCESSING,
|
||||
] as const;
|
||||
|
||||
export class File implements IFile, IListable {
|
||||
export class File extends Entity<IFile> implements IFile {
|
||||
readonly added?: string;
|
||||
readonly allManualRedactionsApplied: boolean;
|
||||
readonly analysisDuration?: number;
|
||||
@ -40,10 +32,11 @@ export class File implements IFile, IListable {
|
||||
readonly numberOfAnalyses: number;
|
||||
readonly numberOfPages?: number;
|
||||
readonly rulesVersion?: number;
|
||||
readonly status: FileStatus;
|
||||
readonly uploader?: string;
|
||||
readonly excludedPages?: number[];
|
||||
readonly hasSuggestions: boolean;
|
||||
readonly processingStatus: ProcessingFileStatus;
|
||||
readonly workflowStatus: WorkflowFileStatus;
|
||||
|
||||
readonly primaryAttribute?: string;
|
||||
lastOpened = false;
|
||||
@ -60,10 +53,10 @@ export class File implements IFile, IListable {
|
||||
readonly isUnderApproval: boolean;
|
||||
readonly canBeApproved: boolean;
|
||||
readonly canBeOpened: boolean;
|
||||
readonly isWorkable: boolean;
|
||||
readonly canBeOCRed: boolean;
|
||||
|
||||
constructor(file: IFile, readonly reviewerName: string, fileAttributesConfig?: IFileAttributesConfig) {
|
||||
super(file);
|
||||
this.added = file.added;
|
||||
this.allManualRedactionsApplied = !!file.allManualRedactionsApplied;
|
||||
this.analysisDuration = file.analysisDuration;
|
||||
@ -89,30 +82,30 @@ export class File implements IFile, IListable {
|
||||
this.lastUploaded = file.lastUploaded;
|
||||
this.legalBasisVersion = file.legalBasisVersion;
|
||||
this.numberOfAnalyses = file.numberOfAnalyses;
|
||||
this.status = ['REPROCESS', 'FULLREPROCESS', 'INDEXING'].includes(file.status) ? FileStatuses.PROCESSING : file.status;
|
||||
this.isError = this.status === FileStatuses.ERROR;
|
||||
this.processingStatus = file.processingStatus;
|
||||
this.workflowStatus = file.workflowStatus;
|
||||
this.isError = this.processingStatus === ProcessingFileStatuses.ERROR;
|
||||
this.numberOfPages = this.isError ? 0 : file.numberOfPages ?? 0;
|
||||
this.rulesVersion = file.rulesVersion;
|
||||
this.uploader = file.uploader;
|
||||
this.excludedPages = file.excludedPages;
|
||||
this.hasSuggestions = !!file.hasSuggestions;
|
||||
|
||||
this.statusSort = StatusSorter[this.status];
|
||||
this.statusSort = StatusSorter[this.workflowStatus];
|
||||
if (this.lastUpdated && this.lastOCRTime) {
|
||||
this.cacheIdentifier = btoa((this.lastUploaded ?? '') + this.lastOCRTime);
|
||||
}
|
||||
this.hintsOnly = this.hasHints && !this.hasRedactions;
|
||||
this.hasNone = !this.hasRedactions && !this.hasHints && !this.hasSuggestions;
|
||||
this.isUnassigned = !this.currentReviewer;
|
||||
this.isProcessing = processingStatuses.includes(this.status);
|
||||
this.isApproved = this.status === FileStatuses.APPROVED;
|
||||
this.isPending = this.status === FileStatuses.UNPROCESSED;
|
||||
this.isUnderReview = this.status === FileStatuses.UNDER_REVIEW;
|
||||
this.isUnderApproval = this.status === FileStatuses.UNDER_APPROVAL;
|
||||
this.isPending = this.processingStatus === ProcessingFileStatuses.UNPROCESSED;
|
||||
this.isProcessing = isProcessingStatuses.includes(this.processingStatus);
|
||||
this.isApproved = this.workflowStatus === WorkflowFileStatuses.APPROVED;
|
||||
this.isUnassigned = this.workflowStatus === WorkflowFileStatuses.UNASSIGNED;
|
||||
this.isUnderReview = this.workflowStatus === WorkflowFileStatuses.UNDER_REVIEW;
|
||||
this.isUnderApproval = this.workflowStatus === WorkflowFileStatuses.UNDER_APPROVAL;
|
||||
this.canBeApproved = !this.analysisRequired && !this.hasSuggestions;
|
||||
this.canBeOpened = !this.isError && !this.isPending && this.numberOfAnalyses > 0;
|
||||
this.isWorkable = !this.isProcessing && this.canBeOpened;
|
||||
this.canBeOCRed = !this.excluded && !this.lastOCRTime && ['UNASSIGNED', 'UNDER_REVIEW', 'UNDER_APPROVAL'].includes(this.status);
|
||||
this.canBeOCRed = !this.excluded && !this.lastOCRTime && (this.isUnassigned || this.isUnderReview || this.isUnderApproval);
|
||||
|
||||
if (fileAttributesConfig) {
|
||||
const primary = fileAttributesConfig.fileAttributeConfigs?.find(c => c.primaryAttribute);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Object containing information on a specific file.
|
||||
*/
|
||||
import { FileStatus } from './types';
|
||||
import { ProcessingFileStatus, WorkflowFileStatus } from './types';
|
||||
import { FileAttributes } from '../file-attributes';
|
||||
|
||||
export interface IFile {
|
||||
@ -130,12 +130,12 @@ export interface IFile {
|
||||
* Shows if the file is soft deleted.
|
||||
*/
|
||||
readonly softDeleted?: string;
|
||||
/**
|
||||
* The status of the file with regard to its analysis an review processes.
|
||||
*/
|
||||
readonly status: FileStatus;
|
||||
/**
|
||||
* The ID of the user who uploaded the file.
|
||||
*/
|
||||
readonly uploader?: string;
|
||||
|
||||
readonly processingStatus: ProcessingFileStatus;
|
||||
|
||||
readonly workflowStatus: WorkflowFileStatus;
|
||||
}
|
||||
|
||||
@ -1,17 +1,32 @@
|
||||
export const FileStatuses = {
|
||||
import { List } from '@iqser/common-ui';
|
||||
|
||||
export const WorkflowFileStatuses = {
|
||||
APPROVED: 'APPROVED',
|
||||
DELETED: 'DELETED',
|
||||
ERROR: 'ERROR',
|
||||
EXCLUDED: 'EXCLUDED',
|
||||
FULLREPROCESS: 'FULLREPROCESS',
|
||||
INDEXING: 'INDEXING',
|
||||
OCR_PROCESSING: 'OCR_PROCESSING',
|
||||
PROCESSING: 'PROCESSING',
|
||||
REPROCESS: 'REPROCESS',
|
||||
UNASSIGNED: 'UNASSIGNED',
|
||||
UNDER_APPROVAL: 'UNDER_APPROVAL',
|
||||
UNDER_REVIEW: 'UNDER_REVIEW',
|
||||
} as const;
|
||||
|
||||
export type WorkflowFileStatus = keyof typeof WorkflowFileStatuses;
|
||||
|
||||
export const ProcessingFileStatuses = {
|
||||
DELETED: 'DELETED',
|
||||
ERROR: 'ERROR',
|
||||
FULLREPROCESS: 'FULLREPROCESS',
|
||||
INDEXING: 'INDEXING',
|
||||
OCR_PROCESSING: 'OCR_PROCESSING',
|
||||
PROCESSED: 'PROCESSED',
|
||||
PROCESSING: 'PROCESSING',
|
||||
REPROCESS: 'REPROCESS',
|
||||
UNPROCESSED: 'UNPROCESSED',
|
||||
} as const;
|
||||
|
||||
export type FileStatus = keyof typeof FileStatuses;
|
||||
export type ProcessingFileStatus = keyof typeof ProcessingFileStatuses;
|
||||
|
||||
export const isProcessingStatuses: List<ProcessingFileStatus> = [
|
||||
ProcessingFileStatuses.REPROCESS,
|
||||
ProcessingFileStatuses.FULLREPROCESS,
|
||||
ProcessingFileStatuses.OCR_PROCESSING,
|
||||
ProcessingFileStatuses.INDEXING,
|
||||
ProcessingFileStatuses.PROCESSING,
|
||||
] as const;
|
||||
|
||||
@ -1,32 +1,22 @@
|
||||
import { FileStatus } from '../../files';
|
||||
import { WorkflowFileStatus } from '../../files';
|
||||
|
||||
type StatusSorterItem = { key: FileStatus } | FileStatus | string;
|
||||
type Sorter = Record<FileStatus, number> & {
|
||||
type StatusSorterItem = { key: WorkflowFileStatus } | WorkflowFileStatus | string;
|
||||
type Sorter = Record<WorkflowFileStatus, number> & {
|
||||
byStatus: <T extends StatusSorterItem>(a: T, b: T) => number;
|
||||
};
|
||||
|
||||
export const StatusSorter: Sorter = {
|
||||
ERROR: 0,
|
||||
DELETED: 0,
|
||||
FULLREPROCESS: 0,
|
||||
EXCLUDED: 0,
|
||||
INDEXING: 0,
|
||||
UNPROCESSED: 1,
|
||||
REPROCESS: 5,
|
||||
PROCESSING: 5,
|
||||
OCR_PROCESSING: 7,
|
||||
|
||||
UNASSIGNED: 10,
|
||||
UNDER_REVIEW: 15,
|
||||
UNDER_APPROVAL: 20,
|
||||
APPROVED: 25,
|
||||
UNASSIGNED: 1,
|
||||
UNDER_REVIEW: 2,
|
||||
UNDER_APPROVAL: 3,
|
||||
APPROVED: 4,
|
||||
byStatus: (a: StatusSorterItem, b: StatusSorterItem): number => {
|
||||
if (typeof a !== typeof b) {
|
||||
throw TypeError('Used different types when calling StatusSorter.byStatus1');
|
||||
}
|
||||
|
||||
const x = typeof a === 'string' ? (a as FileStatus) : a.key;
|
||||
const y = typeof b === 'string' ? (b as FileStatus) : b.key;
|
||||
const x = typeof a === 'string' ? (a as WorkflowFileStatus) : a.key;
|
||||
const y = typeof b === 'string' ? (b as WorkflowFileStatus) : b.key;
|
||||
return StatusSorter[x] - StatusSorter[y];
|
||||
},
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user