From 1bbf76f1b7beaff60bdf2f77120bc21e7ade342e Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Fri, 12 Nov 2021 11:42:19 +0200 Subject: [PATCH] add dossier stats --- .../edit-dossier-general-info.component.ts | 2 +- .../dossier-details-stats.component.html | 26 +++++----- .../dossier-details.component.html | 52 ++++++++++--------- .../dossier-overview-screen.component.ts | 2 +- ...ssiers-listing-dossier-name.component.html | 12 +++-- .../dossiers-listing-screen.component.ts | 4 -- .../entity-services/dossier-stats.service.ts | 26 ++++++++++ .../entity-services/dossiers.service.ts | 43 ++++++++++----- .../red-ui/src/app/state/app-state.service.ts | 16 +++--- libs/red-domain/src/index.ts | 1 + .../lib/dossier-stats/dossier-stats.model.ts | 34 ++++++++++++ .../src/lib/dossier-stats/dossier-stats.ts | 14 +++++ .../red-domain/src/lib/dossier-stats/index.ts | 3 ++ .../red-domain/src/lib/dossier-stats/types.ts | 26 ++++++++++ .../src/lib/dossiers/dossier.model.ts | 18 +++++-- 15 files changed, 208 insertions(+), 71 deletions(-) create mode 100644 apps/red-ui/src/app/services/entity-services/dossier-stats.service.ts create mode 100644 libs/red-domain/src/lib/dossier-stats/dossier-stats.model.ts create mode 100644 libs/red-domain/src/lib/dossier-stats/dossier-stats.ts create mode 100644 libs/red-domain/src/lib/dossier-stats/index.ts create mode 100644 libs/red-domain/src/lib/dossier-stats/types.ts diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.ts index fb680d5fe..6d288c92e 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.ts @@ -71,7 +71,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti dossierTemplateId: [ { value: this.dossier.dossierTemplateId, - disabled: this.dossier.hasFiles, + disabled: this.dossier.stats.hasFiles, }, Validators.required, ], diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossier-overview/components/dossier-details-stats/dossier-details-stats.component.html b/apps/red-ui/src/app/modules/dossier/screens/dossier-overview/components/dossier-details-stats/dossier-details-stats.component.html index 463781593..8ce9e6f46 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossier-overview/components/dossier-details-stats/dossier-details-stats.component.html +++ b/apps/red-ui/src/app/modules/dossier/screens/dossier-overview/components/dossier-details-stats/dossier-details-stats.component.html @@ -1,17 +1,19 @@ -
- - {{ 'dossier-overview.dossier-details.stats.documents' | translate: { count: dossier.files.length } }} -
+ +
+ + {{ 'dossier-overview.dossier-details.stats.documents' | translate: { count: stats.numberOfFiles } }} +
-
- - {{ 'dossier-overview.dossier-details.stats.people' | translate: { count: dossier.memberIds.length } }} -
+
+ + {{ 'dossier-overview.dossier-details.stats.people' | translate: { count: dossier.memberIds.length } }} +
-
- - {{ 'dossier-overview.dossier-details.stats.analysed-pages' | translate: { count: dossier.totalNumberOfPages | number } }} -
+
+ + {{ 'dossier-overview.dossier-details.stats.analysed-pages' | translate: { count: stats.numberOfPages | number } }} +
+
diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossier-overview/components/dossier-details/dossier-details.component.html b/apps/red-ui/src/app/modules/dossier/screens/dossier-overview/components/dossier-details/dossier-details.component.html index 95bad51d7..184e7558a 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossier-overview/components/dossier-details/dossier-details.component.html +++ b/apps/red-ui/src/app/modules/dossier/screens/dossier-overview/components/dossier-details/dossier-details.component.html @@ -38,33 +38,35 @@ >
-
- -
- -
-
- + +
+
-
-
- -
+
+
+ +
+
+ +
+ +
+
diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossier-overview/screen/dossier-overview-screen.component.ts b/apps/red-ui/src/app/modules/dossier/screens/dossier-overview/screen/dossier-overview-screen.component.ts index 6850fd340..fcfc506bc 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossier-overview/screen/dossier-overview-screen.component.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/dossier-overview/screen/dossier-overview-screen.component.ts @@ -156,7 +156,7 @@ export class DossierOverviewScreenComponent extends ListingComponent imple this.calculateData(); this.addSubscription = timer(0, 20 * 1000).subscribe(async () => { - await this._appStateService.reloadActiveDossierFilesIfNecessary(); + await this._appStateService.reloadActiveDossierFiles(); this.calculateData(); }); 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/dossier/screens/dossiers-listing/components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component.html index 754cdc5b7..4473efaa3 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/dossier/screens/dossiers-listing/components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component.html @@ -1,29 +1,35 @@
{{ dossier.dossierName }}
+
{{ getDossierTemplateNameFor(dossier.dossierTemplateId) }}
-
+ +
- {{ dossier.filesLength }} + {{ stats.numberOfFiles }}
+
- {{ dossier.totalNumberOfPages }} + {{ stats.numberOfPages }}
+
{{ dossier.memberIds.length }}
+
{{ dossier.date | date: 'mediumDate' }}
+
{{ dossier.dueDate | date: 'mediumDate' }} 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 50dff12d4..71194edd6 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 @@ -84,10 +84,6 @@ export class DossiersListingScreenComponent await this._appStateService.loadAllDossiers(); this.calculateData(); }); - - this.addSubscription = this._appStateService.fileChanged$.subscribe(() => { - this.calculateData(); - }); } ngAfterViewInit(): void { diff --git a/apps/red-ui/src/app/services/entity-services/dossier-stats.service.ts b/apps/red-ui/src/app/services/entity-services/dossier-stats.service.ts new file mode 100644 index 000000000..cf96b4ce5 --- /dev/null +++ b/apps/red-ui/src/app/services/entity-services/dossier-stats.service.ts @@ -0,0 +1,26 @@ +import { Injectable, Injector } from '@angular/core'; +import { GenericService, mapEach, RequiredParam, Validate } from '@iqser/common-ui'; +import { Observable } from 'rxjs'; +import { DossierStats, IDossierStats } from '@red/domain'; + +@Injectable({ + providedIn: 'root', +}) +export class DossierStatsService extends GenericService { + constructor(protected readonly _injector: Injector) { + super(_injector, 'dossier-stats'); + } + + @Validate() + getFor(@RequiredParam() dossierIds: string[]): Observable { + return this._post(dossierIds).pipe(mapEach(entity => new DossierStats(entity))); + } + + // @Validate() + // loadFor(@RequiredParam() dossierId: string): Observable { + // return this._getOne([dossierId]).pipe( + // map((entity: IDossierStats) => new DossierStats(entity)), + // tap((entity: DossierStats) => this.replace(entity)), + // ); + // } +} diff --git a/apps/red-ui/src/app/services/entity-services/dossiers.service.ts b/apps/red-ui/src/app/services/entity-services/dossiers.service.ts index c89b8fcd2..afca232c5 100644 --- a/apps/red-ui/src/app/services/entity-services/dossiers.service.ts +++ b/apps/red-ui/src/app/services/entity-services/dossiers.service.ts @@ -1,12 +1,14 @@ import { Injectable, Injector } from '@angular/core'; -import { EntitiesService, List, QueryParam, RequiredParam, Toaster, Validate } from '@iqser/common-ui'; +import { EntitiesService, List, QueryParam, RequiredParam, shareLast, Toaster, Validate } from '@iqser/common-ui'; import { Dossier, File, IDossier, IDossierRequest } from '@red/domain'; -import { catchError, map, tap } from 'rxjs/operators'; -import { BehaviorSubject, Observable, of } from 'rxjs'; +import { catchError, filter, map, switchMap, tap } from 'rxjs/operators'; +import { BehaviorSubject, combineLatest, Observable, of, throwError } from 'rxjs'; import { ActivationEnd, Router } from '@angular/router'; import { DictionaryService } from '@shared/services/dictionary.service'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { currentComponentRoute } from '@utils/functions'; +import { HttpErrorResponse } from '@angular/common/http'; +import { DossierStatsService } from '@services/entity-services/dossier-stats.service'; export interface IDossiersStats { totalPeople: number; @@ -20,15 +22,16 @@ const GENERIC_MGS = _('add-dossier-dialog.errors.generic'); providedIn: 'root', }) export class DossiersService extends EntitiesService { - readonly stats$ = this.all$.pipe(map(entities => this._computeStats(entities))); + readonly stats$ = this.all$.pipe(switchMap(entities => this._generalStats$(entities))); readonly activeDossier$: Observable; private readonly _activeDossier$ = new BehaviorSubject(undefined); constructor( - protected readonly _injector: Injector, private readonly _router: Router, - private readonly _dictionaryService: DictionaryService, private readonly _toaster: Toaster, + protected readonly _injector: Injector, + private readonly _dictionaryService: DictionaryService, + private readonly _dossierStatsService: DossierStatsService, ) { super(_injector, Dossier, 'dossier'); this.activeDossier$ = this._activeDossier$.asObservable(); @@ -97,13 +100,18 @@ export class DossiersService extends EntitiesService { @Validate() createOrUpdate(@RequiredParam() dossier: IDossierRequest): Observable { - return this._post(dossier).pipe( - map(updatedDossier => new Dossier(updatedDossier, this.find(updatedDossier.dossierId)?.files ?? [])), + const showToast = (error: HttpErrorResponse) => { + this._toaster.error(error.status === 409 ? DOSSIER_EXISTS_MSG : GENERIC_MGS); + return throwError(error); + }; + + const dossier$ = this._post(dossier).pipe(shareLast()); + const stats$ = dossier$.pipe(switchMap(updatedDossier => this._dossierStatsService.getFor([updatedDossier.dossierId]))); + + return combineLatest([dossier$, stats$]).pipe( + map(([updatedDossier, stats]) => new Dossier(updatedDossier, stats[0], this.find(updatedDossier.dossierId)?.files ?? [])), tap(newDossier => this.replace(newDossier)), - catchError(error => { - this._toaster.error(error.status === 409 ? DOSSIER_EXISTS_MSG : GENERIC_MGS); - return of(undefined); - }), + catchError(showToast), ); } @@ -139,7 +147,7 @@ export class DossiersService extends EntitiesService { entities.forEach(dossier => { dossier.memberIds?.forEach(m => totalPeople.add(m)); - totalAnalyzedPages += dossier.totalNumberOfPages; + totalAnalyzedPages += dossier.stats.numberOfPages; }); return { @@ -147,4 +155,13 @@ export class DossiersService extends EntitiesService { totalAnalyzedPages, }; } + + private _generalStats$(entities: List): Observable { + const stats$ = entities.map(entity => entity.stats$); + return combineLatest(stats$).pipe( + filter(stats => stats.every(s => !!s)), + map(() => this._computeStats(entities)), + shareLast(), + ); + } } diff --git a/apps/red-ui/src/app/state/app-state.service.ts b/apps/red-ui/src/app/state/app-state.service.ts index a2a34a236..0806d07d7 100644 --- a/apps/red-ui/src/app/state/app-state.service.ts +++ b/apps/red-ui/src/app/state/app-state.service.ts @@ -12,6 +12,7 @@ import { DictionaryService } from '@shared/services/dictionary.service'; import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service'; import { FileAttributesService } from '@services/entity-services/file-attributes.service'; import { ReanalysisService } from '@services/reanalysis.service'; +import { DossierStatsService } from '@services/entity-services/dossier-stats.service'; export interface AppState { activeFileId?: string; @@ -35,6 +36,7 @@ export class AppStateService { private readonly _reanalysisService: ReanalysisService, private readonly _dictionaryService: DictionaryService, private readonly _dossierTemplatesService: DossierTemplatesService, + private readonly _dossierStatsService: DossierStatsService, private readonly _fileAttributesService: FileAttributesService, private readonly _userPreferenceService: UserPreferenceService, ) { @@ -90,10 +92,6 @@ export class AppStateService { return this._appState.activeFileId; } - async reloadActiveDossierFilesIfNecessary() { - await this.reloadActiveDossierFiles(); - } - getDictionaryColor(type?: string, dossierTemplateId = this._dossiersService.activeDossier?.dossierTemplateId) { if (!dossierTemplateId) { dossierTemplateId = this.dossierTemplates[0]?.dossierTemplateId; @@ -128,10 +126,14 @@ export class AppStateService { return; } + const dossierIds = dossiers.map(dossier => dossier.dossierId); + const dossierStats = await this._dossierStatsService.getFor(dossierIds).toPromise(); + const mappedDossiers$ = dossiers.map(async p => { const oldDossier = this._dossiersService.find(p.dossierId); const type = oldDossier?.type ?? (await this._getDictionaryFor(p)); - return new Dossier(p, oldDossier?.files ?? [], type); + const stats = dossierStats.find(s => s.dossierId === p.dossierId); + return new Dossier(p, stats, oldDossier?.files ?? [], type); }); const mappedDossiers = await Promise.all(mappedDossiers$); const fileData = await this._filesService.getFor(mappedDossiers.map(p => p.id)).toPromise(); @@ -160,7 +162,7 @@ export class AppStateService { ); const files = activeDossier.files.filter(file => file.fileId !== activeFile.fileId); files.push(activeFile); - const newDossier = new Dossier(activeDossier, files, activeDossier.type); + const newDossier = new Dossier(activeDossier, activeDossier.stats, files, activeDossier.type); this._dossiersService.replace(newDossier); if (activeFile.lastProcessed !== oldProcessedDate) { @@ -512,7 +514,7 @@ export class AppStateService { } } - const newDossier = new Dossier(dossier, newFiles, dossier.type); + const newDossier = new Dossier(dossier, dossier.stats, newFiles, dossier.type); this._dossiersService.replace(newDossier); return newFiles; diff --git a/libs/red-domain/src/index.ts b/libs/red-domain/src/index.ts index 3cd713b40..aee8ff159 100644 --- a/libs/red-domain/src/index.ts +++ b/libs/red-domain/src/index.ts @@ -18,3 +18,4 @@ export * from './lib/reports'; export * from './lib/configuration'; export * from './lib/signature'; export * from './lib/legal-basis'; +export * from './lib/dossier-stats'; diff --git a/libs/red-domain/src/lib/dossier-stats/dossier-stats.model.ts b/libs/red-domain/src/lib/dossier-stats/dossier-stats.model.ts new file mode 100644 index 000000000..e478f0b6f --- /dev/null +++ b/libs/red-domain/src/lib/dossier-stats/dossier-stats.model.ts @@ -0,0 +1,34 @@ +import { IDossierStats } from './dossier-stats'; +import { FileCountPerProcessingStatus, FileCountPerWorkflowStatus } from './types'; + +export class DossierStats implements IDossierStats { + readonly dossierId: string; + readonly fileCountPerProcessingStatus: FileCountPerProcessingStatus; + readonly fileCountPerWorkflowStatus: FileCountPerWorkflowStatus; + readonly hasHintsNoRedactionsFilePresent: boolean; + readonly hasNoFlagsFilePresent: boolean; + readonly hasRedactionsFilePresent: boolean; + readonly hasSuggestionsFilePresent: boolean; + readonly hasUpdatesFilePresent: boolean; + readonly numberOfPages: number; + readonly numberOfFiles: number; + + readonly hasNone: boolean; + readonly hasFiles: boolean; + + constructor(stats: IDossierStats) { + this.dossierId = stats.dossierId; + this.fileCountPerProcessingStatus = stats.fileCountPerProcessingStatus; + this.fileCountPerWorkflowStatus = stats.fileCountPerWorkflowStatus; + this.hasHintsNoRedactionsFilePresent = stats.hasHintsNoRedactionsFilePresent; + this.hasNoFlagsFilePresent = stats.hasNoFlagsFilePresent; + this.hasRedactionsFilePresent = stats.hasRedactionsFilePresent; + this.hasSuggestionsFilePresent = stats.hasSuggestionsFilePresent; + this.hasUpdatesFilePresent = stats.hasUpdatesFilePresent; + this.numberOfPages = stats.numberOfPages; + this.numberOfFiles = stats.numberOfFiles; + + this.hasNone = !this.hasSuggestionsFilePresent && !this.hasRedactionsFilePresent && !this.hasHintsNoRedactionsFilePresent; + this.hasFiles = this.numberOfFiles > 0; + } +} diff --git a/libs/red-domain/src/lib/dossier-stats/dossier-stats.ts b/libs/red-domain/src/lib/dossier-stats/dossier-stats.ts new file mode 100644 index 000000000..022673a00 --- /dev/null +++ b/libs/red-domain/src/lib/dossier-stats/dossier-stats.ts @@ -0,0 +1,14 @@ +import { FileCountPerProcessingStatus, FileCountPerWorkflowStatus } from './types'; + +export interface IDossierStats { + dossierId: string; + fileCountPerProcessingStatus: FileCountPerProcessingStatus; + fileCountPerWorkflowStatus: FileCountPerWorkflowStatus; + hasHintsNoRedactionsFilePresent: boolean; + hasNoFlagsFilePresent: boolean; + hasRedactionsFilePresent: boolean; + hasSuggestionsFilePresent: boolean; + hasUpdatesFilePresent: boolean; + numberOfPages: number; + numberOfFiles: number; +} diff --git a/libs/red-domain/src/lib/dossier-stats/index.ts b/libs/red-domain/src/lib/dossier-stats/index.ts new file mode 100644 index 000000000..82cf12c13 --- /dev/null +++ b/libs/red-domain/src/lib/dossier-stats/index.ts @@ -0,0 +1,3 @@ +export * from './dossier-stats'; +export * from './dossier-stats.model'; +export * from './types'; diff --git a/libs/red-domain/src/lib/dossier-stats/types.ts b/libs/red-domain/src/lib/dossier-stats/types.ts new file mode 100644 index 000000000..d87f9521d --- /dev/null +++ b/libs/red-domain/src/lib/dossier-stats/types.ts @@ -0,0 +1,26 @@ +export const WorkflowFileStatuses = { + APPROVED: 'APPROVED', + UNASSIGNED: 'UNASSIGNED', + UNDER_APPROVAL: 'UNDER_APPROVAL', + UNDER_REVIEW: 'UNDER_REVIEW', +} as const; + +export type WorkflowFileStatus = keyof typeof WorkflowFileStatuses; + +export type FileCountPerWorkflowStatus = { [key in WorkflowFileStatus]?: number }; + +export const ProcessingFileStatuses = { + DELETED: 'DELETED', + ERROR: 'ERROR', + FULLREPROCESS: 'FULLREPROCESS', + INDEXING: 'INDEXING', + OCR_PROCESSING: 'OCR_PROCESSING', + PROCESSED: 'PROCESSED', + PROCESSING: 'PROCESSING', + REPROCESS: 'REPROCESS', + UNPROCESSED: 'UNPROCESSED', +} as const; + +export type ProcessingFileStatus = keyof typeof ProcessingFileStatuses; + +export type FileCountPerProcessingStatus = { [key in ProcessingFileStatus]?: number }; diff --git a/libs/red-domain/src/lib/dossiers/dossier.model.ts b/libs/red-domain/src/lib/dossiers/dossier.model.ts index 5afb12bbb..52c8bab64 100644 --- a/libs/red-domain/src/lib/dossiers/dossier.model.ts +++ b/libs/red-domain/src/lib/dossiers/dossier.model.ts @@ -4,6 +4,8 @@ import { IDossier } from './dossier'; import { DossierStatus } from './types'; import { DownloadFileType } from '../shared'; import { IDictionary } from '../dictionaries'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { DossierStats } from '../dossier-stats'; export class Dossier implements IDossier, IListable { readonly dossierId: string; @@ -25,17 +27,17 @@ export class Dossier implements IDossier, IListable { readonly hasReviewers: boolean; readonly reanalysisRequired = this.files.some(file => file.analysisRequired); - readonly hasFiles = this.files.length > 0; - readonly filesLength = this.files.length; readonly totalNumberOfPages: number; readonly hintsOnly: boolean; readonly hasRedactions: boolean; readonly hasSuggestions: boolean; readonly hasNone: boolean; - readonly hasPendingOrProcessing: boolean; - constructor(dossier: IDossier, readonly files: List = [], public type?: IDictionary) { + readonly stats$: Observable; + private readonly _stats$: BehaviorSubject; + + constructor(dossier: IDossier, stats: DossierStats, readonly files: List = [], public type?: IDictionary) { this.dossierId = dossier.dossierId; this.approverIds = dossier.approverIds; this.date = dossier.date; @@ -54,6 +56,9 @@ export class Dossier implements IDossier, IListable { this.watermarkEnabled = dossier.watermarkEnabled; this.hasReviewers = !!this.memberIds && this.memberIds.length > 1; + this._stats$ = new BehaviorSubject(stats); + this.stats$ = this._stats$.asObservable(); + let hintsOnly = false; let hasRedactions = false; let hasSuggestions = false; @@ -72,7 +77,6 @@ export class Dossier implements IDossier, IListable { this.hasRedactions = hasRedactions; this.hasSuggestions = hasSuggestions; this.totalNumberOfPages = totalNumberOfPages; - this.hasPendingOrProcessing = hasPendingOrProcessing; this.hasNone = !this.hasSuggestions && !this.hasRedactions && !this.hintsOnly; } @@ -88,6 +92,10 @@ export class Dossier implements IDossier, IListable { return this.dossierName; } + get stats(): DossierStats { + return this._stats$.getValue(); + } + hasStatus(status: string): boolean { return !!this.files.find(f => f.status === status); }