Trash with files & dossiers WIP
This commit is contained in:
parent
47ab9b4136
commit
7fa5f6b4f1
@ -1,14 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CanActivate } from '@angular/router';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { TrashDossiersService } from '@services/entity-services/trash-dossiers.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TrashDossiersGuard implements CanActivate {
|
||||
constructor(private readonly _trashDossiersService: TrashDossiersService) {}
|
||||
|
||||
async canActivate(): Promise<boolean> {
|
||||
await firstValueFrom(this._trashDossiersService.loadAll());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -22,7 +22,8 @@ import { DICTIONARY_TYPE, DOSSIER_TEMPLATE_ID } from '@utils/constants';
|
||||
import { DossierTemplateExistsGuard } from '@guards/dossier-template-exists.guard';
|
||||
import { DictionaryExistsGuard } from '@guards/dictionary-exists.guard';
|
||||
import { DossierStatesListingScreenComponent } from './screens/dossier-states-listing/dossier-states-listing-screen.component';
|
||||
import { TrashDossiersGuard } from '@guards/trash-dossiers.guard';
|
||||
import { DossiersGuard } from '../../guards/dossiers.guard';
|
||||
import { ACTIVE_DOSSIERS_SERVICE } from '../../tokens';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: 'dossier-templates', pathMatch: 'full' },
|
||||
@ -200,8 +201,9 @@ const routes: Routes = [
|
||||
component: TrashScreenComponent,
|
||||
canActivate: [CompositeRouteGuard],
|
||||
data: {
|
||||
routeGuards: [AuthGuard, RedRoleGuard, TrashDossiersGuard],
|
||||
routeGuards: [AuthGuard, RedRoleGuard, DossiersGuard],
|
||||
requiredRoles: ['RED_MANAGER'],
|
||||
dossiersService: ACTIVE_DOSSIERS_SERVICE,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@ -48,6 +48,7 @@ import { DossierStatesListingScreenComponent } from './screens/dossier-states-li
|
||||
import { AddEditDossierStateDialogComponent } from './dialogs/add-edit-dossier-state-dialog/add-edit-dossier-state-dialog.component';
|
||||
import { A11yModule } from '@angular/cdk/a11y';
|
||||
import { ConfirmDeleteDossierStateDialogComponent } from './dialogs/confirm-delete-dossier-state-dialog/confirm-delete-dossier-state-dialog.component';
|
||||
import { TrashTableItemComponent } from './screens/trash/trash-table-item/trash-table-item.component';
|
||||
|
||||
const dialogs = [
|
||||
AddEditDossierTemplateDialogComponent,
|
||||
@ -100,6 +101,7 @@ const components = [
|
||||
DossierStatesListingScreenComponent,
|
||||
AddEditDossierStateDialogComponent,
|
||||
ConfirmDeleteDossierStateDialogComponent,
|
||||
TrashTableItemComponent,
|
||||
],
|
||||
providers: [AdminDialogService, AuditService, DigitalSignatureService, LicenseReportService, RulesService, SmtpConfigService],
|
||||
imports: [
|
||||
|
||||
@ -9,7 +9,9 @@ import {
|
||||
} from '@iqser/common-ui';
|
||||
import { DossierState, IDossierState } from '@red/domain';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { firstValueFrom, map, Observable } from 'rxjs';
|
||||
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
|
||||
import { DossierStateService } from '@services/entity-services/dossier-state.service';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { AdminDialogService } from '../../services/admin-dialog.service';
|
||||
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@ -42,58 +42,5 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #tableItemTemplate let-entity="entity">
|
||||
<div *ngIf="cast(entity) as entity">
|
||||
<div class="cell filename">
|
||||
<div [matTooltip]="entity.dossierName" class="table-item-title heading" matTooltipPosition="above">
|
||||
{{ entity.dossierName }}
|
||||
</div>
|
||||
<div class="small-label stats-subtitle">
|
||||
<div>
|
||||
<mat-icon svgIcon="red:user"></mat-icon>
|
||||
{{ entity.memberIds.length }}
|
||||
</div>
|
||||
<div>
|
||||
<mat-icon svgIcon="red:calendar"></mat-icon>
|
||||
{{ entity.date | date: 'mediumDate' }}
|
||||
</div>
|
||||
<div *ngIf="entity.dueDate">
|
||||
<mat-icon svgIcon="red:lightning"></mat-icon>
|
||||
{{ entity.dueDate | date: 'mediumDate' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cell user-column">
|
||||
<redaction-initials-avatar [user]="entity.ownerId" [withName]="true"></redaction-initials-avatar>
|
||||
</div>
|
||||
|
||||
<div class="cell">
|
||||
<span class="small-label">
|
||||
{{ entity.softDeletedTime | date: 'exactDate' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="cell">
|
||||
<div class="small-label">
|
||||
{{ entity.restoreDate | date: 'timeFromNow' }}
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<iqser-circle-button
|
||||
(action)="restore([entity])"
|
||||
*ngIf="entity.canRestore"
|
||||
[tooltip]="'trash.action.restore' | translate"
|
||||
[type]="circleButtonTypes.dark"
|
||||
icon="red:put-back"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="hardDelete([entity])"
|
||||
*ngIf="entity.canHardDelete"
|
||||
[tooltip]="'trash.action.delete' | translate"
|
||||
[type]="circleButtonTypes.dark"
|
||||
icon="iqser:trash"
|
||||
></iqser-circle-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<redaction-trash-table-item (hardDelete)="hardDelete($event)" (restore)="restore($event)" [item]="entity"></redaction-trash-table-item>
|
||||
</ng-template>
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component, forwardRef, Injector } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, forwardRef, Injector, OnInit } from '@angular/core';
|
||||
import {
|
||||
CircleButtonTypes,
|
||||
ConfirmationDialogInput,
|
||||
DefaultListingServicesTmp,
|
||||
EntitiesService,
|
||||
DefaultListingServices,
|
||||
ListingComponent,
|
||||
LoadingService,
|
||||
SortingOrders,
|
||||
@ -12,49 +11,49 @@ import {
|
||||
} from '@iqser/common-ui';
|
||||
import { AdminDialogService } from '../../services/admin-dialog.service';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { Observable } from 'rxjs';
|
||||
import { firstValueFrom, Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||
import { RouterHistoryService } from '@services/router-history.service';
|
||||
import { TrashDossier } from '@red/domain';
|
||||
import { TrashDossiersService } from '@services/entity-services/trash-dossiers.service';
|
||||
import { TrashItem } from '@red/domain';
|
||||
import { TrashService } from '@services/entity-services/trash.service';
|
||||
import { FilesService } from '@services/entity-services/files.service';
|
||||
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
|
||||
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
|
||||
|
||||
@Component({
|
||||
templateUrl: './trash-screen.component.html',
|
||||
styleUrls: ['./trash-screen.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [
|
||||
...DefaultListingServicesTmp,
|
||||
{
|
||||
provide: EntitiesService,
|
||||
useExisting: TrashDossiersService,
|
||||
},
|
||||
{ provide: ListingComponent, useExisting: forwardRef(() => TrashScreenComponent) },
|
||||
],
|
||||
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => TrashScreenComponent) }],
|
||||
})
|
||||
export class TrashScreenComponent extends ListingComponent<TrashDossier> {
|
||||
export class TrashScreenComponent extends ListingComponent<TrashItem> implements OnInit {
|
||||
readonly circleButtonTypes = CircleButtonTypes;
|
||||
readonly tableHeaderLabel = _('trash.table-header.title');
|
||||
readonly canRestoreSelected$ = this._canRestoreSelected$;
|
||||
readonly canHardDeleteSelected$ = this._canHardDeleteSelected$;
|
||||
readonly tableColumnConfigs: TableColumnConfig<TrashDossier>[] = [
|
||||
{ label: _('trash.table-col-names.name'), sortByKey: 'searchKey' },
|
||||
readonly tableColumnConfigs: TableColumnConfig<TrashItem>[] = [
|
||||
{ label: _('trash.table-col-names.name') },
|
||||
{ label: _('trash.table-col-names.owner'), class: 'user-column' },
|
||||
{ label: _('trash.table-col-names.deleted-on'), sortByKey: 'softDeletedTime' },
|
||||
{ label: _('trash.table-col-names.time-to-restore'), sortByKey: 'softDeletedTime' },
|
||||
{ label: _('trash.table-col-names.dossier') },
|
||||
{ label: _('trash.table-col-names.deleted-on') },
|
||||
{ label: _('trash.table-col-names.time-to-restore') },
|
||||
];
|
||||
|
||||
constructor(
|
||||
protected readonly _injector: Injector,
|
||||
private readonly _loadingService: LoadingService,
|
||||
private readonly _trashDossiersService: TrashDossiersService,
|
||||
private readonly _trashService: TrashService,
|
||||
private readonly _activeDossiersService: ActiveDossiersService,
|
||||
private readonly _filesService: FilesService,
|
||||
readonly routerHistoryService: RouterHistoryService,
|
||||
private readonly _adminDialogService: AdminDialogService,
|
||||
private readonly _dossierTemplatesService: DossierTemplatesService,
|
||||
) {
|
||||
super(_injector);
|
||||
|
||||
this.sortingService.setSortingOption({
|
||||
column: 'softDeletedTime',
|
||||
order: SortingOrders.desc,
|
||||
column: 'type',
|
||||
order: SortingOrders.asc,
|
||||
});
|
||||
}
|
||||
|
||||
@ -72,26 +71,38 @@ export class TrashScreenComponent extends ListingComponent<TrashDossier> {
|
||||
);
|
||||
}
|
||||
|
||||
disabledFn = (dossier: TrashDossier) => !dossier.canRestore;
|
||||
async ngOnInit(): Promise<void> {
|
||||
this._loadingService.start();
|
||||
await firstValueFrom(this._dossierTemplatesService.loadAll());
|
||||
const entities: TrashItem[] = await firstValueFrom(this._trashService.all());
|
||||
this.entitiesService.setEntities(entities);
|
||||
this._loadingService.stop();
|
||||
}
|
||||
|
||||
hardDelete(dossiers = this.listingService.selected): void {
|
||||
disabledFn = (dossier: TrashItem) => !dossier.canRestore;
|
||||
|
||||
hardDelete(items = this.listingService.selected): void {
|
||||
const data = new ConfirmationDialogInput({
|
||||
title: _('confirmation-dialog.delete-dossier.title'),
|
||||
title: _('confirmation-dialog.delete-items.title'),
|
||||
titleColor: TitleColors.WARN,
|
||||
question: _('confirmation-dialog.delete-dossier.question'),
|
||||
question: _('confirmation-dialog.delete-items.question'),
|
||||
translateParams: {
|
||||
dossierName: dossiers[0].dossierName,
|
||||
dossiersCount: dossiers.length,
|
||||
name: items[0].name,
|
||||
itemsCount: items.length,
|
||||
},
|
||||
});
|
||||
this._adminDialogService.openDialog('confirm', null, data, () => {
|
||||
const dossierIds: string[] = dossiers.map(d => d.id);
|
||||
this._loadingService.loadWhile(this._trashDossiersService.hardDelete(dossierIds));
|
||||
this._adminDialogService.openDialog('confirm', null, data, async () => {
|
||||
this._loadingService.start();
|
||||
await firstValueFrom(this._trashService.hardDelete(items));
|
||||
items.forEach(item => this.entitiesService.remove(item.id));
|
||||
this._loadingService.stop();
|
||||
});
|
||||
}
|
||||
|
||||
restore(dossiers = this.listingService.selected): void {
|
||||
const dossierIds: string[] = dossiers.map(d => d.id);
|
||||
this._loadingService.loadWhile(this._trashDossiersService.restore(dossierIds));
|
||||
async restore(items = this.listingService.selected): Promise<void> {
|
||||
this._loadingService.start();
|
||||
await firstValueFrom(this._trashService.restore(items));
|
||||
items.forEach(item => this.entitiesService.remove(item.id));
|
||||
this._loadingService.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
<div class="cell filename">
|
||||
<mat-icon [class.low-opacity]="item.isFile" [svgIcon]="item.icon" class="mr-16"></mat-icon>
|
||||
<div>
|
||||
<redaction-file-name-column
|
||||
*ngIf="item.isFile && file(item) as file"
|
||||
[dossierTemplateId]="file.dossierTemplateId"
|
||||
[file]="file"
|
||||
></redaction-file-name-column>
|
||||
|
||||
<ng-container *ngIf="item.isDossier && dossier(item) as dossier">
|
||||
<div [matTooltip]="dossier.name" class="table-item-title heading" matTooltipPosition="above">
|
||||
{{ dossier.name }}
|
||||
</div>
|
||||
<div class="small-label stats-subtitle">
|
||||
<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' }}
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cell user-column">
|
||||
<redaction-initials-avatar [user]="item.ownerId" [withName]="true"></redaction-initials-avatar>
|
||||
</div>
|
||||
|
||||
<div class="cell">
|
||||
<span *ngIf="item.isFile" [routerLink]="fileDossier.routerLink" class="small-label link-action">
|
||||
{{ fileDossier.dossierName }}
|
||||
</span>
|
||||
|
||||
<span *ngIf="item.isDossier" class="small-label">-</span>
|
||||
</div>
|
||||
|
||||
<div class="cell">
|
||||
<span class="small-label">
|
||||
{{ item.softDeletedTime | date: 'exactDate' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="cell">
|
||||
<div class="small-label">
|
||||
{{ item.restoreDate | date: 'timeFromNow' }}
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<iqser-circle-button
|
||||
(action)="restore.emit([item])"
|
||||
*ngIf="item.canRestore"
|
||||
[tooltip]="'trash.action.restore' | translate"
|
||||
[type]="circleButtonTypes.dark"
|
||||
icon="red:put-back"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="hardDelete.emit([item])"
|
||||
*ngIf="item.canHardDelete"
|
||||
[tooltip]="'trash.action.delete' | translate"
|
||||
[type]="circleButtonTypes.dark"
|
||||
icon="iqser:trash"
|
||||
></iqser-circle-button>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,13 @@
|
||||
.filename {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
> mat-icon {
|
||||
min-width: 24px;
|
||||
|
||||
&.low-opacity {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
|
||||
import { Dossier, TrashDossier, TrashFile, TrashItem } from '@red/domain';
|
||||
import { CircleButtonTypes } from '@iqser/common-ui';
|
||||
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-trash-table-item [item]',
|
||||
templateUrl: './trash-table-item.component.html',
|
||||
styleUrls: ['./trash-table-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TrashTableItemComponent implements OnChanges {
|
||||
readonly circleButtonTypes = CircleButtonTypes;
|
||||
|
||||
@Input() item: TrashItem;
|
||||
@Output() restore = new EventEmitter<TrashItem[]>();
|
||||
@Output() hardDelete = new EventEmitter<TrashItem[]>();
|
||||
fileDossier: Dossier;
|
||||
|
||||
constructor(private readonly _activeDossiersService: ActiveDossiersService) {}
|
||||
|
||||
file(item: TrashItem): TrashFile {
|
||||
return item as TrashFile;
|
||||
}
|
||||
|
||||
dossier(item: TrashItem): TrashDossier {
|
||||
return item as TrashDossier;
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.fileDossier = this._activeDossiersService.find(this.dossier(this.item).dossierId);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
<div class="cell">
|
||||
<redaction-dossiers-listing-dossier-name [dossierStats]="stats$ | async" [dossier]="dossier"></redaction-dossiers-listing-dossier-name>
|
||||
<redaction-dossier-name-column [dossierStats]="stats$ | async" [dossier]="dossier"></redaction-dossier-name-column>
|
||||
</div>
|
||||
|
||||
<div class="cell small-label">{{ dossier.archivedTime | date: 'd MMM. yyyy' }}</div>
|
||||
|
||||
@ -20,33 +20,34 @@
|
||||
<mat-icon svgIcon="iqser:pages"></mat-icon>
|
||||
<span>{{ 'dossier-overview.dossier-details.stats.analysed-pages' | translate: { count: stats.numberOfPages | number } }}</span>
|
||||
</div>
|
||||
|
||||
<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>{{ dossierTemplateName }} </span>
|
||||
</div>
|
||||
|
||||
<div (click)="openEditDossierDialog('dossierDictionary')" class="link-property" iqserHelpMode="open_dictionary">
|
||||
<mat-icon svgIcon="red:dictionary"></mat-icon>
|
||||
<span>{{ 'dossier-overview.dossier-details.dictionary' | translate }} </span>
|
||||
</div>
|
||||
|
||||
<!--TODO: Navigate to trash with filter on click?-->
|
||||
<div>
|
||||
<mat-icon svgIcon="iqser:trash"></mat-icon>
|
||||
<span>{{ 'dossier-overview.dossier-details.stats.deleted' | translate: { count: stats.numberOfSoftDeletedFiles } }}</span>
|
||||
</div>
|
||||
</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>{{ dossierTemplateName }} </span>
|
||||
</div>
|
||||
|
||||
<div (click)="openEditDossierDialog('dossierDictionary')" class="link-property" iqserHelpMode="open_dictionary">
|
||||
<mat-icon svgIcon="red:dictionary"></mat-icon>
|
||||
<span>{{ 'dossier-overview.dossier-details.dictionary' | translate }} </span>
|
||||
</div>
|
||||
|
||||
<div (click)="openEditDossierDialog('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"
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
SortingService,
|
||||
Toaster,
|
||||
} from '@iqser/common-ui';
|
||||
import { Dossier, File, IFile } from '@red/domain';
|
||||
import { Dossier, File } from '@red/domain';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { ReanalysisService } from '@services/reanalysis.service';
|
||||
@ -69,7 +69,7 @@ export class DossierOverviewScreenHeaderComponent implements OnInit {
|
||||
const sortedEntities$ = this.listingService.displayed$.pipe(map(entities => this.sortingService.defaultSort(entities)));
|
||||
sortedEntities$.pipe(take(1)).subscribe(entities => {
|
||||
const fileName = this.dossier.dossierName + '.export.csv';
|
||||
const mapper = (file?: IFile) => ({
|
||||
const mapper = (file?: File) => ({
|
||||
...file,
|
||||
assignee: this._userService.getNameForId(file.assignee) || '-',
|
||||
primaryAttribute: this._primaryFileAttributeService.getPrimaryFileAttributeValue(file, this.dossier.dossierTemplateId),
|
||||
|
||||
@ -11,11 +11,9 @@ import { DossierDetailsStatsComponent } from './components/dossier-details-stats
|
||||
import { TableItemComponent } from './components/table-item/table-item.component';
|
||||
import { SharedDossiersModule } from '../dossier/shared/shared-dossiers.module';
|
||||
import { FileWorkloadComponent } from './components/table-item/file-workload/file-workload.component';
|
||||
import { FileStatsComponent } from './components/file-stats/file-stats.component';
|
||||
import { WorkflowItemComponent } from './components/workflow-item/workflow-item.component';
|
||||
import { DossierOverviewScreenHeaderComponent } from './components/screen-header/dossier-overview-screen-header.component';
|
||||
import { ViewModeSelectionComponent } from './components/view-mode-selection/view-mode-selection.component';
|
||||
import { FileNameColumnComponent } from './components/table-item/file-name-column/file-name-column.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@ -36,11 +34,9 @@ const routes: Routes = [
|
||||
DossierDetailsStatsComponent,
|
||||
FileWorkloadComponent,
|
||||
TableItemComponent,
|
||||
FileStatsComponent,
|
||||
WorkflowItemComponent,
|
||||
DossierOverviewScreenHeaderComponent,
|
||||
ViewModeSelectionComponent,
|
||||
FileNameColumnComponent,
|
||||
],
|
||||
imports: [RouterModule.forChild(routes), CommonModule, SharedModule, SharedDossiersModule, IqserIconsModule, TranslateModule],
|
||||
})
|
||||
|
||||
@ -1,94 +0,0 @@
|
||||
<iqser-table
|
||||
[bulkActions]="bulkActions"
|
||||
[headerTemplate]="headerTemplate"
|
||||
[itemSize]="50"
|
||||
[noDataText]="'edit-dossier-dialog.deleted-documents.no-data.title' | translate"
|
||||
[selectionEnabled]="true"
|
||||
[tableColumnConfigs]="tableColumnConfigs"
|
||||
[tableItemClasses]="{ disabled: disabledFn }"
|
||||
noDataIcon="iqser:document"
|
||||
></iqser-table>
|
||||
|
||||
<ng-template #headerTemplate>
|
||||
<div
|
||||
[translateParams]="{ hours: deleteRetentionHours }"
|
||||
[translate]="'edit-dossier-dialog.deleted-documents.instructions'"
|
||||
class="instructions"
|
||||
></div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #bulkActions>
|
||||
<iqser-circle-button
|
||||
(action)="restore()"
|
||||
*ngIf="canRestoreSelected$ | async"
|
||||
[tooltip]="'edit-dossier-dialog.deleted-documents.bulk.restore' | translate"
|
||||
[type]="circleButtonTypes.dark"
|
||||
icon="red:put-back"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="hardDelete()"
|
||||
*ngIf="canDeleteSelected$ | async"
|
||||
[tooltip]="'edit-dossier-dialog.deleted-documents.bulk.delete' | translate"
|
||||
[type]="circleButtonTypes.dark"
|
||||
icon="iqser:trash"
|
||||
></iqser-circle-button>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #tableItemTemplate let-entity="entity">
|
||||
<div *ngIf="cast(entity) as file">
|
||||
<div class="cell filename">
|
||||
<span>{{ file.filename }}</span>
|
||||
</div>
|
||||
|
||||
<div class="cell stats-subtitle">
|
||||
<div class="small-label">
|
||||
<mat-icon svgIcon="iqser:pages"></mat-icon>
|
||||
{{ file.numberOfPages }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cell user-column">
|
||||
<redaction-initials-avatar [user]="file.assignee" [withName]="true"></redaction-initials-avatar>
|
||||
</div>
|
||||
|
||||
<div class="cell">
|
||||
<iqser-status-bar
|
||||
[configs]="[
|
||||
{
|
||||
color: file.workflowStatus,
|
||||
label: fileStatusTranslations[file.workflowStatus] | translate,
|
||||
length: 1,
|
||||
cssClass: 'all-caps-label'
|
||||
}
|
||||
]"
|
||||
[small]="true"
|
||||
></iqser-status-bar>
|
||||
</div>
|
||||
|
||||
<div class="cell">
|
||||
<span class="small-label">{{ file.softDeleted | date: 'exactDate' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="cell">
|
||||
<div class="small-label">{{ file.restoreDate | date: 'timeFromNow' }}</div>
|
||||
<div class="action-buttons">
|
||||
<iqser-circle-button
|
||||
(action)="restore([file])"
|
||||
*ngIf="file.canRestore"
|
||||
[tooltip]="'edit-dossier-dialog.deleted-documents.action.restore' | translate"
|
||||
[type]="circleButtonTypes.dark"
|
||||
icon="red:put-back"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="hardDelete([file])"
|
||||
*ngIf="file.canHardDelete"
|
||||
[tooltip]="'edit-dossier-dialog.deleted-documents.action.delete' | translate"
|
||||
[type]="circleButtonTypes.dark"
|
||||
icon="iqser:trash"
|
||||
></iqser-circle-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
@ -1,16 +0,0 @@
|
||||
@use 'variables';
|
||||
@use 'common-mixins';
|
||||
|
||||
.instructions {
|
||||
color: variables.$grey-7;
|
||||
flex: 1;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
:host ::ng-deep iqser-table cdk-virtual-scroll-viewport {
|
||||
height: calc(100% - 81px) !important;
|
||||
}
|
||||
|
||||
.cell.filename span {
|
||||
@include common-mixins.line-clamp(1);
|
||||
}
|
||||
@ -1,181 +0,0 @@
|
||||
import { Component, forwardRef, Injector, Input, OnInit } from '@angular/core';
|
||||
import { EditDossierSaveResult, EditDossierSectionInterface } from '../edit-dossier-section.interface';
|
||||
import { Dossier, File, IFile } from '@red/domain';
|
||||
import {
|
||||
CircleButtonTypes,
|
||||
ConfirmationDialogInput,
|
||||
DefaultListingServices,
|
||||
getLeftDateTime,
|
||||
IListable,
|
||||
IRouterPath,
|
||||
ListingComponent,
|
||||
LoadingService,
|
||||
SortingOrders,
|
||||
TableColumnConfig,
|
||||
TitleColors,
|
||||
} from '@iqser/common-ui';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import * as moment from 'moment';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { firstValueFrom, Observable, of } from 'rxjs';
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||
import { DossiersDialogService } from '../../../services/dossiers-dialog.service';
|
||||
import { FilesService } from '@services/entity-services/files.service';
|
||||
import { FileManagementService } from '@services/entity-services/file-management.service';
|
||||
import { workflowFileStatusTranslations } from '../../../../../translations/file-status-translations';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { UserService } from '@services/user.service';
|
||||
|
||||
interface FileListItem extends IFile, IListable, IRouterPath {
|
||||
readonly canHardDelete: boolean;
|
||||
readonly canRestore: boolean;
|
||||
readonly restoreDate: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-edit-dossier-deleted-documents',
|
||||
templateUrl: './edit-dossier-deleted-documents.component.html',
|
||||
styleUrls: ['./edit-dossier-deleted-documents.component.scss'],
|
||||
providers: [
|
||||
...DefaultListingServices,
|
||||
{ provide: ListingComponent, useExisting: forwardRef(() => EditDossierDeletedDocumentsComponent) },
|
||||
],
|
||||
})
|
||||
export class EditDossierDeletedDocumentsComponent extends ListingComponent<FileListItem> implements EditDossierSectionInterface, OnInit {
|
||||
readonly fileStatusTranslations = workflowFileStatusTranslations;
|
||||
@Input() dossier: Dossier;
|
||||
readonly changed = false;
|
||||
readonly valid = false;
|
||||
readonly canRestoreSelected$ = this._canRestoreSelected$;
|
||||
readonly canDeleteSelected$ = this._canDeleteSelected$;
|
||||
disabled: boolean;
|
||||
readonly tableColumnConfigs: TableColumnConfig<FileListItem>[] = [
|
||||
{ label: _('edit-dossier-dialog.deleted-documents.table-col-names.name'), width: '3fr' },
|
||||
{ label: _('edit-dossier-dialog.deleted-documents.table-col-names.pages') },
|
||||
{ label: _('edit-dossier-dialog.deleted-documents.table-col-names.assignee'), class: 'user-column' },
|
||||
{ label: _('edit-dossier-dialog.deleted-documents.table-col-names.status') },
|
||||
{ label: _('edit-dossier-dialog.deleted-documents.table-col-names.deleted-on'), sortByKey: 'softDeleted', width: '2fr' },
|
||||
{ label: _('edit-dossier-dialog.deleted-documents.table-col-names.time-to-restore'), sortByKey: 'softDeleted', width: '2fr' },
|
||||
];
|
||||
readonly tableHeaderLabel = _('edit-dossier-dialog.deleted-documents.table-header.label');
|
||||
readonly circleButtonTypes = CircleButtonTypes;
|
||||
readonly deleteRetentionHours = this._configService.values.DELETE_RETENTION_HOURS;
|
||||
|
||||
constructor(
|
||||
protected readonly _injector: Injector,
|
||||
private readonly _fileManagementService: FileManagementService,
|
||||
private readonly _filesService: FilesService,
|
||||
private readonly _loadingService: LoadingService,
|
||||
private readonly _configService: ConfigService,
|
||||
private readonly _dialogService: DossiersDialogService,
|
||||
private readonly _permissionsService: PermissionsService,
|
||||
private readonly _userService: UserService,
|
||||
) {
|
||||
super(_injector);
|
||||
}
|
||||
|
||||
private get _canRestoreSelected$(): Observable<boolean> {
|
||||
return this.listingService.selectedEntities$.pipe(
|
||||
map(entities => entities.length && !entities.find(file => !file.canRestore)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
}
|
||||
|
||||
private get _canDeleteSelected$(): Observable<boolean> {
|
||||
return this.listingService.selectedEntities$.pipe(
|
||||
map(entities => entities.length && !entities.find(file => !file.canHardDelete)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
}
|
||||
|
||||
hardDelete(files = this.listingService.selected) {
|
||||
const data = new ConfirmationDialogInput({
|
||||
title: _('confirmation-dialog.permanently-delete-file.title'),
|
||||
titleColor: TitleColors.WARN,
|
||||
question: _('confirmation-dialog.permanently-delete-file.question'),
|
||||
confirmationText: _('confirmation-dialog.permanently-delete-file.confirmation-text'),
|
||||
requireInput: true,
|
||||
denyText: _('confirmation-dialog.permanently-delete-file.deny-text'),
|
||||
translateParams: {
|
||||
fileName: files[0].filename,
|
||||
filesCount: files.length,
|
||||
},
|
||||
});
|
||||
this._dialogService.openDialog('confirm', null, data, () => {
|
||||
this._loadingService.loadWhile(this._hardDelete(files));
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this._loadingService.start();
|
||||
const files = await firstValueFrom(this._filesService.getDeletedFilesFor(this.dossier.id));
|
||||
this.entitiesService.setEntities(this._toListItems(files));
|
||||
this.sortingService.setSortingOption({
|
||||
column: 'softDeleted',
|
||||
order: SortingOrders.desc,
|
||||
});
|
||||
this._loadingService.stop();
|
||||
}
|
||||
|
||||
revert() {}
|
||||
|
||||
save(): EditDossierSaveResult {
|
||||
return firstValueFrom(of({ success: true }));
|
||||
}
|
||||
|
||||
restore(files = this.listingService.selected) {
|
||||
this._loadingService.loadWhile(this._restore(files));
|
||||
}
|
||||
|
||||
disabledFn = (file: FileListItem) => !file.canRestore;
|
||||
|
||||
private async _restore(files: FileListItem[]): Promise<void> {
|
||||
const fileIds = files.map(f => f.id);
|
||||
await firstValueFrom(this._fileManagementService.restore(files, this.dossier.id));
|
||||
this._removeFromList(fileIds);
|
||||
}
|
||||
|
||||
private async _hardDelete(files: FileListItem[]) {
|
||||
const fileIds = files.map(f => f.id);
|
||||
await firstValueFrom(this._fileManagementService.hardDelete(this.dossier.id, fileIds));
|
||||
this._removeFromList(fileIds);
|
||||
}
|
||||
|
||||
private _removeFromList(ids: string[]): void {
|
||||
const entities = this.entitiesService.all.filter(e => !ids.includes(e.fileId));
|
||||
this.entitiesService.setEntities(entities);
|
||||
}
|
||||
|
||||
private _toListItems(files: IFile[]): FileListItem[] {
|
||||
return files.map(file => this._toListItem(file));
|
||||
}
|
||||
|
||||
private _toListItem(_file: IFile): FileListItem {
|
||||
const file = new File(_file, this._userService.getNameForId(_file.assignee), this.dossier.routerPath);
|
||||
const restoreDate = this._getRestoreDate(_file.softDeleted);
|
||||
return {
|
||||
id: file.fileId,
|
||||
...file,
|
||||
restoreDate,
|
||||
searchKey: file.filename,
|
||||
canRestore: this._canRestore(file, restoreDate),
|
||||
canHardDelete: this._canPerformActions(file),
|
||||
};
|
||||
}
|
||||
|
||||
private _canPerformActions(file: File): boolean {
|
||||
return (
|
||||
this._permissionsService.canHardDeleteOrRestore(this.dossier) &&
|
||||
(this._userService.currentUser.isManager || this._permissionsService.canDeleteFile(file))
|
||||
);
|
||||
}
|
||||
|
||||
private _canRestore(file: File, restoreDate: string): boolean {
|
||||
const { daysLeft, hoursLeft, minutesLeft } = getLeftDateTime(restoreDate);
|
||||
return this._canPerformActions(file) && daysLeft + hoursLeft + minutesLeft > 0;
|
||||
}
|
||||
|
||||
private _getRestoreDate(softDeletedTime: string): string {
|
||||
return moment(softDeletedTime).add(this.deleteRetentionHours, 'hours').toISOString();
|
||||
}
|
||||
}
|
||||
@ -43,11 +43,6 @@
|
||||
*ngIf="activeNav === 'dossierAttributes'"
|
||||
[dossier]="dossier"
|
||||
></redaction-edit-dossier-attributes>
|
||||
|
||||
<redaction-edit-dossier-deleted-documents
|
||||
*ngIf="activeNav === 'deletedDocuments'"
|
||||
[dossier]="dossier"
|
||||
></redaction-edit-dossier-deleted-documents>
|
||||
</div>
|
||||
|
||||
<div *ngIf="showActionButtons" class="dialog-actions">
|
||||
|
||||
@ -9,7 +9,6 @@ import { EditDossierDictionaryComponent } from './dictionary/edit-dossier-dictio
|
||||
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 { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { EditDossierTeamComponent } from './edit-dossier-team/edit-dossier-team.component';
|
||||
@ -18,7 +17,7 @@ import { UserService } from '@services/user.service';
|
||||
import { DossiersService } from '@services/dossiers/dossiers.service';
|
||||
import { dossiersServiceProvider } from '@services/entity-services/dossiers.service.provider';
|
||||
|
||||
type Section = 'dossierInfo' | 'downloadPackage' | 'dossierDictionary' | 'members' | 'dossierAttributes' | 'deletedDocuments';
|
||||
type Section = 'dossierInfo' | 'downloadPackage' | 'dossierDictionary' | 'members' | 'dossierAttributes';
|
||||
|
||||
interface NavItem {
|
||||
key: Section;
|
||||
@ -43,7 +42,6 @@ export class EditDossierDialogComponent extends BaseDialogComponent implements A
|
||||
@ViewChild(EditDossierDictionaryComponent) dictionaryComponent: EditDossierDictionaryComponent;
|
||||
@ViewChild(EditDossierTeamComponent) membersComponent: EditDossierTeamComponent;
|
||||
@ViewChild(EditDossierAttributesComponent) attributesComponent: EditDossierAttributesComponent;
|
||||
@ViewChild(EditDossierDeletedDocumentsComponent) deletedDocumentsComponent: EditDossierDeletedDocumentsComponent;
|
||||
|
||||
private _dossier: Dossier;
|
||||
|
||||
@ -83,22 +81,21 @@ export class EditDossierDialogComponent extends BaseDialogComponent implements A
|
||||
dossierDictionary: this.dictionaryComponent,
|
||||
members: this.membersComponent,
|
||||
dossierAttributes: this.attributesComponent,
|
||||
deletedDocuments: this.deletedDocumentsComponent,
|
||||
}[this.activeNav];
|
||||
}
|
||||
|
||||
get noPaddingTab(): boolean {
|
||||
return ['dossierAttributes', 'deletedDocuments'].includes(this.activeNav);
|
||||
return ['dossierAttributes'].includes(this.activeNav);
|
||||
}
|
||||
|
||||
get showHeading(): boolean {
|
||||
return !['dossierAttributes', 'dossierDictionary', 'deletedDocuments'].includes(this.activeNav);
|
||||
return !['dossierAttributes', 'dossierDictionary'].includes(this.activeNav);
|
||||
}
|
||||
|
||||
get showActionButtons(): boolean {
|
||||
return (
|
||||
(['members'].includes(this.activeNav) && this._userService.currentUser.isManager) ||
|
||||
(!['deletedDocuments'].includes(this.activeNav) && this._permissionsService.canEditDossier(this._dossier))
|
||||
this._permissionsService.canEditDossier(this._dossier)
|
||||
);
|
||||
}
|
||||
|
||||
@ -184,10 +181,6 @@ export class EditDossierDialogComponent extends BaseDialogComponent implements A
|
||||
title: _('edit-dossier-dialog.nav-items.dossier-attributes'),
|
||||
readonly: !this._permissionsService.canEditDossierAttributes(this._dossier),
|
||||
},
|
||||
{
|
||||
key: 'deletedDocuments',
|
||||
sideNavTitle: _('edit-dossier-dialog.nav-items.deleted-documents'),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ import { firstValueFrom } from 'rxjs';
|
||||
import { DOSSIER_TEMPLATE_ID } from '@utils/constants';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { DossiersService } from '@services/dossiers/dossiers.service';
|
||||
import { TrashDossiersService } from '@services/entity-services/trash-dossiers.service';
|
||||
import { TrashService } from '@services/entity-services/trash.service';
|
||||
import { ArchivedDossiersService } from '@services/dossiers/archived-dossiers.service';
|
||||
import { DossierStatesMapService } from '@services/entity-services/dossier-states-map.service';
|
||||
|
||||
@ -42,7 +42,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
|
||||
private readonly _dossierStatesMapService: DossierStatesMapService,
|
||||
private readonly _dossierTemplatesService: DossierTemplatesService,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
private readonly _trashDossiersService: TrashDossiersService,
|
||||
private readonly _trashService: TrashService,
|
||||
private readonly _dossierStatsService: DossierStatsService,
|
||||
private readonly _formBuilder: FormBuilder,
|
||||
private readonly _dialogService: DossiersDialogService,
|
||||
@ -137,7 +137,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
|
||||
});
|
||||
this._dialogService.openDialog('confirm', null, data, async () => {
|
||||
this._loadingService.start();
|
||||
await firstValueFrom(this._trashDossiersService.delete(this.dossier));
|
||||
await firstValueFrom(this._trashService.deleteDossier(this.dossier));
|
||||
this._editDossierDialogRef.close();
|
||||
await this._router.navigate(['main', 'dossiers']);
|
||||
this._loadingService.stop();
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { Dossier, DossierAttributeWithValue, DossierStats } from '@red/domain';
|
||||
import { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
|
||||
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
|
||||
import { FilesService } from '@services/entity-services/files.service';
|
||||
import { firstValueFrom, Observable } from 'rxjs';
|
||||
import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-dossier-details-stats',
|
||||
templateUrl: './dossier-details-stats.component.html',
|
||||
styleUrls: ['./dossier-details-stats.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DossierDetailsStatsComponent implements OnInit {
|
||||
@Input() dossierAttributes: DossierAttributeWithValue[];
|
||||
@Input() dossier: Dossier;
|
||||
|
||||
attributesExpanded = false;
|
||||
dossierTemplateName: string;
|
||||
dossierStats$: Observable<DossierStats>;
|
||||
|
||||
constructor(
|
||||
private readonly _dossierTemplatesService: DossierTemplatesService,
|
||||
private readonly _dialogService: DossiersDialogService,
|
||||
private readonly _filesService: FilesService,
|
||||
private readonly _dossierStatsService: DossierStatsService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.dossierStats$ = this._dossierStatsService.watch$(this.dossier.dossierId);
|
||||
this.dossierTemplateName = this._dossierTemplatesService.find(this.dossier.dossierTemplateId)?.name || '-';
|
||||
}
|
||||
|
||||
openEditDossierDialog(section: string): void {
|
||||
const data = { dossierId: this.dossier.dossierId, section };
|
||||
this._dialogService.openDialog('editDossier', null, data, async () => {
|
||||
await firstValueFrom(this._filesService.loadAll(this.dossier.dossierId, this.dossier.routerPath));
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
|
||||
import { FilterService, mapEach } from '@iqser/common-ui';
|
||||
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { DossierStats, 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 { DossierStatsService } from '@services/dossiers/dossier-stats.service';
|
||||
import { DossierStateService } from '@services/entity-services/dossier-state.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-dossiers-listing-details',
|
||||
templateUrl: './dossiers-listing-details.component.html',
|
||||
styleUrls: ['./dossiers-listing-details.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DossiersListingDetailsComponent {
|
||||
readonly documentsChartData$: Observable<DoughnutChartConfig[]>;
|
||||
readonly dossiersChartData$: Observable<DoughnutChartConfig[]>;
|
||||
|
||||
constructor(
|
||||
readonly filterService: FilterService,
|
||||
readonly activeDossiersService: ActiveDossiersService,
|
||||
private readonly _dossierStatsMap: DossierStatsService,
|
||||
private readonly _translateChartService: TranslateChartService,
|
||||
private readonly _dossierStateService: DossierStateService,
|
||||
private readonly _translateService: TranslateService,
|
||||
) {
|
||||
this.documentsChartData$ = this.activeDossiersService.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.activeDossiersService.all$.pipe(map(() => this._toDossierChartData()));
|
||||
}
|
||||
|
||||
private _toDossierChartData(): DoughnutChartConfig[] {
|
||||
this._dossierStateService.all.forEach(
|
||||
state => (state.dossierCount = this.activeDossiersService.getCountWithState(state.dossierStatusId)),
|
||||
);
|
||||
const configArray: DoughnutChartConfig[] = [
|
||||
...this._dossierStateService.all
|
||||
.reduce((acc, { color, dossierCount, name }) => {
|
||||
const key = name + '-' + color;
|
||||
const item = acc.get(key) ?? Object.assign({}, { value: 0, label: name, color: color });
|
||||
|
||||
return acc.set(key, { ...item, value: item.value + dossierCount });
|
||||
}, new Map<string, DoughnutChartConfig>())
|
||||
.values(),
|
||||
];
|
||||
|
||||
const notAssignedLength = this.activeDossiersService.all.length - configArray.map(v => v.value).reduce((acc, val) => acc + val, 0);
|
||||
configArray.push({
|
||||
value: notAssignedLength,
|
||||
label: this._translateService.instant('edit-dossier-dialog.general-info.form.dossier-status.placeholder'),
|
||||
color: '#E2E4E9',
|
||||
});
|
||||
|
||||
return configArray;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,266 @@
|
||||
import { Injectable, TemplateRef } from '@angular/core';
|
||||
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 { workflowFileStatusTranslations } from '../../../../translations/file-status-translations';
|
||||
import { dossierMemberChecker, dossierStateChecker, dossierTemplateChecker, RedactionFilterSorter } from '@utils/index';
|
||||
import { workloadTranslations } from '../../translations/workload-translations';
|
||||
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
|
||||
import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
|
||||
import { DossierStateService } from '@services/entity-services/dossier-state.service';
|
||||
|
||||
@Injectable()
|
||||
export class ConfigService {
|
||||
constructor(
|
||||
private readonly _translateService: TranslateService,
|
||||
private readonly _userPreferenceService: UserPreferenceService,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _dossierTemplatesService: DossierTemplatesService,
|
||||
private readonly _dossierStatsService: DossierStatsService,
|
||||
private readonly _dossierStateService: DossierStateService,
|
||||
) {}
|
||||
|
||||
get tableConfig(): TableColumnConfig<Dossier>[] {
|
||||
return [
|
||||
{ label: _('dossier-listing.table-col-names.name'), sortByKey: 'searchKey', width: '2fr' },
|
||||
// { label: _('dossier-listing.table-col-names.last-modified') },
|
||||
{ label: _('dossier-listing.table-col-names.needs-work') },
|
||||
{ label: _('dossier-listing.table-col-names.owner'), class: 'user-column' },
|
||||
{ label: _('dossier-listing.table-col-names.documents-status'), class: 'flex-end', width: 'auto' },
|
||||
{ label: _('dossier-listing.table-col-names.dossier-status'), class: 'flex-end' },
|
||||
];
|
||||
}
|
||||
|
||||
get _currentUser(): User {
|
||||
return this._userService.currentUser;
|
||||
}
|
||||
|
||||
_myDossiersChecker = (dw: Dossier) => dw.ownerId === this._currentUser.id;
|
||||
|
||||
_toApproveChecker = (dw: Dossier) => dw.approverIds.includes(this._currentUser.id);
|
||||
|
||||
_toReviewChecker = (dw: Dossier) => dw.memberIds.includes(this._currentUser.id);
|
||||
|
||||
_otherChecker = (dw: Dossier) => !dw.memberIds.includes(this._currentUser.id);
|
||||
|
||||
buttonsConfig(addDossier: () => void): ButtonConfig[] {
|
||||
return [
|
||||
{
|
||||
label: _('dossier-listing.add-new'),
|
||||
action: addDossier,
|
||||
hide: !this._currentUser.isManager,
|
||||
icon: 'iqser:plus',
|
||||
type: 'primary',
|
||||
helpModeKey: 'new_dossier_button',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
filterGroups(entities: Dossier[], needsWorkFilterTemplate: TemplateRef<unknown>) {
|
||||
const allDistinctFileStatus = new Set<string>();
|
||||
const allDistinctPeople = new Set<string>();
|
||||
const allDistinctNeedsWork = new Set<string>();
|
||||
const allDistinctDossierTemplates = new Set<string>();
|
||||
const allDistinctDossierStates = new Set<string>();
|
||||
|
||||
const filterGroups: IFilterGroup[] = [];
|
||||
|
||||
entities?.forEach(entry => {
|
||||
entry.memberIds.forEach(f => allDistinctPeople.add(f));
|
||||
allDistinctDossierTemplates.add(entry.dossierTemplateId);
|
||||
if (entry.dossierStatusId) {
|
||||
allDistinctDossierStates.add(entry.dossierStatusId);
|
||||
}
|
||||
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 dossierStatesFilters = [...allDistinctDossierStates].map(
|
||||
id =>
|
||||
new NestedFilter({
|
||||
id: id,
|
||||
label: this._dossierStateService.find(id).name,
|
||||
}),
|
||||
);
|
||||
|
||||
filterGroups.push({
|
||||
slug: 'dossierStatesFilters',
|
||||
label: this._translateService.instant('filters.dossier-status'),
|
||||
icon: 'red:status',
|
||||
hide: dossierStatesFilters.length <= 1,
|
||||
filters: dossierStatesFilters,
|
||||
checker: dossierStateChecker,
|
||||
});
|
||||
|
||||
const statusFilters = [...allDistinctFileStatus].map(
|
||||
status =>
|
||||
new NestedFilter({
|
||||
id: status,
|
||||
label: this._translateService.instant(workflowFileStatusTranslations[status]),
|
||||
}),
|
||||
);
|
||||
|
||||
filterGroups.push({
|
||||
slug: 'statusFilters',
|
||||
label: this._translateService.instant('filters.documents-status'),
|
||||
icon: 'red:status',
|
||||
filters: statusFilters.sort((a, b) => StatusSorter[a.id] - StatusSorter[b.id]),
|
||||
checker: (dossier: Dossier, filter: INestedFilter) => this._dossierStatusChecker(dossier, filter),
|
||||
});
|
||||
|
||||
const peopleFilters = [...allDistinctPeople].map(
|
||||
userId =>
|
||||
new NestedFilter({
|
||||
id: userId,
|
||||
label: this._userService.getNameForId(userId),
|
||||
}),
|
||||
);
|
||||
|
||||
filterGroups.push({
|
||||
slug: 'peopleFilters',
|
||||
label: this._translateService.instant('filters.people'),
|
||||
icon: 'red:user',
|
||||
filters: peopleFilters,
|
||||
checker: dossierMemberChecker,
|
||||
});
|
||||
|
||||
const needsWorkFilters = [...allDistinctNeedsWork].map(
|
||||
type =>
|
||||
new NestedFilter({
|
||||
id: type,
|
||||
label: workloadTranslations[type],
|
||||
}),
|
||||
);
|
||||
|
||||
filterGroups.push({
|
||||
slug: 'needsWorkFilters',
|
||||
label: this._translateService.instant('filters.needs-work'),
|
||||
icon: 'red:needs-work',
|
||||
filterTemplate: needsWorkFilterTemplate,
|
||||
filters: needsWorkFilters.sort((a, b) => RedactionFilterSorter[a.id] - RedactionFilterSorter[b.id]),
|
||||
checker: (dossier: Dossier, filter: INestedFilter) => this._annotationFilterChecker(dossier, filter),
|
||||
matchAll: true,
|
||||
});
|
||||
|
||||
const dossierTemplateFilters = [...allDistinctDossierTemplates].map(
|
||||
id =>
|
||||
new NestedFilter({
|
||||
id: id,
|
||||
label: this._dossierTemplatesService.find(id)?.name || '-',
|
||||
}),
|
||||
);
|
||||
|
||||
filterGroups.push({
|
||||
slug: 'dossierTemplateFilters',
|
||||
label: this._translateService.instant('filters.dossier-templates'),
|
||||
icon: 'red:template',
|
||||
hide: dossierTemplateFilters.length <= 1,
|
||||
filters: dossierTemplateFilters,
|
||||
checker: dossierTemplateChecker,
|
||||
});
|
||||
|
||||
filterGroups.push({
|
||||
slug: 'quickFilters',
|
||||
filters: this._quickFilters(entities),
|
||||
checker: (dw: Dossier, filter: NestedFilter) => filter.checked && filter.checker(dw),
|
||||
});
|
||||
|
||||
const dossierFilters = entities.map(
|
||||
dossier =>
|
||||
new NestedFilter({
|
||||
id: dossier.dossierName,
|
||||
label: dossier.dossierName,
|
||||
}),
|
||||
);
|
||||
filterGroups.push({
|
||||
slug: 'dossierNameFilter',
|
||||
label: this._translateService.instant('dossier-listing.filters.label'),
|
||||
icon: 'red:folder',
|
||||
filters: dossierFilters,
|
||||
filterceptionPlaceholder: this._translateService.instant('dossier-listing.filters.search'),
|
||||
checker: keyChecker('dossierName'),
|
||||
});
|
||||
|
||||
return filterGroups;
|
||||
}
|
||||
|
||||
private _quickFilters(entities: Dossier[]): NestedFilter[] {
|
||||
const myDossiersLabel = this._translateService.instant('dossier-listing.quick-filters.my-dossiers');
|
||||
const filters = [
|
||||
{
|
||||
id: 'my-dossiers',
|
||||
label: myDossiersLabel,
|
||||
checker: this._myDossiersChecker,
|
||||
disabled: entities.filter(this._myDossiersChecker).length === 0,
|
||||
helpModeKey: 'dossiers_quickfilter_my_dossiers',
|
||||
},
|
||||
{
|
||||
id: 'to-approve',
|
||||
label: this._translateService.instant('dossier-listing.quick-filters.to-approve'),
|
||||
checker: this._toApproveChecker,
|
||||
disabled: entities.filter(this._toApproveChecker).length === 0,
|
||||
},
|
||||
{
|
||||
id: 'to-review',
|
||||
label: this._translateService.instant('dossier-listing.quick-filters.to-review'),
|
||||
checker: this._toReviewChecker,
|
||||
disabled: entities.filter(this._toReviewChecker).length === 0,
|
||||
},
|
||||
{
|
||||
id: 'other',
|
||||
label: this._translateService.instant('dossier-listing.quick-filters.other'),
|
||||
checker: this._otherChecker,
|
||||
disabled: entities.filter(this._otherChecker).length === 0,
|
||||
},
|
||||
].map(filter => new NestedFilter(filter));
|
||||
|
||||
return filters.filter(f => f.label === myDossiersLabel || this._userPreferenceService.areDevFeaturesEnabled);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,115 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { Dossier, File, StatusBarConfigs, User } from '@red/domain';
|
||||
import { List, LoadingService, Toaster } from '@iqser/common-ui';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { FileAssignService } from '../../../../shared/services/file-assign.service';
|
||||
import { workflowFileStatusTranslations } from '../../../../../../translations/file-status-translations';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { FilesService } from '@services/entity-services/files.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, combineLatest, firstValueFrom, Observable, switchMap } from 'rxjs';
|
||||
import { FilePreviewStateService } from '../../services/file-preview-state.service';
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-user-management',
|
||||
templateUrl: './user-management.component.html',
|
||||
styleUrls: ['./user-management.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UserManagementComponent {
|
||||
readonly translations = workflowFileStatusTranslations;
|
||||
readonly statusBarConfig$: Observable<StatusBarConfigs>;
|
||||
readonly assignTooltip$: Observable<string>;
|
||||
readonly canAssignReviewer$: Observable<boolean>;
|
||||
readonly canAssignToSelf$: Observable<boolean>;
|
||||
readonly editingReviewer$ = new BehaviorSubject<boolean>(false);
|
||||
readonly canAssignOrUnassign$: Observable<boolean>;
|
||||
readonly canAssign$: Observable<boolean>;
|
||||
readonly usersOptions$: Observable<List>;
|
||||
private readonly _dossier$: Observable<Dossier>;
|
||||
private readonly _canAssignUser$: Observable<boolean>;
|
||||
private readonly _canUnassignUser$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
readonly fileAssignService: FileAssignService,
|
||||
readonly permissionsService: PermissionsService,
|
||||
readonly userService: UserService,
|
||||
readonly filesService: FilesService,
|
||||
readonly toaster: Toaster,
|
||||
readonly loadingService: LoadingService,
|
||||
readonly translateService: TranslateService,
|
||||
readonly stateService: FilePreviewStateService,
|
||||
private readonly _activeDossiersService: ActiveDossiersService,
|
||||
) {
|
||||
this._dossier$ = this.stateService.file$.pipe(switchMap(file => this._activeDossiersService.getEntityChanged$(file.dossierId)));
|
||||
this.statusBarConfig$ = this.stateService.file$.pipe(map(file => [{ length: 1, color: file.workflowStatus }]));
|
||||
this.assignTooltip$ = this.stateService.file$.pipe(
|
||||
map(file =>
|
||||
file.isUnderApproval
|
||||
? this.translateService.instant(_('dossier-overview.assign-approver'))
|
||||
: file.assignee
|
||||
? this.translateService.instant(_('file-preview.change-reviewer'))
|
||||
: this.translateService.instant(_('file-preview.assign-reviewer')),
|
||||
),
|
||||
);
|
||||
|
||||
this.canAssignToSelf$ = this.stateService.file$.pipe(
|
||||
map(file => this.permissionsService.canAssignToSelf(file)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
this._canAssignUser$ = this.stateService.file$.pipe(
|
||||
map(file => this.permissionsService.canAssignUser(file)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
this._canUnassignUser$ = this.stateService.file$.pipe(
|
||||
map(file => this.permissionsService.canUnassignUser(file)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
this.canAssignOrUnassign$ = combineLatest([this._canAssignUser$, this._canUnassignUser$]).pipe(
|
||||
map(([canAssignUser, canUnassignUser]) => canAssignUser || canUnassignUser),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
this.canAssign$ = combineLatest([this.canAssignToSelf$, this.canAssignOrUnassign$]).pipe(
|
||||
map(([canAssignToSelf, canAssignOrUnassign]) => canAssignToSelf || canAssignOrUnassign),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
this.canAssignReviewer$ = combineLatest([this.stateService.file$, this._canAssignUser$, this._dossier$]).pipe(
|
||||
map(([file, canAssignUser, dossier]) => !file.assignee && canAssignUser && dossier.hasReviewers),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
this.usersOptions$ = combineLatest([this._canUnassignUser$, this.stateService.file$, this._dossier$]).pipe(
|
||||
map(([canUnassignUser, file, dossier]) => {
|
||||
const unassignUser = canUnassignUser ? [undefined] : [];
|
||||
return file.isUnderApproval ? [...dossier.approverIds, ...unassignUser] : [...dossier.memberIds, ...unassignUser];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async assignReviewer(file: File, user: User | string) {
|
||||
const assigneeId = typeof user === 'string' ? user : user?.id;
|
||||
const reviewerName = this.userService.getNameForId(assigneeId);
|
||||
|
||||
const { dossierId, filename } = file;
|
||||
this.loadingService.start();
|
||||
|
||||
if (!assigneeId) {
|
||||
await firstValueFrom(this.filesService.setUnassigned([file], dossierId));
|
||||
} else if (file.isNew || file.isUnderReview) {
|
||||
await firstValueFrom(this.filesService.setReviewerFor([file], dossierId, assigneeId));
|
||||
} else if (file.isUnderApproval) {
|
||||
await firstValueFrom(this.filesService.setUnderApprovalFor([file], dossierId, assigneeId));
|
||||
}
|
||||
|
||||
this.loadingService.stop();
|
||||
|
||||
this.toaster.info(_('assignment.reviewer'), { params: { reviewerName, filename } });
|
||||
this.editingReviewer$.next(false);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,703 @@
|
||||
import { ChangeDetectorRef, Component, HostListener, NgZone, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationExtras, Router } from '@angular/router';
|
||||
import { Core } from '@pdftron/webviewer';
|
||||
import {
|
||||
AutoUnsubscribe,
|
||||
CircleButtonTypes,
|
||||
CustomError,
|
||||
Debounce,
|
||||
ErrorService,
|
||||
FilterService,
|
||||
LoadingService,
|
||||
NestedFilter,
|
||||
OnAttach,
|
||||
OnDetach,
|
||||
processFilters,
|
||||
shareDistinctLast,
|
||||
} from '@iqser/common-ui';
|
||||
import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
|
||||
import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
import { ManualAnnotationResponse } from '@models/file/manual-annotation-response';
|
||||
import { AnnotationDrawService } from './services/annotation-draw.service';
|
||||
import { AnnotationProcessingService } from '../../services/annotation-processing.service';
|
||||
import { File, ViewMode } from '@red/domain';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { combineLatest, firstValueFrom, Observable, of, timer } from 'rxjs';
|
||||
import { UserPreferenceService } from '@services/user-preference.service';
|
||||
import { PdfViewerDataService } from '../../services/pdf-viewer-data.service';
|
||||
import { download } from '@utils/file-download-utils';
|
||||
import { FileWorkloadComponent } from './components/file-workload/file-workload.component';
|
||||
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
|
||||
import { clearStamps, stampPDFPage } from '@utils/page-stamper';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { handleFilterDelta } from '@utils/filter-utils';
|
||||
import { FilesService } from '@services/entity-services/files.service';
|
||||
import { FileManagementService } from '@services/entity-services/file-management.service';
|
||||
import { catchError, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { FilesMapService } from '@services/entity-services/files-map.service';
|
||||
import { WatermarkService } from '@shared/services/watermark.service';
|
||||
import { ExcludedPagesService } from './services/excluded-pages.service';
|
||||
import { ViewModeService } from './services/view-mode.service';
|
||||
import { MultiSelectService } from './services/multi-select.service';
|
||||
import { DocumentInfoService } from './services/document-info.service';
|
||||
import { ReanalysisService } from '../../../../services/reanalysis.service';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { SkippedService } from './services/skipped.service';
|
||||
import { FilePreviewStateService } from './services/file-preview-state.service';
|
||||
import { FileDataModel } from '../../../../models/file/file-data.model';
|
||||
import { filePreviewScreenProviders } from './file-preview-providers';
|
||||
import { ManualAnnotationService } from '../../services/manual-annotation.service';
|
||||
import { DossiersService } from '@services/dossiers/dossiers.service';
|
||||
import { PageRotationService } from './services/page-rotation.service';
|
||||
import { ComponentCanDeactivate } from '../../../../guards/can-deactivate.guard';
|
||||
import { PdfViewer } from './services/pdf-viewer.service';
|
||||
import Annotation = Core.Annotations.Annotation;
|
||||
import PDFNet = Core.PDFNet;
|
||||
|
||||
const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f', 'ArrowUp', 'ArrowDown'];
|
||||
|
||||
@Component({
|
||||
templateUrl: './file-preview-screen.component.html',
|
||||
styleUrls: ['./file-preview-screen.component.scss'],
|
||||
providers: filePreviewScreenProviders,
|
||||
})
|
||||
export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnInit, OnDestroy, OnAttach, OnDetach, ComponentCanDeactivate {
|
||||
readonly circleButtonTypes = CircleButtonTypes;
|
||||
|
||||
dialogRef: MatDialogRef<unknown>;
|
||||
fullScreen = false;
|
||||
selectedAnnotations: AnnotationWrapper[] = [];
|
||||
displayPdfViewer = false;
|
||||
activeViewerPage: number = null;
|
||||
readonly canPerformAnnotationActions$: Observable<boolean>;
|
||||
readonly fileId = this.stateService.fileId;
|
||||
readonly dossierId = this.stateService.dossierId;
|
||||
readonly file$ = this.stateService.file$.pipe(tap(file => this._fileUpdated(file)));
|
||||
ready = false;
|
||||
private _lastPage: string;
|
||||
|
||||
@ViewChild('fileWorkloadComponent') private readonly _workloadComponent: FileWorkloadComponent;
|
||||
@ViewChild('annotationFilterTemplate', {
|
||||
read: TemplateRef,
|
||||
static: false,
|
||||
})
|
||||
private readonly _filterTemplate: TemplateRef<unknown>;
|
||||
|
||||
constructor(
|
||||
readonly permissionsService: PermissionsService,
|
||||
readonly userPreferenceService: UserPreferenceService,
|
||||
readonly stateService: FilePreviewStateService,
|
||||
private readonly _watermarkService: WatermarkService,
|
||||
private readonly _changeDetectorRef: ChangeDetectorRef,
|
||||
private readonly _activatedRoute: ActivatedRoute,
|
||||
private readonly _dialogService: DossiersDialogService,
|
||||
private readonly _router: Router,
|
||||
private readonly _annotationProcessingService: AnnotationProcessingService,
|
||||
private readonly _annotationDrawService: AnnotationDrawService,
|
||||
private readonly _pdfViewerDataService: PdfViewerDataService,
|
||||
private readonly _filesService: FilesService,
|
||||
private readonly _ngZone: NgZone,
|
||||
private readonly _fileManagementService: FileManagementService,
|
||||
private readonly _loadingService: LoadingService,
|
||||
private readonly _filterService: FilterService,
|
||||
private readonly _translateService: TranslateService,
|
||||
private readonly _filesMapService: FilesMapService,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
private readonly _reanalysisService: ReanalysisService,
|
||||
private readonly _errorService: ErrorService,
|
||||
private readonly _pageRotationService: PageRotationService,
|
||||
private readonly _skippedService: SkippedService,
|
||||
private readonly _pdf: PdfViewer,
|
||||
private readonly _manualAnnotationService: ManualAnnotationService,
|
||||
readonly excludedPagesService: ExcludedPagesService,
|
||||
private readonly _viewModeService: ViewModeService,
|
||||
readonly multiSelectService: MultiSelectService,
|
||||
readonly documentInfoService: DocumentInfoService,
|
||||
) {
|
||||
super();
|
||||
this.canPerformAnnotationActions$ = this._canPerformAnnotationActions$;
|
||||
|
||||
document.documentElement.addEventListener('fullscreenchange', () => {
|
||||
if (!document.fullscreenElement) {
|
||||
this.fullScreen = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get changed() {
|
||||
return this._pageRotationService.hasRotations();
|
||||
}
|
||||
|
||||
get visibleAnnotations(): AnnotationWrapper[] {
|
||||
return this._fileData ? this._fileData.getVisibleAnnotations(this._viewModeService.viewMode) : [];
|
||||
}
|
||||
|
||||
get allAnnotations(): AnnotationWrapper[] {
|
||||
return this._fileData ? this._fileData.allAnnotations : [];
|
||||
}
|
||||
|
||||
private get _fileData(): FileDataModel {
|
||||
return this.stateService.fileData;
|
||||
}
|
||||
|
||||
private get _canPerformAnnotationActions$() {
|
||||
const viewMode$ = this._viewModeService.viewMode$.pipe(tap(() => this.#deactivateMultiSelect()));
|
||||
|
||||
return combineLatest([this.stateService.file$, viewMode$, this._viewModeService.compareMode$]).pipe(
|
||||
map(([file, viewMode]) => this.permissionsService.canPerformAnnotationActions(file) && viewMode === 'STANDARD'),
|
||||
shareDistinctLast(),
|
||||
);
|
||||
}
|
||||
|
||||
async save() {
|
||||
await this._pageRotationService.applyRotation();
|
||||
}
|
||||
|
||||
async updateViewMode(): Promise<void> {
|
||||
if (!this._pdf.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
const textHighlightAnnotationIds = this._fileData.textHighlightAnnotations.map(a => a.id);
|
||||
const textHighlightAnnotations = this._pdf.getAnnotations((a: Core.Annotations.Annotation) =>
|
||||
textHighlightAnnotationIds.includes(a.Id),
|
||||
);
|
||||
|
||||
this._pdf.deleteAnnotations(textHighlightAnnotations);
|
||||
|
||||
const ocrAnnotationIds = this._fileData.allAnnotations.filter(a => a.isOCR).map(a => a.id);
|
||||
const annotations = this._pdf.getAnnotations(a => a.getCustomData('redact-manager'));
|
||||
const redactions = annotations.filter(a => a.getCustomData('redaction'));
|
||||
|
||||
switch (this._viewModeService.viewMode) {
|
||||
case 'STANDARD': {
|
||||
this._setAnnotationsColor(redactions, 'annotationColor');
|
||||
const standardEntries = annotations
|
||||
.filter(a => a.getCustomData('changeLogRemoved') === 'false')
|
||||
.filter(a => !ocrAnnotationIds.includes(a.Id));
|
||||
const nonStandardEntries = annotations.filter(a => a.getCustomData('changeLogRemoved') === 'true');
|
||||
this._setAnnotationsOpacity(standardEntries, true);
|
||||
this._pdf.showAnnotations(standardEntries);
|
||||
this._pdf.hideAnnotations(nonStandardEntries);
|
||||
break;
|
||||
}
|
||||
case 'DELTA': {
|
||||
const changeLogEntries = annotations.filter(a => a.getCustomData('changeLog') === 'true');
|
||||
const nonChangeLogEntries = annotations.filter(a => a.getCustomData('changeLog') === 'false');
|
||||
this._setAnnotationsColor(redactions, 'annotationColor');
|
||||
this._setAnnotationsOpacity(changeLogEntries, true);
|
||||
this._pdf.showAnnotations(changeLogEntries);
|
||||
this._pdf.hideAnnotations(nonChangeLogEntries);
|
||||
break;
|
||||
}
|
||||
case 'REDACTED': {
|
||||
const nonRedactionEntries = annotations.filter(a => a.getCustomData('redaction') === 'false');
|
||||
this._setAnnotationsOpacity(redactions);
|
||||
this._setAnnotationsColor(redactions, 'redactionColor');
|
||||
this._pdf.showAnnotations(redactions);
|
||||
this._pdf.hideAnnotations(nonRedactionEntries);
|
||||
break;
|
||||
}
|
||||
case 'TEXT_HIGHLIGHTS': {
|
||||
this._loadingService.start();
|
||||
const textHighlights = await firstValueFrom(this._pdfViewerDataService.loadTextHighlightsFor(this.dossierId, this.fileId));
|
||||
this._pdf.hideAnnotations(annotations);
|
||||
this._fileData.textHighlights = textHighlights;
|
||||
await this._annotationDrawService.drawAnnotations(this._fileData.textHighlightAnnotations);
|
||||
this._loadingService.stop();
|
||||
}
|
||||
}
|
||||
|
||||
await this._stampPDF();
|
||||
this.rebuildFilters();
|
||||
}
|
||||
|
||||
ngOnDetach(): void {
|
||||
this._pageRotationService.clearRotations();
|
||||
this.displayPdfViewer = false;
|
||||
super.ngOnDetach();
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
async ngOnAttach(previousRoute: ActivatedRouteSnapshot): Promise<boolean> {
|
||||
const file = await this.stateService.file;
|
||||
if (!file.canBeOpened) {
|
||||
return this._router.navigate([this._dossiersService.find(this.dossierId)?.routerLink]);
|
||||
}
|
||||
this._viewModeService.compareMode = false;
|
||||
this._viewModeService.switchToStandard();
|
||||
|
||||
await this.ngOnInit();
|
||||
this._lastPage = previousRoute.queryParams.page;
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.ready = false;
|
||||
this._loadingService.start();
|
||||
await this.userPreferenceService.saveLastOpenedFileForDossier(this.dossierId, this.fileId);
|
||||
this._subscribeToFileUpdates();
|
||||
|
||||
const file = await this.stateService.file;
|
||||
if (file?.analysisRequired && !file.excludedFromAutomaticAnalysis) {
|
||||
const reanalyzeFiles = this._reanalysisService.reanalyzeFilesForDossier([file], this.dossierId, { force: true });
|
||||
await firstValueFrom(reanalyzeFiles);
|
||||
}
|
||||
|
||||
this.displayPdfViewer = true;
|
||||
}
|
||||
|
||||
rebuildFilters(deletePreviousAnnotations = false): void {
|
||||
const startTime = new Date().getTime();
|
||||
if (deletePreviousAnnotations) {
|
||||
this._pdf.deleteAnnotations();
|
||||
|
||||
console.log(`[REDACTION] Delete previous annotations time: ${new Date().getTime() - startTime} ms`);
|
||||
}
|
||||
const processStartTime = new Date().getTime();
|
||||
|
||||
const annotationFilters = this._annotationProcessingService.getAnnotationFilter(this.visibleAnnotations);
|
||||
const primaryFilters = this._filterService.getGroup('primaryFilters')?.filters;
|
||||
this._filterService.addFilterGroup({
|
||||
slug: 'primaryFilters',
|
||||
filterTemplate: this._filterTemplate,
|
||||
filters: processFilters(primaryFilters, annotationFilters),
|
||||
});
|
||||
const secondaryFilters = this._filterService.getGroup('secondaryFilters')?.filters;
|
||||
this._filterService.addFilterGroup({
|
||||
slug: 'secondaryFilters',
|
||||
filterTemplate: this._filterTemplate,
|
||||
filters: processFilters(secondaryFilters, AnnotationProcessingService.secondaryAnnotationFilters(this._fileData?.viewedPages)),
|
||||
});
|
||||
console.log(`[REDACTION] Process time: ${new Date().getTime() - processStartTime} ms`);
|
||||
console.log(`[REDACTION] Filter rebuild time: ${new Date().getTime() - startTime}`);
|
||||
}
|
||||
|
||||
handleAnnotationSelected(annotationIds: string[]) {
|
||||
this.selectedAnnotations = annotationIds
|
||||
.map(id => this.visibleAnnotations.find(annotationWrapper => annotationWrapper.id === id))
|
||||
.filter(ann => ann !== undefined);
|
||||
if (this.selectedAnnotations.length > 1) {
|
||||
this.multiSelectService.activate();
|
||||
}
|
||||
this._workloadComponent.scrollToSelectedAnnotation();
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
@Debounce(10)
|
||||
selectAnnotations(annotations?: AnnotationWrapper[]) {
|
||||
if (annotations) {
|
||||
const annotationsToSelect = this.multiSelectService.isActive ? [...this.selectedAnnotations, ...annotations] : annotations;
|
||||
this._pdf.selectAnnotations(annotationsToSelect, this.multiSelectService.isActive);
|
||||
} else {
|
||||
this._pdf.deselectAllAnnotations();
|
||||
}
|
||||
}
|
||||
|
||||
deselectAnnotations(annotations: AnnotationWrapper[]) {
|
||||
this._pdf.deselectAnnotations(annotations);
|
||||
}
|
||||
|
||||
selectPage(pageNumber: number) {
|
||||
this._pdf.navigateToPage(pageNumber);
|
||||
this._workloadComponent?.scrollAnnotationsToPage(pageNumber, 'always');
|
||||
this._lastPage = pageNumber.toString();
|
||||
}
|
||||
|
||||
openManualAnnotationDialog(manualRedactionEntryWrapper: ManualRedactionEntryWrapper) {
|
||||
this._ngZone.run(() => {
|
||||
this.dialogRef = this._dialogService.openDialog(
|
||||
'manualAnnotation',
|
||||
null,
|
||||
{ manualRedactionEntryWrapper, dossierId: this.dossierId },
|
||||
async (entryWrapper: ManualRedactionEntryWrapper) => {
|
||||
const addAnnotation$ = this._manualAnnotationService
|
||||
.addAnnotation(entryWrapper.manualRedactionEntry, this.dossierId, this.fileId)
|
||||
.pipe(catchError(() => of(undefined)));
|
||||
const addAnnotationResponse = await firstValueFrom(addAnnotation$);
|
||||
const response = new ManualAnnotationResponse(entryWrapper, addAnnotationResponse);
|
||||
|
||||
if (response?.annotationId) {
|
||||
const annotation = this._pdf.annotationManager.getAnnotationById(response.manualRedactionEntryWrapper.rectId);
|
||||
this._pdf.deleteAnnotations([annotation]);
|
||||
const distinctPages = manualRedactionEntryWrapper.manualRedactionEntry.positions
|
||||
.map(p => p.page)
|
||||
.filter((item, pos, self) => self.indexOf(item) === pos);
|
||||
for (const page of distinctPages) {
|
||||
await this._reloadAnnotationsForPage(page);
|
||||
}
|
||||
await this.updateViewMode();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
toggleFullScreen() {
|
||||
this.fullScreen = !this.fullScreen;
|
||||
if (this.fullScreen) {
|
||||
this._openFullScreen();
|
||||
} else {
|
||||
this.closeFullScreen();
|
||||
}
|
||||
}
|
||||
|
||||
handleArrowEvent($event: KeyboardEvent): void {
|
||||
if (['ArrowUp', 'ArrowDown'].includes($event.key)) {
|
||||
if (this.selectedAnnotations.length === 1) {
|
||||
this._workloadComponent.navigateAnnotations($event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('window:keyup', ['$event'])
|
||||
handleKeyEvent($event: KeyboardEvent) {
|
||||
if (this._router.url.indexOf('/file/') < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ALL_HOTKEY_ARRAY.includes($event.key) || this.dialogRef?.getState() === MatDialogState.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (['Escape'].includes($event.key)) {
|
||||
this.fullScreen = false;
|
||||
this.closeFullScreen();
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
if (['f', 'F'].includes($event.key)) {
|
||||
// if you type in an input, don't toggle full-screen
|
||||
if ($event.target instanceof HTMLInputElement || $event.target instanceof HTMLTextAreaElement) {
|
||||
return;
|
||||
}
|
||||
this.toggleFullScreen();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async viewerPageChanged($event: any) {
|
||||
if (typeof $event !== 'number') {
|
||||
return;
|
||||
}
|
||||
|
||||
this._scrollViews();
|
||||
this.multiSelectService.deactivate();
|
||||
|
||||
// Add current page in URL query params
|
||||
const extras: NavigationExtras = {
|
||||
queryParams: { page: $event },
|
||||
queryParamsHandling: 'merge',
|
||||
replaceUrl: true,
|
||||
};
|
||||
await this._router.navigate([], extras);
|
||||
|
||||
this.activeViewerPage = this._pdf.currentPage;
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
@Debounce()
|
||||
async viewerReady() {
|
||||
this.ready = true;
|
||||
this._pdf.ready = true;
|
||||
|
||||
await this._reloadAnnotations();
|
||||
this._setExcludedPageStyles();
|
||||
|
||||
this._pdf.documentViewer.addEventListener('pageComplete', () => {
|
||||
this._setExcludedPageStyles();
|
||||
});
|
||||
|
||||
// Go to initial page from query params
|
||||
const pageNumber: string = this._lastPage || this._activatedRoute.snapshot.queryParams.page;
|
||||
if (pageNumber) {
|
||||
setTimeout(() => {
|
||||
this.selectPage(parseInt(pageNumber, 10));
|
||||
this.activeViewerPage = this._pdf.currentPage;
|
||||
this._scrollViews();
|
||||
this._changeDetectorRef.markForCheck();
|
||||
this._loadingService.stop();
|
||||
});
|
||||
} else {
|
||||
this._loadingService.stop();
|
||||
}
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
async annotationsChangedByReviewAction(annotation?: AnnotationWrapper) {
|
||||
this.multiSelectService.deactivate();
|
||||
const file = await this.stateService.file;
|
||||
const fileReloaded = await firstValueFrom(this._filesService.reload(this.dossierId, file));
|
||||
if (!fileReloaded) {
|
||||
await this._reloadAnnotationsForPage(annotation?.pageNumber ?? this.activeViewerPage);
|
||||
}
|
||||
}
|
||||
|
||||
closeFullScreen() {
|
||||
if (!!document.fullscreenElement && document.exitFullscreen) {
|
||||
document.exitFullscreen().then();
|
||||
}
|
||||
}
|
||||
|
||||
async switchView(viewMode: ViewMode) {
|
||||
this._viewModeService.viewMode = viewMode;
|
||||
await this.updateViewMode();
|
||||
this._scrollViews();
|
||||
}
|
||||
|
||||
async downloadOriginalFile(file: File) {
|
||||
const originalFile = this._fileManagementService.downloadOriginalFile(
|
||||
this.dossierId,
|
||||
this.fileId,
|
||||
'response',
|
||||
file.cacheIdentifier,
|
||||
);
|
||||
download(await firstValueFrom(originalFile), file.filename);
|
||||
}
|
||||
|
||||
#deactivateMultiSelect(): void {
|
||||
this.multiSelectService.deactivate();
|
||||
this._pdf.deselectAllAnnotations();
|
||||
this.handleAnnotationSelected([]);
|
||||
}
|
||||
|
||||
private _setExcludedPageStyles() {
|
||||
const file = this._filesMapService.get(this.dossierId, this.fileId);
|
||||
setTimeout(() => {
|
||||
const iframeDoc = this._pdf.UI.iframeWindow.document;
|
||||
const pageContainer = iframeDoc.getElementById(`pageWidgetContainer${this.activeViewerPage}`);
|
||||
if (pageContainer) {
|
||||
if (file.excludedPages.includes(this.activeViewerPage)) {
|
||||
pageContainer.classList.add('excluded-page');
|
||||
} else {
|
||||
pageContainer.classList.remove('excluded-page');
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
private async _stampPDF() {
|
||||
const pdfDoc = await this._pdf.documentViewer.getDocument().getPDFDoc();
|
||||
const file = await this.stateService.file;
|
||||
const allPages = [...Array(file.numberOfPages).keys()].map(page => page + 1);
|
||||
|
||||
if (!pdfDoc || !this._pdf.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
await clearStamps(pdfDoc, this._pdf.PDFNet, allPages);
|
||||
|
||||
if (this._viewModeService.isRedacted) {
|
||||
const dossier = await this.stateService.dossier;
|
||||
if (dossier.watermarkPreviewEnabled) {
|
||||
await this._stampPreview(pdfDoc, dossier.dossierTemplateId);
|
||||
}
|
||||
} else {
|
||||
await this._stampExcludedPages(pdfDoc, file.excludedPages);
|
||||
}
|
||||
this._pdf.documentViewer.refreshAll();
|
||||
this._pdf.documentViewer.updateView([this.activeViewerPage], this.activeViewerPage);
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
private async _stampPreview(document: PDFNet.PDFDoc, dossierTemplateId: string) {
|
||||
const watermark = await this._watermarkService.getWatermark(dossierTemplateId).toPromise();
|
||||
await stampPDFPage(
|
||||
document,
|
||||
this._pdf.PDFNet,
|
||||
watermark.text,
|
||||
watermark.fontSize,
|
||||
watermark.fontType,
|
||||
watermark.orientation,
|
||||
watermark.opacity,
|
||||
watermark.hexColor,
|
||||
Array.from({ length: await document.getPageCount() }, (x, i) => i + 1),
|
||||
);
|
||||
}
|
||||
|
||||
private async _stampExcludedPages(document: PDFNet.PDFDoc, excludedPages: number[]) {
|
||||
if (excludedPages && excludedPages.length > 0) {
|
||||
await stampPDFPage(
|
||||
document,
|
||||
this._pdf.PDFNet,
|
||||
this._translateService.instant('file-preview.excluded-from-redaction') as string,
|
||||
17,
|
||||
'courier',
|
||||
'TOP_LEFT',
|
||||
50,
|
||||
'#dd4d50',
|
||||
excludedPages,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async _fileUpdated(file: File): Promise<void> {
|
||||
await this._loadFileData(file);
|
||||
await this._reloadAnnotations();
|
||||
}
|
||||
|
||||
private _subscribeToFileUpdates(): void {
|
||||
this.addActiveScreenSubscription = timer(0, 5000)
|
||||
.pipe(
|
||||
switchMap(() => this.stateService.file$),
|
||||
switchMap(file => this._filesService.reload(this.dossierId, file)),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.addActiveScreenSubscription = this._dossiersService
|
||||
.getEntityDeleted$(this.dossierId)
|
||||
.pipe(tap(() => this._handleDeletedDossier()))
|
||||
.subscribe();
|
||||
|
||||
this.addActiveScreenSubscription = this._filesMapService
|
||||
.watchDeleted$(this.fileId)
|
||||
.pipe(tap(() => this._handleDeletedFile()))
|
||||
.subscribe();
|
||||
|
||||
this.addActiveScreenSubscription = this._skippedService.hideSkipped$
|
||||
.pipe(tap(hideSkipped => this._handleIgnoreAnnotationsDrawing(hideSkipped)))
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private _handleDeletedDossier(): void {
|
||||
this._errorService.set(
|
||||
new CustomError(_('error.deleted-entity.file-dossier.label'), _('error.deleted-entity.file-dossier.action'), 'iqser:expand'),
|
||||
);
|
||||
}
|
||||
|
||||
private _handleDeletedFile(): void {
|
||||
this._errorService.set(
|
||||
new CustomError(_('error.deleted-entity.file.label'), _('error.deleted-entity.file.action'), 'iqser:expand'),
|
||||
);
|
||||
}
|
||||
|
||||
private async _loadFileData(file: File): Promise<void | boolean> {
|
||||
if (!file || file.isError) {
|
||||
const dossier = await this.stateService.dossier;
|
||||
return this._router.navigate([dossier.routerLink]);
|
||||
}
|
||||
|
||||
if (file.isUnprocessed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stateService.fileData = await firstValueFrom(this._pdfViewerDataService.loadDataFor(file));
|
||||
}
|
||||
|
||||
@Debounce(0)
|
||||
private _scrollViews() {
|
||||
this._workloadComponent?.scrollQuickNavigation();
|
||||
this._workloadComponent?.scrollAnnotations();
|
||||
}
|
||||
|
||||
private async _reloadAnnotations() {
|
||||
this._deleteAnnotations();
|
||||
await this._cleanupAndRedrawAnnotations();
|
||||
await this.updateViewMode();
|
||||
}
|
||||
|
||||
private async _reloadAnnotationsForPage(page: number) {
|
||||
const file = await this.stateService.file;
|
||||
// if this action triggered a re-processing,
|
||||
// we don't want to redraw for this page since they will get redrawn as soon as processing ends;
|
||||
if (file.isProcessing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPageAnnotations = this.visibleAnnotations.filter(a => a.pageNumber === page);
|
||||
this._fileData.redactionLog = await firstValueFrom(this._pdfViewerDataService.loadRedactionLogFor(this.dossierId, this.fileId));
|
||||
|
||||
this._deleteAnnotations(currentPageAnnotations);
|
||||
await this._cleanupAndRedrawAnnotations(annotation => annotation.pageNumber === page);
|
||||
}
|
||||
|
||||
private _deleteAnnotations(annotationsToDelete?: AnnotationWrapper[]) {
|
||||
if (!this._pdf.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!annotationsToDelete) {
|
||||
this._pdf.deleteAnnotations();
|
||||
}
|
||||
annotationsToDelete?.forEach(annotation => {
|
||||
this._findAndDeleteAnnotation(annotation.id);
|
||||
});
|
||||
}
|
||||
|
||||
private async _cleanupAndRedrawAnnotations(newAnnotationsFilter?: (annotation: AnnotationWrapper) => boolean) {
|
||||
if (!this._pdf.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFilters = this._filterService.getGroup('primaryFilters')?.filters || [];
|
||||
this.rebuildFilters();
|
||||
|
||||
const startTime = new Date().getTime();
|
||||
const annotations = this._fileData.allAnnotations;
|
||||
const newAnnotations = newAnnotationsFilter ? annotations.filter(newAnnotationsFilter) : annotations;
|
||||
|
||||
if (currentFilters) {
|
||||
this._handleDeltaAnnotationFilters(currentFilters, this.visibleAnnotations);
|
||||
}
|
||||
|
||||
await this._annotationDrawService.drawAnnotations(newAnnotations);
|
||||
console.log(`[REDACTION] Annotations redraw time: ${new Date().getTime() - startTime} ms for ${newAnnotations.length} annotations`);
|
||||
}
|
||||
|
||||
private _handleDeltaAnnotationFilters(currentFilters: NestedFilter[], newAnnotations: AnnotationWrapper[]) {
|
||||
const primaryFilterGroup = this._filterService.getGroup('primaryFilters');
|
||||
const primaryFilters = primaryFilterGroup.filters;
|
||||
const secondaryFilters = this._filterService.getGroup('secondaryFilters').filters;
|
||||
const hasAnyFilterSet = [...primaryFilters, ...secondaryFilters].find(f => f.checked || f.indeterminate);
|
||||
|
||||
if (!hasAnyFilterSet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newPageSpecificFilters = this._annotationProcessingService.getAnnotationFilter(newAnnotations);
|
||||
|
||||
handleFilterDelta(currentFilters, newPageSpecificFilters, primaryFilters);
|
||||
this._filterService.addFilterGroup({
|
||||
...primaryFilterGroup,
|
||||
filters: primaryFilters,
|
||||
});
|
||||
}
|
||||
|
||||
private _findAndDeleteAnnotation(id: string) {
|
||||
const viewerAnnotation = this._pdf.annotationManager.getAnnotationById(id);
|
||||
if (viewerAnnotation) {
|
||||
this._pdf.deleteAnnotations([viewerAnnotation]);
|
||||
}
|
||||
}
|
||||
|
||||
private _openFullScreen() {
|
||||
const documentElement = document.documentElement;
|
||||
if (documentElement.requestFullscreen) {
|
||||
documentElement.requestFullscreen().then();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleIgnoreAnnotationsDrawing(hideSkipped: boolean): void {
|
||||
const ignored = this._pdf.getAnnotations(a => a.getCustomData('skipped'));
|
||||
if (hideSkipped) {
|
||||
this._pdf.hideAnnotations(ignored);
|
||||
} else {
|
||||
this._pdf.showAnnotations(ignored);
|
||||
}
|
||||
}
|
||||
|
||||
private _setAnnotationsOpacity(annotations: Annotation[], restoreToOriginal: boolean = false) {
|
||||
annotations.forEach(annotation => {
|
||||
annotation['Opacity'] = restoreToOriginal ? parseFloat(annotation.getCustomData('opacity')) : 1;
|
||||
});
|
||||
}
|
||||
|
||||
private _setAnnotationsColor(annotations: Annotation[], customData: string) {
|
||||
annotations.forEach(annotation => {
|
||||
const color = this._annotationDrawService.convertColor(annotation.getCustomData(customData));
|
||||
annotation['StrokeColor'] = color;
|
||||
annotation['FillColor'] = color;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,600 @@
|
||||
import { EventEmitter, Inject, Injectable, NgZone } from '@angular/core';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { ManualAnnotationService } from '../../../services/manual-annotation.service';
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
import { Observable } from 'rxjs';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { getFirstRelevantTextPart } from '@utils/functions';
|
||||
import { AnnotationPermissions } from '@models/file/annotation.permissions';
|
||||
import { DossiersDialogService } from '../../../services/dossiers-dialog.service';
|
||||
import { BASE_HREF } from '../../../../../tokens';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { Core } from '@pdftron/webviewer';
|
||||
import { Dossier, IAddRedactionRequest, ILegalBasisChangeRequest, IRectangle, IResizeRequest } from '@red/domain';
|
||||
import { toPosition } from '../../../utils/pdf-calculation.utils';
|
||||
import { AnnotationDrawService } from './annotation-draw.service';
|
||||
import { translateQuads } from '@utils/pdf-coordinates';
|
||||
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
|
||||
import {
|
||||
AcceptRecommendationData,
|
||||
AcceptRecommendationDialogComponent,
|
||||
AcceptRecommendationReturnType,
|
||||
} from '../dialogs/accept-recommendation-dialog/accept-recommendation-dialog.component';
|
||||
import { defaultDialogConfig } from '@iqser/common-ui';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { FilePreviewStateService } from './file-preview-state.service';
|
||||
import { PdfViewer } from './pdf-viewer.service';
|
||||
import Annotation = Core.Annotations.Annotation;
|
||||
|
||||
@Injectable()
|
||||
export class AnnotationActionsService {
|
||||
constructor(
|
||||
@Inject(BASE_HREF) private readonly _baseHref: string,
|
||||
private readonly _ngZone: NgZone,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _permissionsService: PermissionsService,
|
||||
private readonly _manualAnnotationService: ManualAnnotationService,
|
||||
private readonly _translateService: TranslateService,
|
||||
private readonly _dialogService: DossiersDialogService,
|
||||
private readonly _dialog: MatDialog,
|
||||
private readonly _pdf: PdfViewer,
|
||||
private readonly _annotationDrawService: AnnotationDrawService,
|
||||
private readonly _activeDossiersService: ActiveDossiersService,
|
||||
private readonly _screenStateService: FilePreviewStateService,
|
||||
) {}
|
||||
|
||||
private get _dossier(): Dossier {
|
||||
return this._activeDossiersService.find(this._screenStateService.dossierId);
|
||||
}
|
||||
|
||||
acceptSuggestion($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter<AnnotationWrapper>) {
|
||||
$event?.stopPropagation();
|
||||
const { dossierId, fileId } = this._screenStateService;
|
||||
annotations.forEach(annotation => {
|
||||
this._processObsAndEmit(
|
||||
this._manualAnnotationService.approve(annotation.id, dossierId, fileId, annotation.isModifyDictionary),
|
||||
annotation,
|
||||
annotationsChanged,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
rejectSuggestion($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter<AnnotationWrapper>) {
|
||||
$event?.stopPropagation();
|
||||
const { dossierId, fileId } = this._screenStateService;
|
||||
annotations.forEach(annotation => {
|
||||
this._processObsAndEmit(
|
||||
this._manualAnnotationService.declineOrRemoveRequest(annotation, dossierId, fileId),
|
||||
annotation,
|
||||
annotationsChanged,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
forceAnnotation(
|
||||
$event: MouseEvent,
|
||||
annotations: AnnotationWrapper[],
|
||||
annotationsChanged: EventEmitter<AnnotationWrapper>,
|
||||
hint: boolean = false,
|
||||
) {
|
||||
const { dossierId, fileId } = this._screenStateService;
|
||||
const data = { dossier: this._dossier, annotations, hint };
|
||||
this._dialogService.openDialog('forceAnnotation', $event, data, (request: ILegalBasisChangeRequest) => {
|
||||
annotations.forEach(annotation => {
|
||||
this._processObsAndEmit(
|
||||
this._manualAnnotationService.force(
|
||||
{
|
||||
...request,
|
||||
annotationId: annotation.id,
|
||||
},
|
||||
dossierId,
|
||||
fileId,
|
||||
),
|
||||
annotation,
|
||||
annotationsChanged,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
changeLegalBasis($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter<AnnotationWrapper>) {
|
||||
const { dossierId, fileId } = this._screenStateService;
|
||||
this._dialogService.openDialog(
|
||||
'changeLegalBasis',
|
||||
$event,
|
||||
{ annotations, dossier: this._dossier },
|
||||
(data: { comment: string; legalBasis: string; section: string; value: string }) => {
|
||||
annotations.forEach(annotation => {
|
||||
this._processObsAndEmit(
|
||||
this._manualAnnotationService.changeLegalBasis(
|
||||
annotation.annotationId,
|
||||
dossierId,
|
||||
fileId,
|
||||
data.section,
|
||||
data.value,
|
||||
data.legalBasis,
|
||||
data.comment,
|
||||
),
|
||||
annotation,
|
||||
annotationsChanged,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
removeOrSuggestRemoveAnnotation(
|
||||
$event: MouseEvent,
|
||||
annotations: AnnotationWrapper[],
|
||||
removeFromDictionary: boolean,
|
||||
annotationsChanged: EventEmitter<AnnotationWrapper>,
|
||||
) {
|
||||
const data = {
|
||||
annotationsToRemove: annotations,
|
||||
removeFromDictionary,
|
||||
dossier: this._dossier,
|
||||
hint: annotations[0].hintDictionary,
|
||||
};
|
||||
const { dossierId, fileId } = this._screenStateService;
|
||||
this._dialogService.openDialog('removeAnnotations', $event, data, (result: { comment: string }) => {
|
||||
annotations.forEach(annotation => {
|
||||
this._processObsAndEmit(
|
||||
this._manualAnnotationService.removeOrSuggestRemoveAnnotation(
|
||||
annotation,
|
||||
dossierId,
|
||||
fileId,
|
||||
result.comment,
|
||||
removeFromDictionary,
|
||||
),
|
||||
annotation,
|
||||
annotationsChanged,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
markAsFalsePositive($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter<AnnotationWrapper>) {
|
||||
annotations.forEach(annotation => {
|
||||
this._markAsFalsePositive($event, annotation, this._getFalsePositiveText(annotation), annotationsChanged);
|
||||
});
|
||||
}
|
||||
|
||||
recategorizeImages($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter<AnnotationWrapper>) {
|
||||
const data = { annotations, dossier: this._dossier };
|
||||
const { dossierId, fileId } = this._screenStateService;
|
||||
this._dialogService.openDialog('recategorizeImage', $event, data, (res: { type: string; comment: string }) => {
|
||||
annotations.forEach(annotation => {
|
||||
this._processObsAndEmit(
|
||||
this._manualAnnotationService.recategorizeImg(annotation.annotationId, dossierId, fileId, res.type, res.comment),
|
||||
annotation,
|
||||
annotationsChanged,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
undoDirectAction($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter<AnnotationWrapper>) {
|
||||
$event?.stopPropagation();
|
||||
|
||||
const { dossierId, fileId } = this._screenStateService;
|
||||
annotations.forEach(annotation => {
|
||||
this._processObsAndEmit(
|
||||
this._manualAnnotationService.undoRequest(annotation, dossierId, fileId),
|
||||
annotation,
|
||||
annotationsChanged,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
convertRecommendationToAnnotation(
|
||||
$event: any,
|
||||
recommendations: AnnotationWrapper[],
|
||||
annotationsChanged: EventEmitter<AnnotationWrapper>,
|
||||
) {
|
||||
$event?.stopPropagation();
|
||||
|
||||
const { dossierId, fileId } = this._screenStateService;
|
||||
const dialogRef = this._dialog.open<AcceptRecommendationDialogComponent, AcceptRecommendationData, AcceptRecommendationReturnType>(
|
||||
AcceptRecommendationDialogComponent,
|
||||
{ ...defaultDialogConfig, autoFocus: true, data: { annotations: recommendations, dossierId } },
|
||||
);
|
||||
const dialogClosed = dialogRef.afterClosed().pipe(filter(value => !!value && !!value.annotations));
|
||||
dialogClosed.subscribe(({ annotations, comment: commentText }) => {
|
||||
const comment = commentText ? { text: commentText } : undefined;
|
||||
annotations.forEach(annotation => {
|
||||
this._processObsAndEmit(
|
||||
this._manualAnnotationService.addRecommendation(annotation, dossierId, fileId, comment),
|
||||
annotation,
|
||||
annotationsChanged,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getViewerAvailableActions(
|
||||
dossier: Dossier,
|
||||
annotations: AnnotationWrapper[],
|
||||
annotationsChanged: EventEmitter<AnnotationWrapper>,
|
||||
): Record<string, unknown>[] {
|
||||
const availableActions = [];
|
||||
const annotationPermissions = annotations.map(annotation => ({
|
||||
annotation,
|
||||
permissions: AnnotationPermissions.forUser(
|
||||
this._permissionsService.isApprover(dossier),
|
||||
this._userService.currentUser,
|
||||
annotation,
|
||||
),
|
||||
}));
|
||||
|
||||
// you can only resize one annotation at a time
|
||||
const canResize = annotationPermissions.length === 1 && annotationPermissions[0].permissions.canResizeAnnotation;
|
||||
if (canResize) {
|
||||
const firstAnnotation = annotations[0];
|
||||
// if we already entered resize-mode previously
|
||||
if (firstAnnotation.resizing) {
|
||||
availableActions.push({
|
||||
type: 'actionButton',
|
||||
img: this._convertPath('/assets/icons/general/check.svg'),
|
||||
title: this._translateService.instant('annotation-actions.resize-accept.label'),
|
||||
onClick: () =>
|
||||
this._ngZone.run(() => {
|
||||
this.acceptResize(null, firstAnnotation, annotationsChanged);
|
||||
}),
|
||||
});
|
||||
availableActions.push({
|
||||
type: 'actionButton',
|
||||
img: this._convertPath('/assets/icons/general/close.svg'),
|
||||
title: this._translateService.instant('annotation-actions.resize-cancel.label'),
|
||||
onClick: () =>
|
||||
this._ngZone.run(() => {
|
||||
this.cancelResize(null, firstAnnotation, annotationsChanged);
|
||||
}),
|
||||
});
|
||||
return availableActions;
|
||||
}
|
||||
|
||||
availableActions.push({
|
||||
type: 'actionButton',
|
||||
img: this._convertPath('/assets/icons/general/resize.svg'),
|
||||
title: this._translateService.instant('annotation-actions.resize.label'),
|
||||
onClick: () => this._ngZone.run(() => this.resize(null, annotations[0])),
|
||||
});
|
||||
}
|
||||
|
||||
const canChangeLegalBasis = annotationPermissions.reduce((acc, next) => acc && next.permissions.canChangeLegalBasis, true);
|
||||
if (canChangeLegalBasis) {
|
||||
availableActions.push({
|
||||
type: 'actionButton',
|
||||
img: this._convertPath('/assets/icons/general/edit.svg'),
|
||||
title: this._translateService.instant('annotation-actions.edit-reason.label'),
|
||||
onClick: () =>
|
||||
this._ngZone.run(() => {
|
||||
this.changeLegalBasis(null, annotations, annotationsChanged);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const canRecategorizeImage = annotationPermissions.reduce((acc, next) => acc && next.permissions.canRecategorizeImage, true);
|
||||
if (canRecategorizeImage) {
|
||||
availableActions.push({
|
||||
type: 'actionButton',
|
||||
img: this._convertPath('/assets/icons/general/thumb-down.svg'),
|
||||
title: this._translateService.instant('annotation-actions.recategorize-image'),
|
||||
onClick: () =>
|
||||
this._ngZone.run(() => {
|
||||
this.recategorizeImages(null, annotations, annotationsChanged);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const canRemoveOrSuggestToRemoveFromDictionary = annotationPermissions.reduce(
|
||||
(acc, next) => acc && next.permissions.canRemoveOrSuggestToRemoveFromDictionary,
|
||||
true,
|
||||
);
|
||||
if (canRemoveOrSuggestToRemoveFromDictionary) {
|
||||
availableActions.push({
|
||||
type: 'actionButton',
|
||||
img: this._convertPath('/assets/icons/general/remove-from-dict.svg'),
|
||||
title: this._translateService.instant('annotation-actions.remove-annotation.remove-from-dict'),
|
||||
onClick: () =>
|
||||
this._ngZone.run(() => {
|
||||
this.removeOrSuggestRemoveAnnotation(null, annotations, true, annotationsChanged);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const canAcceptRecommendation = annotationPermissions.reduce((acc, next) => acc && next.permissions.canAcceptRecommendation, true);
|
||||
if (canAcceptRecommendation) {
|
||||
availableActions.push({
|
||||
type: 'actionButton',
|
||||
img: this._convertPath('/assets/icons/general/check.svg'),
|
||||
title: this._translateService.instant('annotation-actions.accept-recommendation.label'),
|
||||
onClick: () =>
|
||||
this._ngZone.run(() => {
|
||||
this.convertRecommendationToAnnotation(null, annotations, annotationsChanged);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const canAcceptSuggestion = annotationPermissions.reduce((acc, next) => acc && next.permissions.canAcceptSuggestion, true);
|
||||
if (canAcceptSuggestion) {
|
||||
availableActions.push({
|
||||
type: 'actionButton',
|
||||
img: this._convertPath('/assets/icons/general/check.svg'),
|
||||
title: this._translateService.instant('annotation-actions.accept-suggestion.label'),
|
||||
onClick: () =>
|
||||
this._ngZone.run(() => {
|
||||
this.acceptSuggestion(null, annotations, annotationsChanged);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const canUndo = annotationPermissions.reduce((acc, next) => acc && next.permissions.canUndo, true);
|
||||
if (canUndo) {
|
||||
availableActions.push({
|
||||
type: 'actionButton',
|
||||
img: this._convertPath('/assets/icons/general/undo.svg'),
|
||||
title: this._translateService.instant('annotation-actions.undo'),
|
||||
onClick: () =>
|
||||
this._ngZone.run(() => {
|
||||
this.undoDirectAction(null, annotations, annotationsChanged);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const canMarkAsFalsePositive = annotationPermissions.reduce((acc, next) => acc && next.permissions.canMarkAsFalsePositive, true);
|
||||
if (canMarkAsFalsePositive) {
|
||||
availableActions.push({
|
||||
type: 'actionButton',
|
||||
img: this._convertPath('/assets/icons/general/thumb-down.svg'),
|
||||
title: this._translateService.instant('annotation-actions.remove-annotation.false-positive'),
|
||||
onClick: () =>
|
||||
this._ngZone.run(() => {
|
||||
this.markAsFalsePositive(null, annotations, annotationsChanged);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const canForceRedaction = annotationPermissions.reduce((acc, next) => acc && next.permissions.canForceRedaction, true);
|
||||
if (canForceRedaction) {
|
||||
availableActions.push({
|
||||
type: 'actionButton',
|
||||
img: this._convertPath('/assets/icons/general/thumb-up.svg'),
|
||||
title: this._translateService.instant('annotation-actions.force-redaction.label'),
|
||||
onClick: () =>
|
||||
this._ngZone.run(() => {
|
||||
this.forceAnnotation(null, annotations, annotationsChanged);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const canForceHint = annotationPermissions.reduce((acc, next) => acc && next.permissions.canForceHint, true);
|
||||
if (canForceHint) {
|
||||
availableActions.push({
|
||||
type: 'actionButton',
|
||||
img: this._convertPath('/assets/icons/general/thumb-up.svg'),
|
||||
title: this._translateService.instant('annotation-actions.force-hint.label'),
|
||||
onClick: () =>
|
||||
this._ngZone.run(() => {
|
||||
this.forceAnnotation(null, annotations, annotationsChanged, true);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const canRejectSuggestion = annotationPermissions.reduce((acc, next) => acc && next.permissions.canRejectSuggestion, true);
|
||||
if (canRejectSuggestion) {
|
||||
availableActions.push({
|
||||
type: 'actionButton',
|
||||
img: this._convertPath('/assets/icons/general/close.svg'),
|
||||
title: this._translateService.instant('annotation-actions.reject-suggestion'),
|
||||
onClick: () =>
|
||||
this._ngZone.run(() => {
|
||||
this.rejectSuggestion(null, annotations, annotationsChanged);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const canRemoveOrSuggestToRemoveOnlyHere = annotationPermissions.reduce(
|
||||
(acc, next) => acc && next.permissions.canRemoveOrSuggestToRemoveOnlyHere,
|
||||
true,
|
||||
);
|
||||
if (canRemoveOrSuggestToRemoveOnlyHere) {
|
||||
availableActions.push({
|
||||
type: 'actionButton',
|
||||
img: this._convertPath('/assets/icons/general/trash.svg'),
|
||||
title: this._translateService.instant('annotation-actions.remove-annotation.only-here'),
|
||||
onClick: () =>
|
||||
this._ngZone.run(() => {
|
||||
this.removeOrSuggestRemoveAnnotation(null, annotations, false, annotationsChanged);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return availableActions;
|
||||
}
|
||||
|
||||
updateHiddenAnnotation(annotations: AnnotationWrapper[], viewerAnnotations: Annotation[], hidden: boolean) {
|
||||
const annotationId = viewerAnnotations[0].Id;
|
||||
const annotationToBeUpdated = annotations.find((a: AnnotationWrapper) => a.annotationId === annotationId);
|
||||
annotationToBeUpdated.hidden = hidden;
|
||||
}
|
||||
|
||||
resize($event: MouseEvent, annotationWrapper: AnnotationWrapper) {
|
||||
$event?.stopPropagation();
|
||||
|
||||
annotationWrapper.resizing = true;
|
||||
|
||||
const viewerAnnotation = this._pdf.annotationManager.getAnnotationById(annotationWrapper.id);
|
||||
viewerAnnotation.ReadOnly = false;
|
||||
viewerAnnotation.Hidden = false;
|
||||
viewerAnnotation.disableRotationControl();
|
||||
this._pdf.annotationManager.redrawAnnotation(viewerAnnotation);
|
||||
this._pdf.annotationManager.selectAnnotation(viewerAnnotation);
|
||||
|
||||
this._annotationDrawService.annotationToQuads(viewerAnnotation);
|
||||
}
|
||||
|
||||
acceptResize($event: MouseEvent, annotationWrapper: AnnotationWrapper, annotationsChanged?: EventEmitter<AnnotationWrapper>) {
|
||||
const data = { dossier: this._dossier };
|
||||
const fileId = this._screenStateService.fileId;
|
||||
this._dialogService.openDialog('resizeAnnotation', $event, data, async (result: { comment: string }) => {
|
||||
const textAndPositions = await this._extractTextAndPositions(annotationWrapper.id);
|
||||
const text =
|
||||
annotationWrapper.value === 'Rectangle' ? 'Rectangle' : annotationWrapper.isImage ? 'Image' : textAndPositions.text;
|
||||
|
||||
const resizeRequest: IResizeRequest = {
|
||||
annotationId: annotationWrapper.id,
|
||||
comment: result.comment,
|
||||
positions: textAndPositions.positions,
|
||||
value: text,
|
||||
};
|
||||
|
||||
this._processObsAndEmit(
|
||||
this._manualAnnotationService.resizeOrSuggestToResize(annotationWrapper, data.dossier.dossierId, fileId, resizeRequest),
|
||||
annotationWrapper,
|
||||
annotationsChanged,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
cancelResize($event: MouseEvent, annotationWrapper: AnnotationWrapper, annotationsChanged: EventEmitter<AnnotationWrapper>) {
|
||||
$event?.stopPropagation();
|
||||
|
||||
annotationWrapper.resizing = false;
|
||||
|
||||
const viewerAnnotation = this._pdf.annotationManager.getAnnotationById(annotationWrapper.id);
|
||||
viewerAnnotation.ReadOnly = false;
|
||||
this._pdf.annotationManager.redrawAnnotation(viewerAnnotation);
|
||||
this._pdf.annotationManager.deselectAllAnnotations();
|
||||
annotationsChanged.emit(annotationWrapper);
|
||||
}
|
||||
|
||||
private _processObsAndEmit(
|
||||
obs: Observable<unknown>,
|
||||
annotation: AnnotationWrapper,
|
||||
annotationsChanged: EventEmitter<AnnotationWrapper>,
|
||||
) {
|
||||
obs.subscribe({
|
||||
next: () => {
|
||||
annotationsChanged.emit(annotation);
|
||||
},
|
||||
error: () => {
|
||||
annotationsChanged.emit();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _getFalsePositiveText(annotation: AnnotationWrapper) {
|
||||
if (annotation.canBeMarkedAsFalsePositive) {
|
||||
let text: string;
|
||||
if (annotation.hasTextAfter) {
|
||||
text = getFirstRelevantTextPart(annotation.textAfter, 'FORWARD');
|
||||
return text ? (annotation.value + text).trim() : annotation.value;
|
||||
}
|
||||
if (annotation.hasTextAfter) {
|
||||
text = getFirstRelevantTextPart(annotation.textBefore, 'BACKWARD');
|
||||
return text ? (text + annotation.value).trim() : annotation.value;
|
||||
} else {
|
||||
return annotation.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _markAsFalsePositive(
|
||||
$event: MouseEvent,
|
||||
annotation: AnnotationWrapper,
|
||||
text: string,
|
||||
annotationsChanged: EventEmitter<AnnotationWrapper>,
|
||||
) {
|
||||
$event?.stopPropagation();
|
||||
|
||||
const falsePositiveRequest: IAddRedactionRequest = {};
|
||||
falsePositiveRequest.reason = annotation.id;
|
||||
falsePositiveRequest.value = text;
|
||||
falsePositiveRequest.type = 'false_positive';
|
||||
falsePositiveRequest.positions = annotation.positions;
|
||||
falsePositiveRequest.addToDictionary = true;
|
||||
falsePositiveRequest.comment = { text: 'False Positive' };
|
||||
const { dossierId, fileId } = this._screenStateService;
|
||||
|
||||
this._processObsAndEmit(
|
||||
this._manualAnnotationService.addAnnotation(falsePositiveRequest, dossierId, fileId),
|
||||
annotation,
|
||||
annotationsChanged,
|
||||
);
|
||||
}
|
||||
|
||||
private _convertPath(path: string): string {
|
||||
return this._baseHref + path;
|
||||
}
|
||||
|
||||
private async _extractTextAndPositions(annotationId: string) {
|
||||
const viewerAnnotation = this._pdf.annotationManager.getAnnotationById(annotationId);
|
||||
|
||||
const document = await this._pdf.documentViewer.getDocument().getPDFDoc();
|
||||
const page = await document.getPage(viewerAnnotation.getPageNumber());
|
||||
if (viewerAnnotation instanceof this._pdf.Annotations.TextHighlightAnnotation) {
|
||||
const words = [];
|
||||
const rectangles: IRectangle[] = [];
|
||||
for (const quad of viewerAnnotation.Quads) {
|
||||
const rect = toPosition(
|
||||
viewerAnnotation.getPageNumber(),
|
||||
this._pdf.documentViewer.getPageHeight(viewerAnnotation.getPageNumber()),
|
||||
this._translateQuads(viewerAnnotation.getPageNumber(), quad),
|
||||
);
|
||||
rectangles.push(rect);
|
||||
|
||||
// TODO: this is an educated guess for lines that are close together
|
||||
// TODO: so that we do not extract text from line above/line below
|
||||
const percentHeightOffset = rect.height / 10;
|
||||
|
||||
const pdfNetRect = new this._pdf.instance.Core.PDFNet.Rect(
|
||||
rect.topLeft.x,
|
||||
rect.topLeft.y + percentHeightOffset,
|
||||
rect.topLeft.x + rect.width,
|
||||
rect.topLeft.y + rect.height - percentHeightOffset,
|
||||
);
|
||||
const quadWords = await this._extractTextFromRect(page, pdfNetRect);
|
||||
words.push(...quadWords);
|
||||
}
|
||||
|
||||
console.log(words.join(' '));
|
||||
|
||||
return {
|
||||
text: words.join(' '),
|
||||
positions: rectangles,
|
||||
};
|
||||
} else {
|
||||
const rect = toPosition(
|
||||
viewerAnnotation.getPageNumber(),
|
||||
this._pdf.documentViewer.getPageHeight(viewerAnnotation.getPageNumber()),
|
||||
this._annotationDrawService.annotationToQuads(viewerAnnotation),
|
||||
);
|
||||
return {
|
||||
positions: [rect],
|
||||
text: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private _translateQuads(page: number, quads: any) {
|
||||
const rotation = this._pdf.documentViewer.getCompleteRotation(page);
|
||||
return translateQuads(page, rotation, quads);
|
||||
}
|
||||
|
||||
private async _extractTextFromRect(page: Core.PDFNet.Page, rect: Core.PDFNet.Rect) {
|
||||
const txt = await this._pdf.PDFNet.TextExtractor.create();
|
||||
txt.begin(page, rect); // Read the page.
|
||||
|
||||
const words: string[] = [];
|
||||
// Extract words one by one.
|
||||
let line = await txt.getFirstLine();
|
||||
for (; await line.isValid(); line = await line.getNextLine()) {
|
||||
for (let word = await line.getFirstWord(); await word.isValid(); word = await word.getNextWord()) {
|
||||
words.push(await word.getString());
|
||||
}
|
||||
}
|
||||
return words;
|
||||
}
|
||||
}
|
||||
@ -13,7 +13,6 @@ import { EditDossierDownloadPackageComponent } from '../dialogs/edit-dossier-dia
|
||||
import { EditDossierDictionaryComponent } from '../dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component';
|
||||
import { EditDossierAttributesComponent } from '../dialogs/edit-dossier-dialog/attributes/edit-dossier-attributes.component';
|
||||
import { EditDossierTeamComponent } from '../dialogs/edit-dossier-dialog/edit-dossier-team/edit-dossier-team.component';
|
||||
import { EditDossierDeletedDocumentsComponent } from '../dialogs/edit-dossier-dialog/deleted-documents/edit-dossier-deleted-documents.component';
|
||||
import { DateColumnComponent } from './components/date-column/date-column.component';
|
||||
|
||||
const components = [
|
||||
@ -23,7 +22,6 @@ const components = [
|
||||
EditDossierDictionaryComponent,
|
||||
EditDossierAttributesComponent,
|
||||
EditDossierTeamComponent,
|
||||
EditDossierDeletedDocumentsComponent,
|
||||
FileActionsComponent,
|
||||
DateColumnComponent,
|
||||
];
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<ng-container *ngIf="stats$ | async as stats">
|
||||
<div class="cell">
|
||||
<redaction-dossiers-listing-dossier-name [dossierStats]="stats" [dossier]="dossier"></redaction-dossiers-listing-dossier-name>
|
||||
<redaction-dossier-name-column [dossierStats]="stats" [dossier]="dossier"></redaction-dossier-name-column>
|
||||
</div>
|
||||
|
||||
<div class="cell">
|
||||
|
||||
@ -1,18 +1,27 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { Dossier, DossierStats } from '@red/domain';
|
||||
import { DossierStats } from '@red/domain';
|
||||
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
|
||||
import * as moment from 'moment';
|
||||
import { List } from '@iqser/common-ui';
|
||||
|
||||
const DUE_DATE_WARN_DAYS = 14;
|
||||
|
||||
interface PartialDossier {
|
||||
readonly dossierName: string;
|
||||
readonly dossierTemplateId: string;
|
||||
readonly dueDate?: string;
|
||||
readonly date?: string;
|
||||
readonly memberIds: List;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-dossiers-listing-dossier-name',
|
||||
templateUrl: './dossiers-listing-dossier-name.component.html',
|
||||
styleUrls: ['./dossiers-listing-dossier-name.component.scss'],
|
||||
selector: 'redaction-dossier-name-column',
|
||||
templateUrl: './dossier-name-column.component.html',
|
||||
styleUrls: ['./dossier-name-column.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DossiersListingDossierNameComponent {
|
||||
@Input() dossier: Dossier;
|
||||
export class DossierNameColumnComponent {
|
||||
@Input() dossier: PartialDossier;
|
||||
@Input() dossierStats: DossierStats;
|
||||
|
||||
constructor(private readonly _dossierTemplatesService: DossierTemplatesService) {}
|
||||
@ -0,0 +1,21 @@
|
||||
<div>
|
||||
<div
|
||||
[class.error]="file.isError"
|
||||
[class.initial-processing]="file.isInitialProcessing"
|
||||
[matTooltip]="file.filename"
|
||||
class="table-item-title"
|
||||
matTooltipPosition="above"
|
||||
>
|
||||
{{ file.filename }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="primaryAttribute" class="small-label">
|
||||
<div class="primary-attribute">
|
||||
<span [matTooltip]="primaryAttribute" matTooltipPosition="above">
|
||||
{{ primaryAttribute }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<redaction-file-stats [file]="file"></redaction-file-stats>
|
||||
@ -0,0 +1,18 @@
|
||||
@use 'common-mixins';
|
||||
|
||||
.table-item-title {
|
||||
max-width: 25vw;
|
||||
|
||||
&.error {
|
||||
color: var(--iqser-red-1);
|
||||
}
|
||||
|
||||
&.initial-processing {
|
||||
color: var(--iqser-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
.primary-attribute {
|
||||
padding-top: 6px;
|
||||
@include common-mixins.line-clamp(1);
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
|
||||
import { PrimaryFileAttributeService } from '@services/primary-file-attribute.service';
|
||||
import { FileAttributes } from '@red/domain';
|
||||
|
||||
interface PartialFile {
|
||||
readonly isError: boolean;
|
||||
readonly isInitialProcessing: boolean;
|
||||
readonly filename: string;
|
||||
readonly numberOfPages: number;
|
||||
readonly excludedPages: number[];
|
||||
readonly lastOCRTime?: string;
|
||||
readonly fileAttributes: FileAttributes;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-file-name-column',
|
||||
templateUrl: './file-name-column.component.html',
|
||||
styleUrls: ['./file-name-column.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FileNameColumnComponent implements OnChanges {
|
||||
@Input() file: PartialFile;
|
||||
@Input() dossierTemplateId: string;
|
||||
primaryAttribute: string;
|
||||
|
||||
constructor(private readonly _primaryFileAttributeService: PrimaryFileAttributeService) {}
|
||||
|
||||
ngOnChanges() {
|
||||
this.primaryAttribute = this._primaryFileAttributeService.getPrimaryFileAttributeValue(this.file, this.dossierTemplateId);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
<div class="small-label stats-subtitle">
|
||||
<div>
|
||||
<mat-icon svgIcon="iqser:pages"></mat-icon>
|
||||
{{ file.numberOfPages }}
|
||||
</div>
|
||||
<div>
|
||||
<mat-icon svgIcon="red:exclude-pages"></mat-icon>
|
||||
{{ file.excludedPages.length }}
|
||||
</div>
|
||||
<div *ngIf="file.lastOCRTime" [matTooltipPosition]="'above'" [matTooltip]="'dossier-overview.ocr-performed' | translate">
|
||||
<mat-icon svgIcon="iqser:ocr"></mat-icon>
|
||||
{{ file.lastOCRTime | date: 'mediumDate' }}
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,11 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-file-stats',
|
||||
templateUrl: './file-stats.component.html',
|
||||
styleUrls: ['./file-stats.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FileStatsComponent {
|
||||
@Input() file: { numberOfPages: number; excludedPages: number[]; lastOCRTime?: string };
|
||||
}
|
||||
@ -185,7 +185,7 @@ export class DictionaryService extends EntitiesService<Dictionary, IDictionary>
|
||||
|
||||
for (const dictionary of this._dictionariesMapService.get(dossierTemplateId)) {
|
||||
if (!dictionary.virtual && dictionary.addToDictionaryAction) {
|
||||
possibleDictionaries.push(dictionary as Dictionary);
|
||||
possibleDictionaries.push(dictionary);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -29,6 +29,8 @@ import { ExpandableFileActionsComponent } from './components/expandable-file-act
|
||||
import { ProcessingIndicatorComponent } from '@shared/components/processing-indicator/processing-indicator.component';
|
||||
import { DossierStateComponent } from '@shared/components/dossier-state/dossier-state.component';
|
||||
import { DossiersListingDossierNameComponent } from '@shared/components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component';
|
||||
import { FileStatsComponent } from './components/file-stats/file-stats.component';
|
||||
import { FileNameColumnComponent } from '@shared/components/file-name-column/file-name-column.component';
|
||||
|
||||
const buttons = [FileDownloadBtnComponent, UserButtonComponent];
|
||||
|
||||
@ -47,6 +49,8 @@ const components = [
|
||||
ProcessingIndicatorComponent,
|
||||
DossierStateComponent,
|
||||
DossiersListingDossierNameComponent,
|
||||
FileStatsComponent,
|
||||
FileNameColumnComponent,
|
||||
|
||||
...buttons,
|
||||
];
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { EntitiesService, List, mapEach, RequiredParam, Toaster, Validate } from '@iqser/common-ui';
|
||||
import { DossierTemplate, IDossierTemplate } from '@red/domain';
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { forkJoin, Observable, of } from 'rxjs';
|
||||
import { forkJoin, Observable, of, throwError } from 'rxjs';
|
||||
import { FileAttributesService } from './file-attributes.service';
|
||||
import { catchError, mapTo, switchMap, tap } from 'rxjs/operators';
|
||||
import { DossierTemplateStatsService } from '@services/entity-services/dossier-template-stats.service';
|
||||
|
||||
@ -3,8 +3,11 @@ import { Injector, ProviderToken } from '@angular/core';
|
||||
import { DossiersService } from '../dossiers/dossiers.service';
|
||||
|
||||
export const dossiersServiceResolver = (injector: Injector) => {
|
||||
const route = injector.get<ActivatedRoute>(ActivatedRoute);
|
||||
const token: ProviderToken<DossiersService> = (route.firstChild || route).snapshot.data.dossiersService;
|
||||
let route: ActivatedRoute = injector.get<ActivatedRoute>(ActivatedRoute);
|
||||
while (route.firstChild) {
|
||||
route = route.firstChild;
|
||||
}
|
||||
const token: ProviderToken<DossiersService> = route.snapshot.data.dossiersService;
|
||||
return injector.get<DossiersService>(token);
|
||||
};
|
||||
|
||||
|
||||
@ -26,21 +26,6 @@ export class FileManagementService extends GenericService<unknown> {
|
||||
return super._post(fileIds, `delete/${dossierId}`).pipe(switchMap(() => this._filesService.loadAll(dossierId, routerPath)));
|
||||
}
|
||||
|
||||
@Validate()
|
||||
hardDelete(@RequiredParam() dossierId: string, @RequiredParam() fileIds: List) {
|
||||
const queryParams = fileIds.map<QueryParam>(id => ({ key: 'fileIds', value: id }));
|
||||
return super
|
||||
.delete({}, `delete/hard-delete/${dossierId}`, queryParams)
|
||||
.pipe(switchMap(() => this._dossierStatsService.getFor([dossierId])));
|
||||
}
|
||||
|
||||
@Validate()
|
||||
restore(@RequiredParam() files: List<IRouterPath>, @RequiredParam() dossierId: string) {
|
||||
const fileIds = files.map(f => f.id);
|
||||
const routerPath: string = files[0].routerPath;
|
||||
return this._post(fileIds, `delete/restore/${dossierId}`).pipe(switchMap(() => this._filesService.loadAll(dossierId, routerPath)));
|
||||
}
|
||||
|
||||
@Validate()
|
||||
rotatePage(@RequiredParam() body: IPageRotationRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) {
|
||||
return this._post(body, `rotate/${dossierId}/${fileId}`);
|
||||
|
||||
@ -99,12 +99,4 @@ export class FilesService extends EntitiesService<File, IFile> {
|
||||
switchMap(() => this.loadAll(dossierId, routerPath)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the deleted files for a dossier.
|
||||
*/
|
||||
@Validate()
|
||||
getDeletedFilesFor(@RequiredParam() dossierId: string): Observable<IFile[]> {
|
||||
return this.getAll(`${this._defaultModelPath}/softdeleted/${dossierId}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,83 +0,0 @@
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { EntitiesService, mapEach, QueryParam, RequiredParam, Toaster, Validate } from '@iqser/common-ui';
|
||||
import { Dossier, IDossier, TrashDossier } from '@red/domain';
|
||||
import { catchError, switchMap, tap } from 'rxjs/operators';
|
||||
import { firstValueFrom, Observable, of } from 'rxjs';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import * as moment from 'moment';
|
||||
import { ConfigService } from '../config.service';
|
||||
import { PermissionsService } from '../permissions.service';
|
||||
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
|
||||
|
||||
export interface IDossiersStats {
|
||||
totalPeople: number;
|
||||
totalAnalyzedPages: number;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class TrashDossiersService extends EntitiesService<TrashDossier, IDossier> {
|
||||
constructor(
|
||||
protected readonly _injector: Injector,
|
||||
private readonly _toaster: Toaster,
|
||||
private readonly _configService: ConfigService,
|
||||
private readonly _permissionsService: PermissionsService,
|
||||
private readonly _activeDossiersService: ActiveDossiersService,
|
||||
) {
|
||||
super(_injector, TrashDossier, 'dossier');
|
||||
}
|
||||
|
||||
loadAll(): Observable<TrashDossier[]> {
|
||||
return this.#getDeleted().pipe(
|
||||
mapEach(
|
||||
dossier => new TrashDossier(dossier, this.#getRestoreDate(dossier), this._permissionsService.canDeleteDossier(dossier)),
|
||||
),
|
||||
tap(dossiers => this.setEntities(dossiers)),
|
||||
);
|
||||
}
|
||||
|
||||
delete(dossier: Dossier): Observable<unknown> {
|
||||
const showToast = () => {
|
||||
this._toaster.error(_('dossier-listing.delete.delete-failed'), { params: dossier });
|
||||
return of({});
|
||||
};
|
||||
return super.delete(dossier.dossierId).pipe(
|
||||
switchMap(() => this._activeDossiersService.loadAll()),
|
||||
catchError(showToast),
|
||||
);
|
||||
}
|
||||
|
||||
@Validate()
|
||||
restore(@RequiredParam() dossierIds: string[]): Promise<unknown> {
|
||||
return firstValueFrom(
|
||||
this._post(dossierIds, 'deleted-dossiers/restore').pipe(
|
||||
switchMap(() => this._activeDossiersService.loadAll()),
|
||||
tap(() => this.#removeDossiers(dossierIds)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@Validate()
|
||||
hardDelete(@RequiredParam() dossierIds: string[]): Promise<unknown> {
|
||||
const body = dossierIds.map<QueryParam>(id => ({ key: 'dossierId', value: id }));
|
||||
return firstValueFrom(
|
||||
super.delete(body, 'deleted-dossiers/hard-delete', body).pipe(
|
||||
switchMap(() => this._activeDossiersService.loadAll()),
|
||||
tap(() => this.#removeDossiers(dossierIds)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#getRestoreDate(dossier: IDossier): string {
|
||||
return moment(dossier.softDeletedTime).add(this._configService.values.DELETE_RETENTION_HOURS, 'hours').toISOString();
|
||||
}
|
||||
|
||||
#getDeleted(): Observable<IDossier[]> {
|
||||
return this.getAll('deleted-dossiers');
|
||||
}
|
||||
|
||||
#removeDossiers(dossierIds: string[]): void {
|
||||
this.setEntities(this.all.filter(dossier => !dossierIds.includes(dossier.id)));
|
||||
}
|
||||
}
|
||||
138
apps/red-ui/src/app/services/entity-services/trash.service.ts
Normal file
138
apps/red-ui/src/app/services/entity-services/trash.service.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { GenericService, List, mapEach, QueryParam, RequiredParam, Toaster, Validate } from '@iqser/common-ui';
|
||||
import { Dossier, File, IDossier, IFile, TrashDossier, TrashFile, TrashItem } from '@red/domain';
|
||||
import { catchError, switchMap, take } from 'rxjs/operators';
|
||||
import { forkJoin, map, Observable, of } from 'rxjs';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { ConfigService } from '../config.service';
|
||||
import { PermissionsService } from '../permissions.service';
|
||||
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { flatMap } from 'lodash';
|
||||
import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
|
||||
import { FilesService } from '@services/entity-services/files.service';
|
||||
|
||||
export interface IDossiersStats {
|
||||
totalPeople: number;
|
||||
totalAnalyzedPages: number;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class TrashService extends GenericService<TrashItem> {
|
||||
constructor(
|
||||
protected readonly _injector: Injector,
|
||||
private readonly _toaster: Toaster,
|
||||
private readonly _configService: ConfigService,
|
||||
private readonly _permissionsService: PermissionsService,
|
||||
private readonly _activeDossiersService: ActiveDossiersService,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _dossierStatsService: DossierStatsService,
|
||||
private readonly _filesService: FilesService,
|
||||
) {
|
||||
super(_injector, '');
|
||||
}
|
||||
|
||||
deleteDossier(dossier: Dossier): Observable<unknown> {
|
||||
const showToast = () => {
|
||||
this._toaster.error(_('dossier-listing.delete.delete-failed'), { params: dossier });
|
||||
return of({});
|
||||
};
|
||||
return this.delete(dossier.dossierId, 'dossier').pipe(
|
||||
switchMap(() => this._activeDossiersService.loadAll()),
|
||||
catchError(showToast),
|
||||
);
|
||||
}
|
||||
|
||||
@Validate()
|
||||
restore(@RequiredParam() items: TrashItem[]): Observable<unknown> {
|
||||
return this.#executeAction(
|
||||
items,
|
||||
(dossierIds: string[]) => this._restoreDossiers(dossierIds),
|
||||
(dossierId: string, fileIds: string[]) => this._restoreFiles(dossierId, fileIds),
|
||||
);
|
||||
}
|
||||
|
||||
@Validate()
|
||||
hardDelete(@RequiredParam() items: TrashItem[]): Observable<unknown> {
|
||||
return this.#executeAction(
|
||||
items,
|
||||
(dossierIds: string[]) => this._hardDeleteDossiers(dossierIds),
|
||||
(dossierId: string, fileIds: string[]) => this._hardDeleteFiles(dossierId, fileIds),
|
||||
);
|
||||
}
|
||||
|
||||
getDossiers(): Observable<TrashDossier[]> {
|
||||
return this.getAll<IDossier[]>('deleted-dossiers').pipe(
|
||||
mapEach(
|
||||
dossier =>
|
||||
new TrashDossier(
|
||||
dossier,
|
||||
this._configService.values.DELETE_RETENTION_HOURS as number,
|
||||
this._permissionsService.canDeleteDossier(dossier),
|
||||
),
|
||||
),
|
||||
// TODO: API to include deleted dossiers
|
||||
// switchMap(dossiers => this._dossierStatsService.getFor(dossiers.map(d => d.id)).pipe(mapTo(dossiers))),
|
||||
);
|
||||
}
|
||||
|
||||
getFiles(dossierIds = this._activeDossiersService.all.map(d => d.id)): Observable<TrashFile[]> {
|
||||
return this._post<Record<string, IFile[]>>(dossierIds, 'status/softdeleted').pipe(
|
||||
map(res => flatMap(Object.values(res))),
|
||||
mapEach(file => new File(file, this._userService.getNameForId(file.assignee), this._activeDossiersService.routerPath)),
|
||||
mapEach(file => {
|
||||
const dossierTemplateId = this._activeDossiersService.find(file.dossierId).dossierTemplateId;
|
||||
return new TrashFile(
|
||||
file,
|
||||
dossierTemplateId,
|
||||
this._configService.values.DELETE_RETENTION_HOURS as number,
|
||||
this._permissionsService.canDeleteFile(file),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
all(): Observable<TrashItem[]> {
|
||||
return forkJoin([this.getDossiers(), this.getFiles()]).pipe(map(result => flatMap<TrashItem>(result)));
|
||||
}
|
||||
|
||||
#executeAction(items: TrashItem[], dossiersFn: (...args) => Observable<unknown>, filesFn: (...args) => Observable<unknown>) {
|
||||
const requests$ = [];
|
||||
|
||||
const dossierIds = items.filter(i => i.isDossier).map(i => i.id);
|
||||
if (dossierIds.length) {
|
||||
requests$.push(dossiersFn(dossierIds));
|
||||
}
|
||||
|
||||
const files = items.filter(i => i.isFile) as TrashFile[];
|
||||
const dossierId: string = files.length ? files[0].dossierId : '';
|
||||
const fileIds = files.map(i => i.id);
|
||||
if (files.length) {
|
||||
requests$.push(filesFn(dossierId, fileIds));
|
||||
}
|
||||
|
||||
return forkJoin(requests$.map(r => r.pipe(take(1))));
|
||||
}
|
||||
|
||||
private _restoreDossiers(@RequiredParam() dossierIds: string[]): Observable<unknown> {
|
||||
return this._post(dossierIds, 'deleted-dossiers/restore').pipe(switchMap(() => this._activeDossiersService.loadAll()));
|
||||
}
|
||||
|
||||
private _hardDeleteDossiers(@RequiredParam() dossierIds: string[]): Observable<unknown> {
|
||||
const body = dossierIds.map<QueryParam>(id => ({ key: 'dossierId', value: id }));
|
||||
return this.delete(body, 'deleted-dossiers/hard-delete', body);
|
||||
}
|
||||
|
||||
private _hardDeleteFiles(@RequiredParam() dossierId: string, @RequiredParam() fileIds: List) {
|
||||
const queryParams = fileIds.map<QueryParam>(id => ({ key: 'fileIds', value: id }));
|
||||
return super
|
||||
.delete({}, `delete/hard-delete/${dossierId}`, queryParams)
|
||||
.pipe(switchMap(() => this._dossierStatsService.getFor([dossierId])));
|
||||
}
|
||||
|
||||
private _restoreFiles(@RequiredParam() dossierId: string, @RequiredParam() fileIds: List) {
|
||||
return this._post(fileIds, `delete/restore/${dossierId}`).pipe(switchMap(() => this._filesService.loadAll(dossierId, 'dossiers')));
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FileAttributesService } from './entity-services/file-attributes.service';
|
||||
import { IFile } from '@red/domain';
|
||||
import { FileAttributes } from '@red/domain';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@ -8,7 +8,7 @@ import { IFile } from '@red/domain';
|
||||
export class PrimaryFileAttributeService {
|
||||
constructor(private readonly _fileAttributesService: FileAttributesService) {}
|
||||
|
||||
getPrimaryFileAttributeValue(file: IFile, dossierTemplateId: string) {
|
||||
getPrimaryFileAttributeValue(file: { fileAttributes: FileAttributes }, dossierTemplateId: string) {
|
||||
const fileAttributesConfig = this._fileAttributesService.getFileAttributeConfig(dossierTemplateId);
|
||||
|
||||
let primaryAttribute;
|
||||
|
||||
@ -517,17 +517,15 @@
|
||||
"question": "Möchten Sie fortfahren?",
|
||||
"title": "Dokument löschen"
|
||||
},
|
||||
"delete-items": {
|
||||
"question": "",
|
||||
"title": ""
|
||||
},
|
||||
"delete-justification": {
|
||||
"question": "Möchten Sie {count, plural, one{diese Begründung} other{diese Begründung}} wirklich löschen?",
|
||||
"title": "{count, plural, one{{justificationName}} other{ausgewählte Begründungen}} löschen"
|
||||
},
|
||||
"input-label": "Bitte geben Sie unten Folgendes ein, um fortzufahren",
|
||||
"permanently-delete-file": {
|
||||
"confirmation-text": "{filesCount, plural, one{Document} other{Documents}} löschen",
|
||||
"deny-text": "{filesCount, plural, one{Dokument} other{Dokumente}} behalten",
|
||||
"question": "Möchten Sie {filesCount, plural, one{dieses Dokument} other{diese Dokumente}} wirklich löschen?",
|
||||
"title": "{filesCount, plural, one{{fileName}} other{ausgewählte Dokumente}} löschen"
|
||||
},
|
||||
"report-template-same-name": {
|
||||
"confirmation-text": "Ja. Hochladen fortsetzen",
|
||||
"deny-text": "Nein. Hochladen abbrechen",
|
||||
@ -1026,31 +1024,6 @@
|
||||
},
|
||||
"change-successful": "Dossier wurde aktualisiert.",
|
||||
"delete-successful": "Dossier wurde gelöscht.",
|
||||
"deleted-documents": {
|
||||
"action": {
|
||||
"delete": "Endgültig löschen",
|
||||
"restore": "Wiederherstellen"
|
||||
},
|
||||
"bulk": {
|
||||
"delete": "Ausgewählte Dokumente endgültig löschen",
|
||||
"restore": "Ausgewählte Dokumente wiederherstellen"
|
||||
},
|
||||
"instructions": "Gelöschte Objekte können bis zu {hours} Stunden nach ihrer Löschung wiederhergestellt werden",
|
||||
"no-data": {
|
||||
"title": "Es sind keine gelöschten Dokumente vorhanden."
|
||||
},
|
||||
"table-col-names": {
|
||||
"assignee": "Bevollmächtigter",
|
||||
"deleted-on": "Gelöscht am",
|
||||
"name": "Name",
|
||||
"pages": "Seiten",
|
||||
"status": "Status",
|
||||
"time-to-restore": "Verbleibende Zeit für Wiederherstellung"
|
||||
},
|
||||
"table-header": {
|
||||
"label": "{length} {length, plural, one{gelöschtes Dokument} other{gelöschte Dokumente}}"
|
||||
}
|
||||
},
|
||||
"dictionary": {
|
||||
"display-name": {
|
||||
"cancel": "Abbrechen",
|
||||
@ -1087,7 +1060,6 @@
|
||||
"missing-owner": "",
|
||||
"nav-items": {
|
||||
"choose-download": "Wählen Sie die Dokumente für Ihr Download-Paket aus:",
|
||||
"deleted-documents": "Gelöschte Dokumente",
|
||||
"dictionary": "Wörterbuch",
|
||||
"dossier-attributes": "Dossier-Attribute",
|
||||
"dossier-dictionary": "Dossier-Wörterbuch",
|
||||
@ -1851,6 +1823,7 @@
|
||||
},
|
||||
"table-col-names": {
|
||||
"deleted-on": "Gelöscht am",
|
||||
"dossier": "",
|
||||
"name": "Name",
|
||||
"owner": "Eigentümer",
|
||||
"time-to-restore": "Verbleibende Zeit für Wiederherstellung"
|
||||
|
||||
@ -517,17 +517,15 @@
|
||||
"question": "Do you wish to proceed?",
|
||||
"title": "Delete Document"
|
||||
},
|
||||
"delete-items": {
|
||||
"question": "Are you sure you want to delete {itemsCount, plural, one{this item} other{these items}}?",
|
||||
"title": "Delete {itemsCount, plural, one{{name}} other{Selected Items}}"
|
||||
},
|
||||
"delete-justification": {
|
||||
"question": "Are you sure you want to delete {count, plural, one{this justification} other{these justifications}}?",
|
||||
"title": "Delete {count, plural, one{{justificationName}} other{Selected Justifications}}"
|
||||
},
|
||||
"input-label": "To proceed please type below",
|
||||
"permanently-delete-file": {
|
||||
"confirmation-text": "Delete {filesCount, plural, one{Document} other{Documents}}",
|
||||
"deny-text": "Keep {filesCount, plural, one{Document} other{Documents}}",
|
||||
"question": "Are you sure you want to delete {filesCount, plural, one{this document} other{these documents}}?",
|
||||
"title": "Delete {filesCount, plural, one{{fileName}} other{Selected Documents}}"
|
||||
},
|
||||
"report-template-same-name": {
|
||||
"confirmation-text": "Yes. Continue upload",
|
||||
"deny-text": "No. Cancel Upload",
|
||||
@ -1026,31 +1024,6 @@
|
||||
},
|
||||
"change-successful": "Dossier {dossierName} was updated.",
|
||||
"delete-successful": "Dossier {dossierName} was deleted.",
|
||||
"deleted-documents": {
|
||||
"action": {
|
||||
"delete": "Delete Forever",
|
||||
"restore": "Restore"
|
||||
},
|
||||
"bulk": {
|
||||
"delete": "Forever Delete Selected Documents",
|
||||
"restore": "Restore Selected Documents"
|
||||
},
|
||||
"instructions": "Deleted items can be restored up to {hours} hours from their deletion",
|
||||
"no-data": {
|
||||
"title": "There are no deleted documents."
|
||||
},
|
||||
"table-col-names": {
|
||||
"assignee": "Assignee",
|
||||
"deleted-on": "Deleted On",
|
||||
"name": "Name",
|
||||
"pages": "Pages",
|
||||
"status": "Status",
|
||||
"time-to-restore": "Time To Restore"
|
||||
},
|
||||
"table-header": {
|
||||
"label": "{length} deleted {length, plural, one{document} other{documents}}"
|
||||
}
|
||||
},
|
||||
"dictionary": {
|
||||
"display-name": {
|
||||
"cancel": "Cancel",
|
||||
@ -1087,7 +1060,6 @@
|
||||
"missing-owner": "You cannot edit the dossier because the owner is missing!",
|
||||
"nav-items": {
|
||||
"choose-download": "Choose what is included at download:",
|
||||
"deleted-documents": "Deleted Documents",
|
||||
"dictionary": "Dictionary",
|
||||
"dossier-attributes": "Dossier Attributes",
|
||||
"dossier-dictionary": "Dossier Dictionary",
|
||||
@ -1839,24 +1811,25 @@
|
||||
"restore": "Restore"
|
||||
},
|
||||
"bulk": {
|
||||
"delete": "Forever Delete Selected Dossiers",
|
||||
"restore": "Restore Selected Dossiers"
|
||||
"delete": "Forever Delete Selected Items",
|
||||
"restore": "Restore Selected Items"
|
||||
},
|
||||
"label": "Trash",
|
||||
"no-data": {
|
||||
"title": "There are no dossiers yet."
|
||||
"title": "There are no deleted items yet."
|
||||
},
|
||||
"no-match": {
|
||||
"title": "No dossiers match your current filters."
|
||||
"title": "No items match your current filters."
|
||||
},
|
||||
"table-col-names": {
|
||||
"deleted-on": "Deleted on",
|
||||
"dossier": "Dossier",
|
||||
"name": "Name",
|
||||
"owner": "Owner",
|
||||
"owner": "Owner/assignee",
|
||||
"time-to-restore": "Time to restore"
|
||||
},
|
||||
"table-header": {
|
||||
"title": "{length} deleted {length, plural, one{dossier} other{dossiers}}"
|
||||
"title": "{length} deleted {length, plural, one{item} other{items}}"
|
||||
}
|
||||
},
|
||||
"type": "Type",
|
||||
|
||||
@ -21,5 +21,5 @@ export * from './lib/signature';
|
||||
export * from './lib/legal-basis';
|
||||
export * from './lib/dossier-stats';
|
||||
export * from './lib/dossier-state';
|
||||
export * from './lib/trash-dossier';
|
||||
export * from './lib/trash';
|
||||
export * from './lib/text-highlight';
|
||||
|
||||
@ -43,6 +43,7 @@ export class DossierStats implements IDossierStats {
|
||||
readonly numberOfFiles: number;
|
||||
readonly numberOfProcessingFiles: number;
|
||||
readonly processingStats: ProcessingStats;
|
||||
readonly numberOfSoftDeletedFiles: number;
|
||||
|
||||
readonly hasFiles: boolean;
|
||||
|
||||
@ -58,6 +59,7 @@ export class DossierStats implements IDossierStats {
|
||||
this.hasUpdatesFilePresent = stats.hasUpdatesFilePresent;
|
||||
this.numberOfPages = stats.numberOfPages;
|
||||
this.numberOfFiles = stats.numberOfFiles;
|
||||
this.numberOfSoftDeletedFiles = stats.numberOfSoftDeletedFiles;
|
||||
this.numberOfProcessingFiles = Object.entries<number>(this.fileCountPerProcessingStatus)
|
||||
.filter(([key]) => isProcessingStatuses.includes(key as ProcessingFileStatus))
|
||||
.reduce((count, [, value]) => count + value, 0);
|
||||
|
||||
@ -12,4 +12,5 @@ export interface IDossierStats {
|
||||
hasUpdatesFilePresent: boolean;
|
||||
numberOfPages: number;
|
||||
numberOfFiles: number;
|
||||
numberOfSoftDeletedFiles: number;
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ export class File extends Entity<IFile> implements IFile, IRouterPath {
|
||||
readonly dossierId: string;
|
||||
readonly excluded: boolean;
|
||||
readonly excludedFromAutomaticAnalysis: boolean;
|
||||
readonly fileAttributes?: FileAttributes;
|
||||
readonly fileAttributes: FileAttributes;
|
||||
readonly fileId: string;
|
||||
readonly filename: string;
|
||||
readonly hasAnnotationComments: boolean;
|
||||
@ -25,6 +25,7 @@ export class File extends Entity<IFile> implements IFile, IRouterPath {
|
||||
readonly hasRedactions: boolean;
|
||||
readonly hasUpdates: boolean;
|
||||
readonly lastOCRTime?: string;
|
||||
readonly softDeletedTime?: string;
|
||||
readonly lastProcessed?: string;
|
||||
readonly lastReviewer?: string;
|
||||
readonly lastApprover?: string;
|
||||
@ -32,7 +33,7 @@ export class File extends Entity<IFile> implements IFile, IRouterPath {
|
||||
readonly lastUploaded?: string;
|
||||
readonly legalBasisVersion?: number;
|
||||
readonly numberOfAnalyses: number;
|
||||
readonly numberOfPages?: number;
|
||||
readonly numberOfPages: number;
|
||||
readonly rulesVersion?: number;
|
||||
readonly uploader?: string;
|
||||
readonly excludedPages: number[];
|
||||
@ -72,7 +73,6 @@ export class File extends Entity<IFile> implements IFile, IRouterPath {
|
||||
this.dossierId = file.dossierId;
|
||||
this.excluded = !!file.excluded;
|
||||
this.excludedFromAutomaticAnalysis = !!file.excludedFromAutomaticAnalysis;
|
||||
this.fileAttributes = file.fileAttributes;
|
||||
this.fileId = file.fileId;
|
||||
this.filename = file.filename;
|
||||
this.hasAnnotationComments = !!file.hasAnnotationComments;
|
||||
@ -81,6 +81,7 @@ export class File extends Entity<IFile> implements IFile, IRouterPath {
|
||||
this.hasRedactions = !!file.hasRedactions;
|
||||
this.hasUpdates = !!file.hasUpdates;
|
||||
this.lastOCRTime = file.lastOCRTime;
|
||||
this.softDeletedTime = file.softDeletedTime;
|
||||
this.lastProcessed = file.lastProcessed;
|
||||
this.lastReviewer = file.lastReviewer;
|
||||
this.lastApprover = file.lastApprover;
|
||||
@ -115,9 +116,8 @@ export class File extends Entity<IFile> implements IFile, IRouterPath {
|
||||
this.canBeOpened = !this.isError && !this.isUnprocessed && this.numberOfAnalyses > 0;
|
||||
this.canBeOCRed = !this.excluded && !this.lastOCRTime && (this.isNew || this.isUnderReview || this.isUnderApproval);
|
||||
|
||||
if (!this.fileAttributes || !this.fileAttributes.attributeIdToValue) {
|
||||
this.fileAttributes = { attributeIdToValue: {} };
|
||||
}
|
||||
this.fileAttributes =
|
||||
file.fileAttributes && file.fileAttributes.attributeIdToValue ? file.fileAttributes : { attributeIdToValue: {} };
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
|
||||
@ -137,7 +137,7 @@ export interface IFile {
|
||||
/**
|
||||
* Shows if the file is soft deleted.
|
||||
*/
|
||||
readonly softDeleted?: string;
|
||||
readonly softDeletedTime?: string;
|
||||
/**
|
||||
* The ID of the user who uploaded the file.
|
||||
*/
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from './trash-dossier.model';
|
||||
@ -1,68 +0,0 @@
|
||||
import { getLeftDateTime, List } from '@iqser/common-ui';
|
||||
import { DownloadFileType } from '../shared';
|
||||
import { DossierStatus, IDossier } from '../dossiers';
|
||||
|
||||
export class TrashDossier implements IDossier {
|
||||
readonly dossierId: string;
|
||||
readonly dossierTemplateId: string;
|
||||
readonly ownerId: string;
|
||||
readonly memberIds: List;
|
||||
readonly approverIds: List;
|
||||
readonly reportTemplateIds: List;
|
||||
readonly dossierName: string;
|
||||
readonly dossierStatusId: string;
|
||||
readonly date: string;
|
||||
readonly dueDate?: string;
|
||||
readonly description?: string;
|
||||
readonly downloadFileTypes?: List<DownloadFileType>;
|
||||
readonly hardDeletedTime?: string;
|
||||
readonly softDeletedTime?: string;
|
||||
readonly startDate?: string;
|
||||
readonly status: DossierStatus;
|
||||
readonly watermarkEnabled: boolean;
|
||||
readonly watermarkPreviewEnabled: boolean;
|
||||
readonly archivedTime: string;
|
||||
readonly hasReviewers: boolean;
|
||||
readonly canRestore: boolean;
|
||||
|
||||
constructor(dossier: IDossier, readonly restoreDate: string, readonly canHardDelete: boolean) {
|
||||
this.dossierId = dossier.dossierId;
|
||||
this.approverIds = dossier.approverIds;
|
||||
this.date = dossier.date;
|
||||
this.description = dossier.description;
|
||||
this.dossierName = dossier.dossierName;
|
||||
this.dossierStatusId = dossier.dossierStatusId;
|
||||
this.dossierTemplateId = dossier.dossierTemplateId;
|
||||
this.downloadFileTypes = dossier.downloadFileTypes;
|
||||
this.dueDate = dossier.dueDate;
|
||||
this.hardDeletedTime = dossier.hardDeletedTime;
|
||||
this.memberIds = dossier.memberIds;
|
||||
this.ownerId = dossier.ownerId;
|
||||
this.reportTemplateIds = dossier.reportTemplateIds;
|
||||
this.softDeletedTime = dossier.softDeletedTime;
|
||||
this.startDate = dossier.startDate;
|
||||
this.status = dossier.status;
|
||||
this.watermarkEnabled = dossier.watermarkEnabled;
|
||||
this.watermarkPreviewEnabled = dossier.watermarkPreviewEnabled;
|
||||
this.archivedTime = dossier.archivedTime;
|
||||
this.hasReviewers = !!this.memberIds && this.memberIds.length > 1;
|
||||
|
||||
this.canRestore = this.#canRestoreDossier(restoreDate);
|
||||
// Because of migrations, for some this is not set
|
||||
this.softDeletedTime = dossier.softDeletedTime || '-';
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this.dossierId;
|
||||
}
|
||||
|
||||
get searchKey(): string {
|
||||
return this.dossierName;
|
||||
}
|
||||
|
||||
#canRestoreDossier(restoreDate: string): boolean {
|
||||
const { daysLeft, hoursLeft, minutesLeft } = getLeftDateTime(restoreDate);
|
||||
|
||||
return daysLeft >= 0 && hoursLeft >= 0 && minutesLeft > 0;
|
||||
}
|
||||
}
|
||||
3
libs/red-domain/src/lib/trash/index.ts
Normal file
3
libs/red-domain/src/lib/trash/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './trash.item';
|
||||
export * from './trash-dossier.model';
|
||||
export * from './trash-file.model';
|
||||
43
libs/red-domain/src/lib/trash/trash-dossier.model.ts
Normal file
43
libs/red-domain/src/lib/trash/trash-dossier.model.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { List } from '@iqser/common-ui';
|
||||
import { IDossier } from '../dossiers';
|
||||
import { TrashItem } from './trash.item';
|
||||
|
||||
export class TrashDossier extends TrashItem {
|
||||
readonly type = 'dossier';
|
||||
readonly icon = 'red:folder';
|
||||
|
||||
readonly dossierId: string;
|
||||
readonly dossierTemplateId: string;
|
||||
readonly dossierName: string;
|
||||
readonly memberIds: List;
|
||||
readonly date: string;
|
||||
readonly dueDate?: string;
|
||||
readonly ownerId: string;
|
||||
readonly softDeletedTime: string;
|
||||
|
||||
constructor(dossier: IDossier, protected readonly _retentionHours: number, readonly canHardDelete: boolean) {
|
||||
super(_retentionHours, dossier.softDeletedTime, canHardDelete);
|
||||
this.dossierId = dossier.dossierId;
|
||||
this.dossierTemplateId = dossier.dossierTemplateId;
|
||||
this.date = dossier.date;
|
||||
this.dossierName = dossier.dossierName;
|
||||
this.dueDate = dossier.dueDate;
|
||||
this.memberIds = dossier.memberIds;
|
||||
this.ownerId = dossier.ownerId;
|
||||
|
||||
// Because of migrations, for some this is not set
|
||||
this.softDeletedTime = dossier.softDeletedTime || '-';
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this.dossierId;
|
||||
}
|
||||
|
||||
get searchKey(): string {
|
||||
return this.dossierName;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.dossierName;
|
||||
}
|
||||
}
|
||||
56
libs/red-domain/src/lib/trash/trash-file.model.ts
Normal file
56
libs/red-domain/src/lib/trash/trash-file.model.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { TrashItem } from './trash.item';
|
||||
import { File, IFile } from '../files';
|
||||
import { FileAttributes } from '../file-attributes';
|
||||
|
||||
export class TrashFile extends TrashItem implements Partial<IFile> {
|
||||
readonly type = 'file';
|
||||
readonly icon = 'iqser:document';
|
||||
|
||||
readonly dossierId: string;
|
||||
readonly fileId: string;
|
||||
readonly filename: string;
|
||||
readonly assignee?: string;
|
||||
|
||||
readonly numberOfPages: number;
|
||||
readonly excludedPages: number[];
|
||||
readonly lastOCRTime?: string;
|
||||
readonly fileAttributes: FileAttributes;
|
||||
|
||||
readonly isError: boolean;
|
||||
readonly isInitialProcessing: boolean;
|
||||
|
||||
constructor(
|
||||
file: File,
|
||||
readonly dossierTemplateId: string,
|
||||
protected readonly _retentionHours: number,
|
||||
readonly canHardDelete: boolean,
|
||||
) {
|
||||
super(_retentionHours, file.softDeletedTime, canHardDelete);
|
||||
this.fileId = file.fileId;
|
||||
this.dossierId = file.dossierId;
|
||||
this.filename = file.filename;
|
||||
this.assignee = file.assignee;
|
||||
this.numberOfPages = file.numberOfPages || 0;
|
||||
this.excludedPages = file.excludedPages || [];
|
||||
this.lastOCRTime = file.lastOCRTime;
|
||||
this.fileAttributes = file.fileAttributes;
|
||||
this.isError = file.isError;
|
||||
this.isInitialProcessing = file.isInitialProcessing;
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this.fileId;
|
||||
}
|
||||
|
||||
get searchKey(): string {
|
||||
return this.filename;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.filename;
|
||||
}
|
||||
|
||||
get ownerId(): string | undefined {
|
||||
return this.assignee;
|
||||
}
|
||||
}
|
||||
43
libs/red-domain/src/lib/trash/trash.item.ts
Normal file
43
libs/red-domain/src/lib/trash/trash.item.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { getLeftDateTime } from '@iqser/common-ui';
|
||||
import * as moment from 'moment';
|
||||
|
||||
export abstract class TrashItem {
|
||||
abstract readonly type: 'dossier' | 'file';
|
||||
abstract readonly ownerId?: string;
|
||||
abstract readonly dossierId: string;
|
||||
abstract readonly icon: string;
|
||||
readonly canRestore: boolean;
|
||||
readonly restoreDate: string;
|
||||
|
||||
protected constructor(
|
||||
protected readonly _retentionHours: number,
|
||||
readonly softDeletedTime: string | undefined,
|
||||
readonly canHardDelete: boolean,
|
||||
) {
|
||||
this.restoreDate = this.#restoreDate;
|
||||
this.canRestore = this.#canRestore;
|
||||
}
|
||||
|
||||
abstract get id(): string;
|
||||
|
||||
abstract get searchKey(): string;
|
||||
|
||||
abstract get name(): string;
|
||||
|
||||
get isDossier(): boolean {
|
||||
return this.type === 'dossier';
|
||||
}
|
||||
|
||||
get isFile(): boolean {
|
||||
return this.type === 'file';
|
||||
}
|
||||
|
||||
get #canRestore(): boolean {
|
||||
const { daysLeft, hoursLeft, minutesLeft } = getLeftDateTime(this.restoreDate);
|
||||
return daysLeft >= 0 && hoursLeft >= 0 && minutesLeft > 0;
|
||||
}
|
||||
|
||||
get #restoreDate(): string {
|
||||
return moment(this.softDeletedTime).add(this._retentionHours, 'hours').toISOString();
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user