add dossier stats

This commit is contained in:
Dan Percic 2021-11-12 11:42:19 +02:00
parent 41bc7a0055
commit 1bbf76f1b7
15 changed files with 208 additions and 71 deletions

View File

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

View File

@ -1,17 +1,19 @@
<div>
<mat-icon svgIcon="iqser:document"></mat-icon>
<span>{{ 'dossier-overview.dossier-details.stats.documents' | translate: { count: dossier.files.length } }}</span>
</div>
<ng-container *ngIf="dossier.stats$ | async as stats">
<div>
<mat-icon svgIcon="iqser:document"></mat-icon>
<span>{{ 'dossier-overview.dossier-details.stats.documents' | translate: { count: stats.numberOfFiles } }}</span>
</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
<span>{{ 'dossier-overview.dossier-details.stats.people' | translate: { count: dossier.memberIds.length } }}</span>
</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
<span>{{ 'dossier-overview.dossier-details.stats.people' | translate: { count: dossier.memberIds.length } }}</span>
</div>
<div>
<mat-icon svgIcon="iqser:pages"></mat-icon>
<span>{{ 'dossier-overview.dossier-details.stats.analysed-pages' | translate: { count: dossier.totalNumberOfPages | number } }}</span>
</div>
<div>
<mat-icon svgIcon="iqser:pages"></mat-icon>
<span>{{ 'dossier-overview.dossier-details.stats.analysed-pages' | translate: { count: stats.numberOfPages | number } }}</span>
</div>
</ng-container>
<div *ngIf="dossier.date | date: 'd MMM. yyyy' as date">
<mat-icon svgIcon="red:calendar"></mat-icon>

View File

@ -38,33 +38,35 @@
></redaction-team-members>
</div>
<div *ngIf="dossier.hasFiles" class="mt-24">
<redaction-simple-doughnut-chart
[config]="calculateChartConfig(dossier)"
[radius]="63"
[strokeWidth]="15"
[subtitle]="'dossier-overview.dossier-details.charts.documents-in-dossier' | translate"
direction="row"
></redaction-simple-doughnut-chart>
</div>
<div *ngIf="dossier.hasFiles && needsWorkFilters$ | async as filters" class="mt-24 legend pb-32">
<div
(click)="filterService.toggleFilter('needsWorkFilters', filter.id)"
*ngFor="let filter of filters"
[class.active]="filter.checked"
>
<redaction-type-filter [filter]="filter"></redaction-type-filter>
<ng-container *ngIf="dossier.stats$ | async as stats">
<div *ngIf="stats.hasFiles" class="mt-24">
<redaction-simple-doughnut-chart
[config]="calculateChartConfig(dossier)"
[radius]="63"
[strokeWidth]="15"
[subtitle]="'dossier-overview.dossier-details.charts.documents-in-dossier' | translate"
direction="row"
></redaction-simple-doughnut-chart>
</div>
</div>
<div [class.mt-24]="!dossier.hasFiles" class="pb-32">
<redaction-dossier-details-stats
(openDossierDictionaryDialog)="openDossierDictionaryDialog.emit()"
[dossierAttributes]="dossierAttributes"
[dossier]="dossier"
></redaction-dossier-details-stats>
</div>
<div *ngIf="stats.hasFiles && needsWorkFilters$ | async as filters" class="mt-24 legend pb-32">
<div
(click)="filterService.toggleFilter('needsWorkFilters', filter.id)"
*ngFor="let filter of filters"
[class.active]="filter.checked"
>
<redaction-type-filter [filter]="filter"></redaction-type-filter>
</div>
</div>
<div [class.mt-24]="!stats.hasFiles" class="pb-32">
<redaction-dossier-details-stats
(openDossierDictionaryDialog)="openDossierDictionaryDialog.emit()"
[dossierAttributes]="dossierAttributes"
[dossier]="dossier"
></redaction-dossier-details-stats>
</div>
</ng-container>
<div *ngIf="dossier.description as description" class="pb-32">
<div class="heading" translate="dossier-overview.dossier-details.description"></div>

View File

@ -156,7 +156,7 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
this.calculateData();
this.addSubscription = timer(0, 20 * 1000).subscribe(async () => {
await this._appStateService.reloadActiveDossierFilesIfNecessary();
await this._appStateService.reloadActiveDossierFiles();
this.calculateData();
});

View File

@ -1,29 +1,35 @@
<div [matTooltip]="dossier.dossierName" class="table-item-title heading mb-6" matTooltipPosition="above">
{{ dossier.dossierName }}
</div>
<div class="small-label stats-subtitle mb-6">
<div>
<mat-icon svgIcon="red:template"></mat-icon>
{{ getDossierTemplateNameFor(dossier.dossierTemplateId) }}
</div>
</div>
<div class="small-label stats-subtitle">
<div *ngIf="dossier.stats$ | async as stats" class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="iqser:document"></mat-icon>
{{ dossier.filesLength }}
{{ stats.numberOfFiles }}
</div>
<div>
<mat-icon svgIcon="iqser:pages"></mat-icon>
{{ dossier.totalNumberOfPages }}
{{ stats.numberOfPages }}
</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
{{ dossier.memberIds.length }}
</div>
<div>
<mat-icon svgIcon="red:calendar"></mat-icon>
{{ dossier.date | date: 'mediumDate' }}
</div>
<div *ngIf="dossier.dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
{{ dossier.dueDate | date: 'mediumDate' }}

View File

@ -84,10 +84,6 @@ export class DossiersListingScreenComponent
await this._appStateService.loadAllDossiers();
this.calculateData();
});
this.addSubscription = this._appStateService.fileChanged$.subscribe(() => {
this.calculateData();
});
}
ngAfterViewInit(): void {

View File

@ -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<IDossierStats> {
constructor(protected readonly _injector: Injector) {
super(_injector, 'dossier-stats');
}
@Validate()
getFor(@RequiredParam() dossierIds: string[]): Observable<DossierStats[]> {
return this._post<IDossierStats[]>(dossierIds).pipe(mapEach(entity => new DossierStats(entity)));
}
// @Validate()
// loadFor(@RequiredParam() dossierId: string): Observable<DossierStats> {
// return this._getOne<IDossierStats>([dossierId]).pipe(
// map((entity: IDossierStats) => new DossierStats(entity)),
// tap((entity: DossierStats) => this.replace(entity)),
// );
// }
}

View File

@ -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<Dossier, IDossier> {
readonly stats$ = this.all$.pipe(map(entities => this._computeStats(entities)));
readonly stats$ = this.all$.pipe(switchMap(entities => this._generalStats$(entities)));
readonly activeDossier$: Observable<Dossier | undefined>;
private readonly _activeDossier$ = new BehaviorSubject<Dossier | undefined>(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<Dossier, IDossier> {
@Validate()
createOrUpdate(@RequiredParam() dossier: IDossierRequest): Observable<Dossier | undefined> {
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<Dossier, IDossier> {
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<Dossier, IDossier> {
totalAnalyzedPages,
};
}
private _generalStats$(entities: List<Dossier>): Observable<IDossiersStats> {
const stats$ = entities.map(entity => entity.stats$);
return combineLatest(stats$).pipe(
filter(stats => stats.every(s => !!s)),
map(() => this._computeStats(entities)),
shareLast(),
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
export * from './dossier-stats';
export * from './dossier-stats.model';
export * from './types';

View File

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

View File

@ -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<File> = [], public type?: IDictionary) {
readonly stats$: Observable<DossierStats>;
private readonly _stats$: BehaviorSubject<DossierStats>;
constructor(dossier: IDossier, stats: DossierStats, readonly files: List<File> = [], 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<DossierStats>(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);
}