diff --git a/apps/red-ui/src/app/app-routing.module.ts b/apps/red-ui/src/app/app-routing.module.ts index 45c892ee1..909b4d090 100644 --- a/apps/red-ui/src/app/app-routing.module.ts +++ b/apps/red-ui/src/app/app-routing.module.ts @@ -6,8 +6,9 @@ import { BaseScreenComponent } from '@components/base-screen/base-screen.compone import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router'; import { NgModule } from '@angular/core'; import { DownloadsListScreenComponent } from '@components/downloads-list-screen/downloads-list-screen.component'; -import { DossiersGuard } from '@guards/dossiers.guard'; import { DossierTemplatesGuard } from '@guards/dossier-templates.guard'; +import { DossiersGuard } from '@guards/dossiers.guard'; +import { ACTIVE_DOSSIERS_SERVICE, ARCHIVED_DOSSIERS_SERVICE } from './tokens'; const routes: Routes = [ { @@ -38,6 +39,18 @@ const routes: Routes = [ data: { routeGuards: [AuthGuard, RedRoleGuard, DossierTemplatesGuard, DossiersGuard], requiredRoles: ['RED_USER', 'RED_MANAGER'], + dossiersService: ACTIVE_DOSSIERS_SERVICE, + }, + }, + { + path: 'main/archive', + component: BaseScreenComponent, + loadChildren: () => import('./modules/archive/archive.module').then(m => m.ArchiveModule), + canActivate: [CompositeRouteGuard], + data: { + routeGuards: [AuthGuard, RedRoleGuard, DossierTemplatesGuard, DossiersGuard], + requiredRoles: ['RED_USER', 'RED_MANAGER'], + dossiersService: ARCHIVED_DOSSIERS_SERVICE, }, }, { @@ -54,6 +67,16 @@ const routes: Routes = [ routeGuards: [AuthGuard, RedRoleGuard], }, }, + { + path: 'main/search', + component: BaseScreenComponent, + loadChildren: () => import('./modules/search/search.module').then(m => m.SearchModule), + canActivate: [CompositeRouteGuard], + data: { + routeGuards: [AuthGuard, RedRoleGuard, DossiersGuard], + requiredRoles: ['RED_USER', 'RED_MANAGER'], + }, + }, { path: '**', redirectTo: 'main/dossiers', diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts index 460e10808..07bb430b0 100644 --- a/apps/red-ui/src/app/app.module.ts +++ b/apps/red-ui/src/app/app.module.ts @@ -20,7 +20,7 @@ import { AppRoutingModule } from './app-routing.module'; import { SharedModule } from '@shared/shared.module'; import { FileUploadDownloadModule } from '@upload-download/file-upload-download.module'; import { PlatformLocation } from '@angular/common'; -import { BASE_HREF } from './tokens'; +import { ACTIVE_DOSSIERS_SERVICE, ARCHIVED_DOSSIERS_SERVICE, BASE_HREF } from './tokens'; import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'; import { GlobalErrorHandler } from '@utils/global-error-handler.service'; import { REDMissingTranslationHandler } from '@utils/missing-translations-handler'; @@ -44,6 +44,8 @@ import { GeneralSettingsService } from '@services/general-settings.service'; import { BreadcrumbsComponent } from '@components/breadcrumbs/breadcrumbs.component'; import { UserPreferenceService } from '@services/user-preference.service'; import { UserService } from '@services/user.service'; +import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service'; +import { ArchivedDossiersService } from '@services/dossiers/archived-dossiers.service'; export function httpLoaderFactory(httpClient: HttpClient, configService: ConfigService): PruningTranslationLoader { return new PruningTranslationLoader(httpClient, '/assets/i18n/', `.json?version=${configService.values.FRONTEND_APP_VERSION}`); @@ -144,6 +146,14 @@ const components = [AppComponent, AuthErrorComponent, NotificationsComponent, Sp useFactory: (configService: ConfigService) => configService.values.MAX_RETRIES_ON_SERVER_ERROR, deps: [ConfigService], }, + { + provide: ACTIVE_DOSSIERS_SERVICE, + useExisting: ActiveDossiersService, + }, + { + provide: ARCHIVED_DOSSIERS_SERVICE, + useExisting: ArchivedDossiersService, + }, DatePipe, ], bootstrap: [AppComponent], diff --git a/apps/red-ui/src/app/components/base-screen/base-screen.component.html b/apps/red-ui/src/app/components/base-screen/base-screen.component.html index b54d99c37..8733bb8b9 100644 --- a/apps/red-ui/src/app/components/base-screen/base-screen.component.html +++ b/apps/red-ui/src/app/components/base-screen/base-screen.component.html @@ -3,7 +3,7 @@
- - diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-actions/dossiers-listing-actions.component.ts b/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-actions/dossiers-listing-actions.component.ts index 80b56d553..42ad618cb 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-actions/dossiers-listing-actions.component.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-actions/dossiers-listing-actions.component.ts @@ -8,7 +8,7 @@ import { LongPressEvent } from '@shared/directives/long-press.directive'; import { UserPreferenceService } from '@services/user-preference.service'; import { FilesMapService } from '@services/entity-services/files-map.service'; import { ReanalysisService } from '@services/reanalysis.service'; -import { DossiersService } from '@services/entity-services/dossiers.service'; +import { ActiveDossiersService } from '../../../../../../services/dossiers/active-dossiers.service'; import { firstValueFrom } from 'rxjs'; @Component({ @@ -32,13 +32,17 @@ export class DossiersListingActionsComponent implements OnChanges { constructor( private readonly _reanalysisService: ReanalysisService, private readonly _userService: UserService, - private readonly _dossiersService: DossiersService, + private readonly _activeDossiersService: ActiveDossiersService, readonly permissionsService: PermissionsService, readonly filesMapService: FilesMapService, private readonly _dialogService: DossiersDialogService, private readonly _userPreferenceService: UserPreferenceService, ) {} + get scrollableParentView(): ScrollableParentView { + return ScrollableParentViews.VIRTUAL_SCROLL; + } + ngOnChanges() { this.files = this.filesMapService.get(this.dossier.dossierId); this.displayReanalyseBtn = this.permissionsService.displayReanalyseBtn(this.dossier) && this.analysisForced; @@ -52,12 +56,8 @@ export class DossiersListingActionsComponent implements OnChanges { this._dialogService.openDialog('editDossier', $event, { dossierId }); } - async reanalyseDossier($event: MouseEvent, id: string): Promise { + async reanalyseDossier($event: MouseEvent, dossier: Dossier): Promise { $event.stopPropagation(); - await firstValueFrom(this._reanalysisService.reanalyzeDossier(id)); - } - - get scrollableParentView(): ScrollableParentView { - return ScrollableParentViews.VIRTUAL_SCROLL; + await firstValueFrom(this._reanalysisService.reanalyzeDossier(dossier)); } } diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-details/dossiers-listing-details.component.html b/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-details/dossiers-listing-details.component.html index 114807d07..3cd68756d 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-details/dossiers-listing-details.component.html +++ b/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-details/dossiers-listing-details.component.html @@ -4,10 +4,10 @@ [config]="config" [radius]="80" [strokeWidth]="15" - [subtitle]="'dossier-listing.stats.charts.dossiers' | translate: { count: dossiersService.all.length }" + [subtitle]="'dossier-listing.stats.charts.dossiers' | translate: { count: activeDossiersService.all.length }" > -
+
diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-details/dossiers-listing-details.component.ts b/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-details/dossiers-listing-details.component.ts index 08527c58a..f329fc38c 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-details/dossiers-listing-details.component.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-details/dossiers-listing-details.component.ts @@ -1,14 +1,14 @@ 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 { DossiersService } from '@services/entity-services/dossiers.service'; +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 { workflowFileStatusTranslations } from '../../../../../../translations/file-status-translations'; import { TranslateChartService } from '@services/translate-chart.service'; import { filter, map, switchMap } from 'rxjs/operators'; -import { DossierStatsService } from '@services/entity-services/dossier-stats.service'; -import { DossierStateService } from '../../../../../../services/entity-services/dossier-state.service'; +import { DossierStatsService } from '../../../../../../services/dossiers/dossier-stats.service'; +import { DossierStateService } from '@services/entity-services/dossier-state.service'; import { TranslateService } from '@ngx-translate/core'; @Component({ @@ -23,25 +23,25 @@ export class DossiersListingDetailsComponent { constructor( readonly filterService: FilterService, - readonly dossiersService: DossiersService, + readonly activeDossiersService: ActiveDossiersService, private readonly _dossierStatsMap: DossierStatsService, private readonly _translateChartService: TranslateChartService, private readonly _dossierStateService: DossierStateService, private readonly _translateService: TranslateService, ) { - this.documentsChartData$ = this.dossiersService.all$.pipe( + 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.dossiersService.all$.pipe(map(() => this._toDossierChartData())); + this.dossiersChartData$ = this.activeDossiersService.all$.pipe(map(() => this._toDossierChartData())); } private _toDossierChartData(): DoughnutChartConfig[] { this._dossierStateService.all.forEach( - state => (state.dossierCount = this.dossiersService.getCountWithState(state.dossierStatusId)), + state => (state.dossierCount = this.activeDossiersService.getCountWithState(state.dossierStatusId)), ); const configArray: DoughnutChartConfig[] = [ ...this._dossierStateService.all @@ -54,7 +54,7 @@ export class DossiersListingDetailsComponent { .values(), ]; - const notAssignedLength = this.dossiersService.all.length - configArray.map(v => v.value).reduce((acc, val) => acc + val, 0); + 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'), diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-status/dossiers-listing-status.component.html b/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-status/dossiers-listing-status.component.html deleted file mode 100644 index 3df7ebc04..000000000 --- a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-status/dossiers-listing-status.component.html +++ /dev/null @@ -1,13 +0,0 @@ - -
-
{{ currentState.name }}
- -
-
- - -
-
{{ 'edit-dossier-dialog.general-info.form.dossier-status.placeholder' | translate }}
- -
-
diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/table-item/table-item.component.html b/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/table-item/table-item.component.html index 606997852..1adaefc28 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/table-item/table-item.component.html +++ b/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/table-item/table-item.component.html @@ -21,7 +21,7 @@
- +
diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/table-item/table-item.component.ts b/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/table-item/table-item.component.ts index 0151c3320..209e5bebf 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/table-item/table-item.component.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/table-item/table-item.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; import { Dossier, DossierStats } from '@red/domain'; -import { DossierStatsService } from '@services/entity-services/dossier-stats.service'; +import { DossierStatsService } from '../../../../../../services/dossiers/dossier-stats.service'; import { BehaviorSubject, Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/config.service.ts b/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/config.service.ts index 19ad1c220..4cf52fe54 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/config.service.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/config.service.ts @@ -5,12 +5,12 @@ 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 { 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/entity-services/dossier-stats.service'; -import { DossierStateService } from '../../../../services/entity-services/dossier-state.service'; +import { DossierStatsService } from '../../../../services/dossiers/dossier-stats.service'; +import { DossierStateService } from '@services/entity-services/dossier-state.service'; @Injectable() export class ConfigService { diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/dossiers-listing.module.ts b/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/dossiers-listing.module.ts index 29af016b3..d36444a4d 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/dossiers-listing.module.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/dossiers-listing.module.ts @@ -7,12 +7,10 @@ import { RouterModule, Routes } from '@angular/router'; import { DossiersListingActionsComponent } from './components/dossiers-listing-actions/dossiers-listing-actions.component'; import { SharedModule } from '@shared/shared.module'; import { DossiersListingDetailsComponent } from './components/dossiers-listing-details/dossiers-listing-details.component'; -import { DossiersListingDossierNameComponent } from './components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component'; import { ConfigService } from './config.service'; import { TableItemComponent } from './components/table-item/table-item.component'; import { SharedDossiersModule } from '../../shared/shared-dossiers.module'; import { DossierWorkloadColumnComponent } from './components/dossier-workload-column/dossier-workload-column.component'; -import { DossiersListingStatusComponent } from './components/dossiers-listing-status/dossiers-listing-status.component'; import { DossierDocumentsStatusComponent } from './components/dossier-documents-status/dossier-documents-status.component'; const routes: Routes = [ @@ -29,10 +27,8 @@ const routes: Routes = [ DossiersListingScreenComponent, DossiersListingActionsComponent, DossiersListingDetailsComponent, - DossiersListingDossierNameComponent, DossierWorkloadColumnComponent, TableItemComponent, - DossiersListingStatusComponent, DossierDocumentsStatusComponent, ], providers: [ConfigService], diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/screen/dossiers-listing-screen.component.ts b/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/screen/dossiers-listing-screen.component.ts index d70a2ae00..1d78c6fe4 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/screen/dossiers-listing-screen.component.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/screen/dossiers-listing-screen.component.ts @@ -8,7 +8,7 @@ import { DossiersDialogService } from '../../../services/dossiers-dialog.service import { DefaultListingServicesTmp, EntitiesService, ListingComponent, OnAttach, TableComponent } from '@iqser/common-ui'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { ConfigService } from '../config.service'; -import { DossiersService } from '@services/entity-services/dossiers.service'; +import { ActiveDossiersService } from '../../../../../services/dossiers/active-dossiers.service'; import { FilesService } from '@services/entity-services/files.service'; import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service'; import { tap } from 'rxjs/operators'; @@ -18,7 +18,7 @@ import { tap } from 'rxjs/operators'; styleUrls: ['./dossiers-listing-screen.component.scss'], providers: [ ...DefaultListingServicesTmp, - { provide: EntitiesService, useExisting: DossiersService }, + { provide: EntitiesService, useExisting: ActiveDossiersService }, { provide: ListingComponent, useExisting: forwardRef(() => DossiersListingScreenComponent) }, ], changeDetection: ChangeDetectionStrategy.OnPush, @@ -40,7 +40,7 @@ export class DossiersListingScreenComponent extends ListingComponent im protected readonly _injector: Injector, private readonly _userService: UserService, readonly permissionsService: PermissionsService, - private readonly _dossiersService: DossiersService, + private readonly _activeDossiersService: ActiveDossiersService, private readonly _dialogService: DossiersDialogService, private readonly _translateChartService: TranslateChartService, private readonly _configService: ConfigService, @@ -55,7 +55,7 @@ export class DossiersListingScreenComponent extends ListingComponent im } ngOnInit(): void { - this.addSubscription = this._dossiersService.all$.pipe(tap(() => this._computeAllFilters())).subscribe(); + this.addSubscription = this._activeDossiersService.all$.pipe(tap(() => this._computeAllFilters())).subscribe(); } ngOnAttach(): void { diff --git a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/annotation-actions/annotation-actions.component.ts b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/annotation-actions/annotation-actions.component.ts index 1edf49830..6d83d104c 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/annotation-actions/annotation-actions.component.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/annotation-actions/annotation-actions.component.ts @@ -72,6 +72,10 @@ export class AnnotationActionsComponent implements OnChanges { return this.annotations?.length === 1 && this.annotations?.[0].resizing; } + get scrollableParentView(): ScrollableParentView { + return ScrollableParentViews.ANNOTATIONS_LIST; + } + async ngOnChanges(): Promise { await this._setPermissions(); } @@ -115,10 +119,6 @@ export class AnnotationActionsComponent implements OnChanges { this.annotationActionsService.cancelResize($event, this.viewer, this.annotations[0], this.annotationsChanged); } - get scrollableParentView(): ScrollableParentView { - return ScrollableParentViews.ANNOTATIONS_LIST; - } - private async _setPermissions() { const dossier = await this._state.dossier; this.annotationPermissions = AnnotationPermissions.forUser( diff --git a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/document-info/document-info.component.ts b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/document-info/document-info.component.ts index 323443061..2e4dc612d 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/document-info/document-info.component.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/document-info/document-info.component.ts @@ -3,7 +3,7 @@ import { DossiersDialogService } from '../../../../services/dossiers-dialog.serv import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service'; import { DocumentInfoService } from '../../services/document-info.service'; import { combineLatest, Observable, switchMap } from 'rxjs'; -import { PermissionsService } from '../../../../../../services/permissions.service'; +import { PermissionsService } from '@services/permissions.service'; import { FilePreviewStateService } from '../../services/file-preview-state.service'; import { map } from 'rxjs/operators'; import { File } from '@red/domain'; diff --git a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/file-workload/file-workload.component.html b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/file-workload/file-workload.component.html index ba8892922..cbfbcaa6b 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/file-workload/file-workload.component.html +++ b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/file-workload/file-workload.component.html @@ -40,7 +40,7 @@
- +
diff --git a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/page-exclusion/page-exclusion.component.ts b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/page-exclusion/page-exclusion.component.ts index ef40421ca..c0d1a0e23 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/page-exclusion/page-exclusion.component.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/page-exclusion/page-exclusion.component.ts @@ -67,7 +67,7 @@ export class PageExclusionComponent { endPage, }; }); - const excludePages$ = this._reanalysisService.excludePages({ pageRanges }, this._state.dossierId, this._state.fileId); + const excludePages$ = this._reanalysisService.excludePages({ pageRanges }, file.dossierId, file); await firstValueFrom(excludePages$); this._inputComponent.reset(); } catch (e) { @@ -78,12 +78,13 @@ export class PageExclusionComponent { async includePagesRange(range: IPageRange): Promise { this._loadingService.start(); + const file = await this._state.file; const includePages$ = this._reanalysisService.includePages( { pageRanges: [range], }, - this._state.dossierId, - this._state.fileId, + file.dossierId, + file, ); await firstValueFrom(includePages$); this._inputComponent.reset(); diff --git a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/user-management/user-management.component.ts b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/user-management/user-management.component.ts index 075571ce1..7da635d51 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/user-management/user-management.component.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/user-management/user-management.component.ts @@ -3,7 +3,7 @@ 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 { 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'; @@ -11,7 +11,7 @@ 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 { DossiersService } from '@services/entity-services/dossiers.service'; +import { ActiveDossiersService } from '../../../../../../services/dossiers/active-dossiers.service'; @Component({ selector: 'redaction-user-management', @@ -42,9 +42,9 @@ export class UserManagementComponent { readonly loadingService: LoadingService, readonly translateService: TranslateService, readonly stateService: FilePreviewStateService, - private readonly _dossiersService: DossiersService, + private readonly _activeDossiersService: ActiveDossiersService, ) { - this._dossier$ = this.stateService.file$.pipe(switchMap(file => this._dossiersService.getEntityChanged$(file.dossierId))); + 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 => @@ -96,15 +96,15 @@ export class UserManagementComponent { const assigneeId = typeof user === 'string' ? user : user?.id; const reviewerName = this.userService.getNameForId(assigneeId); - const { dossierId, fileId, filename } = file; + const { dossierId, filename } = file; this.loadingService.start(); if (!assigneeId) { - await firstValueFrom(this.filesService.setUnassigned([fileId], dossierId)); + await firstValueFrom(this.filesService.setUnassigned([file], dossierId)); } else if (file.isNew || file.isUnderReview) { - await firstValueFrom(this.filesService.setReviewerFor([fileId], dossierId, assigneeId)); + await firstValueFrom(this.filesService.setReviewerFor([file], dossierId, assigneeId)); } else if (file.isUnderApproval) { - await firstValueFrom(this.filesService.setUnderApprovalFor([fileId], dossierId, assigneeId)); + await firstValueFrom(this.filesService.setUnderApprovalFor([file], dossierId, assigneeId)); } this.loadingService.stop(); diff --git a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/dialogs/accept-recommendation-dialog/accept-recommendation-dialog.component.ts b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/dialogs/accept-recommendation-dialog/accept-recommendation-dialog.component.ts index 7b513bd5b..23486c7f9 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/dialogs/accept-recommendation-dialog/accept-recommendation-dialog.component.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/dialogs/accept-recommendation-dialog/accept-recommendation-dialog.component.ts @@ -3,7 +3,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { PermissionsService } from '@services/permissions.service'; import { Dictionary, Dossier } from '@red/domain'; -import { DossiersService } from '@services/entity-services/dossiers.service'; +import { ActiveDossiersService } from '../../../../../../services/dossiers/active-dossiers.service'; import { BaseDialogComponent } from '@iqser/common-ui'; import { DictionaryService } from '@shared/services/dictionary.service'; import { ManualAnnotationService } from '../../../../services/manual-annotation.service'; @@ -33,14 +33,14 @@ export class AcceptRecommendationDialogComponent extends BaseDialogComponent imp private readonly _formBuilder: FormBuilder, private readonly _manualAnnotationService: ManualAnnotationService, private readonly _permissionsService: PermissionsService, - private readonly _dossiersService: DossiersService, + private readonly _activeDossiersService: ActiveDossiersService, private readonly _dictionaryService: DictionaryService, protected readonly _injector: Injector, protected readonly _dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) readonly data: AcceptRecommendationData, ) { super(_injector, _dialogRef); - this._dossier = this._dossiersService.find(this.data.dossierId); + this._dossier = this._activeDossiersService.find(this.data.dossierId); this.isDocumentAdmin = this._permissionsService.isApprover(this._dossier); this.form = this._getForm(); this.initialFormValue = this.form.getRawValue(); diff --git a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-providers.ts b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-providers.ts index 1ce08b6a8..4cb277023 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-providers.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-providers.ts @@ -10,6 +10,9 @@ import { FilePreviewStateService } from './services/file-preview-state.service'; import { PdfViewerDataService } from '../../services/pdf-viewer-data.service'; import { AnnotationReferencesService } from './services/annotation-references.service'; import { FilterService } from '@iqser/common-ui'; +import { ManualAnnotationService } from '../../services/manual-annotation.service'; +import { AnnotationProcessingService } from '../../services/annotation-processing.service'; +import { dossiersServiceProvider } from '@services/entity-services/dossiers.service.provider'; import { PageRotationService } from './services/page-rotation.service'; import { PdfViewer } from './services/pdf-viewer.service'; @@ -28,4 +31,7 @@ export const filePreviewScreenProviders = [ AnnotationReferencesService, PageRotationService, PdfViewer, + ManualAnnotationService, + AnnotationProcessingService, + dossiersServiceProvider, ]; diff --git a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-screen.component.ts index 70d08c0ea..6af1c52dc 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-screen.component.ts @@ -34,7 +34,6 @@ 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 { DossiersService } from '@services/entity-services/dossiers.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'; @@ -50,6 +49,7 @@ 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 Annotation = Core.Annotations.Annotation; @@ -254,7 +254,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni const file = await this.stateService.file; if (file?.analysisRequired && !file.excludedFromAutomaticAnalysis) { - const reanalyzeFiles = this._reanalysisService.reanalyzeFilesForDossier([this.fileId], this.dossierId, { force: true }); + const reanalyzeFiles = this._reanalysisService.reanalyzeFilesForDossier([file], this.dossierId, { force: true }); await firstValueFrom(reanalyzeFiles); } @@ -449,7 +449,8 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni async annotationsChangedByReviewAction(annotation?: AnnotationWrapper) { this.multiSelectService.deactivate(); - const fileReloaded = await firstValueFrom(this._filesService.reload(this.dossierId, this.fileId)); + 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); } @@ -574,7 +575,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni private _subscribeToFileUpdates(): void { this.addActiveScreenSubscription = timer(0, 5000) - .pipe(switchMap(() => this._filesService.reload(this.dossierId, this.fileId))) + .pipe( + switchMap(() => this.stateService.file$), + switchMap(file => this._filesService.reload(this.dossierId, file)), + ) .subscribe(); this.addActiveScreenSubscription = this._dossiersService diff --git a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/services/annotation-actions.service.ts b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/services/annotation-actions.service.ts index 29b31a0e8..9678c87ae 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/services/annotation-actions.service.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/services/annotation-actions.service.ts @@ -14,7 +14,7 @@ import { Dossier, IAddRedactionRequest, ILegalBasisChangeRequest, IRectangle, IR import { toPosition } from '../../../utils/pdf-calculation.utils'; import { AnnotationDrawService } from './annotation-draw.service'; import { translateQuads } from '@utils/pdf-coordinates'; -import { DossiersService } from '@services/entity-services/dossiers.service'; +import { ActiveDossiersService } from '../../../../../services/dossiers/active-dossiers.service'; import { AcceptRecommendationData, AcceptRecommendationDialogComponent, @@ -38,12 +38,12 @@ export class AnnotationActionsService { private readonly _dialogService: DossiersDialogService, private readonly _dialog: MatDialog, private readonly _annotationDrawService: AnnotationDrawService, - private readonly _dossiersService: DossiersService, + private readonly _activeDossiersService: ActiveDossiersService, private readonly _screenStateService: FilePreviewStateService, ) {} private get _dossier(): Dossier { - return this._dossiersService.find(this._screenStateService.dossierId); + return this._activeDossiersService.find(this._screenStateService.dossierId); } acceptSuggestion($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter) { diff --git a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/services/annotation-draw.service.ts b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/services/annotation-draw.service.ts index 5f3a32f1f..9fb5591fa 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/services/annotation-draw.service.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/services/annotation-draw.service.ts @@ -3,7 +3,6 @@ import { Core, WebViewerInstance } from '@pdftron/webviewer'; import { hexToRgb } from '@utils/functions'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { UserPreferenceService } from '@services/user-preference.service'; -import { DossiersService } from '@services/entity-services/dossiers.service'; import { RedactionLogService } from '../../../services/redaction-log.service'; import { environment } from '@environments/environment'; @@ -11,6 +10,7 @@ import { IRectangle, ISectionGrid, ISectionRectangle } from '@red/domain'; import { SkippedService } from './skipped.service'; import { firstValueFrom } from 'rxjs'; import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service'; +import { DossiersService } from '../../../../../services/dossiers/dossiers.service'; import Annotation = Core.Annotations.Annotation; @Injectable() diff --git a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/services/file-preview-state.service.ts b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/services/file-preview-state.service.ts index 5c43e1fc6..ea78bb349 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/services/file-preview-state.service.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/services/file-preview-state.service.ts @@ -1,15 +1,16 @@ -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { BehaviorSubject, firstValueFrom, from, Observable, pairwise, switchMap } from 'rxjs'; import { FileDataModel } from '@models/file/file-data.model'; import { Dossier, File } from '@red/domain'; -import { DossiersService } from '@services/entity-services/dossiers.service'; import { ActivatedRoute } from '@angular/router'; import { FilesMapService } from '@services/entity-services/files-map.service'; -import { PermissionsService } from '../../../../../services/permissions.service'; +import { PermissionsService } from '@services/permissions.service'; import { boolFactory } from '@iqser/common-ui'; import { filter, startWith } from 'rxjs/operators'; import { FileManagementService } from '@services/entity-services/file-management.service'; import { DOSSIER_ID, FILE_ID } from '@utils/constants'; +import { DossiersService } from '../../../../../services/dossiers/dossiers.service'; +import { dossiersServiceResolver } from '@services/entity-services/dossiers.service.provider'; import { wipeFilesCache } from '../../../../../../../../../libs/red-cache/src'; @Injectable() @@ -28,19 +29,19 @@ export class FilePreviewStateService { readonly #fileData$ = new BehaviorSubject(undefined); constructor( - dossiersService: DossiersService, - filesMapService: FilesMapService, - permissionsService: PermissionsService, - activatedRoute: ActivatedRoute, private readonly _fileManagementService: FileManagementService, + private readonly _injector: Injector, + private readonly _route: ActivatedRoute, + private readonly _filesMapService: FilesMapService, + private readonly _permissionsService: PermissionsService, ) { - this.fileId = activatedRoute.snapshot.paramMap.get(FILE_ID); - this.dossierId = activatedRoute.snapshot.paramMap.get(DOSSIER_ID); - this.dossierTemplateId = dossiersService.find(this.dossierId).dossierTemplateId; + this.fileId = _route.snapshot.paramMap.get(FILE_ID); + this.dossierId = _route.snapshot.paramMap.get(DOSSIER_ID); + this.dossierTemplateId = this._dossiersService.find(this.dossierId).dossierTemplateId; - this.dossier$ = dossiersService.getEntityChanged$(this.dossierId); - this.file$ = filesMapService.watch$(this.dossierId, this.fileId); - [this.isReadonly$, this.isWritable$] = boolFactory(this.file$, file => !permissionsService.canPerformAnnotationActions(file)); + this.dossier$ = this._dossiersService.getEntityChanged$(this.dossierId); + this.file$ = _filesMapService.watch$(this.dossierId, this.fileId); + [this.isReadonly$, this.isWritable$] = boolFactory(this.file$, file => !_permissionsService.canPerformAnnotationActions(file)); this.fileData$ = this.#fileData$.asObservable().pipe(filter(value => !!value)); this.blob$ = this.#blob$; @@ -66,6 +67,10 @@ export class FilePreviewStateService { return firstValueFrom(this.blob$); } + private get _dossiersService(): DossiersService { + return dossiersServiceResolver(this._injector); + } + get #blob$() { return this.file$.pipe( startWith(undefined), diff --git a/apps/red-ui/src/app/modules/dossier/services/manual-annotation.service.ts b/apps/red-ui/src/app/modules/dossier/services/manual-annotation.service.ts index fbf1e8c9b..6c14dc6cd 100644 --- a/apps/red-ui/src/app/modules/dossier/services/manual-annotation.service.ts +++ b/apps/red-ui/src/app/modules/dossier/services/manual-annotation.service.ts @@ -17,7 +17,7 @@ import { AnnotationActionMode } from '../models/annotation-action-mode.model'; import { annotationActionsTranslations } from '../translations/annotation-actions-translations'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; -import { DossiersService } from '@services/entity-services/dossiers.service'; +import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service'; import { Observable } from 'rxjs'; import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service'; import { ManualRedactionEntryType } from '@models/file/manual-redaction-entry.wrapper'; @@ -32,7 +32,7 @@ export class ManualAnnotationService extends GenericService private readonly _dictionariesMapService: DictionariesMapService, private readonly _toaster: Toaster, private readonly _permissionsService: PermissionsService, - private readonly _dossiersService: DossiersService, + private readonly _activeDossiersService: ActiveDossiersService, protected readonly _injector: Injector, ) { super(_injector, 'manualRedaction'); @@ -372,7 +372,7 @@ export class ManualAnnotationService extends GenericService } private _dossier(dossierId: string): Dossier { - return this._dossiersService.find(dossierId); + return this._activeDossiersService.find(dossierId); } private _getMessage(mode: AnnotationActionMode, modifyDictionary?: boolean, error = false, isConflict = false) { diff --git a/apps/red-ui/src/app/modules/dossier/shared/components/file-actions/file-actions.component.ts b/apps/red-ui/src/app/modules/dossier/shared/components/file-actions/file-actions.component.ts index e32acc196..a59492f02 100644 --- a/apps/red-ui/src/app/modules/dossier/shared/components/file-actions/file-actions.component.ts +++ b/apps/red-ui/src/app/modules/dossier/shared/components/file-actions/file-actions.component.ts @@ -28,7 +28,7 @@ import { UserService } from '@services/user.service'; import { UserPreferenceService } from '@services/user-preference.service'; import { LongPressEvent } from '@shared/directives/long-press.directive'; import { FileAssignService } from '../../services/file-assign.service'; -import { DossiersService } from '@services/entity-services/dossiers.service'; +import { ActiveDossiersService } from '../../../../../services/dossiers/active-dossiers.service'; import { FileManagementService } from '@services/entity-services/file-management.service'; import { FilesService } from '@services/entity-services/files.service'; import { ReanalysisService, ReanalyzeQueryParams } from '@services/reanalysis.service'; @@ -75,6 +75,7 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy, showUnderApproval = false; showApprove = false; canToggleAnalysis = false; + showToggleAnalysis = false; showStatusBar = false; showOpenDocument = false; showReanalyseFilePreview = false; @@ -95,7 +96,7 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy, @Optional() private readonly _documentInfoService: DocumentInfoService, @Optional() private readonly _pageRotationService: PageRotationService, private readonly _permissionsService: PermissionsService, - private readonly _dossiersService: DossiersService, + private readonly _activeDossiersService: ActiveDossiersService, private readonly _dialogService: DossiersDialogService, private readonly _fileAssignService: FileAssignService, private readonly _loadingService: LoadingService, @@ -129,28 +130,28 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy, return [ { type: ActionTypes.circleBtn, - action: $event => this._openDeleteFileDialog($event), + action: ($event: MouseEvent) => this._openDeleteFileDialog($event), tooltip: _('dossier-overview.delete.action'), icon: 'iqser:trash', show: this.showDelete, }, { type: ActionTypes.circleBtn, - action: $event => this._assign($event), + action: ($event: MouseEvent) => this._assign($event), tooltip: this.assignTooltip, icon: 'red:assign', show: this.showAssign, }, { type: ActionTypes.circleBtn, - action: $event => this._assignToMe($event), + action: ($event: MouseEvent) => this._assignToMe($event), tooltip: _('dossier-overview.assign-me'), icon: 'red:assign-me', show: this.showAssignToSelf, }, { type: ActionTypes.circleBtn, - action: $event => this._triggerImportRedactions($event), + action: ($event: MouseEvent) => this._triggerImportRedactions($event), tooltip: _('dossier-overview.import-redactions'), icon: 'red:import_redactions', show: this.showImportRedactions, @@ -180,21 +181,21 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy, }, { type: ActionTypes.circleBtn, - action: $event => this._setFileUnderApproval($event), + action: ($event: MouseEvent) => this._setFileUnderApproval($event), tooltip: _('dossier-overview.under-approval'), icon: 'red:ready-for-approval', show: this.showUnderApproval, }, { type: ActionTypes.circleBtn, - action: $event => this._setFileUnderReview($event), + action: ($event: MouseEvent) => this._setFileUnderReview($event), tooltip: _('dossier-overview.under-review'), icon: 'red:undo', show: this.showUnderReview, }, { type: ActionTypes.circleBtn, - action: $event => this.setFileApproved($event), + action: ($event: MouseEvent) => this.setFileApproved($event), tooltip: this.file.canBeApproved ? _('dossier-overview.approve') : _('dossier-overview.approve-disabled'), icon: 'red:approved', disabled: !this.file.canBeApproved, @@ -202,14 +203,14 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy, }, { type: ActionTypes.circleBtn, - action: $event => this._toggleAutomaticAnalysis($event), + action: ($event: MouseEvent) => this._toggleAutomaticAnalysis($event), tooltip: _('dossier-overview.stop-auto-analysis'), icon: 'red:disable-analysis', show: this.canDisableAutoAnalysis, }, { type: ActionTypes.circleBtn, - action: $event => this._reanalyseFile($event), + action: ($event: MouseEvent) => this._reanalyseFile($event), tooltip: _('file-preview.reanalyse-notification'), tooltipClass: 'warn small', icon: 'iqser:refresh', @@ -217,7 +218,7 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy, }, { type: ActionTypes.circleBtn, - action: $event => this._toggleAutomaticAnalysis($event), + action: ($event: MouseEvent) => this._toggleAutomaticAnalysis($event), tooltip: _('dossier-overview.start-auto-analysis'), buttonType: this.isFilePreview ? CircleButtonTypes.warn : CircleButtonTypes.default, icon: 'red:enable-analysis', @@ -225,21 +226,21 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy, }, { type: ActionTypes.circleBtn, - action: $event => this._setFileUnderApproval($event), + action: ($event: MouseEvent) => this._setFileUnderApproval($event), tooltip: _('dossier-overview.under-approval'), icon: 'red:undo', show: this.showUndoApproval, }, { type: ActionTypes.circleBtn, - action: $event => this._ocrFile($event), + action: ($event: MouseEvent) => this._ocrFile($event), tooltip: _('dossier-overview.ocr-file'), icon: 'iqser:ocr', show: this.showOCR, }, { type: ActionTypes.circleBtn, - action: $event => this._reanalyseFile($event), + action: ($event: MouseEvent) => this._reanalyseFile($event), tooltip: _('dossier-overview.reanalyse.action'), icon: 'iqser:refresh', show: this.showReanalyseDossierOverview, @@ -251,13 +252,13 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy, tooltip: this.toggleTooltip, class: { 'mr-24': this.isDossierOverviewList }, checked: !this.file.excluded, - show: true, + show: this.showToggleAnalysis, }, ].filter(btn => btn.show); } ngOnInit() { - this.addSubscription = this._dossiersService + this.addSubscription = this._activeDossiersService .getEntityChanged$(this.file.dossierId) .pipe(tap(() => this._setup())) .subscribe(); @@ -332,8 +333,8 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy, async () => { this._loadingService.start(); try { - const dossier = this._dossiersService.find(this.file.dossierId); - await firstValueFrom(this._fileManagementService.delete([this.file.fileId], this.file.dossierId)); + const dossier = this._activeDossiersService.find(this.file.dossierId); + await firstValueFrom(this._fileManagementService.delete([this.file], this.file.dossierId)); await this._router.navigate([dossier.routerLink]); } catch (error) { this._toaster.error(_('error.http.generic'), { params: error }); @@ -362,7 +363,7 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy, force: true, triggeredByUser: true, }; - await firstValueFrom(this._reanalysisService.reanalyzeFilesForDossier([this.file.fileId], this.file.dossierId, params)); + await firstValueFrom(this._reanalysisService.reanalyzeFilesForDossier([this.file], this.file.dossierId, params)); } private async _toggleAutomaticAnalysis($event: MouseEvent) { @@ -383,7 +384,7 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy, await firstValueFrom(this._pageRotationService.showConfirmationDialogIfHasRotations()); } this._loadingService.start(); - await firstValueFrom(this._reanalysisService.ocrFiles([this.file.fileId], this.file.dossierId)); + await firstValueFrom(this._reanalysisService.ocrFiles([this.file], this.file.dossierId)); this._loadingService.stop(); } @@ -393,7 +394,7 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy, private async _toggleAnalysis() { this._loadingService.start(); - await firstValueFrom(this._reanalysisService.toggleAnalysis(this.file.dossierId, [this.file.fileId], !this.file.excluded)); + await firstValueFrom(this._reanalysisService.toggleAnalysis(this.file.dossierId, [this.file], !this.file.excluded)); this._loadingService.stop(); } @@ -415,8 +416,9 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy, this.showApprove = this._permissionsService.isReadyForApproval(this.file) && !this.isDossierOverviewWorkflow; this.canToggleAnalysis = this._permissionsService.canToggleAnalysis(this.file); + this.showToggleAnalysis = this._permissionsService.showToggleAnalysis(this.file); this.showDelete = this._permissionsService.canDeleteFile(this.file); - this.showOCR = this.file.canBeOCRed; + this.showOCR = this._permissionsService.canOcrFile(this.file); this.canReanalyse = this._permissionsService.canReanalyseFile(this.file); this.canDisableAutoAnalysis = this._permissionsService.canDisableAutoAnalysis([this.file]); this.canEnableAutoAnalysis = this._permissionsService.canEnableAutoAnalysis([this.file]); @@ -442,7 +444,7 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy, private async _setFileApproved() { this._loadingService.start(); - await firstValueFrom(this._filesService.setApprovedFor([this.file.id], this.file.dossierId)); + await firstValueFrom(this._filesService.setApprovedFor([this.file], this.file.dossierId)); this._loadingService.stop(); } } diff --git a/apps/red-ui/src/app/modules/dossier/shared/services/file-assign.service.ts b/apps/red-ui/src/app/modules/dossier/shared/services/file-assign.service.ts index 0694e0588..49c61d10e 100644 --- a/apps/red-ui/src/app/modules/dossier/shared/services/file-assign.service.ts +++ b/apps/red-ui/src/app/modules/dossier/shared/services/file-assign.service.ts @@ -5,7 +5,7 @@ import { DossiersDialogService } from '../../services/dossiers-dialog.service'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { FilesService } from '@services/entity-services/files.service'; import { ConfirmationDialogInput, LoadingService, Toaster } from '@iqser/common-ui'; -import { DossiersService } from '@services/entity-services/dossiers.service'; +import { ActiveDossiersService } from '../../../../services/dossiers/active-dossiers.service'; import { firstValueFrom, Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; @@ -15,7 +15,7 @@ export class FileAssignService { private readonly _dialogService: DossiersDialogService, private readonly _userService: UserService, private readonly _filesService: FilesService, - private readonly _dossiersService: DossiersService, + private readonly _activeDossiersService: ActiveDossiersService, private readonly _loadingService: LoadingService, private readonly _toaster: Toaster, ) {} @@ -53,7 +53,7 @@ export class FileAssignService { $event?.stopPropagation(); const currentUserId = this._userService.currentUser.id; - const currentDossier = this._dossiersService.find(file.dossierId); + const currentDossier = this._activeDossiersService.find(file.dossierId); const eligibleUsersIds = this._getUserIds(mode, currentDossier); if (file.isNew) { @@ -81,28 +81,11 @@ export class FileAssignService { this._loadingService.start(); try { if (!userId) { - await firstValueFrom( - this._filesService.setUnassigned( - files.map(f => f.fileId), - files[0].dossierId, - ), - ); + await firstValueFrom(this._filesService.setUnassigned(files, files[0].dossierId)); } else if (mode === 'reviewer') { - await firstValueFrom( - this._filesService.setReviewerFor( - files.map(f => f.fileId), - files[0].dossierId, - userId, - ), - ); + await firstValueFrom(this._filesService.setReviewerFor(files, files[0].dossierId, userId)); } else { - await firstValueFrom( - this._filesService.setUnderApprovalFor( - files.map(f => f.fileId), - files[0].dossierId, - userId, - ), - ); + await firstValueFrom(this._filesService.setUnderApprovalFor(files, files[0].dossierId, userId)); } } catch (error) { this._toaster.error(_('error.http.generic'), { params: error }); @@ -117,11 +100,7 @@ export class FileAssignService { private _assignReviewerToCurrentUser(files: File[]): Observable { this._loadingService.start(); return this._filesService - .setReviewerFor( - files.map(f => f.fileId), - files[0].dossierId, - this._userService.currentUser.id, - ) + .setReviewerFor(files, files[0].dossierId, this._userService.currentUser.id) .pipe(tap(() => this._loadingService.stop())); } } diff --git a/apps/red-ui/src/app/modules/dossier/shared/shared-dossiers.module.ts b/apps/red-ui/src/app/modules/dossier/shared/shared-dossiers.module.ts index b7cded317..4a59416c5 100644 --- a/apps/red-ui/src/app/modules/dossier/shared/shared-dossiers.module.ts +++ b/apps/red-ui/src/app/modules/dossier/shared/shared-dossiers.module.ts @@ -2,17 +2,40 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FileAssignService } from './services/file-assign.service'; import { FileActionsComponent } from './components/file-actions/file-actions.component'; -import { IqserIconsModule } from '@iqser/common-ui'; import { SharedModule } from '@shared/shared.module'; import { RedactionImportService } from './services/redaction-import.service'; +import { DossiersDialogService } from '../services/dossiers-dialog.service'; +import { DocumentInfoDialogComponent } from '../dialogs/document-info-dialog/document-info-dialog.component'; +import { EditDossierDialogComponent } from '../dialogs/edit-dossier-dialog/edit-dossier-dialog.component'; +import { AddDossierDialogComponent } from '../dialogs/add-dossier-dialog/add-dossier-dialog.component'; +import { AssignReviewerApproverDialogComponent } from '../dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component'; +import { EditDossierGeneralInfoComponent } from '../dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component'; +import { EditDossierDownloadPackageComponent } from '../dialogs/edit-dossier-dialog/download-package/edit-dossier-download-package.component'; +import { EditDossierDictionaryComponent } from '../dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component'; +import { 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 = [FileActionsComponent, DateColumnComponent]; +const components = [ + FileActionsComponent, + DocumentInfoDialogComponent, + EditDossierGeneralInfoComponent, + EditDossierDownloadPackageComponent, + EditDossierDictionaryComponent, + EditDossierAttributesComponent, + EditDossierTeamComponent, + EditDossierDeletedDocumentsComponent, + FileActionsComponent, + DateColumnComponent, +]; +const dialogs = [EditDossierDialogComponent, AddDossierDialogComponent, AssignReviewerApproverDialogComponent]; +const services = [DossiersDialogService, FileAssignService, RedactionImportService]; @NgModule({ - declarations: [...components], - exports: [...components], - providers: [FileAssignService, RedactionImportService], - imports: [CommonModule, IqserIconsModule, SharedModule], + declarations: [...components, ...dialogs], + exports: [...components, ...dialogs], + providers: [...services], + imports: [CommonModule, SharedModule], }) export class SharedDossiersModule {} diff --git a/apps/red-ui/src/app/modules/icons/icons.module.ts b/apps/red-ui/src/app/modules/icons/icons.module.ts index 3513cf42d..bb2e7eff9 100644 --- a/apps/red-ui/src/app/modules/icons/icons.module.ts +++ b/apps/red-ui/src/app/modules/icons/icons.module.ts @@ -13,6 +13,7 @@ export class IconsModule { const icons = [ 'ai', 'approved', + 'archive', 'arrow-up', 'assign', 'assign-me', diff --git a/apps/red-ui/src/app/modules/dossier/screens/search-screen/search-screen.component.html b/apps/red-ui/src/app/modules/search/search-screen/search-screen.component.html similarity index 93% rename from apps/red-ui/src/app/modules/dossier/screens/search-screen/search-screen.component.html rename to apps/red-ui/src/app/modules/search/search-screen/search-screen.component.html index 5c44f4aca..b63970902 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/search-screen/search-screen.component.html +++ b/apps/red-ui/src/app/modules/search/search-screen/search-screen.component.html @@ -74,8 +74,11 @@ >
-
- {{ item.dossierName }} +
+
+ + {{ item.dossierName }} +
diff --git a/apps/red-ui/src/app/modules/dossier/screens/search-screen/search-screen.component.scss b/apps/red-ui/src/app/modules/search/search-screen/search-screen.component.scss similarity index 100% rename from apps/red-ui/src/app/modules/dossier/screens/search-screen/search-screen.component.scss rename to apps/red-ui/src/app/modules/search/search-screen/search-screen.component.scss diff --git a/apps/red-ui/src/app/modules/dossier/screens/search-screen/search-screen.component.ts b/apps/red-ui/src/app/modules/search/search-screen/search-screen.component.ts similarity index 68% rename from apps/red-ui/src/app/modules/dossier/screens/search-screen/search-screen.component.ts rename to apps/red-ui/src/app/modules/search/search-screen/search-screen.component.ts index 71a3b1244..0fae5de35 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/search-screen/search-screen.component.ts +++ b/apps/red-ui/src/app/modules/search/search-screen/search-screen.component.ts @@ -14,13 +14,14 @@ import { combineLatest, Observable } from 'rxjs'; import { debounceTime, map, startWith, switchMap, tap } from 'rxjs/operators'; import { ActivatedRoute, Router } from '@angular/router'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; -import { workflowFileStatusTranslations } from '../../translations/file-status-translations'; +import { workflowFileStatusTranslations } from '../../../translations/file-status-translations'; import { TranslateService } from '@ngx-translate/core'; import { RouterHistoryService } from '@services/router-history.service'; -import { DossiersService } from '@services/entity-services/dossiers.service'; -import { Dossier, IMatchedDocument, ISearchListItem, ISearchResponse } from '@red/domain'; +import { Dossier, DossierStatuses, IMatchedDocument, ISearchListItem, ISearchResponse } from '@red/domain'; import { FilesMapService } from '@services/entity-services/files-map.service'; import { PlatformSearchService } from '@services/entity-services/platform-search.service'; +import { ActiveDossiersService } from '../../../services/dossiers/active-dossiers.service'; +import { ArchivedDossiersService } from '../../../services/dossiers/archived-dossiers.service'; @Component({ templateUrl: './search-screen.component.html', @@ -43,8 +44,14 @@ export class SearchScreenComponent extends ListingComponent imp readonly searchResults$ = combineLatest([this._queryChanged, this._filtersChanged$]).pipe( tap(() => this._loadingService.start()), - tap(([query, dossierIds]) => this._updateNavigation(query, dossierIds)), - switchMap(([query, dossierIds]) => this._platformSearchService.search({ query, dossierIds })), + tap(([query, [dossierIds, onlyActive]]) => this._updateNavigation(query, dossierIds, onlyActive)), + switchMap(([query, [dossierIds, onlyActive]]) => + this._platformSearchService.search({ + query, + dossierIds, + dossierStatus: onlyActive ? [] : [DossierStatuses.ACTIVE, DossierStatuses.ARCHIVED], + }), + ), map(searchResult => this._toMatchedDocuments(searchResult)), map(docs => this._toListItems(docs)), tap(result => this.entitiesService.setEntities(result)), @@ -56,7 +63,8 @@ export class SearchScreenComponent extends ListingComponent imp protected readonly _injector: Injector, private readonly _activatedRoute: ActivatedRoute, private readonly _loadingService: LoadingService, - private readonly _dossiersService: DossiersService, + private readonly _activeDossiersService: ActiveDossiersService, + private readonly _archivedDossiersService: ArchivedDossiersService, readonly routerHistoryService: RouterHistoryService, private readonly _translateService: TranslateService, private readonly _filesMapService: FilesMapService, @@ -71,21 +79,28 @@ export class SearchScreenComponent extends ListingComponent imp const checked = dossierIds.includes(id); return new NestedFilter({ id, label: dossierName, checked }); }; + const allDossiers = [...this._activeDossiersService.all, ...this._archivedDossiersService.all]; const dossierNameFilter: IFilterGroup = { slug: 'dossiers', label: this._translateService.instant('search-screen.filters.by-dossier'), filterceptionPlaceholder: this._translateService.instant('search-screen.filters.search-placeholder'), icon: 'red:folder', - filters: this._dossiersService.all.map(dossierToFilter), + filters: allDossiers.map(dossierToFilter), checker: keyChecker('dossierId'), }; this.filterService.addFilterGroups([dossierNameFilter]); + const onlyActiveLabel = this._translateService.instant('search-screen.filters.only-active'); + this.filterService.addSingleFilter({ id: 'onlyActiveDossiers', label: onlyActiveLabel, checked: this._routeOnlyActive }); } private get _routeDossierIds(): string[] { return this._activatedRoute.snapshot.queryParamMap.get('dossierIds').split(','); } + private get _routeOnlyActive(): boolean { + return this._activatedRoute.snapshot.queryParamMap.get('onlyActive') === 'true'; + } + private get _routeQuery(): string { return this._activatedRoute.snapshot.queryParamMap.get('query'); } @@ -98,10 +113,15 @@ export class SearchScreenComponent extends ListingComponent imp ); } - private get _filtersChanged$(): Observable { - return this.filterService.filterGroups$.pipe( - map(groups => groups[0].filters.filter(v => v.checked).map(v => v.id)), - startWith(this._routeDossierIds), + private get _filtersChanged$(): Observable<[string[], boolean]> { + const onlyActiveDossiers$ = this.filterService.getSingleFilter('onlyActiveDossiers').pipe(map(f => !!f.checked)); + const filterGroups$ = this.filterService.filterGroups$; + return combineLatest([filterGroups$, onlyActiveDossiers$]).pipe( + map(([groups, onlyActive]) => { + const dossierIds: string[] = groups[0].filters.filter(v => v.checked).map(v => v.id); + return [dossierIds, onlyActive]; + }), + startWith<[string[], boolean]>([this._routeDossierIds, this._routeOnlyActive]), ); } @@ -110,8 +130,8 @@ export class SearchScreenComponent extends ListingComponent imp this.searchService.searchValue = newQuery ?? ''; } - private _updateNavigation(query?: string, dossierIds?: string[]): Promise { - const queryParams = { query, dossierIds: dossierIds.join(',') }; + private _updateNavigation(query?: string, dossierIds?: string[], onlyActive?: boolean): Promise { + const queryParams = { query, dossierIds: dossierIds.join(','), onlyActive }; return this._router.navigate([], { queryParams, replaceUrl: true }); } @@ -123,24 +143,29 @@ export class SearchScreenComponent extends ListingComponent imp return matchedDocuments.map(document => this._toListItem(document)).filter(value => value); } - private _toListItem({ dossierId, fileId, unmatchedTerms, highlights, score }: IMatchedDocument): ISearchListItem { + private _toListItem({ dossierId, fileId, unmatchedTerms, highlights, score, dossierStatus }: IMatchedDocument): ISearchListItem { const file = this._filesMapService.get(dossierId, fileId); if (!file) { return undefined; } + const dossier = (dossierStatus === DossierStatuses.ARCHIVED ? this._archivedDossiersService : this._activeDossiersService).find( + dossierId, + ); + return { id: fileId, dossierId, + dossierStatus, unmatched: unmatchedTerms || null, highlights, status: file.workflowStatus, assignee: file.assignee, numberOfPages: file.numberOfPages, - dossierName: this._dossiersService.find(dossierId).dossierName, + dossierName: dossier.dossierName, filename: file.filename, searchKey: score.toString(), - routerLink: `/main/dossiers/${dossierId}/file/${fileId}`, + routerLink: file.routerLink, }; } } diff --git a/apps/red-ui/src/app/modules/search/search.module.ts b/apps/red-ui/src/app/modules/search/search.module.ts new file mode 100644 index 000000000..7a79b48ab --- /dev/null +++ b/apps/red-ui/src/app/modules/search/search.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SearchScreenComponent } from './search-screen/search-screen.component'; +import { RouterModule } from '@angular/router'; +import { CommonUiModule } from '@iqser/common-ui'; +import { SharedModule } from '../shared/shared.module'; + +const routes = [{ path: '', component: SearchScreenComponent }]; + +@NgModule({ + declarations: [SearchScreenComponent], + imports: [CommonModule, RouterModule.forChild(routes), CommonUiModule, SharedModule], +}) +export class SearchModule {} diff --git a/apps/red-ui/src/app/modules/shared/components/buttons/file-download-btn/file-download-btn.component.ts b/apps/red-ui/src/app/modules/shared/components/buttons/file-download-btn/file-download-btn.component.ts index 8009960da..0b595cdf7 100644 --- a/apps/red-ui/src/app/modules/shared/components/buttons/file-download-btn/file-download-btn.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/buttons/file-download-btn/file-download-btn.component.ts @@ -38,7 +38,7 @@ export class FileDownloadBtnComponent implements OnChanges { async downloadFiles($event: MouseEvent) { $event.stopPropagation(); const dossierId = this.files[0].dossierId; - const filesIds = this.files.map(f => f.fileId); + const filesIds = this.files.map(f => f.id); await firstValueFrom(this._fileDownloadService.downloadFiles(filesIds, dossierId)); this._toaster.info(_('download-status.queued')); } diff --git a/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.html b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.html index 8b54d5c02..8050b4264 100644 --- a/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.html +++ b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.html @@ -51,7 +51,7 @@
-
+
{{ selectDossier.dossierName | translate }} diff --git a/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts index 839c80693..e8d4ea708 100644 --- a/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts @@ -5,7 +5,7 @@ import { catchError, map, take, tap } from 'rxjs/operators'; import { Dictionary, Dossier, DossierTemplate } from '@red/domain'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { DictionaryService } from '@shared/services/dictionary.service'; -import { DossiersService } from '@services/entity-services/dossiers.service'; +import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service'; import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service'; import { EditorComponent } from '@shared/components/editor/editor.component'; import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service'; @@ -47,7 +47,7 @@ export class DictionaryManagerComponent implements OnChanges { constructor( private readonly _dictionaryService: DictionaryService, private readonly _dictionariesMapService: DictionariesMapService, - readonly dossiersService: DossiersService, + readonly activeDossiersService: ActiveDossiersService, readonly dossierTemplatesService: DossierTemplatesService, ) {} diff --git a/apps/red-ui/src/app/modules/shared/components/dossier-status/dossier-status.component.html b/apps/red-ui/src/app/modules/shared/components/dossier-status/dossier-status.component.html new file mode 100644 index 000000000..f4c5f96bf --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/components/dossier-status/dossier-status.component.html @@ -0,0 +1,6 @@ +
+
+ {{ currentState?.name || ('edit-dossier-dialog.general-info.form.dossier-status.placeholder' | translate) }} +
+ +
diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-status/dossiers-listing-status.component.scss b/apps/red-ui/src/app/modules/shared/components/dossier-status/dossier-status.component.scss similarity index 100% rename from apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-status/dossiers-listing-status.component.scss rename to apps/red-ui/src/app/modules/shared/components/dossier-status/dossier-status.component.scss diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-status/dossiers-listing-status.component.ts b/apps/red-ui/src/app/modules/shared/components/dossier-status/dossier-status.component.ts similarity index 57% rename from apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-status/dossiers-listing-status.component.ts rename to apps/red-ui/src/app/modules/shared/components/dossier-status/dossier-status.component.ts index e3994df3d..7ad21eca7 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-status/dossiers-listing-status.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/dossier-status/dossier-status.component.ts @@ -1,15 +1,14 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core'; -import { Dossier } from '../../../../../../../../../../libs/red-domain/src'; -import { DossierStateService } from '../../../../../../services/entity-services/dossier-state.service'; -import { DossierState } from '@red/domain'; +import { Dossier, DossierState } from '@red/domain'; +import { DossierStateService } from '@services/entity-services/dossier-state.service'; @Component({ - selector: 'redaction-dossiers-listing-status', - templateUrl: './dossiers-listing-status.component.html', - styleUrls: ['./dossiers-listing-status.component.scss'], + selector: 'redaction-dossier-status', + templateUrl: './dossier-status.component.html', + styleUrls: ['./dossier-status.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DossiersListingStatusComponent implements OnInit, OnChanges { +export class DossierStatusComponent implements OnInit, OnChanges { @Input() dossier: Dossier; currentState: DossierState; diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component.html b/apps/red-ui/src/app/modules/shared/components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component.html similarity index 95% rename from apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component.html rename to apps/red-ui/src/app/modules/shared/components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component.html index c12eabe47..dea2c6bfa 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component.html +++ b/apps/red-ui/src/app/modules/shared/components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component.html @@ -9,7 +9,7 @@
-
+
{{ dossierStats.numberOfFiles }} diff --git a/apps/red-ui/src/app/modules/shared/components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component.scss b/apps/red-ui/src/app/modules/shared/components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component.ts b/apps/red-ui/src/app/modules/shared/components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component.ts similarity index 75% rename from apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component.ts rename to apps/red-ui/src/app/modules/shared/components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component.ts index 30a26f8b5..fab9ad4de 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossiers-listing/components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component.ts @@ -1,8 +1,6 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { Dossier, DossierStats } from '@red/domain'; import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service'; -import { DossierStatsService } from '@services/entity-services/dossier-stats.service'; -import { DossiersService } from '@services/entity-services/dossiers.service'; import * as moment from 'moment'; const DUE_DATE_WARN_DAYS = 14; @@ -17,11 +15,7 @@ export class DossiersListingDossierNameComponent { @Input() dossier: Dossier; @Input() dossierStats: DossierStats; - constructor( - private readonly _dossierTemplatesService: DossierTemplatesService, - private readonly _dossierStatsService: DossierStatsService, - private readonly _dossiersService: DossiersService, - ) {} + constructor(private readonly _dossierTemplatesService: DossierTemplatesService) {} get approachingDueDate(): boolean { return this._dueDateDaysDiff >= 0 && this._dueDateDaysDiff <= DUE_DATE_WARN_DAYS; diff --git a/apps/red-ui/src/app/modules/shared/components/expandable-file-actions/expandable-file-actions.component.ts b/apps/red-ui/src/app/modules/shared/components/expandable-file-actions/expandable-file-actions.component.ts index b2f3ae75c..c3cf688a2 100644 --- a/apps/red-ui/src/app/modules/shared/components/expandable-file-actions/expandable-file-actions.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/expandable-file-actions/expandable-file-actions.component.ts @@ -63,7 +63,7 @@ export class ExpandableFileActionsComponent implements OnChanges { private async _downloadFiles($event: MouseEvent, files: File[]) { $event.stopPropagation(); const dossierId = files[0].dossierId; - const filesIds = files.map(f => f.fileId); + const filesIds = files.map(f => f.id); await firstValueFrom(this._fileDownloadService.downloadFiles(filesIds, dossierId)); this._toaster.info(_('download-status.queued')); } diff --git a/apps/red-ui/src/app/modules/shared/pipes/date.pipe.ts b/apps/red-ui/src/app/modules/shared/pipes/date.pipe.ts index d1ec3f3a2..10734a943 100644 --- a/apps/red-ui/src/app/modules/shared/pipes/date.pipe.ts +++ b/apps/red-ui/src/app/modules/shared/pipes/date.pipe.ts @@ -5,7 +5,7 @@ import { DatePipe as BaseDatePipe } from '@angular/common'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { getLeftDateTime } from '@iqser/common-ui'; -const MONTH_NAMES = { +const MONTH_NAMES: Record = { 0: _('months.jan'), 1: _('months.feb'), 2: _('months.mar'), @@ -29,16 +29,18 @@ export class DatePipe extends BaseDatePipe implements PipeTransform { } transform(value: null | undefined, format?: string, timezone?: string, locale?: string): null; - transform(value: Date | string | number | null | undefined, format?: string, timezone?: string, locale?: string): string | null; - transform(value: any, format?: string, timezone?: string, locale?: string): string { - if (format === 'timeFromNow') { - return this._getTimeFromNow(value); - } - if (format === 'sophisticatedDate') { - return this._getSophisticatedDate(value); - } - if (format === 'exactDate') { - return this._getExactDate(value); + transform(value: Date | string | number, format?: string, timezone?: string, locale?: string): string; + transform(value: Date | string | number | null | undefined, format?: string, timezone?: string, locale?: string): string | null { + if (typeof value === 'string') { + if (format === 'timeFromNow') { + return this._getTimeFromNow(value); + } + if (format === 'sophisticatedDate') { + return this._getSophisticatedDate(value); + } + if (format === 'exactDate') { + return this._getExactDate(value); + } } return super.transform(value, format, timezone, locale); } diff --git a/apps/red-ui/src/app/modules/shared/shared.module.ts b/apps/red-ui/src/app/modules/shared/shared.module.ts index 939fd0eb7..a185ccf62 100644 --- a/apps/red-ui/src/app/modules/shared/shared.module.ts +++ b/apps/red-ui/src/app/modules/shared/shared.module.ts @@ -27,6 +27,8 @@ import { TeamMembersComponent } from './components/team-members/team-members.com 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 { DossierStatusComponent } from '@shared/components/dossier-status/dossier-status.component'; +import { DossiersListingDossierNameComponent } from '@shared/components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component'; const buttons = [FileDownloadBtnComponent, UserButtonComponent]; @@ -43,6 +45,8 @@ const components = [ TeamMembersComponent, ExpandableFileActionsComponent, ProcessingIndicatorComponent, + DossierStatusComponent, + DossiersListingDossierNameComponent, ...buttons, ]; diff --git a/apps/red-ui/src/app/modules/upload-download/file-drop/file-drop.component.ts b/apps/red-ui/src/app/modules/upload-download/file-drop/file-drop.component.ts index ca0b84888..29b4dc609 100644 --- a/apps/red-ui/src/app/modules/upload-download/file-drop/file-drop.component.ts +++ b/apps/red-ui/src/app/modules/upload-download/file-drop/file-drop.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component, HostListener, Inject } from '@angular/cor import { FileUploadService } from '../services/file-upload.service'; import { OverlayRef } from '@angular/cdk/overlay'; import { StatusOverlayService } from '../services/status-overlay.service'; -import { DossiersService } from '@services/entity-services/dossiers.service'; +import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service'; import { handleFileDrop } from '@utils/file-drop-utils'; import { FileUploadModel } from '@upload-download/model/file-upload.model'; import { DOSSIER_ID } from '../../../tokens'; @@ -15,7 +15,7 @@ export class FileDropComponent { constructor( private readonly _dialogRef: OverlayRef, private readonly _fileUploadService: FileUploadService, - private readonly _dossiersService: DossiersService, + private readonly _activeDossiersService: ActiveDossiersService, private readonly _changeDetectorRef: ChangeDetectorRef, private readonly _statusOverlayService: StatusOverlayService, @Inject(DOSSIER_ID) private readonly _dossierId: string, @@ -34,7 +34,7 @@ export class FileDropComponent { @HostListener('drop', ['$event']) onDrop(event: DragEvent) { - const dossier = this._dossiersService.find(this._dossierId); + const dossier = this._activeDossiersService.find(this._dossierId); handleFileDrop(event, dossier, this.uploadFiles.bind(this)); } diff --git a/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts b/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts index 5494c0e81..674e28b8a 100644 --- a/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts +++ b/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts @@ -42,7 +42,7 @@ export class FileUploadService extends GenericService impleme super(_injector, 'upload'); const fileFetch$ = this._fetchFiles$.pipe( throttleTime(250), - switchMap(dossierId => this._filesService.loadAll(dossierId)), + switchMap(dossierId => this._filesService.loadAll(dossierId, 'dossiers')), ); this._subscriptions.add(fileFetch$.subscribe()); const interval$ = interval(2500).pipe(tap(() => this._handleUploads())); diff --git a/apps/red-ui/src/app/services/breadcrumbs.service.ts b/apps/red-ui/src/app/services/breadcrumbs.service.ts index a6a082738..16f90f59d 100644 --- a/apps/red-ui/src/app/services/breadcrumbs.service.ts +++ b/apps/red-ui/src/app/services/breadcrumbs.service.ts @@ -1,21 +1,28 @@ -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { List } from '@iqser/common-ui'; import { ActivatedRouteSnapshot, IsActiveMatchOptions, NavigationEnd, Router } from '@angular/router'; import { BehaviorSubject, Observable, of } from 'rxjs'; import { filter, pluck } from 'rxjs/operators'; import { FilesMapService } from '@services/entity-services/files-map.service'; -import { DossiersService } from '@services/entity-services/dossiers.service'; import { TranslateService } from '@ngx-translate/core'; import { BreadcrumbTypes } from '@red/domain'; import { DOSSIER_ID, FILE_ID } from '@utils/constants'; +import { DossiersService } from '@services/dossiers/dossiers.service'; +import { dossiersServiceResolver } from '@services/entity-services/dossiers.service.provider'; export type RouterLinkActiveOptions = { exact: boolean } | IsActiveMatchOptions; +export type BreadcrumbDisplayType = 'text' | 'dropdown'; export interface Breadcrumb { readonly name$: Observable; - readonly routerLink?: string[]; - readonly routerLinkActiveOptions?: RouterLinkActiveOptions | undefined; - readonly clamp?: boolean; + readonly type: BreadcrumbDisplayType; + readonly options: { + readonly routerLink?: string[]; + readonly routerLinkActiveOptions?: RouterLinkActiveOptions | undefined; + readonly clamp?: boolean; + readonly options?: Breadcrumb[]; + readonly activeOption?: Breadcrumb; + }; } export type Breadcrumbs = List; @@ -28,10 +35,10 @@ export class BreadcrumbsService { private readonly _store$ = new BehaviorSubject([]); constructor( + private readonly _injector: Injector, private readonly _router: Router, private readonly _translateService: TranslateService, private readonly _filesMapService: FilesMapService, - private readonly _dossiersService: DossiersService, ) { this.breadcrumbs$ = this._store$.asObservable(); @@ -46,6 +53,32 @@ export class BreadcrumbsService { return this._store$.value; } + private get _dossiersService(): DossiersService { + return dossiersServiceResolver(this._injector); + } + + private get _mainBreadcrumb(): Breadcrumb { + return { + name$: of(this._translateService.instant('top-bar.navigation-items.dossiers')), + type: 'text' as BreadcrumbDisplayType, + options: { + routerLink: ['/main', 'dossiers'], + routerLinkActiveOptions: { exact: true }, + }, + }; + } + + private get _archiveBreadcrumb(): Breadcrumb { + return { + name$: of(this._translateService.instant('top-bar.navigation-items.archived-dossiers')), + type: 'text' as BreadcrumbDisplayType, + options: { + routerLink: ['/main', 'archive'], + routerLinkActiveOptions: { exact: true }, + }, + }; + } + private _append(breadcrumb: Breadcrumb) { this._store$.next([...this._store$.value, breadcrumb]); } @@ -60,10 +93,26 @@ export class BreadcrumbsService { return; } - for (const breadcrumb of route.data.breadcrumbs || []) { + const breadcrumbs = route.data.breadcrumbs || []; + + if (breadcrumbs.length === 1) { + if (breadcrumbs[0] === BreadcrumbTypes.main) { + this._addMainDropdownBreadcrumb('active'); + return; + } + if (breadcrumbs[0] === BreadcrumbTypes.archive) { + this._addMainDropdownBreadcrumb('archived'); + return; + } + } + + for (const breadcrumb of breadcrumbs) { switch (breadcrumb) { case BreadcrumbTypes.main: - this._addMainBreadcrumb(); + this._append(this._mainBreadcrumb); + break; + case BreadcrumbTypes.archive: + this._append(this._archiveBreadcrumb); break; case BreadcrumbTypes.dossier: this._addDossierBreadcrumb(route); @@ -75,31 +124,46 @@ export class BreadcrumbsService { } } - private _addMainBreadcrumb(): void { + private _addMainDropdownBreadcrumb(type: 'active' | 'archived'): void { + const activeDossiers: Breadcrumb = this._mainBreadcrumb; + const archivedDossiers: Breadcrumb = this._archiveBreadcrumb; + const activeOption = type === 'active' ? activeDossiers : archivedDossiers; + this._append({ - name$: of(this._translateService.instant('top-bar.navigation-items.dossiers')), - routerLink: ['/main', 'dossiers'], - routerLinkActiveOptions: { exact: true }, + name$: activeOption.name$, + type: 'dropdown' as BreadcrumbDisplayType, + options: { + options: [activeDossiers, archivedDossiers], + activeOption, + }, }); } private _addDossierBreadcrumb(route: ActivatedRouteSnapshot): void { + const dossiersService = this._dossiersService; const dossierId = route.paramMap.get(DOSSIER_ID); this._append({ - name$: this._dossiersService.getEntityChanged$(dossierId).pipe(pluck('dossierName')), - routerLink: ['/main', 'dossiers', dossierId], - routerLinkActiveOptions: { exact: true }, - clamp: true, + name$: dossiersService.getEntityChanged$(dossierId).pipe(pluck('dossierName')), + type: 'text' as BreadcrumbDisplayType, + options: { + routerLink: ['/main', dossiersService.routerPath, dossierId], + routerLinkActiveOptions: { exact: true }, + clamp: true, + }, }); } private _addFileBreadcrumb(route: ActivatedRouteSnapshot): void { const dossierId = route.paramMap.get(DOSSIER_ID); const fileId = route.paramMap.get(FILE_ID); + const dossiersService = this._dossiersService; this._append({ name$: this._filesMapService.watch$(dossierId, fileId).pipe(pluck('filename')), - routerLink: ['/main', 'dossiers', dossierId, 'file', fileId], - clamp: true, + type: 'text' as BreadcrumbDisplayType, + options: { + routerLink: ['/main', dossiersService.routerPath, dossierId, 'file', fileId], + clamp: true, + }, }); } } diff --git a/apps/red-ui/src/app/services/dossiers/active-dossiers.service.ts b/apps/red-ui/src/app/services/dossiers/active-dossiers.service.ts new file mode 100644 index 000000000..af72b2eb5 --- /dev/null +++ b/apps/red-ui/src/app/services/dossiers/active-dossiers.service.ts @@ -0,0 +1,30 @@ +import { Injectable, Injector } from '@angular/core'; +import { switchMap, tap } from 'rxjs/operators'; +import { timer } from 'rxjs'; +import { CHANGED_CHECK_INTERVAL } from '../../utils/constants'; +import { DossiersService } from './dossiers.service'; + +export interface IDossiersStats { + totalPeople: number; + totalAnalyzedPages: number; +} + +@Injectable({ + providedIn: 'root', +}) +export class ActiveDossiersService extends DossiersService { + constructor(protected readonly _injector: Injector) { + super(_injector, 'dossier', 'dossiers'); + + timer(CHANGED_CHECK_INTERVAL, CHANGED_CHECK_INTERVAL) + .pipe( + switchMap(() => this.loadOnlyChanged()), + tap(changes => this._emitFileChanges(changes)), + ) + .subscribe(); + } + + getCountWithState(dossierStatusId: string): number { + return this.all.filter(dossier => dossier.dossierStatusId === dossierStatusId).length; + } +} diff --git a/apps/red-ui/src/app/services/dossiers/archived-dossiers.service.ts b/apps/red-ui/src/app/services/dossiers/archived-dossiers.service.ts new file mode 100644 index 000000000..75ae0b05c --- /dev/null +++ b/apps/red-ui/src/app/services/dossiers/archived-dossiers.service.ts @@ -0,0 +1,39 @@ +import { Injectable, Injector } from '@angular/core'; +import { Dossier } from '@red/domain'; +import { catchError, tap } from 'rxjs/operators'; +import { Observable, of } from 'rxjs'; +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; +import { ActiveDossiersService } from './active-dossiers.service'; +import { DossiersService } from './dossiers.service'; +import { FilesMapService } from '../entity-services/files-map.service'; + +@Injectable({ providedIn: 'root' }) +export class ArchivedDossiersService extends DossiersService { + constructor( + protected readonly _injector: Injector, + private readonly _activeDossiersService: ActiveDossiersService, + private readonly _filesMapService: FilesMapService, + ) { + super(_injector, 'archived-dossiers', 'archive'); + } + + archive(dossiers: Dossier[]): Observable { + const showArchiveFailedToast = () => { + this._toaster.error(_('dossier-listing.archive.archive-failed'), { params: dossiers }); + return of({}); + }; + + const archivedDossiersIds = dossiers.map(d => d.dossierId); + + return this._post(archivedDossiersIds, `${this._defaultModelPath}/archive`).pipe( + tap(() => this.#removeFromActiveDossiers(archivedDossiersIds)), + catchError(showArchiveFailedToast), + ); + } + + #removeFromActiveDossiers(archivedDossiersIds: string[]): void { + const remainingEntities = this._activeDossiersService.all.filter(d => !archivedDossiersIds.includes(d.dossierId)); + this._activeDossiersService.setEntities(remainingEntities); + this._filesMapService.delete(archivedDossiersIds); + } +} diff --git a/apps/red-ui/src/app/services/entity-services/dossier-stats.service.ts b/apps/red-ui/src/app/services/dossiers/dossier-stats.service.ts similarity index 94% rename from apps/red-ui/src/app/services/entity-services/dossier-stats.service.ts rename to apps/red-ui/src/app/services/dossiers/dossier-stats.service.ts index 52085cd92..aa244511b 100644 --- a/apps/red-ui/src/app/services/entity-services/dossier-stats.service.ts +++ b/apps/red-ui/src/app/services/dossiers/dossier-stats.service.ts @@ -3,7 +3,7 @@ import { StatsService } from '@iqser/common-ui'; import { DossierStats, IDossierStats } from '@red/domain'; import { DOSSIER_ID } from '@utils/constants'; import { Observable, of } from 'rxjs'; -import { UserService } from '@services/user.service'; +import { UserService } from '../user.service'; @Injectable({ providedIn: 'root', diff --git a/apps/red-ui/src/app/services/entity-services/dossiers.service.ts b/apps/red-ui/src/app/services/dossiers/dossiers.service.ts similarity index 52% rename from apps/red-ui/src/app/services/entity-services/dossiers.service.ts rename to apps/red-ui/src/app/services/dossiers/dossiers.service.ts index 6c007257b..ad2c166b4 100644 --- a/apps/red-ui/src/app/services/entity-services/dossiers.service.ts +++ b/apps/red-ui/src/app/services/dossiers/dossiers.service.ts @@ -1,69 +1,42 @@ -import { Injectable, Injector } from '@angular/core'; import { EntitiesService, List, mapEach, QueryParam, RequiredParam, shareLast, Toaster, Validate } from '@iqser/common-ui'; -import { Dossier, DossierStats, IDossier, IDossierRequest } from '@red/domain'; +import { Dossier, DossierStats, IChangesDetails, IDossier, IDossierChanges, IDossierRequest } from '@red/domain'; +import { combineLatest, EMPTY, forkJoin, Observable, of, Subject, throwError } from 'rxjs'; import { catchError, filter, map, mapTo, pluck, switchMap, tap } from 'rxjs/operators'; -import { combineLatest, firstValueFrom, forkJoin, Observable, of, Subject, throwError, timer } from 'rxjs'; -import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; +import { Injector } from '@angular/core'; +import { DossierStateService } from '../entity-services/dossier-state.service'; +import { DossierStatsService } from './dossier-stats.service'; +import { IDossiersStats } from './active-dossiers.service'; import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; -import { DossierStatsService } from '@services/entity-services/dossier-stats.service'; -import { CHANGED_CHECK_INTERVAL } from '@utils/constants'; -import { DossierStateService } from '@services/entity-services/dossier-state.service'; - -export interface IDossiersStats { - totalPeople: number; - totalAnalyzedPages: number; -} - -interface DossierChange { - readonly dossierChanges: boolean; - readonly dossierId: string; - readonly fileChanges: boolean; -} - -type DossierChanges = readonly DossierChange[]; - -interface ChangesDetails { - readonly dossierChanges: DossierChanges; -} +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; const DOSSIER_EXISTS_MSG = _('add-dossier-dialog.errors.dossier-already-exists'); const GENERIC_MSG = _('add-dossier-dialog.errors.generic'); -@Injectable({ - providedIn: 'root', -}) -export class DossiersService extends EntitiesService { - readonly generalStats$ = this.all$.pipe(switchMap(entities => this.#generalStats$(entities))); +export abstract class DossiersService extends EntitiesService { readonly dossierFileChanges$ = new Subject(); + readonly generalStats$ = this.all$.pipe(switchMap(entities => this.#generalStats$(entities))); + protected readonly _dossierStatsService = this._injector.get(DossierStatsService); + protected readonly _dossierStateService = this._injector.get(DossierStateService); + protected readonly _toaster = this._injector.get(Toaster); - constructor( - private readonly _toaster: Toaster, - protected readonly _injector: Injector, - private readonly _dossierStatsService: DossierStatsService, - private readonly _dossierStateService: DossierStateService, - ) { - super(_injector, Dossier, 'dossier'); - - timer(CHANGED_CHECK_INTERVAL, CHANGED_CHECK_INTERVAL) - .pipe( - switchMap(() => this.loadOnlyChanged()), - tap(changes => this.#emitFileChanges(changes)), - ) - .subscribe(); + protected constructor(protected readonly _injector: Injector, protected readonly _path: string, readonly routerPath: string) { + super(_injector, Dossier, _path); } - loadAll(): Observable { - const dossierIds = (dossiers: Dossier[]) => dossiers.map(d => d.id); - return this.getAll().pipe( - mapEach(entity => new Dossier(entity)), - /* Load stats before updating entities */ - switchMap(dossiers => this._dossierStatsService.getFor(dossierIds(dossiers)).pipe(mapTo(dossiers))), - switchMap(dossiers => this._dossierStateService.loadAllForAllTemplates().pipe(mapTo(dossiers))), - tap(dossiers => this.setEntities(dossiers)), + @Validate() + createOrUpdate(@RequiredParam() dossier: IDossierRequest): Observable { + const showToast = (error: HttpErrorResponse) => { + this._toaster.error(error.status === HttpStatusCode.Conflict ? DOSSIER_EXISTS_MSG : GENERIC_MSG); + return EMPTY; + }; + + return this._post(dossier, 'dossier').pipe( + switchMap(newDossier => this.loadAll().pipe(map(() => this.find(newDossier.dossierId)))), + catchError(showToast), ); } - loadOnlyChanged(): Observable { + loadOnlyChanged(): Observable { const removeIfNotFound = (id: string) => catchError((error: HttpErrorResponse) => { if (error.status === HttpStatusCode.NotFound) { @@ -73,78 +46,47 @@ export class DossiersService extends EntitiesService { return throwError(() => error); }); - const load = (changes: DossierChanges) => + const load = (changes: IDossierChanges) => changes.map(change => this._load(change.dossierId).pipe(removeIfNotFound(change.dossierId))); return this.hasChangesDetails$().pipe( - pluck('dossierChanges'), switchMap(dossierChanges => forkJoin(load(dossierChanges)).pipe(mapTo(dossierChanges))), tap(() => this._updateLastChanged()), ); } - hasChangesDetails$(): Observable { + loadAll(): Observable { + const dossierIds = (dossiers: Dossier[]) => dossiers.map(d => d.id); + return this.getAll().pipe( + mapEach(entity => new Dossier(entity, this.routerPath)), + /* Load stats before updating entities */ + switchMap(dossiers => this._dossierStatsService.getFor(dossierIds(dossiers)).pipe(mapTo(dossiers))), + switchMap(dossiers => this._dossierStateService.loadAllForAllTemplates().pipe(mapTo(dossiers))), + tap(dossiers => this.setEntities(dossiers)), + ); + } + + hasChangesDetails$(): Observable { const body = { value: this._lastCheckedForChanges.get('root') ?? '0' }; - return this._post(body, `${this._defaultModelPath}/changes/details`).pipe( + return this._post(body, `${this._defaultModelPath}/changes/details`).pipe( filter(changes => changes.dossierChanges.length > 0), + pluck('dossierChanges'), ); } - @Validate() - createOrUpdate(@RequiredParam() dossier: IDossierRequest): Observable { - const showToast = (error: HttpErrorResponse) => { - this._toaster.error(error.status === HttpStatusCode.Conflict ? DOSSIER_EXISTS_MSG : GENERIC_MSG); - return of(undefined); - }; - - return this._post(dossier).pipe( - switchMap(newDossier => this.loadAll().pipe(map(() => this.find(newDossier.dossierId)))), - catchError(showToast), - ); + protected _emitFileChanges(dossierChanges: IDossierChanges): void { + dossierChanges.filter(change => change.fileChanges).forEach(change => this.dossierFileChanges$.next(change.dossierId)); } - getDeleted(): Promise { - return firstValueFrom(this.getAll('deleted-dossiers')); - } - - delete(dossier: Dossier): Observable { - const updateDossiers = () => { - this.setEntities(this.all.filter(d => d.dossierId !== dossier.dossierId)); - }; - const showToast = () => { - this._toaster.error(_('dossier-listing.delete.delete-failed'), { params: dossier }); - return of({}); - }; - return super.delete(dossier.dossierId).pipe(tap(updateDossiers), catchError(showToast)); - } - - @Validate() - restore(@RequiredParam() dossierIds: List): Promise { - return firstValueFrom(this._post(dossierIds, 'deleted-dossiers/restore')); - } - - @Validate() - hardDelete(@RequiredParam() dossierIds: List): Promise { - const body = dossierIds.map(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; - } - - private _load(id: string, queryParams?: List): Observable { + private _load(id: string): Observable { + const queryParams: List = [{ key: 'includeArchived', value: this._path === 'archived-dossiers' }]; return super._getOne([id], this._defaultModelPath, queryParams).pipe( - map(entity => new Dossier(entity)), + map(entity => new Dossier(entity, 'dossiers')), tap(dossier => this.replace(dossier)), switchMap(dossier => this._dossierStatsService.getFor([dossier.dossierId])), ); } - #emitFileChanges(dossierChanges: DossierChanges): void { - dossierChanges.filter(change => change.fileChanges).forEach(change => this.dossierFileChanges$.next(change.dossierId)); - } - #computeStats(entities: List): IDossiersStats { let totalAnalyzedPages = 0; const totalPeople = new Set(); diff --git a/apps/red-ui/src/app/services/entity-services/dossier-state.service.ts b/apps/red-ui/src/app/services/entity-services/dossier-state.service.ts index e3c3fb7bd..cfe8f536a 100644 --- a/apps/red-ui/src/app/services/entity-services/dossier-state.service.ts +++ b/apps/red-ui/src/app/services/entity-services/dossier-state.service.ts @@ -14,7 +14,7 @@ export class DossierStateService extends EntitiesService(body, this._defaultModelPath); } @@ -27,7 +27,7 @@ export class DossierStateService extends EntitiesService template.dossierTemplateId), mapEach(id => this.loadAllForTemplate(id)), - switchMap(all => forkJoin(all).pipe(defaultIfEmpty([]))), + switchMap(all => forkJoin(all).pipe(defaultIfEmpty([] as DossierState[][]))), map(value => value.flatMap(item => item)), tap(value => this.setEntities(value)), ); diff --git a/apps/red-ui/src/app/services/entity-services/dossiers.service.provider.ts b/apps/red-ui/src/app/services/entity-services/dossiers.service.provider.ts new file mode 100644 index 000000000..0b176f0d7 --- /dev/null +++ b/apps/red-ui/src/app/services/entity-services/dossiers.service.provider.ts @@ -0,0 +1,15 @@ +import { ActivatedRoute } from '@angular/router'; +import { Injector, ProviderToken } from '@angular/core'; +import { DossiersService } from '../dossiers/dossiers.service'; + +export const dossiersServiceResolver = (injector: Injector) => { + const route = injector.get(ActivatedRoute); + const token: ProviderToken = (route.firstChild || route).snapshot.data.dossiersService; + return injector.get(token); +}; + +export const dossiersServiceProvider = { + provide: DossiersService, + useFactory: dossiersServiceResolver, + deps: [Injector], +}; diff --git a/apps/red-ui/src/app/services/entity-services/file-attributes.service.ts b/apps/red-ui/src/app/services/entity-services/file-attributes.service.ts index 15e982731..bc1046d9a 100644 --- a/apps/red-ui/src/app/services/entity-services/file-attributes.service.ts +++ b/apps/red-ui/src/app/services/entity-services/file-attributes.service.ts @@ -16,10 +16,6 @@ export class FileAttributesService extends EntitiesService { } @Validate() - delete(@RequiredParam() fileIds: List, @RequiredParam() dossierId: string) { - return super._post(fileIds, `delete/${dossierId}`).pipe(switchMap(() => this._filesService.loadAll(dossierId))); + delete(@RequiredParam() files: List, @RequiredParam() dossierId: string) { + const fileIds = files.map(f => f.id); + const routerPath: string = files[0].routerPath; + return super._post(fileIds, `delete/${dossierId}`).pipe(switchMap(() => this._filesService.loadAll(dossierId, routerPath))); } @Validate() @@ -33,8 +35,10 @@ export class FileManagementService extends GenericService { } @Validate() - restore(@RequiredParam() body: List, @RequiredParam() dossierId: string) { - return this._post(body, `delete/restore/${dossierId}`).pipe(switchMap(() => this._filesService.loadAll(dossierId))); + restore(@RequiredParam() files: List, @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() diff --git a/apps/red-ui/src/app/services/entity-services/files-map.service.ts b/apps/red-ui/src/app/services/entity-services/files-map.service.ts index d71f717d1..bfe6587aa 100644 --- a/apps/red-ui/src/app/services/entity-services/files-map.service.ts +++ b/apps/red-ui/src/app/services/entity-services/files-map.service.ts @@ -12,7 +12,11 @@ export class FilesMapService extends EntitiesMapService { replaceFiles(files: File[], property: keyof IFile, generateValue: Function) { const newFiles = files.map( file => - new File({ ...file, [property]: generateValue(file[property]), lastUpdated: new Date().toISOString() }, file.reviewerName), + new File( + { ...file, [property]: generateValue(file[property]), lastUpdated: new Date().toISOString() }, + file.reviewerName, + file.routerPath, + ), ); this.replace(newFiles); } diff --git a/apps/red-ui/src/app/services/entity-services/files.service.ts b/apps/red-ui/src/app/services/entity-services/files.service.ts index 8a8109f46..6d76d9d1f 100644 --- a/apps/red-ui/src/app/services/entity-services/files.service.ts +++ b/apps/red-ui/src/app/services/entity-services/files.service.ts @@ -1,11 +1,11 @@ import { Injectable, Injector } from '@angular/core'; -import { EntitiesService, List, mapEach, RequiredParam, Validate } from '@iqser/common-ui'; +import { EntitiesService, IRouterPath, List, mapEach, RequiredParam, Validate } from '@iqser/common-ui'; import { File, IFile } from '@red/domain'; import { Observable } from 'rxjs'; import { UserService } from '../user.service'; import { FilesMapService } from '@services/entity-services/files-map.service'; import { map, mapTo, switchMap, tap } from 'rxjs/operators'; -import { DossierStatsService } from '@services/entity-services/dossier-stats.service'; +import { DossierStatsService } from '@services/dossiers/dossier-stats.service'; @Injectable({ providedIn: 'root', @@ -21,40 +21,50 @@ export class FilesService extends EntitiesService { } /** Reload dossier files + stats. */ - loadAll(dossierId: string) { - const files$ = this.getFor(dossierId).pipe(mapEach(file => new File(file, this._userService.getNameForId(file.assignee)))); + loadAll(dossierId: string, routerPath: string) { + const files$ = this.getFor(dossierId).pipe( + mapEach(file => new File(file, this._userService.getNameForId(file.assignee), routerPath)), + ); const loadStats$ = files$.pipe(switchMap(files => this._dossierStatsService.getFor([dossierId]).pipe(mapTo(files)))); return loadStats$.pipe(tap(files => this._filesMapService.set(dossierId, files))); } - reload(dossierId: string, fileId: string): Observable { - return super._getOne([dossierId, fileId]).pipe( - map(file => new File(file, this._userService.getNameForId(file.assignee))), - switchMap(file => this._dossierStatsService.getFor([dossierId]).pipe(mapTo(file))), - map(file => this._filesMapService.replace([file])), + reload(dossierId: string, file: File): Observable { + return super._getOne([dossierId, file.id]).pipe( + map(_file => new File(_file, this._userService.getNameForId(_file.assignee), file.routerPath)), + switchMap(_file => this._dossierStatsService.getFor([dossierId]).pipe(mapTo(_file))), + map(_file => this._filesMapService.replace([_file])), ); } @Validate() - setUnassigned(@RequiredParam() fileIds: List, @RequiredParam() dossierId: string) { + setUnassigned(@RequiredParam() files: List, @RequiredParam() dossierId: string) { const url = `${this._defaultModelPath}/set-assignee/${dossierId}/bulk`; - return this._post(fileIds, url).pipe(switchMap(() => this.loadAll(dossierId))); + const fileIds = files.map(f => f.id); + const routerPath: string = files[0].routerPath; + return this._post(fileIds, url).pipe(switchMap(() => this.loadAll(dossierId, routerPath))); } @Validate() - setUnderApprovalFor(@RequiredParam() fileIds: List, @RequiredParam() dossierId: string, assigneeId: string) { + setUnderApprovalFor(@RequiredParam() files: List, @RequiredParam() dossierId: string, assigneeId: string) { const url = `${this._defaultModelPath}/under-approval/${dossierId}/bulk`; - return this._post(fileIds, url, [{ key: 'assigneeId', value: assigneeId }]).pipe(switchMap(() => this.loadAll(dossierId))); + const fileIds = files.map(f => f.id); + const routerPath: string = files[0].routerPath; + return this._post(fileIds, url, [{ key: 'assigneeId', value: assigneeId }]).pipe( + switchMap(() => this.loadAll(dossierId, routerPath)), + ); } /** * Assigns a reviewer for a list of files. */ @Validate() - setReviewerFor(@RequiredParam() filesIds: List, @RequiredParam() dossierId: string, assigneeId: string) { + setReviewerFor(@RequiredParam() files: List, @RequiredParam() dossierId: string, assigneeId: string) { const url = `${this._defaultModelPath}/under-review/${dossierId}/bulk`; - return this._post(filesIds, url, [{ key: 'assigneeId', value: assigneeId }]).pipe( - switchMap(() => this.loadAll(dossierId)), + const fileIds = files.map(f => f.id); + const routerPath: string = files[0].routerPath; + return this._post(fileIds, url, [{ key: 'assigneeId', value: assigneeId }]).pipe( + switchMap(() => this.loadAll(dossierId, routerPath)), ); } @@ -62,9 +72,11 @@ export class FilesService extends EntitiesService { * Sets the status APPROVED for a list of files. */ @Validate() - setApprovedFor(@RequiredParam() filesIds: List, @RequiredParam() dossierId: string) { - return this._post(filesIds, `${this._defaultModelPath}/approved/${dossierId}/bulk`).pipe( - switchMap(() => this.loadAll(dossierId)), + setApprovedFor(@RequiredParam() files: List, @RequiredParam() dossierId: string) { + const fileIds = files.map(f => f.id); + const routerPath: string = files[0].routerPath; + return this._post(fileIds, `${this._defaultModelPath}/approved/${dossierId}/bulk`).pipe( + switchMap(() => this.loadAll(dossierId, routerPath)), ); } @@ -72,9 +84,11 @@ export class FilesService extends EntitiesService { * Sets the status UNDER_REVIEW for a list of files. */ @Validate() - setUnderReviewFor(@RequiredParam() filesIds: List, @RequiredParam() dossierId: string) { - return this._post(filesIds, `${this._defaultModelPath}/under-review/${dossierId}/bulk`).pipe( - switchMap(() => this.loadAll(dossierId)), + setUnderReviewFor(@RequiredParam() files: List, @RequiredParam() dossierId: string) { + const fileIds = files.map(f => f.id); + const routerPath: string = files[0].routerPath; + return this._post(fileIds, `${this._defaultModelPath}/under-review/${dossierId}/bulk`).pipe( + switchMap(() => this.loadAll(dossierId, routerPath)), ); } diff --git a/apps/red-ui/src/app/services/entity-services/platform-search.service.ts b/apps/red-ui/src/app/services/entity-services/platform-search.service.ts index 000d563c1..abda973f3 100644 --- a/apps/red-ui/src/app/services/entity-services/platform-search.service.ts +++ b/apps/red-ui/src/app/services/entity-services/platform-search.service.ts @@ -1,24 +1,27 @@ import { Injectable, Injector } from '@angular/core'; import { GenericService } from '@iqser/common-ui'; -import { IMatchedDocument, ISearchInput, ISearchRequest, ISearchResponse } from '@red/domain'; +import { Dossier, IMatchedDocument, ISearchInput, ISearchRequest, ISearchResponse } from '@red/domain'; import { Observable, of, zip } from 'rxjs'; import { mapTo, switchMap } from 'rxjs/operators'; -import { DossiersService } from './dossiers.service'; +import { ActiveDossiersService } from '../dossiers/active-dossiers.service'; import { FilesMapService } from './files-map.service'; import { FilesService } from './files.service'; +import { ArchivedDossiersService } from '../dossiers/archived-dossiers.service'; +import { DossiersService } from '../dossiers/dossiers.service'; @Injectable({ providedIn: 'root' }) export class PlatformSearchService extends GenericService { constructor( protected readonly _injector: Injector, private readonly _filesService: FilesService, - private readonly _dossiersService: DossiersService, + private readonly _activeDossiersService: ActiveDossiersService, + private readonly _archivedDossiersService: ArchivedDossiersService, private readonly _filesMapService: FilesMapService, ) { super(_injector, 'search-v2'); } - search({ dossierIds, query }: ISearchInput): Observable { + search({ dossierIds, query, dossierStatus }: ISearchInput): Observable { if (!query) { return of({ matchedDocuments: [], @@ -32,21 +35,26 @@ export class PlatformSearchService extends GenericService { page: 0, returnSections: false, pageSize: 300, + dossierStatus, }; return this._post(body).pipe(switchMap(searchValue => this._loadMissingFiles$(searchValue))); } - private _loadMissingFiles$(searchResponse: ISearchResponse): Observable { - const documentsOfActiveDossiers = searchResponse.matchedDocuments.filter(document => this._dossiersService.has(document.dossierId)); + private _dossiersWithMissingFiles(searchResponse: ISearchResponse, service: DossiersService): Dossier[] { + const documentsOfType = searchResponse.matchedDocuments.filter(document => service.has(document.dossierId)); const fileNotLoaded = ({ dossierId, fileId }: IMatchedDocument) => !this._filesMapService.get(dossierId, fileId); - const dossiersWithNotLoadedFiles = documentsOfActiveDossiers.filter(fileNotLoaded).map(document => document.dossierId); - - const dossierIds = Array.from(new Set(dossiersWithNotLoadedFiles)); - return dossierIds.length ? this._loadFilesFor$(dossierIds).pipe(mapTo(searchResponse)) : of(searchResponse); + const dossiersWithNotLoadedFiles = documentsOfType.filter(fileNotLoaded).map(document => document.dossierId); + return Array.from(new Set(dossiersWithNotLoadedFiles)).map(dossierId => service.find(dossierId)); } - private _loadFilesFor$(dossierIds: string[]) { - return zip(...dossierIds.map(dossierId => this._filesService.loadAll(dossierId))); + private _loadMissingFiles$(searchResponse: ISearchResponse): Observable { + const services = [this._activeDossiersService, this._archivedDossiersService]; + const dossiers = services.map(service => this._dossiersWithMissingFiles(searchResponse, service)).flat(); + return dossiers.length ? this._loadFilesFor$(dossiers).pipe(mapTo(searchResponse)) : of(searchResponse); + } + + private _loadFilesFor$(dossiers: Dossier[]) { + return zip(...dossiers.map(dossier => this._filesService.loadAll(dossier.id, dossier.routerPath))); } } diff --git a/apps/red-ui/src/app/services/entity-services/trash-dossiers.service.ts b/apps/red-ui/src/app/services/entity-services/trash-dossiers.service.ts new file mode 100644 index 000000000..158ff48d5 --- /dev/null +++ b/apps/red-ui/src/app/services/entity-services/trash-dossiers.service.ts @@ -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/dossiers/active-dossiers.service'; + +export interface IDossiersStats { + totalPeople: number; + totalAnalyzedPages: number; +} + +@Injectable({ + providedIn: 'root', +}) +export class TrashDossiersService extends EntitiesService { + 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 { + 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 { + 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 { + return firstValueFrom( + this._post(dossierIds, 'deleted-dossiers/restore').pipe( + switchMap(() => this._activeDossiersService.loadAll()), + tap(() => this.#removeDossiers(dossierIds)), + ), + ); + } + + @Validate() + hardDelete(@RequiredParam() dossierIds: string[]): Promise { + const body = dossierIds.map(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 { + return this.getAll('deleted-dossiers'); + } + + #removeDossiers(dossierIds: string[]): void { + this.setEntities(this.all.filter(dossier => !dossierIds.includes(dossier.id))); + } +} diff --git a/apps/red-ui/src/app/services/notifications.service.ts b/apps/red-ui/src/app/services/notifications.service.ts index 9d798fcb5..f47cce9c5 100644 --- a/apps/red-ui/src/app/services/notifications.service.ts +++ b/apps/red-ui/src/app/services/notifications.service.ts @@ -7,7 +7,7 @@ import { INotification, Notification, NotificationTypes } from '@red/domain'; import { map, switchMap } from 'rxjs/operators'; import { notificationsTranslations } from '../translations/notifications-translations'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; -import { DossiersService } from '@services/entity-services/dossiers.service'; +import { ActiveDossiersService } from './dossiers/active-dossiers.service'; import { UserService } from '@services/user.service'; import { FilesMapService } from '@services/entity-services/files-map.service'; @@ -18,7 +18,7 @@ export class NotificationsService extends GenericService { constructor( protected readonly _injector: Injector, private readonly _translateService: TranslateService, - private readonly _dossiersService: DossiersService, + private readonly _activeDossiersService: ActiveDossiersService, private readonly _userService: UserService, private readonly _filesMapService: FilesMapService, ) { @@ -67,7 +67,7 @@ export class NotificationsService extends GenericService { private _translate(notification: INotification, translation: string): string { const fileId = notification.target.fileId; const dossierId = notification.target.dossierId; - const dossier = this._dossiersService.find(dossierId); + const dossier = this._activeDossiersService.find(dossierId); const files = this._filesMapService.get(dossierId); const file = files?.find(f => f.fileId === fileId); diff --git a/apps/red-ui/src/app/services/permissions.service.ts b/apps/red-ui/src/app/services/permissions.service.ts index 366aaf617..befafdb3f 100644 --- a/apps/red-ui/src/app/services/permissions.service.ts +++ b/apps/red-ui/src/app/services/permissions.service.ts @@ -1,19 +1,24 @@ -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { UserService } from './user.service'; import { Dossier, File, IComment, IDossier } from '@red/domain'; -import { DossiersService } from './entity-services/dossiers.service'; +import { DossiersService } from '@services/dossiers/dossiers.service'; +import { ActivatedRoute } from '@angular/router'; +import { dossiersServiceResolver } from '@services/entity-services/dossiers.service.provider'; import { FilesMapService } from '@services/entity-services/files-map.service'; -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class PermissionsService { constructor( private readonly _userService: UserService, - private readonly _dossiersService: DossiersService, + private readonly _route: ActivatedRoute, + private readonly _injector: Injector, private readonly _filesMapService: FilesMapService, ) {} + private get _dossiersService(): DossiersService { + return dossiersServiceResolver(this._injector); + } + isReviewerOrApprover(file: File): boolean { const dossier = this._getDossier(file); return this.isFileAssignee(file) || this.isApprover(dossier); @@ -23,9 +28,20 @@ export class PermissionsService { return this.isApprover(dossier) && !!this._filesMapService.get(dossier.dossierId).find(f => f.analysisRequired); } + canUploadFiles(dossier: Dossier): boolean { + return dossier.isActive; + } + + canDownloadCsvReport(dossier: Dossier): boolean { + return dossier.isActive; + } + canEditFileAttributes(file: File): boolean { const dossier = this._getDossier(file); - return ((file.isUnderReview || file.isNew) && this.isDossierMember(dossier)) || (file.isUnderApproval && this.isApprover(dossier)); + return ( + this._isActive(file) && + (((file.isUnderReview || file.isNew) && this.isDossierMember(dossier)) || (file.isUnderApproval && this.isApprover(dossier))) + ); } canToggleAnalysis(file: File | File[]): boolean { @@ -34,6 +50,11 @@ export class PermissionsService { return sameState && files.reduce((acc, _file) => this._canToggleAnalysis(_file) && acc, true); } + showToggleAnalysis(file: File | File[]): boolean { + const files = file instanceof File ? [file] : file; + return this._isActive(files[0]); + } + canReanalyseFile(file: File | File[]): boolean { const files = file instanceof File ? [file] : file; return files.reduce((acc, _file) => this._canReanalyseFile(_file) && acc, true); @@ -57,6 +78,16 @@ export class PermissionsService { return files.reduce((acc, _file) => this._canDeleteFile(_file, dossier) && acc, true); } + canHardDeleteOrRestore(dossier: Dossier): boolean { + return dossier.isActive; + } + + canOcrFile(file: File | File[]): boolean { + const files = file instanceof File ? [file] : file; + const dossier = this._getDossier(files[0]); + return files.reduce((acc, _file) => this._canOcrFile(_file, dossier) && acc, true); + } + canAssignToSelf(file: File | File[]): boolean { const files = file instanceof File ? [file] : file; const dossier = this._getDossier(files[0]); @@ -113,7 +144,13 @@ export class PermissionsService { // TODO: Remove '?', after we make sure file is loaded before page canPerformAnnotationActions(file: File): boolean { - return !file.isOcrProcessing && !file.excluded && (file?.isUnderReview || file?.isUnderApproval) && this.isFileAssignee(file); + return ( + this._isActive(file) && + !file.isOcrProcessing && + !file.excluded && + (file?.isUnderReview || file?.isUnderApproval) && + this.isFileAssignee(file) + ); } canUndoApproval(file: File | File[]): boolean { @@ -138,10 +175,30 @@ export class PermissionsService { return dossier.ownerId === this._userService.currentUser.id; } + canArchiveDossier(dossier: Dossier): boolean { + return dossier.isActive && dossier.ownerId === this._userService.currentUser.id; + } + canEditDossier(dossier: Dossier, user = this._userService.currentUser): boolean { return user.isManager && !!dossier?.ownerId; } + canEditDossierDictionary(dossier: Dossier, user = this._userService.currentUser): boolean { + return dossier.isActive && this.canEditDossier(dossier, user); + } + + canEditDossierDictionaryDisplayName(dossier: Dossier, user = this._userService.currentUser): boolean { + return dossier.isActive && this.isOwner(dossier, user); + } + + canEditDossierDictionaryAddAction(dossier: Dossier, user = this._userService.currentUser): boolean { + return dossier.isActive && this.isOwner(dossier, user); + } + + canEditDossierAttributes(dossier: Dossier, user = this._userService.currentUser): boolean { + return dossier.isActive && this.isOwner(dossier, user); + } + isAdmin(user = this._userService.currentUser): boolean { return user.isAdmin; } @@ -160,54 +217,70 @@ export class PermissionsService { } canImportRedactions(file: File) { - return (this.isFileAssignee(file) || this.isApprover(this._getDossier(file))) && !file.isApproved; + return this._isActive(file) && (this.isFileAssignee(file) || this.isApprover(this._getDossier(file))) && !file.isApproved; + } + + private _canOcrFile(file: File, dossier: Dossier): boolean { + return dossier.isActive && file.canBeOCRed; } private _canToggleAnalysis(file: File): boolean { - return this.isFileAssignee(file) && (file.isNew || file.isUnderReview || file.isUnderApproval); + return this._isActive(file) && this.isFileAssignee(file) && (file.isNew || file.isUnderReview || file.isUnderApproval); } // https://jira.iqser.com/browse/RED-2787 private _canDeleteFile(file: File, dossier: Dossier): boolean { return ( - file.isNew || - (file.isUnderReview && !file.assignee && this.isDossierMember(dossier)) || - (file.isUnderApproval && !file.assignee && this.isApprover(dossier)) || - (file.assignee && !file.isApproved && (this.isFileAssignee(file) || this.isOwner(dossier))) + dossier.isActive && + (file.isNew || + (file.isUnderReview && !file.assignee && this.isDossierMember(dossier)) || + (file.isUnderApproval && !file.assignee && this.isApprover(dossier)) || + (file.assignee && !file.isApproved && (this.isFileAssignee(file) || this.isOwner(dossier)))) ); } private _canReanalyseFile(file: File): boolean { - return this.isReviewerOrApprover(file) && file.analysisRequired; + return this._isActive(file) && this.isReviewerOrApprover(file) && file.analysisRequired; } private _canEnableAutoAnalysis(file: File): boolean { - return file.excludedFromAutomaticAnalysis && file.assignee === this._userService.currentUser.id && !file.isApproved; + return ( + this._isActive(file) && + file.excludedFromAutomaticAnalysis && + file.assignee === this._userService.currentUser.id && + !file.isApproved + ); } private _canDisableAutoAnalysis(file: File): boolean { - return !file.excludedFromAutomaticAnalysis && file.assignee === this._userService.currentUser.id && !file.isApproved; + return ( + this._isActive(file) && + !file.excludedFromAutomaticAnalysis && + file.assignee === this._userService.currentUser.id && + !file.isApproved + ); } private _canAssignToSelf(file: File, dossier: Dossier): boolean { - const precondition = this.isDossierMember(dossier) && !this.isFileAssignee(file) && !file.isError && !file.isProcessing; + const precondition = + this._isActive(file) && this.isDossierMember(dossier) && !this.isFileAssignee(file) && !file.isError && !file.isProcessing; return precondition && (file.isNew || file.isUnderReview || (file.isUnderApproval && this.isApprover(dossier))); } private _canSetUnderApproval(file: File): boolean { - return file.isUnderReview && this.isReviewerOrApprover(file); + return this._isActive(file) && file.isUnderReview && this.isReviewerOrApprover(file); } private _canUndoApproval(file: File, dossier: Dossier): boolean { - return file.isApproved && this.isApprover(dossier); + return this._isActive(file) && file.isApproved && this.isApprover(dossier); } private _canBeApproved(file: File): boolean { - return file.canBeApproved; + return this._isActive(file) && file.canBeApproved; } private _canAssignUser(file: File, dossier: Dossier) { - const precondition = !file.isProcessing && !file.isError && !file.isApproved && this.isApprover(dossier); + const precondition = this._isActive(file) && !file.isProcessing && !file.isError && !file.isApproved && this.isApprover(dossier); if (precondition) { if ((file.isNew || file.isUnderReview) && dossier.hasReviewers) { @@ -221,14 +294,20 @@ export class PermissionsService { } private _canUnassignUser(file: File, dossier: Dossier) { - return (file.isUnderReview || file.isUnderApproval) && (this.isFileAssignee(file) || this.isApprover(dossier)); + return ( + this._isActive(file) && (file.isUnderReview || file.isUnderApproval) && (this.isFileAssignee(file) || this.isApprover(dossier)) + ); } private _canSetUnderReview(file: File): boolean { - return file.isUnderApproval; + return this._isActive(file) && file.isUnderApproval; } private _getDossier(file: File): Dossier { return this._dossiersService.find(file.dossierId); } + + private _isActive(file: File): boolean { + return this._getDossier(file).isActive; + } } diff --git a/apps/red-ui/src/app/services/reanalysis.service.ts b/apps/red-ui/src/app/services/reanalysis.service.ts index 03d002626..d58a2e406 100644 --- a/apps/red-ui/src/app/services/reanalysis.service.ts +++ b/apps/red-ui/src/app/services/reanalysis.service.ts @@ -1,6 +1,6 @@ import { Injectable, Injector } from '@angular/core'; -import { GenericService, List, QueryParam, RequiredParam, Toaster, Validate } from '@iqser/common-ui'; -import { File, IPageExclusionRequest } from '@red/domain'; +import { GenericService, IRouterPath, List, QueryParam, RequiredParam, Toaster, Validate } from '@iqser/common-ui'; +import { Dossier, File, IPageExclusionRequest } from '@red/domain'; import { catchError, switchMap, tap } from 'rxjs/operators'; import { FilesService } from './entity-services/files.service'; import { FilesMapService } from './entity-services/files-map.service'; @@ -26,17 +26,19 @@ export class ReanalysisService extends GenericService { } @Validate() - excludePages(@RequiredParam() body: IPageExclusionRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) { - return this._post(body, `exclude-pages/${dossierId}/${fileId}`).pipe(switchMap(() => this._filesService.reload(dossierId, fileId))); + excludePages(@RequiredParam() body: IPageExclusionRequest, @RequiredParam() dossierId: string, @RequiredParam() file: File) { + return this._post(body, `exclude-pages/${dossierId}/${file.id}`).pipe(switchMap(() => this._filesService.reload(dossierId, file))); } @Validate() - includePages(@RequiredParam() body: IPageExclusionRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) { - return this._post(body, `include-pages/${dossierId}/${fileId}`).pipe(switchMap(() => this._filesService.reload(dossierId, fileId))); + includePages(@RequiredParam() body: IPageExclusionRequest, @RequiredParam() dossierId: string, @RequiredParam() file: File) { + return this._post(body, `include-pages/${dossierId}/${file.id}`).pipe(switchMap(() => this._filesService.reload(dossierId, file))); } @Validate() - reanalyzeFilesForDossier(@RequiredParam() fileIds: List, @RequiredParam() dossierId: string, params?: ReanalyzeQueryParams) { + reanalyzeFilesForDossier(@RequiredParam() files: List, @RequiredParam() dossierId: string, params?: ReanalyzeQueryParams) { + const fileIds = files.map(f => f.id); + const routerPath: string = files[0].routerPath; const queryParams: QueryParam[] = []; if (params?.force) { queryParams.push({ key: 'force', value: true }); @@ -45,18 +47,22 @@ export class ReanalysisService extends GenericService { queryParams.push({ key: 'triggeredByUser', value: true }); } - return this._post(fileIds, `reanalyze/${dossierId}/bulk`, queryParams).pipe(switchMap(() => this._filesService.loadAll(dossierId))); + return this._post(fileIds, `reanalyze/${dossierId}/bulk`, queryParams).pipe( + switchMap(() => this._filesService.loadAll(dossierId, routerPath)), + ); } @Validate() - toggleAnalysis(@RequiredParam() dossierId: string, @RequiredParam() fileIds: string[], excluded?: boolean) { + toggleAnalysis(@RequiredParam() dossierId: string, @RequiredParam() files: List, excluded?: boolean) { + const fileIds = files.map(f => f.id); + const routerPath: string = files[0].routerPath; const queryParams: QueryParam[] = []; if (excluded) { queryParams.push({ key: 'excluded', value: excluded }); } return this._post(fileIds, `toggle-analysis/${dossierId}/bulk`, queryParams).pipe( - switchMap(() => this._filesService.loadAll(dossierId)), + switchMap(() => this._filesService.loadAll(dossierId, routerPath)), ); } @@ -80,17 +86,24 @@ export class ReanalysisService extends GenericService { } @Validate() - ocrFiles(@RequiredParam() fileIds: List, @RequiredParam() dossierId: string) { - return this._post(fileIds, `ocr/reanalyze/${dossierId}/bulk`).pipe(switchMap(() => this._filesService.loadAll(dossierId))); + ocrFiles(@RequiredParam() files: List, @RequiredParam() dossierId: string) { + const fileIds = files.map(f => f.id); + const routerPath: string = files[0].routerPath; + return this._post(fileIds, `ocr/reanalyze/${dossierId}/bulk`).pipe( + switchMap(() => this._filesService.loadAll(dossierId, routerPath)), + ); } @Validate() - reanalyzeDossier(@RequiredParam() dossierId: string, force?: boolean) { + reanalyzeDossier(@RequiredParam() dossier: Dossier, force?: boolean) { + const { dossierId, routerPath } = dossier; const queryParams: QueryParam[] = []; if (force) { queryParams.push({ key: 'force', value: force }); } - return this._post({}, `reanalyze/${dossierId}`, queryParams).pipe(switchMap(() => this._filesService.loadAll(dossierId))); + return this._post({}, `reanalyze/${dossierId}`, queryParams).pipe( + switchMap(() => this._filesService.loadAll(dossierId, routerPath)), + ); } } diff --git a/apps/red-ui/src/app/services/router-history.service.ts b/apps/red-ui/src/app/services/router-history.service.ts index bbd98d1c2..5e87a8b82 100644 --- a/apps/red-ui/src/app/services/router-history.service.ts +++ b/apps/red-ui/src/app/services/router-history.service.ts @@ -10,7 +10,7 @@ export class RouterHistoryService { constructor(private readonly _router: Router) { this._router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe((event: NavigationEnd) => { - if (event.url.startsWith('/main/dossiers') && !event.url.includes('/search')) { + if (event.url.startsWith('/main/dossiers')) { this._lastDossiersScreen = event.url; } }); diff --git a/apps/red-ui/src/app/tokens.ts b/apps/red-ui/src/app/tokens.ts index ca56f808f..ce8f6a595 100644 --- a/apps/red-ui/src/app/tokens.ts +++ b/apps/red-ui/src/app/tokens.ts @@ -2,3 +2,6 @@ import { InjectionToken } from '@angular/core'; export const BASE_HREF: InjectionToken = new InjectionToken('BASE_HREF'); export const DOSSIER_ID: InjectionToken = new InjectionToken('DOSSIER_ID'); + +export const ACTIVE_DOSSIERS_SERVICE = new InjectionToken('Active dossiers service'); +export const ARCHIVED_DOSSIERS_SERVICE = new InjectionToken('Archived dossiers service'); diff --git a/apps/red-ui/src/app/modules/dossier/translations/file-status-translations.ts b/apps/red-ui/src/app/translations/file-status-translations.ts similarity index 100% rename from apps/red-ui/src/app/modules/dossier/translations/file-status-translations.ts rename to apps/red-ui/src/app/translations/file-status-translations.ts diff --git a/apps/red-ui/src/assets/i18n/de.json b/apps/red-ui/src/assets/i18n/de.json index c647382a9..2c978b185 100644 --- a/apps/red-ui/src/assets/i18n/de.json +++ b/apps/red-ui/src/assets/i18n/de.json @@ -424,16 +424,16 @@ "warning": "" }, "confirm-delete-file-attribute": { - "cancel": "{type, select, single{Attribut} bulk{Attribute} other{}} behalten", - "delete": "{type, select, single{Attribut} bulk{Attribute} other{}} löschen", + "cancel": "{count, plural, one{Attribut} other{Attribute}} behalten", + "delete": "{count, plural, one{Attribut} other{Attribute}} löschen", + "file-impacted-documents": "Alle Dokumente {count, plural, one{ist} other{sind}} betroffen", "dossier-impacted-documents": "", - "dossier-lost-details": "", - "file-impacted-documents": "Alle Dokumente {type, select, single{ist} bulk{sind} other{}} betroffen", "file-lost-details": "Alle in die Dokumente eingegebenen Daten gehen verloren", - "impacted-report": "{count}", - "title": "{type, select, single{{name}} bulk{Datei-Attribute} other{}} löschen", + "dossier-lost-details": "", + "title": "{count, plural, one{{name}} other{Datei-Attribute}} löschen", "toast-error": "Bitte bestätigen Sie, dass Ihnen die Konsequenzen dieser Aktion bewusst sind!", - "warning": "Achtung: Diese Aktion kann nicht rückgängig gemacht werden!" + "warning": "Achtung: Diese Aktion kann nicht rückgängig gemacht werden!", + "impacted-report": "{reportsCount}" }, "confirm-delete-users": { "cancel": "{usersCount, plural, one{Benutzer} other{Benutzer}} behalten", @@ -1730,6 +1730,7 @@ }, "filters": { "by-dossier": "Nach Dossier filtern", + "only-active": "", "search-placeholder": "Dossiername..." }, "missing": "Fehlt", @@ -1739,7 +1740,7 @@ "table-header": "{length} {length, plural, one{Suchergebnis} other{Suchergebnisse}}" }, "search": { - "entire-platform": "ganze Plattform", + "active-dossiers": "ganze Plattform", "placeholder": "Nach Dokumenten oder Dokumenteninhalt suchen", "this-dossier": "in diesem Dossier" }, diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index b9efdb33b..cbccfdcba 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -315,6 +315,23 @@ "text-highlight": "Highlight" }, "annotations": "Annotations", + "archived-dossiers-listing": { + "no-data": { + "title": "No archived dossiers." + }, + "no-match": { + "title": "No archived dossiers match your current filters." + }, + "table-col-names": { + "dossier-status": "Dossier Status", + "last-modified": "Archived Time", + "name": "Name", + "owner": "Owner" + }, + "table-header": { + "title": "{length} Archived {length, plural, one{Dossier} other{Dossiers}}" + } + }, "assign-dossier-owner": { "dialog": { "approvers": "Approvers", @@ -411,6 +428,17 @@ } }, "configurations": "Configurations", + "confirm-archive-dossier": { + "archive": "Archive Dossier", + "cancel": "Cancel", + "checkbox": { + "documents": "All documents will be archived and cannot be put back to active" + }, + "details": "Restoring an archived dossier is not possible anymore, once it got archived.", + "title": "Archive {dossierName}", + "toast-error": "Please confirm that you understand the ramifications of your action!", + "warning": "Are you sure you want to archive the dossier?" + }, "confirm-delete-dossier-state": { "cancel": "Cancel", "delete": "Delete", @@ -424,14 +452,14 @@ "warning": "The {name} status is assigned to {count} {count, plural, one{Dossier} other{Dossiers}}." }, "confirm-delete-file-attribute": { - "cancel": "Keep {type, select, single{Attribute} bulk{Attributes} other{}}", - "delete": "Delete {type, select, single{Attribute} bulk{Attributes} other{}}", + "cancel": "Keep {count, plural, one{Attribute} other{Attributes}}", + "delete": "Delete {count, plural, one{Attribute} other{Attributes}}", "dossier-impacted-documents": "All dossiers based on this template will be affected", "dossier-lost-details": "All values for this attribute will be lost", - "file-impacted-documents": "All documents {type, select, single{it is} bulk{they are} other{}} used on will be impacted", + "file-impacted-documents": "All documents {count, plural, one{it is} other{they are}} used on will be impacted", "file-lost-details": "All inputted details on the documents will be lost", - "impacted-report": "{count} reports use the placeholder for this attribute and need to be adjusted", - "title": "Delete {type, select, single{{name}} bulk{File Attributes} other{}}", + "impacted-report": "{reportsCount} reports use the placeholder for this attribute and need to be adjusted", + "title": "Delete {count, plural, one{{name}} other{File Attributes}}", "toast-error": "Please confirm that you understand the ramifications of your action!", "warning": "Warning: this cannot be undone!" }, @@ -692,6 +720,11 @@ }, "dossier-listing": { "add-new": "New Dossier", + "archive": { + "action": "Archive Dossier", + "archive-failed": "Failed to archive dossier {dossierName}!", + "archive-succeeded": "Successfully archived dossier {dossierName}." + }, "delete": { "action": "Delete Dossier", "delete-failed": "Failed to delete dossier: {dossierName}" @@ -802,7 +835,7 @@ }, "no-data": { "action": "Upload Document", - "title": "There are no documents yet." + "title": "There are no documents in this dossier." }, "no-match": { "title": "No documents match your current filters." @@ -1226,6 +1259,7 @@ "reanalyse-notification": "This document was not processed with the latest rule/dictionary set. Analyze now to get updated annotations.", "redacted": "Preview", "redacted-tooltip": "Redaction preview shows only redactions. Consider this a preview for the final redacted version. This view is only available if the file has no pending changes & doesn't require a reanalysis", + "reset-filters": "", "standard": "Standard", "standard-tooltip": "Standard Workload view shows all hints, redactions, recommendations & suggestions. This view allows editing.", "tabs": { @@ -1601,6 +1635,7 @@ "processing": "Processing" }, "readonly": "Read only", + "readonly-archived": "Read only (archived)", "recategorize-image-dialog": { "actions": { "cancel": "Cancel", @@ -1730,6 +1765,7 @@ }, "filters": { "by-dossier": "Filter by Dossier", + "only-active": "Active dossiers only", "search-placeholder": "Dossier name..." }, "missing": "Missing", @@ -1739,7 +1775,8 @@ "table-header": "{length} search {length, plural, one{result} other{results}}" }, "search": { - "entire-platform": "across all dossiers", + "active-dossiers": "documents in active dossiers", + "all-dossiers": "all documents", "placeholder": "Search documents...", "this-dossier": "in this dossier" }, @@ -1768,6 +1805,7 @@ }, "top-bar": { "navigation-items": { + "archived-dossiers": "Archived Dossiers", "back": "Back", "dossiers": "Active Dossiers", "my-account": { diff --git a/apps/red-ui/src/assets/icons/general/archive.svg b/apps/red-ui/src/assets/icons/general/archive.svg new file mode 100644 index 000000000..0d4414f55 --- /dev/null +++ b/apps/red-ui/src/assets/icons/general/archive.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/libs/red-domain/src/index.ts b/libs/red-domain/src/index.ts index 2858caf86..8220a575e 100644 --- a/libs/red-domain/src/index.ts +++ b/libs/red-domain/src/index.ts @@ -20,4 +20,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/text-highlight'; diff --git a/libs/red-domain/src/lib/dossiers/dossier-changes.ts b/libs/red-domain/src/lib/dossiers/dossier-changes.ts new file mode 100644 index 000000000..a63406f4c --- /dev/null +++ b/libs/red-domain/src/lib/dossiers/dossier-changes.ts @@ -0,0 +1,11 @@ +export interface IDossierChange { + readonly dossierChanges: boolean; + readonly dossierId: string; + readonly fileChanges: boolean; +} + +export type IDossierChanges = readonly IDossierChange[]; + +export interface IChangesDetails { + readonly dossierChanges: IDossierChanges; +} diff --git a/libs/red-domain/src/lib/dossiers/dossier.model.ts b/libs/red-domain/src/lib/dossiers/dossier.model.ts index e2a79d550..fba166c8a 100644 --- a/libs/red-domain/src/lib/dossiers/dossier.model.ts +++ b/libs/red-domain/src/lib/dossiers/dossier.model.ts @@ -1,9 +1,9 @@ -import { IListable, List } from '@iqser/common-ui'; +import { IListable, IRouterPath, List } from '@iqser/common-ui'; import { IDossier } from './dossier'; -import { DossierStatus } from './types'; +import { DossierStatus, DossierStatuses } from './types'; import { DownloadFileType } from '../shared'; -export class Dossier implements IDossier, IListable { +export class Dossier implements IDossier, IListable, IRouterPath { readonly dossierId: string; readonly dossierTemplateId: string; readonly ownerId: string; @@ -22,9 +22,10 @@ export class Dossier implements IDossier, IListable { readonly status: DossierStatus; readonly watermarkEnabled: boolean; readonly watermarkPreviewEnabled: boolean; + readonly archivedTime: string; readonly hasReviewers: boolean; - constructor(dossier: IDossier) { + constructor(dossier: IDossier, readonly routerPath: string) { this.dossierId = dossier.dossierId; this.approverIds = dossier.approverIds; this.date = dossier.date; @@ -43,6 +44,7 @@ export class Dossier implements IDossier, IListable { this.status = dossier.status; this.watermarkEnabled = dossier.watermarkEnabled; this.watermarkPreviewEnabled = dossier.watermarkPreviewEnabled; + this.archivedTime = dossier.archivedTime; this.hasReviewers = !!this.memberIds && this.memberIds.length > 1; } @@ -51,13 +53,17 @@ export class Dossier implements IDossier, IListable { } get routerLink(): string { - return `/main/dossiers/${this.dossierId}`; + return `/main/${this.routerPath}/${this.dossierId}`; } get searchKey(): string { return this.dossierName; } + get isActive(): boolean { + return this.status === DossierStatuses.ACTIVE; + } + hasMember(memberId: string): boolean { return !!this.memberIds && this.memberIds.indexOf(memberId) >= 0; } diff --git a/libs/red-domain/src/lib/dossiers/dossier.ts b/libs/red-domain/src/lib/dossiers/dossier.ts index 1ff219033..59689a5d9 100644 --- a/libs/red-domain/src/lib/dossiers/dossier.ts +++ b/libs/red-domain/src/lib/dossiers/dossier.ts @@ -21,4 +21,5 @@ export interface IDossier { readonly status: DossierStatus; readonly watermarkEnabled: boolean; readonly watermarkPreviewEnabled: boolean; + readonly archivedTime: string; } diff --git a/libs/red-domain/src/lib/dossiers/index.ts b/libs/red-domain/src/lib/dossiers/index.ts index 5f42d97f7..994f4274c 100644 --- a/libs/red-domain/src/lib/dossiers/index.ts +++ b/libs/red-domain/src/lib/dossiers/index.ts @@ -2,3 +2,4 @@ export * from './dossier'; export * from './dossier.request'; export * from './dossier.model'; export * from './types'; +export * from './dossier-changes'; diff --git a/libs/red-domain/src/lib/dossiers/types.ts b/libs/red-domain/src/lib/dossiers/types.ts index 34a2be57c..f667390d4 100644 --- a/libs/red-domain/src/lib/dossiers/types.ts +++ b/libs/red-domain/src/lib/dossiers/types.ts @@ -1,6 +1,7 @@ export const DossierStatuses = { ACTIVE: 'ACTIVE', DELETED: 'DELETED', + ARCHIVED: 'ARCHIVED', } as const; export type DossierStatus = keyof typeof DossierStatuses; diff --git a/libs/red-domain/src/lib/files/file.model.ts b/libs/red-domain/src/lib/files/file.model.ts index ef369ad6f..60b2911f1 100644 --- a/libs/red-domain/src/lib/files/file.model.ts +++ b/libs/red-domain/src/lib/files/file.model.ts @@ -1,10 +1,10 @@ -import { Entity } from '@iqser/common-ui'; +import { Entity, IRouterPath } from '@iqser/common-ui'; import { StatusSorter } from '../shared'; import { isProcessingStatuses, ProcessingFileStatus, ProcessingFileStatuses, WorkflowFileStatus, WorkflowFileStatuses } from './types'; import { IFile } from './file'; import { FileAttributes } from '../file-attributes'; -export class File extends Entity implements IFile { +export class File extends Entity implements IFile, IRouterPath { readonly added?: string; readonly allManualRedactionsApplied: boolean; readonly analysisDuration?: number; @@ -59,7 +59,7 @@ export class File extends Entity implements IFile { readonly canBeOpened: boolean; readonly canBeOCRed: boolean; - constructor(file: IFile, readonly reviewerName: string) { + constructor(file: IFile, readonly reviewerName: string, readonly routerPath: string) { super(file); this.added = file.added; this.allManualRedactionsApplied = !!file.allManualRedactionsApplied; @@ -129,6 +129,6 @@ export class File extends Entity implements IFile { } get routerLink(): string | undefined { - return this.canBeOpened ? `/main/dossiers/${this.dossierId}/file/${this.fileId}` : undefined; + return this.canBeOpened ? `/main/${this.routerPath}/${this.dossierId}/file/${this.fileId}` : undefined; } } diff --git a/libs/red-domain/src/lib/search/matched-document.ts b/libs/red-domain/src/lib/search/matched-document.ts index e6b8b5f3b..65d41ddce 100644 --- a/libs/red-domain/src/lib/search/matched-document.ts +++ b/libs/red-domain/src/lib/search/matched-document.ts @@ -1,10 +1,12 @@ import { IMatchedSection } from './matched-section'; import { List } from '@iqser/common-ui'; +import { DossierStatus } from '../dossiers'; export interface IMatchedDocument { containsAllMatchedSections?: boolean; dossierId?: string; dossierTemplateId?: string; + dossierStatus?: DossierStatus; fileId?: string; highlights?: { [key: string]: List }; matchedSections?: List; diff --git a/libs/red-domain/src/lib/search/search-input.ts b/libs/red-domain/src/lib/search/search-input.ts index 597b6391f..dc0786951 100644 --- a/libs/red-domain/src/lib/search/search-input.ts +++ b/libs/red-domain/src/lib/search/search-input.ts @@ -1,6 +1,8 @@ import { List } from '@iqser/common-ui'; +import { DossierStatus } from '../dossiers'; export interface ISearchInput { readonly query: string; readonly dossierIds?: List; + readonly dossierStatus?: List; } diff --git a/libs/red-domain/src/lib/search/search-list-item.ts b/libs/red-domain/src/lib/search/search-list-item.ts index 1b5006a56..0091366b8 100644 --- a/libs/red-domain/src/lib/search/search-list-item.ts +++ b/libs/red-domain/src/lib/search/search-list-item.ts @@ -1,7 +1,9 @@ import { IListable, List } from '@iqser/common-ui'; +import { DossierStatus } from '../dossiers'; export interface ISearchListItem extends IListable { readonly dossierId: string; + readonly dossierStatus: DossierStatus; readonly filename: string; readonly assignee: string; readonly unmatched: List | null; diff --git a/libs/red-domain/src/lib/search/search.request.ts b/libs/red-domain/src/lib/search/search.request.ts index 104e9b097..e9ae5c23e 100644 --- a/libs/red-domain/src/lib/search/search.request.ts +++ b/libs/red-domain/src/lib/search/search.request.ts @@ -1,11 +1,13 @@ import { List } from '@iqser/common-ui'; +import { DossierStatus } from '../dossiers'; export interface ISearchRequest { - dossierIds?: List; - dossierTemplateIds?: List; - fileId?: string; - page?: number; - pageSize?: number; - queryString?: string; - returnSections?: boolean; + readonly dossierIds?: List; + readonly dossierTemplateIds?: List; + readonly dossierStatus?: List; + readonly fileId?: string; + readonly page?: number; + readonly pageSize?: number; + readonly queryString?: string; + readonly returnSections?: boolean; } diff --git a/libs/red-domain/src/lib/shared/breadcrumb-types.ts b/libs/red-domain/src/lib/shared/breadcrumb-types.ts index 3366c25a2..74ed8ae69 100644 --- a/libs/red-domain/src/lib/shared/breadcrumb-types.ts +++ b/libs/red-domain/src/lib/shared/breadcrumb-types.ts @@ -1,7 +1,8 @@ -export type BreadcrumbType = 'main' | 'dossier' | 'file'; +export type BreadcrumbType = 'main' | 'dossier' | 'file' | 'archive'; export const BreadcrumbTypes = { main: 'main' as BreadcrumbType, dossier: 'dossier' as BreadcrumbType, file: 'file' as BreadcrumbType, + archive: 'archive' as BreadcrumbType, }; diff --git a/libs/red-domain/src/lib/trash-dossier/index.ts b/libs/red-domain/src/lib/trash-dossier/index.ts new file mode 100644 index 000000000..a90f3227f --- /dev/null +++ b/libs/red-domain/src/lib/trash-dossier/index.ts @@ -0,0 +1 @@ +export * from './trash-dossier.model'; diff --git a/libs/red-domain/src/lib/trash-dossier/trash-dossier.model.ts b/libs/red-domain/src/lib/trash-dossier/trash-dossier.model.ts new file mode 100644 index 000000000..aca8adf9d --- /dev/null +++ b/libs/red-domain/src/lib/trash-dossier/trash-dossier.model.ts @@ -0,0 +1,68 @@ +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; + 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; + } +}