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: [ dossierTemplateId: [
{ {
value: this.dossier.dossierTemplateId, value: this.dossier.dossierTemplateId,
disabled: this.dossier.hasFiles, disabled: this.dossier.stats.hasFiles,
}, },
Validators.required, Validators.required,
], ],

View File

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

View File

@ -38,33 +38,35 @@
></redaction-team-members> ></redaction-team-members>
</div> </div>
<div *ngIf="dossier.hasFiles" class="mt-24"> <ng-container *ngIf="dossier.stats$ | async as stats">
<redaction-simple-doughnut-chart <div *ngIf="stats.hasFiles" class="mt-24">
[config]="calculateChartConfig(dossier)" <redaction-simple-doughnut-chart
[radius]="63" [config]="calculateChartConfig(dossier)"
[strokeWidth]="15" [radius]="63"
[subtitle]="'dossier-overview.dossier-details.charts.documents-in-dossier' | translate" [strokeWidth]="15"
direction="row" [subtitle]="'dossier-overview.dossier-details.charts.documents-in-dossier' | translate"
></redaction-simple-doughnut-chart> direction="row"
</div> ></redaction-simple-doughnut-chart>
<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>
</div> </div>
</div>
<div [class.mt-24]="!dossier.hasFiles" class="pb-32"> <div *ngIf="stats.hasFiles && needsWorkFilters$ | async as filters" class="mt-24 legend pb-32">
<redaction-dossier-details-stats <div
(openDossierDictionaryDialog)="openDossierDictionaryDialog.emit()" (click)="filterService.toggleFilter('needsWorkFilters', filter.id)"
[dossierAttributes]="dossierAttributes" *ngFor="let filter of filters"
[dossier]="dossier" [class.active]="filter.checked"
></redaction-dossier-details-stats> >
</div> <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 *ngIf="dossier.description as description" class="pb-32">
<div class="heading" translate="dossier-overview.dossier-details.description"></div> <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.calculateData();
this.addSubscription = timer(0, 20 * 1000).subscribe(async () => { this.addSubscription = timer(0, 20 * 1000).subscribe(async () => {
await this._appStateService.reloadActiveDossierFilesIfNecessary(); await this._appStateService.reloadActiveDossierFiles();
this.calculateData(); this.calculateData();
}); });

View File

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

View File

@ -84,10 +84,6 @@ export class DossiersListingScreenComponent
await this._appStateService.loadAllDossiers(); await this._appStateService.loadAllDossiers();
this.calculateData(); this.calculateData();
}); });
this.addSubscription = this._appStateService.fileChanged$.subscribe(() => {
this.calculateData();
});
} }
ngAfterViewInit(): void { 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 { 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 { Dossier, File, IDossier, IDossierRequest } from '@red/domain';
import { catchError, map, tap } from 'rxjs/operators'; import { catchError, filter, map, switchMap, tap } from 'rxjs/operators';
import { BehaviorSubject, Observable, of } from 'rxjs'; import { BehaviorSubject, combineLatest, Observable, of, throwError } from 'rxjs';
import { ActivationEnd, Router } from '@angular/router'; import { ActivationEnd, Router } from '@angular/router';
import { DictionaryService } from '@shared/services/dictionary.service'; import { DictionaryService } from '@shared/services/dictionary.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { currentComponentRoute } from '@utils/functions'; import { currentComponentRoute } from '@utils/functions';
import { HttpErrorResponse } from '@angular/common/http';
import { DossierStatsService } from '@services/entity-services/dossier-stats.service';
export interface IDossiersStats { export interface IDossiersStats {
totalPeople: number; totalPeople: number;
@ -20,15 +22,16 @@ const GENERIC_MGS = _('add-dossier-dialog.errors.generic');
providedIn: 'root', providedIn: 'root',
}) })
export class DossiersService extends EntitiesService<Dossier, IDossier> { 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>; readonly activeDossier$: Observable<Dossier | undefined>;
private readonly _activeDossier$ = new BehaviorSubject<Dossier | undefined>(undefined); private readonly _activeDossier$ = new BehaviorSubject<Dossier | undefined>(undefined);
constructor( constructor(
protected readonly _injector: Injector,
private readonly _router: Router, private readonly _router: Router,
private readonly _dictionaryService: DictionaryService,
private readonly _toaster: Toaster, private readonly _toaster: Toaster,
protected readonly _injector: Injector,
private readonly _dictionaryService: DictionaryService,
private readonly _dossierStatsService: DossierStatsService,
) { ) {
super(_injector, Dossier, 'dossier'); super(_injector, Dossier, 'dossier');
this.activeDossier$ = this._activeDossier$.asObservable(); this.activeDossier$ = this._activeDossier$.asObservable();
@ -97,13 +100,18 @@ export class DossiersService extends EntitiesService<Dossier, IDossier> {
@Validate() @Validate()
createOrUpdate(@RequiredParam() dossier: IDossierRequest): Observable<Dossier | undefined> { createOrUpdate(@RequiredParam() dossier: IDossierRequest): Observable<Dossier | undefined> {
return this._post(dossier).pipe( const showToast = (error: HttpErrorResponse) => {
map(updatedDossier => new Dossier(updatedDossier, this.find(updatedDossier.dossierId)?.files ?? [])), 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)), tap(newDossier => this.replace(newDossier)),
catchError(error => { catchError(showToast),
this._toaster.error(error.status === 409 ? DOSSIER_EXISTS_MSG : GENERIC_MGS);
return of(undefined);
}),
); );
} }
@ -139,7 +147,7 @@ export class DossiersService extends EntitiesService<Dossier, IDossier> {
entities.forEach(dossier => { entities.forEach(dossier => {
dossier.memberIds?.forEach(m => totalPeople.add(m)); dossier.memberIds?.forEach(m => totalPeople.add(m));
totalAnalyzedPages += dossier.totalNumberOfPages; totalAnalyzedPages += dossier.stats.numberOfPages;
}); });
return { return {
@ -147,4 +155,13 @@ export class DossiersService extends EntitiesService<Dossier, IDossier> {
totalAnalyzedPages, 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 { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { FileAttributesService } from '@services/entity-services/file-attributes.service'; import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { ReanalysisService } from '@services/reanalysis.service'; import { ReanalysisService } from '@services/reanalysis.service';
import { DossierStatsService } from '@services/entity-services/dossier-stats.service';
export interface AppState { export interface AppState {
activeFileId?: string; activeFileId?: string;
@ -35,6 +36,7 @@ export class AppStateService {
private readonly _reanalysisService: ReanalysisService, private readonly _reanalysisService: ReanalysisService,
private readonly _dictionaryService: DictionaryService, private readonly _dictionaryService: DictionaryService,
private readonly _dossierTemplatesService: DossierTemplatesService, private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dossierStatsService: DossierStatsService,
private readonly _fileAttributesService: FileAttributesService, private readonly _fileAttributesService: FileAttributesService,
private readonly _userPreferenceService: UserPreferenceService, private readonly _userPreferenceService: UserPreferenceService,
) { ) {
@ -90,10 +92,6 @@ export class AppStateService {
return this._appState.activeFileId; return this._appState.activeFileId;
} }
async reloadActiveDossierFilesIfNecessary() {
await this.reloadActiveDossierFiles();
}
getDictionaryColor(type?: string, dossierTemplateId = this._dossiersService.activeDossier?.dossierTemplateId) { getDictionaryColor(type?: string, dossierTemplateId = this._dossiersService.activeDossier?.dossierTemplateId) {
if (!dossierTemplateId) { if (!dossierTemplateId) {
dossierTemplateId = this.dossierTemplates[0]?.dossierTemplateId; dossierTemplateId = this.dossierTemplates[0]?.dossierTemplateId;
@ -128,10 +126,14 @@ export class AppStateService {
return; return;
} }
const dossierIds = dossiers.map(dossier => dossier.dossierId);
const dossierStats = await this._dossierStatsService.getFor(dossierIds).toPromise();
const mappedDossiers$ = dossiers.map(async p => { const mappedDossiers$ = dossiers.map(async p => {
const oldDossier = this._dossiersService.find(p.dossierId); const oldDossier = this._dossiersService.find(p.dossierId);
const type = oldDossier?.type ?? (await this._getDictionaryFor(p)); 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 mappedDossiers = await Promise.all(mappedDossiers$);
const fileData = await this._filesService.getFor(mappedDossiers.map(p => p.id)).toPromise(); 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); const files = activeDossier.files.filter(file => file.fileId !== activeFile.fileId);
files.push(activeFile); 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); this._dossiersService.replace(newDossier);
if (activeFile.lastProcessed !== oldProcessedDate) { 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); this._dossiersService.replace(newDossier);
return newFiles; return newFiles;

View File

@ -18,3 +18,4 @@ export * from './lib/reports';
export * from './lib/configuration'; export * from './lib/configuration';
export * from './lib/signature'; export * from './lib/signature';
export * from './lib/legal-basis'; 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 { DossierStatus } from './types';
import { DownloadFileType } from '../shared'; import { DownloadFileType } from '../shared';
import { IDictionary } from '../dictionaries'; import { IDictionary } from '../dictionaries';
import { BehaviorSubject, Observable } from 'rxjs';
import { DossierStats } from '../dossier-stats';
export class Dossier implements IDossier, IListable { export class Dossier implements IDossier, IListable {
readonly dossierId: string; readonly dossierId: string;
@ -25,17 +27,17 @@ export class Dossier implements IDossier, IListable {
readonly hasReviewers: boolean; readonly hasReviewers: boolean;
readonly reanalysisRequired = this.files.some(file => file.analysisRequired); readonly reanalysisRequired = this.files.some(file => file.analysisRequired);
readonly hasFiles = this.files.length > 0;
readonly filesLength = this.files.length;
readonly totalNumberOfPages: number; readonly totalNumberOfPages: number;
readonly hintsOnly: boolean; readonly hintsOnly: boolean;
readonly hasRedactions: boolean; readonly hasRedactions: boolean;
readonly hasSuggestions: boolean; readonly hasSuggestions: boolean;
readonly hasNone: 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.dossierId = dossier.dossierId;
this.approverIds = dossier.approverIds; this.approverIds = dossier.approverIds;
this.date = dossier.date; this.date = dossier.date;
@ -54,6 +56,9 @@ export class Dossier implements IDossier, IListable {
this.watermarkEnabled = dossier.watermarkEnabled; this.watermarkEnabled = dossier.watermarkEnabled;
this.hasReviewers = !!this.memberIds && this.memberIds.length > 1; this.hasReviewers = !!this.memberIds && this.memberIds.length > 1;
this._stats$ = new BehaviorSubject<DossierStats>(stats);
this.stats$ = this._stats$.asObservable();
let hintsOnly = false; let hintsOnly = false;
let hasRedactions = false; let hasRedactions = false;
let hasSuggestions = false; let hasSuggestions = false;
@ -72,7 +77,6 @@ export class Dossier implements IDossier, IListable {
this.hasRedactions = hasRedactions; this.hasRedactions = hasRedactions;
this.hasSuggestions = hasSuggestions; this.hasSuggestions = hasSuggestions;
this.totalNumberOfPages = totalNumberOfPages; this.totalNumberOfPages = totalNumberOfPages;
this.hasPendingOrProcessing = hasPendingOrProcessing;
this.hasNone = !this.hasSuggestions && !this.hasRedactions && !this.hintsOnly; this.hasNone = !this.hasSuggestions && !this.hasRedactions && !this.hintsOnly;
} }
@ -88,6 +92,10 @@ export class Dossier implements IDossier, IListable {
return this.dossierName; return this.dossierName;
} }
get stats(): DossierStats {
return this._stats$.getValue();
}
hasStatus(status: string): boolean { hasStatus(status: string): boolean {
return !!this.files.find(f => f.status === status); return !!this.files.find(f => f.status === status);
} }