Trash dossiers service

This commit is contained in:
Adina Țeudan 2022-02-22 23:31:47 +02:00
parent 0a05eb9a6c
commit e50b5bd567
12 changed files with 207 additions and 126 deletions

View File

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

@ -3,7 +3,7 @@ import { RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { NotificationsScreenComponent } from './notifications-screen/notifications-screen.component';
import { PendingChangesGuard } from '../../../../guards/can-deactivate.guard';
import { PendingChangesGuard } from '@guards/can-deactivate.guard';
const routes = [{ path: '', component: NotificationsScreenComponent, canDeactivate: [PendingChangesGuard] }];

View File

@ -3,7 +3,7 @@ import { RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { UserProfileScreenComponent } from './user-profile-screen/user-profile-screen.component';
import { PendingChangesGuard } from '../../../../guards/can-deactivate.guard';
import { PendingChangesGuard } from '@guards/can-deactivate.guard';
const routes = [{ path: '', component: UserProfileScreenComponent, canDeactivate: [PendingChangesGuard] }];

View File

@ -17,11 +17,12 @@ import { TrashScreenComponent } from './screens/trash/trash-screen.component';
import { GeneralConfigScreenComponent } from './screens/general-config/general-config-screen.component';
import { BaseAdminScreenComponent } from './base-admin-screen/base-admin-screen.component';
import { BaseDossierTemplateScreenComponent } from './base-dossier-templates-screen/base-dossier-template-screen.component';
import { DossierTemplatesGuard } from '../../guards/dossier-templates.guard';
import { DossierTemplatesGuard } from '@guards/dossier-templates.guard';
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 { 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';
const routes: Routes = [
{ path: '', redirectTo: 'dossier-templates', pathMatch: 'full' },
@ -200,7 +201,7 @@ const routes: Routes = [
component: TrashScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard],
routeGuards: [AuthGuard, RedRoleGuard, TrashDossiersGuard],
requiredRoles: ['RED_MANAGER'],
},
},

View File

@ -1,45 +1,42 @@
import { ChangeDetectionStrategy, Component, forwardRef, Injector, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, forwardRef, Injector } from '@angular/core';
import {
CircleButtonTypes,
ConfirmationDialogInput,
DefaultListingServices,
IListable,
DefaultListingServicesTmp,
EntitiesService,
ListingComponent,
LoadingService,
SortingOrders,
TableColumnConfig,
TitleColors,
} from '@iqser/common-ui';
import { ConfigService } from '@services/config.service';
import * as moment from 'moment';
import { ActiveDossiersService } from '@services/entity-services/active-dossiers.service';
import { AdminDialogService } from '../../services/admin-dialog.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { Observable } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { getLeftDateTime } from '@utils/functions';
import { RouterHistoryService } from '@services/router-history.service';
import { IDossier } from '@red/domain';
import { PermissionsService } from '@services/permissions.service';
interface DossierListItem extends IDossier, IListable {
readonly canRestore: boolean;
readonly canHardDelete: boolean;
readonly restoreDate: string;
}
import { TrashDossier } from '@red/domain';
import { TrashDossiersService } from '@services/entity-services/trash-dossiers.service';
@Component({
templateUrl: './trash-screen.component.html',
styleUrls: ['./trash-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => TrashScreenComponent) }],
providers: [
...DefaultListingServicesTmp,
{
provide: EntitiesService,
useExisting: TrashDossiersService,
},
{ provide: ListingComponent, useExisting: forwardRef(() => TrashScreenComponent) },
],
})
export class TrashScreenComponent extends ListingComponent<DossierListItem> implements OnInit {
export class TrashScreenComponent extends ListingComponent<TrashDossier> {
readonly circleButtonTypes = CircleButtonTypes;
readonly tableHeaderLabel = _('trash.table-header.title');
readonly canRestoreSelected$ = this._canRestoreSelected$;
readonly canHardDeleteSelected$ = this._canHardDeleteSelected$;
readonly tableColumnConfigs: TableColumnConfig<DossierListItem>[] = [
readonly tableColumnConfigs: TableColumnConfig<TrashDossier>[] = [
{ label: _('trash.table-col-names.name'), sortByKey: 'searchKey' },
{ label: _('trash.table-col-names.owner'), class: 'user-column' },
{ label: _('trash.table-col-names.deleted-on'), sortByKey: 'softDeletedTime' },
@ -49,13 +46,16 @@ export class TrashScreenComponent extends ListingComponent<DossierListItem> impl
constructor(
protected readonly _injector: Injector,
private readonly _loadingService: LoadingService,
private readonly _permissionsService: PermissionsService,
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _trashDossiersService: TrashDossiersService,
readonly routerHistoryService: RouterHistoryService,
private readonly _configService: ConfigService,
private readonly _adminDialogService: AdminDialogService,
) {
super(_injector);
this.sortingService.setSortingOption({
column: 'softDeletedTime',
order: SortingOrders.desc,
});
}
private get _canRestoreSelected$(): Observable<boolean> {
@ -72,17 +72,7 @@ export class TrashScreenComponent extends ListingComponent<DossierListItem> impl
);
}
disabledFn = (dossier: DossierListItem) => !dossier.canRestore;
async ngOnInit(): Promise<void> {
this._loadingService.start();
await this._loadDossiersData();
this.sortingService.setSortingOption({
column: 'softDeletedTime',
order: SortingOrders.desc,
});
this._loadingService.stop();
}
disabledFn = (dossier: TrashDossier) => !dossier.canRestore;
hardDelete(dossiers = this.listingService.selected): void {
const data = new ConfirmationDialogInput({
@ -95,60 +85,13 @@ export class TrashScreenComponent extends ListingComponent<DossierListItem> impl
},
});
this._adminDialogService.openDialog('confirm', null, data, () => {
this._loadingService.loadWhile(this._hardDelete(dossiers));
const dossierIds: string[] = dossiers.map(d => d.id);
this._loadingService.loadWhile(this._trashDossiersService.hardDelete(dossierIds));
});
}
restore(dossiers = this.listingService.selected): void {
this._loadingService.loadWhile(this._restore(dossiers));
}
private _getRestoreDate(softDeletedTime: string): string {
return moment(softDeletedTime).add(this._configService.values.DELETE_RETENTION_HOURS, 'hours').toISOString();
}
private async _loadDossiersData(): Promise<void> {
this.entitiesService.setEntities(this._toListItems(await this._activeDossiersService.getDeleted()).filter(d => d.canRestore));
}
private _canRestoreDossier(restoreDate: string): boolean {
const { daysLeft, hoursLeft, minutesLeft } = getLeftDateTime(restoreDate);
return daysLeft >= 0 && hoursLeft >= 0 && minutesLeft > 0;
}
private _toListItems(dossiers: IDossier[]): DossierListItem[] {
return dossiers.map(dossier => this._toListItem(dossier));
}
private _toListItem(dossier: IDossier): DossierListItem {
const restoreDate = this._getRestoreDate(dossier.softDeletedTime);
return {
id: dossier.dossierId,
...dossier,
searchKey: dossier.dossierName,
restoreDate,
canRestore: this._canRestoreDossier(restoreDate),
canHardDelete: this._permissionsService.canDeleteDossier(dossier),
// Because of migrations, for some this is not set
softDeletedTime: dossier.softDeletedTime || '-',
};
}
private async _restore(dossiers: DossierListItem[]): Promise<void> {
const dossierIds = dossiers.map(d => d.id);
await this._activeDossiersService.restore(dossierIds);
this._removeFromList(dossierIds);
}
private async _hardDelete(dossiers: DossierListItem[]) {
const dossierIds = dossiers.map(d => d.id);
await this._activeDossiersService.hardDelete(dossierIds);
this._removeFromList(dossierIds);
}
private _removeFromList(ids: string[]): void {
const entities = this.entitiesService.all.filter(e => !ids.includes(e.id));
this.entitiesService.setEntities(entities);
const dossierIds: string[] = dossiers.map(d => d.id);
this._loadingService.loadWhile(this._trashDossiersService.restore(dossierIds));
}
}

View File

@ -5,7 +5,7 @@ import { ArchivedDossiersScreenComponent } from './screens/archived-dossiers-scr
import { DOSSIER_ID } from '@utils/constants';
import { CompositeRouteGuard } from '@iqser/common-ui';
import { ARCHIVED_DOSSIERS_SERVICE } from '../../tokens';
import { DossierFilesGuard } from '../../guards/dossier-files-guard';
import { DossierFilesGuard } from '@guards/dossier-files-guard';
const routes: Routes = [
{

View File

@ -17,6 +17,7 @@ import { DossierStateService } from '@services/entity-services/dossier-state.ser
import { DOSSIER_TEMPLATE_ID } from '@utils/constants';
import { TranslateService } from '@ngx-translate/core';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { TrashDossiersService } from '@services/entity-services/trash-dossiers.service';
@Component({
selector: 'redaction-edit-dossier-general-info',
@ -39,6 +40,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
private readonly _dossierStateService: DossierStateService,
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dossiersService: DossiersService,
private readonly _trashDossiersService: TrashDossiersService,
private readonly _dossierStatsService: DossierStatsService,
private readonly _formBuilder: FormBuilder,
private readonly _dialogService: DossiersDialogService,
@ -139,7 +141,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
},
});
this._dialogService.openDialog('confirm', null, data, async () => {
await firstValueFrom(this._dossiersService.delete(this.dossier));
await firstValueFrom(this._trashDossiersService.delete(this.dossier));
this._editDossierDialogRef.close();
this._router.navigate(['main', 'dossiers']).then(() => this.#notifyDossierDeleted());
});

View File

@ -1,9 +1,6 @@
import { Injectable, Injector } from '@angular/core';
import { List, QueryParam, RequiredParam, Validate } from '@iqser/common-ui';
import { Dossier, IDossier } from '@red/domain';
import { catchError, switchMap, tap } from 'rxjs/operators';
import { firstValueFrom, Observable, of, timer } from 'rxjs';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { switchMap, tap } from 'rxjs/operators';
import { timer } from 'rxjs';
import { CHANGED_CHECK_INTERVAL } from '@utils/constants';
import { DossiersService } from '@services/entity-services/dossiers.service';
@ -27,37 +24,7 @@ export class ActiveDossiersService extends DossiersService {
.subscribe();
}
getDeleted(): Promise<IDossier[]> {
return firstValueFrom(this.getAll('deleted-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(
tap(() => this.#removeDossiers([dossier])),
catchError(showToast),
);
}
@Validate()
restore(@RequiredParam() dossierIds: List): Promise<unknown> {
return firstValueFrom(this._post(dossierIds, 'deleted-dossiers/restore').pipe(switchMap(() => this.loadAll())));
}
@Validate()
hardDelete(@RequiredParam() dossierIds: List): Promise<unknown> {
const body = dossierIds.map<QueryParam>(id => ({ key: 'dossierId', value: id }));
return firstValueFrom(super.delete(body, 'deleted-dossiers/hard-delete', body));
}
getCountWithState(dossierStatusId: string): number {
return this.all.filter(dossier => dossier.dossierStatusId === dossierStatusId).length;
}
#removeDossiers(dossiers: Dossier[]): void {
this.setEntities(this.all.filter(dossier => !dossiers.find(d => dossier.id === d.id)));
}
}

View File

@ -0,0 +1,83 @@
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/entity-services/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

@ -20,3 +20,4 @@ export * from './lib/signature';
export * from './lib/legal-basis';
export * from './lib/dossier-stats';
export * from './lib/dossier-state';
export * from './lib/trash-dossier';

View File

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

View File

@ -0,0 +1,69 @@
import { List } from '@iqser/common-ui';
import { DownloadFileType } from '../shared';
import { DossierStatus, IDossier } from '@red/domain';
import { getLeftDateTime } from '@utils/functions';
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;
}
}