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