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:
Dan Percic 2021-11-15 23:28:13 +01:00
commit d404d0bd41
98 changed files with 2278 additions and 1611 deletions

View File

@ -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

View File

@ -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

View File

@ -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),

View File

@ -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,
);
}
}

View File

@ -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>

View File

@ -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) {

View File

@ -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>

View File

@ -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 {

View File

@ -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>

View File

@ -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,

View File

@ -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"

View File

@ -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[]) {

View File

@ -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>

View File

@ -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],
});
}
}

View File

@ -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,

View File

@ -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>

View File

@ -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();
}

View File

@ -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,
],

View File

@ -1 +0,0 @@
<redaction-team-members-manager (save)="updateDossier.emit()" [dossier]="dossier"></redaction-team-members-manager>

View File

@ -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();
}
}

View File

@ -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,

View File

@ -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"

View File

@ -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() {

View File

@ -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>

View File

@ -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,
});
}

View File

@ -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

View File

@ -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 } });
}
}

View File

@ -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>

View File

@ -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;
}

View 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>

View File

@ -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);
}
}

View File

@ -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"

View File

@ -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;
}
}
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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>();
}

View File

@ -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();

View File

@ -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],

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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,

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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');
}
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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"

View File

@ -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);
}
}

View File

@ -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' }}

View File

@ -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;

View File

@ -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>

View File

@ -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) {}
}

View File

@ -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;
}
}
};
}

View File

@ -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],

View File

@ -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>

View File

@ -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);
}

View File

@ -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>

View File

@ -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();

View File

@ -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,

View File

@ -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([]);
}

View File

@ -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"

View File

@ -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;
}
}

View File

@ -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');
});
}
}

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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,

View File

@ -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],

View File

@ -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'),
};

View File

@ -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',

View File

@ -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();
}
}

View File

@ -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) => {

View File

@ -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();
}
}

View File

@ -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(),
);
}
}

View File

@ -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)),
);
}
}

View File

@ -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)))));
}

View File

@ -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,

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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",

View File

@ -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

View 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

View File

@ -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

View File

@ -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';

View 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;
}
}

View 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;
}

View File

@ -0,0 +1,3 @@
export * from './dossier-stats';
export * from './dossier-stats.model';
export * from './types';

View File

@ -0,0 +1,4 @@
import { ProcessingFileStatus, WorkflowFileStatus } from '../files';
export type FileCountPerWorkflowStatus = { [key in WorkflowFileStatus]?: number };
export type FileCountPerProcessingStatus = { [key in ProcessingFileStatus]?: number };

View File

@ -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;
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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;

View File

@ -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];
},
};