Merge branch 'RED-3505'

This commit is contained in:
Adina Țeudan 2022-03-15 19:32:49 +02:00
commit d6ce59952c
55 changed files with 646 additions and 729 deletions

View File

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

View File

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

View File

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

View File

@ -9,12 +9,12 @@ 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 { firstValueFrom, Observable } 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';
import { DossierStatesMapService } from '@services/entity-services/dossier-states-map.service';
import { tap } from 'rxjs/operators';
import { map, tap } from 'rxjs/operators';
import { PermissionsService } from '@services/permissions.service';
import { DossierStatesService } from '@services/entity-services/dossier-states.service';

View File

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

View File

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

View File

@ -0,0 +1,57 @@
<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>
<redaction-dossier-name-column
*ngIf="item.isDossier"
[dossierStats]="dossierStats$ | async"
[dossier]="dossier(item)"
></redaction-dossier-name-column>
</div>
</div>
<div class="cell user-column">
<redaction-initials-avatar [user]="item.ownerId" [withName]="true"></redaction-initials-avatar>
</div>
<div class="cell">
<a *ngIf="item.isFile && fileDossier$ | async as fileDossier" [routerLink]="fileDossier.routerLink" class="small-label link-action">
{{ fileDossier.dossierName }}
</a>
<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>

View File

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

View File

@ -0,0 +1,41 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { Dossier, DossierStats, TrashDossier, TrashFile, TrashItem } from '@red/domain';
import { CircleButtonTypes } from '@iqser/common-ui';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { Observable } from 'rxjs';
import { DossierStatsService } from '@services/dossiers/dossier-stats.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$: Observable<Dossier>;
dossierStats$: Observable<DossierStats>;
constructor(private readonly _activeDossiersService: ActiveDossiersService, readonly dossierStatsService: DossierStatsService) {}
file(item: TrashItem): TrashFile {
return item as TrashFile;
}
dossier(item: TrashItem): TrashDossier {
return item as TrashDossier;
}
ngOnChanges(): void {
if (this.item.isFile) {
this.fileDossier$ = this._activeDossiersService.getEntityChanged$(this.file(this.item).dossierId);
}
if (this.item.isDossier) {
this.dossierStats$ = this.dossierStatsService.watch$(this.dossier(this.item).id);
}
}
}

View File

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

View File

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

View File

@ -1,12 +1,10 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Dossier, DossierAttributeWithValue, DossierStats } from '@red/domain';
import { DossiersDialogService } from '../../../dossier/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 { map, switchMap } from 'rxjs/operators';
import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { DossiersDialogService } from '../../../dossier/services/dossiers-dialog.service';
@Component({
selector: 'redaction-dossier-details-stats',
@ -20,7 +18,6 @@ export class DossierDetailsStatsComponent implements OnInit {
attributesExpanded = false;
dossierTemplateName: string;
deletedFilesCount$: Observable<number>;
dossierStats$: Observable<DossierStats>;
constructor(
@ -28,15 +25,10 @@ export class DossierDetailsStatsComponent implements OnInit {
private readonly _dialogService: DossiersDialogService,
private readonly _filesService: FilesService,
private readonly _dossierStatsService: DossierStatsService,
private readonly _filesMapService: FilesMapService,
) {}
ngOnInit() {
this.dossierStats$ = this._dossierStatsService.watch$(this.dossier.dossierId);
this.deletedFilesCount$ = this._filesMapService.get$(this.dossier.dossierId).pipe(
switchMap(() => this._filesService.getDeletedFilesFor(this.dossier.id)),
map(files => files.length),
);
this.dossierTemplateName = this._dossierTemplatesService.find(this.dossier.dossierTemplateId)?.name || '-';
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,10 +12,10 @@
<div *ngIf="dossierStats" class="stats-subtitle">
<div class="small-label">
<mat-icon svgIcon="iqser:document"></mat-icon>
{{ dossierStats.numberOfFiles }}
{{ dossier.isSoftDeleted ? dossierStats.numberOfSoftDeletedFiles : dossierStats.numberOfFiles }}
</div>
<div class="small-label">
<div *ngIf="!dossier.isSoftDeleted" class="small-label">
<mat-icon svgIcon="iqser:pages"></mat-icon>
{{ dossierStats.numberOfPages }}
</div>

View File

@ -2,17 +2,27 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Dossier, 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 extends Partial<Dossier> {
readonly dossierName: string;
readonly dossierTemplateId: string;
readonly dueDate?: string;
readonly date?: string;
readonly memberIds: List;
readonly isSoftDeleted: boolean;
}
@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) {}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,14 @@
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="iqser:pages"></mat-icon>
{{ file.numberOfPages }}
</div>
<div>
<mat-icon svgIcon="red:exclude-pages"></mat-icon>
{{ file.excludedPages.length }}
</div>
<div *ngIf="file.lastOCRTime" [matTooltipPosition]="'above'" [matTooltip]="'dossier-overview.ocr-performed' | translate">
<mat-icon svgIcon="iqser:ocr"></mat-icon>
{{ file.lastOCRTime | date: 'mediumDate' }}
</div>
</div>

View File

@ -0,0 +1,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 };
}

View File

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

View File

@ -28,7 +28,9 @@ import { EditorComponent } from './components/editor/editor.component';
import { ExpandableFileActionsComponent } from './components/expandable-file-actions/expandable-file-actions.component';
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';
import { DossierNameColumnComponent } from '@shared/components/dossier-name-column/dossier-name-column.component';
const buttons = [FileDownloadBtnComponent, UserButtonComponent];
@ -46,7 +48,9 @@ const components = [
ExpandableFileActionsComponent,
ProcessingIndicatorComponent,
DossierStateComponent,
DossiersListingDossierNameComponent,
DossierNameColumnComponent,
FileStatsComponent,
FileNameColumnComponent,
...buttons,
];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,137 @@
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),
),
),
switchMap(dossiers => this._dossierStatsService.getFor(dossiers.map(d => d.id) as string[]).pipe(map(() => 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')));
}
}

View File

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

View File

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

View File

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

@ -1 +1 @@
Subproject commit a6c4093d3553c4d0f95e496678e2a8f1cab0b9de
Subproject commit 8a992aa440ff24d1244e24edea3ce75fdadbebd5

View File

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

View File

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

View File

@ -12,4 +12,5 @@ export interface IDossierStats {
hasUpdatesFilePresent: boolean;
numberOfPages: number;
numberOfFiles: number;
numberOfSoftDeletedFiles: number;
}

View File

@ -64,6 +64,10 @@ export class Dossier implements IDossier, IListable, IRouterPath {
return this.status === DossierStatuses.ACTIVE;
}
get isSoftDeleted(): boolean {
return this.status === DossierStatuses.DELETED;
}
hasMember(memberId: string): boolean {
return !!this.memberIds && this.memberIds.indexOf(memberId) >= 0;
}

View File

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

View File

@ -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.
*/

View File

@ -1 +0,0 @@
export * from './trash-dossier.model';

View File

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

View File

@ -0,0 +1,3 @@
export * from './trash.item';
export * from './trash-dossier.model';
export * from './trash-file.model';

View File

@ -0,0 +1,45 @@
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 isSoftDeleted = true;
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;
}
}

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

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