Pull request #363: RED-3796

Merge in RED/ui from RED-3796 to master

* commit '19f6b057e748b5aacbd5722cb8df340e72cf1c2b':
  RED-3796: common-ui
  RED-3796: Redirect to last opened template
  RED-3796: Fixed provider, filter dossiers, initialize add dossier
  RED-3796: Working downloads & search
  RED-3796: Dossiers cache for notifications
  RED-3796: Empty dossier template checks
  RED-3796: Dossier template stats
  RED-3796: Use dossier template stats endpoint
  RED-3796: Removed routerPath
  RED-3796: Navigation WIP (no files)
  RED-3796: Dashboard #2 (empty dossier template)
  RED-3796: Dashboard #1
This commit is contained in:
Adina Teudan 2022-05-10 16:07:51 +02:00
commit a73feab16d
117 changed files with 1498 additions and 968 deletions

View File

@ -6,83 +6,107 @@ 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 { DossierTemplatesGuard } from '@guards/dossier-templates.guard';
import { DossiersGuard } from '@guards/dossiers.guard';
import { ACTIVE_DOSSIERS_SERVICE, ARCHIVED_DOSSIERS_SERVICE } from './tokens';
import { FeaturesGuard } from '@guards/features-guard.service';
import { DOSSIERS_ARCHIVE } from '@utils/constants';
import { ARCHIVE_ROUTE, DOSSIER_TEMPLATE_ID, DOSSIERS_ARCHIVE, DOSSIERS_ROUTE } from '@utils/constants';
import { DossierTemplatesGuard } from '@guards/dossier-templates.guard';
import { DossierTemplateExistsGuard } from '@guards/dossier-template-exists.guard';
import { DashboardGuard } from '@guards/dashboard-guard.service';
const routes: Routes = [
{
path: '',
redirectTo: 'main/dossiers',
redirectTo: 'main',
pathMatch: 'full',
},
{
path: 'main',
component: BaseScreenComponent,
children: [
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full',
},
{
path: 'account',
loadChildren: () => import('./modules/account/account.module').then(m => m.AccountModule),
},
{
path: 'admin',
loadChildren: () => import('./modules/admin/admin.module').then(m => m.AdminModule),
},
{
path: 'dashboard',
loadChildren: () => import('./modules/dashboard/dashboard.module').then(m => m.DashboardModule),
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, DossierTemplatesGuard, DashboardGuard],
requiredRoles: ['RED_USER', 'RED_MANAGER'],
},
},
{
path: 'downloads',
component: DownloadsListScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard],
},
},
{
path: 'search',
loadChildren: () => import('./modules/search/search.module').then(m => m.SearchModule),
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard],
requiredRoles: ['RED_USER', 'RED_MANAGER'],
},
},
{
path: `:${DOSSIER_TEMPLATE_ID}`,
children: [
{
path: `${DOSSIERS_ROUTE}`,
loadChildren: () => import('./modules/dossier/dossiers.module').then(m => m.DossiersModule),
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [DossiersGuard],
dossiersService: ACTIVE_DOSSIERS_SERVICE,
},
},
{
path: `${ARCHIVE_ROUTE}`,
loadChildren: () => import('./modules/archive/archive.module').then(m => m.ArchiveModule),
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [FeaturesGuard, DossiersGuard],
dossiersService: ARCHIVED_DOSSIERS_SERVICE,
features: [DOSSIERS_ARCHIVE],
},
},
{
path: '**',
redirectTo: `${DOSSIERS_ROUTE}`,
pathMatch: 'full',
},
],
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, DossierTemplatesGuard, DashboardGuard, DossierTemplateExistsGuard],
requiredRoles: ['RED_USER', 'RED_MANAGER'],
},
},
],
},
{
path: 'auth-error',
component: AuthErrorComponent,
canActivate: [AuthGuard],
},
{
path: 'main/account',
component: BaseScreenComponent,
loadChildren: () => import('./modules/account/account.module').then(m => m.AccountModule),
},
{
path: 'main/admin',
component: BaseScreenComponent,
loadChildren: () => import('./modules/admin/admin.module').then(m => m.AdminModule),
},
{
path: 'main/dossiers',
component: BaseScreenComponent,
loadChildren: () => import('./modules/dossier/dossiers.module').then(m => m.DossiersModule),
canActivate: [CompositeRouteGuard],
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: [FeaturesGuard, AuthGuard, RedRoleGuard, DossierTemplatesGuard, DossiersGuard],
requiredRoles: ['RED_USER', 'RED_MANAGER'],
dossiersService: ARCHIVED_DOSSIERS_SERVICE,
features: [DOSSIERS_ARCHIVE],
},
},
{
path: 'main/downloads',
component: BaseScreenComponent,
children: [
{
path: '',
component: DownloadsListScreenComponent,
},
],
canActivate: [CompositeRouteGuard],
data: {
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',
redirectTo: 'main',
pathMatch: 'full',
},
];

View File

@ -163,6 +163,7 @@ const components = [AppComponent, AuthErrorComponent, NotificationsComponent, Sp
multi: true,
useFactory: configurationInitializer,
deps: [
BASE_HREF,
KeycloakService,
Title,
ConfigService,

View File

@ -7,12 +7,12 @@
<redaction-breadcrumbs></redaction-breadcrumbs>
</div>
<div class="logo">
<a [routerLink]="['/']" class="logo">
<iqser-hidden-action (action)="userPreferenceService.toggleDevFeatures()">
<iqser-logo icon="red:logo"></iqser-logo>
</iqser-hidden-action>
<div class="app-name">{{ titleService.getTitle() }}</div>
</div>
</a>
<div class="actions flex-2">
<div class="buttons">

View File

@ -7,10 +7,10 @@ import { TranslateService } from '@ngx-translate/core';
import { SpotlightSearchAction } from '@components/spotlight-search/spotlight-search-action';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { filter, map, startWith } from 'rxjs/operators';
import { HelpModeService, shareDistinctLast } from '@iqser/common-ui';
import { shareDistinctLast } from '@iqser/common-ui';
import { BreadcrumbsService } from '@services/breadcrumbs.service';
import { FeaturesService } from '@services/features.service';
import { DOSSIERS_ARCHIVE } from '@utils/constants';
import { ARCHIVE_ROUTE, DOSSIERS_ARCHIVE, DOSSIERS_ROUTE } from '@utils/constants';
interface MenuItem {
readonly id: string;
@ -91,7 +91,6 @@ export class BaseScreenComponent {
readonly userPreferenceService: UserPreferenceService,
readonly titleService: Title,
readonly breadcrumbsService: BreadcrumbsService,
readonly helpModeService: HelpModeService,
) {}
private get _hideSearchThisDossier() {
@ -100,7 +99,7 @@ export class BaseScreenComponent {
return true;
}
const isDossierOverview = (routerLink.includes('dossiers') || routerLink.includes('archive')) && routerLink.length === 3;
const isDossierOverview = (routerLink.includes(DOSSIERS_ROUTE) || routerLink.includes(ARCHIVE_ROUTE)) && routerLink.length === 3;
return !isDossierOverview;
}

View File

@ -5,6 +5,7 @@ import { Notification } from '@red/domain';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { firstValueFrom, Observable } from 'rxjs';
import { shareLast } from '@iqser/common-ui';
import { DossiersCacheService } from '@services/dossiers/dossiers-cache.service';
interface NotificationsGroup {
date: string;
@ -21,7 +22,11 @@ export class NotificationsComponent {
readonly hasUnreadNotifications$: Observable<boolean>;
readonly groupedNotifications$: Observable<NotificationsGroup[]>;
constructor(private readonly _notificationsService: NotificationsService, private readonly _datePipe: DatePipe) {
constructor(
private readonly _notificationsService: NotificationsService,
private readonly _datePipe: DatePipe,
private readonly _dossiersCacheService: DossiersCacheService,
) {
this.groupedNotifications$ = this._notificationsService.all$.pipe(map(notifications => this._groupNotifications(notifications)));
this.hasUnreadNotifications$ = this._hasUnreadNotifications$;
}

View File

@ -0,0 +1,14 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate } from '@angular/router';
import { firstValueFrom } from 'rxjs';
import { DashboardStatsService } from '@services/dossier-templates/dashboard-stats.service';
@Injectable({ providedIn: 'root' })
export class DashboardGuard implements CanActivate {
constructor(private readonly _dashboardStatsService: DashboardStatsService) {}
async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
await firstValueFrom(this._dashboardStatsService.loadAll());
return true;
}
}

View File

@ -3,7 +3,7 @@ import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { FilesService } from '@services/entity-services/files.service';
import { firstValueFrom } from 'rxjs';
import { DOSSIER_ID } from '@utils/constants';
import { DOSSIER_ID, DOSSIER_TEMPLATE_ID } from '@utils/constants';
import { DossiersService } from '@services/dossiers/dossiers.service';
@Injectable({ providedIn: 'root' })
@ -17,16 +17,17 @@ export class DossierFilesGuard implements CanActivate {
async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
const dossierId = route.paramMap.get(DOSSIER_ID);
const dossierTemplateId = route.paramMap.get(DOSSIER_TEMPLATE_ID);
const token: ProviderToken<DossiersService> = route.data.dossiersService;
const dossiersService: DossiersService = this._injector.get<DossiersService>(token);
if (!dossiersService.has(dossierId)) {
await this._router.navigate(['/main', dossiersService.routerPath]);
await this._router.navigate(['/main', dossierTemplateId]);
return false;
}
if (!this._filesMapService.has(dossierId)) {
await firstValueFrom(this._filesService.loadAll(dossierId, dossiersService.routerPath));
await firstValueFrom(this._filesService.loadAll(dossierId));
}
return true;
}

View File

@ -1,17 +1,19 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DOSSIER_TEMPLATE_ID } from '@utils/constants';
import { DashboardStatsService } from '@services/dossier-templates/dashboard-stats.service';
@Injectable({ providedIn: 'root' })
export class DossierTemplateExistsGuard implements CanActivate {
constructor(private readonly _dossierTemplatesService: DossierTemplatesService, private readonly _router: Router) {}
constructor(private readonly _dashboardStatsService: DashboardStatsService, private readonly _router: Router) {}
async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
const dossierTemplateId: string = route.paramMap.get(DOSSIER_TEMPLATE_ID);
if (!this._dossierTemplatesService.find(dossierTemplateId)) {
await this._router.navigate(['main', 'admin', 'dossier-templates']);
const dossiersListView = !route.pathFromRoot.find(r => r.routeConfig?.path === 'admin');
const dossierTemplateStats = this._dashboardStatsService.find(dossierTemplateId);
if (!dossierTemplateStats || (dossiersListView && dossierTemplateStats.isEmpty)) {
const routerPath = dossiersListView ? [''] : ['main', 'admin', 'dossier-templates'];
await this._router.navigate(routerPath);
return false;
}

View File

@ -1,15 +1,11 @@
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { firstValueFrom } from 'rxjs';
import { DictionaryService } from '@services/entity-services/dictionary.service';
@Injectable({ providedIn: 'root' })
export class DossierTemplatesGuard implements CanActivate {
constructor(
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dictionaryService: DictionaryService,
) {}
constructor(private readonly _dossierTemplatesService: DossierTemplatesService) {}
async canActivate(): Promise<boolean> {
await firstValueFrom(this._dossierTemplatesService.loadAll());

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { DOSSIER_TEMPLATE_ID, ENTITY_TYPE } from '@utils/constants';
import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
@Injectable({ providedIn: 'root' })
export class EntityExistsGuard implements CanActivate {

View File

@ -1,3 +1,3 @@
:host {
display: flex;
flex-direction: row;
}

View File

@ -6,7 +6,7 @@ import { AdminDialogService } from '../services/admin-dialog.service';
import { DictionaryService } from '@services/entity-services/dictionary.service';
import { UserService } from '@services/user.service';
import { LoadingService } from '@iqser/common-ui';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service';
import { map } from 'rxjs/operators';
import { PermissionsService } from '@services/permissions.service';

View File

@ -1,5 +1,5 @@
import { Component, Input } from '@angular/core';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { Observable, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { ActivatedRoute } from '@angular/router';

View File

@ -23,7 +23,7 @@ export class AddEditDossierStateDialogComponent extends BaseDialogComponent {
private readonly _formBuilder: FormBuilder,
private readonly _loadingService: LoadingService,
private readonly _toaster: Toaster,
private readonly _dossierStateService: DossierStatesService,
private readonly _dossierStatesService: DossierStatesService,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddEditDossierStateDialogComponent>,
@Inject(MAT_DIALOG_DATA) readonly data: DialogData,
@ -45,7 +45,7 @@ export class AddEditDossierStateDialogComponent extends BaseDialogComponent {
};
this._loadingService.start();
try {
await firstValueFrom(this._dossierStateService.createOrUpdate(dossierState));
await firstValueFrom(this._dossierStatesService.createOrUpdate(dossierState));
this._toaster.success(_('add-edit-dossier-state.success'), { params: { type: this.type } });
this._dialogRef.close();
} catch (e) {}

View File

@ -3,7 +3,7 @@ import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/fo
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { applyIntervalConstraints } from '@utils/date-inputs-utils';
import { downloadTypesTranslations } from '../../../../translations/download-types-translations';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { BaseDialogComponent, LoadingService, Toaster } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { DossierTemplate, DownloadFileType, IDossierTemplate } from '@red/domain';

View File

@ -1,8 +1,8 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DossierTemplatesService } from '../../../../services/entity-services/dossier-templates.service';
import { DossierTemplate, IDossierTemplate } from '../../../../../../../../libs/red-domain/src';
import { LoadingService, Toaster } from '../../../../../../../../libs/common-ui/src';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { DossierTemplate } from '@red/domain';
import { LoadingService, Toaster } from '@iqser/common-ui';
import { firstValueFrom } from 'rxjs';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@ -11,8 +11,8 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
styleUrls: ['./clone-dossier-template-dialog.component.scss'],
})
export class CloneDossierTemplateDialogComponent {
private readonly _dossierTemplate: DossierTemplate;
nameOfClonedDossierTemplate: string;
private readonly _dossierTemplate: DossierTemplate;
constructor(
private readonly _toaster: Toaster,
@ -25,6 +25,17 @@ export class CloneDossierTemplateDialogComponent {
this.nameOfClonedDossierTemplate = this._getCloneName();
}
async save() {
this._loadingService.start();
try {
await firstValueFrom(this._dossierTemplatesService.clone(this.dossierTemplateId, this.nameOfClonedDossierTemplate));
this._dialogRef.close(true);
} catch (error: any) {
this._toaster.error(_('clone-dossier-template.error.generic'), { error });
}
this._loadingService.stop();
}
private _getCloneName(): string | null {
const templateName = this._dossierTemplate.name.trim();
@ -47,15 +58,4 @@ export class CloneDossierTemplateDialogComponent {
}
return `Clone of ${nameOfClonedTemplate}`;
}
async save() {
this._loadingService.start();
try {
await firstValueFrom(this._dossierTemplatesService.clone(this.dossierTemplateId, this.nameOfClonedDossierTemplate));
this._dialogRef.close(true);
} catch (error: any) {
this._toaster.error(_('clone-dossier-template.error.generic'), { error });
}
this._loadingService.stop();
}
}

View File

@ -29,7 +29,7 @@ export class ConfirmDeleteDossierStateDialogComponent {
private readonly _formBuilder: FormBuilder,
private readonly _loadingService: LoadingService,
private readonly _toaster: Toaster,
private readonly _dossierStateService: DossierStatesService,
private readonly _dossierStatesService: DossierStatesService,
private readonly _dialogRef: MatDialogRef<ConfirmDeleteDossierStateDialogComponent>,
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _archivedDossiersService: ArchivedDossiersService,
@ -55,7 +55,7 @@ export class ConfirmDeleteDossierStateDialogComponent {
async save(): Promise<void> {
this._loadingService.start();
await firstValueFrom(this._dossierStateService.deleteState(this.data.toBeDeletedState, this.replaceDossierStatusId));
await firstValueFrom(this._dossierStatesService.deleteState(this.data.toBeDeletedState, this.replaceDossierStatusId));
await firstValueFrom(
forkJoin([this._activeDossiersService.loadAll().pipe(take(1)), this._archivedDossiersService.loadAll().pipe(take(1))]),
);

View File

@ -4,7 +4,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { FileAttributeEncodingTypes, IFileAttributesConfig } from '@red/domain';
import { fileAttributeEncodingTypesTranslations } from '../../translations/file-attribute-encoding-types-translations';
import { BaseDialogComponent, Toaster } from '@iqser/common-ui';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { firstValueFrom } from 'rxjs';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { FileAttributesService } from '@services/entity-services/file-attributes.service';

View File

@ -13,7 +13,7 @@ import { defaultColorsTranslations } from '../../translations/default-colors-tra
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { UserService } from '@services/user.service';
import { DictionaryService } from '@services/entity-services/dictionary.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { firstValueFrom } from 'rxjs';
import { ActivatedRoute } from '@angular/router';
import { DOSSIER_TEMPLATE_ID } from '@utils/constants';

View File

@ -15,7 +15,7 @@ import {
import { UserService } from '@services/user.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { RouterHistoryService } from '@services/router-history.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { firstValueFrom } from 'rxjs';
@Component({

View File

@ -28,7 +28,7 @@ import { HttpStatusCode } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { ReportTemplateService } from '../../../../services/report-template.service';
import { ActivatedRoute } from '@angular/router';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { DOSSIER_TEMPLATE_ID } from '@utils/constants';
@Component({

View File

@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { DossierTemplate, DossierTemplateStats } from '@red/domain';

View File

@ -3,7 +3,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Justification } from '@red/domain';
import { JustificationsService } from '@services/entity-services/justifications.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { BaseDialogComponent, LoadingService } from '@iqser/common-ui';
import { firstValueFrom } from 'rxjs';

View File

@ -10,7 +10,7 @@ import {
} from '@iqser/common-ui';
import { AddEditJustificationDialogComponent } from './add-edit-justification-dialog/add-edit-justification-dialog.component';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { JustificationsService } from '@services/entity-services/justifications.service';
import { Justification } from '@red/domain';
import { firstValueFrom } from 'rxjs';

View File

@ -10,7 +10,7 @@ import {
import { removeBraces } from '@utils/functions';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { AdminDialogService } from '../../../services/admin-dialog.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { ReportTemplateService } from '@services/report-template.service';
import { BehaviorSubject, firstValueFrom } from 'rxjs';
import { ActivatedRoute } from '@angular/router';

View File

@ -18,7 +18,7 @@ import { TrashItem } from '@red/domain';
import { TrashService } from '@services/entity-services/trash.service';
import { FilesService } from '@services/entity-services/files.service';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
@Component({
templateUrl: './trash-screen.component.html',

View File

@ -3,7 +3,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { AdminDialogService } from '../../../services/admin-dialog.service';
import { CircleButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { UserService } from '@services/user.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { firstValueFrom } from 'rxjs';
import { DictionaryService } from '@services/entity-services/dictionary.service';
import { DOSSIER_TEMPLATE_ID } from '@utils/constants';
@ -37,6 +37,7 @@ export class DossierTemplateActionsComponent implements OnInit {
openEditDossierTemplateDialog($event: MouseEvent) {
this._dialogService.openDialog('addEditDossierTemplate', $event, this.dossierTemplateId);
}
openCloneDossierTemplateDialog($event: MouseEvent) {
this._dialogService.openDialog('cloneDossierTemplate', $event, this.dossierTemplateId);
}

View File

@ -12,14 +12,14 @@ const routes: Routes = [
path: '',
pathMatch: 'full',
component: ArchivedDossiersScreenComponent,
data: { breadcrumbs: [BreadcrumbTypes.archive] },
data: { breadcrumbs: [BreadcrumbTypes.dossierTemplate] },
},
{
path: `:${DOSSIER_ID}`,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [DossierFilesGuard],
breadcrumbs: [BreadcrumbTypes.archive, BreadcrumbTypes.dossier],
breadcrumbs: [BreadcrumbTypes.dossierTemplate, BreadcrumbTypes.dossier],
dossiersService: ARCHIVED_DOSSIERS_SERVICE,
},
loadChildren: () => import('../dossier-overview/dossier-overview.module').then(m => m.DossierOverviewModule),
@ -29,7 +29,7 @@ const routes: Routes = [
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [DossierFilesGuard],
breadcrumbs: [BreadcrumbTypes.archive, BreadcrumbTypes.dossier, BreadcrumbTypes.file],
breadcrumbs: [BreadcrumbTypes.dossierTemplate, BreadcrumbTypes.dossier, BreadcrumbTypes.file],
dossiersService: ARCHIVED_DOSSIERS_SERVICE,
},
loadChildren: () => import('../file-preview/file-preview.module').then(m => m.FilePreviewModule),

View File

@ -22,7 +22,7 @@ export class TableItemComponent implements OnChanges {
ngOnChanges() {
if (this.dossier) {
this.#ngOnChanges$.next(this.dossier.dossierId);
this.#ngOnChanges$.next(this.dossier.id);
}
}
}

View File

@ -1,5 +1,9 @@
<section>
<iqser-page-header></iqser-page-header>
<iqser-page-header>
<ng-container slot="beforeFilters">
<redaction-dossiers-type-switch></redaction-dossiers-type-switch>
</ng-container>
</iqser-page-header>
<div class="overlay-shadow"></div>

View File

@ -1,32 +1,45 @@
import { ChangeDetectionStrategy, Component, forwardRef, Injector, OnInit } from '@angular/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { DefaultListingServicesTmp, EntitiesService, ListingComponent } from '@iqser/common-ui';
import { ArchivedDossiersService } from '@services/dossiers/archived-dossiers.service';
import { DefaultListingServices, ListingComponent } from '@iqser/common-ui';
import { Dossier } from '@red/domain';
import { ConfigService } from '../../services/config.service';
import { tap } from 'rxjs/operators';
import { ArchivedDossiersService } from '@services/dossiers/archived-dossiers.service';
import { DOSSIER_TEMPLATE_ID } from '@utils/constants';
import { Router } from '@angular/router';
@Component({
selector: 'redaction-archived-dossiers-screen',
templateUrl: './archived-dossiers-screen.component.html',
styleUrls: ['./archived-dossiers-screen.component.scss'],
providers: [
...DefaultListingServicesTmp,
{ provide: EntitiesService, useExisting: ArchivedDossiersService },
{ provide: ListingComponent, useExisting: forwardRef(() => ArchivedDossiersScreenComponent) },
],
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => ArchivedDossiersScreenComponent) }],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ArchivedDossiersScreenComponent extends ListingComponent<Dossier> implements OnInit {
readonly tableColumnConfigs = this._configService.tableConfig;
readonly tableHeaderLabel = _('archived-dossiers-listing.table-header.title');
private readonly _dossierTemplateId: string;
constructor(protected readonly _injector: Injector, private readonly _configService: ConfigService) {
constructor(
protected readonly _injector: Injector,
private readonly _configService: ConfigService,
private readonly _archivedDossiersService: ArchivedDossiersService,
private readonly _router: Router,
) {
super(_injector);
this._dossierTemplateId = this._router.routerState.snapshot.root.firstChild.firstChild.paramMap.get(DOSSIER_TEMPLATE_ID);
this._router.routeReuseStrategy.shouldReuseRoute = () => false;
console.log(this._dossierTemplateId);
}
ngOnInit(): void {
this.addSubscription = this.entitiesService.all$.pipe(tap(() => this._computeAllFilters())).subscribe();
this.addSubscription = this._archivedDossiersService.all$
.pipe(
tap(dossiers => console.log(dossiers.map(d => d.dossierTemplateId))),
tap(dossiers => this.entitiesService.setEntities(dossiers.filter(d => d.dossierTemplateId === this._dossierTemplateId))),
)
.subscribe();
}
private _computeAllFilters() {

View File

@ -5,7 +5,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { dossierMemberChecker, dossierTemplateChecker } from '@utils/index';
import { UserService } from '@services/user.service';
import { TranslateService } from '@ngx-translate/core';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
@Injectable()
export class ConfigService {

View File

@ -1,5 +1,5 @@
import { Inject, Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, Router } from '@angular/router';
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
import { UserService } from '@services/user.service';
import { ConfigService } from '@services/config.service';
@ -19,12 +19,12 @@ export class AuthGuard extends KeycloakAuthGuard {
super(_router, _keycloak);
}
async isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
async isAccessAllowed(route: ActivatedRouteSnapshot): Promise<boolean> {
if (!this.authenticated) {
const kcIdpHint = route.queryParamMap.get('kc_idp_hint');
await this._keycloak.login({
idpHint: kcIdpHint ?? this._configService.values.OAUTH_IDP_HINT,
redirectUri: window.location.origin + this._baseHref + state.url,
redirectUri: window.location.href,
});
return false;
}

View File

@ -49,18 +49,16 @@ export class RedRoleGuard implements CanActivate {
return;
}
if (!this._userService.currentUser.isUser && state.url.startsWith('/main/dossiers')) {
this._router.navigate(['/main/admin']);
obs.next(false);
obs.complete();
return;
}
if (route.data.requiredRoles) {
if (this._userService.hasAnyRole(route.data.requiredRoles)) {
obs.next(true);
obs.complete();
} else {
this._router.navigate(['/main/dossiers']);
if (!this._userService.currentUser.isUser) {
this._router.navigate(['/main/admin']);
} else {
this._router.navigate(['/']);
}
obs.next(false);
obs.complete();
}

View File

@ -0,0 +1,77 @@
<a
*ngIf="stats as dossierTemplate"
[class.empty]="dossierTemplate.isEmpty"
[routerLink]="dossierTemplate.isEmpty ? null : ['..', dossierTemplate.dossierTemplateId]"
class="dialog"
>
<ng-container *ngIf="!dossierTemplate.isEmpty; else empty">
<div class="flex-2">
<div class="heading mb-6">{{ dossierTemplate.name }}</div>
<div class="stats-subtitle">
<div>
<mat-icon svgIcon="red:archive"></mat-icon>
<span
[innerHTML]="
'dossier-template-stats.archived-dossiers' | translate: { count: dossierTemplate.numberOfArchivedDossiers }
"
></span>
</div>
<div>
<mat-icon svgIcon="iqser:trash"></mat-icon>
<span
[innerHTML]="
'dossier-template-stats.deleted-dossiers' | translate: { count: dossierTemplate.numberOfDeletedDossiers }
"
></span>
</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
<span [innerHTML]="'dossier-template-stats.total-people' | translate: { count: dossierTemplate.numberOfPeople }"></span>
</div>
<div>
<mat-icon svgIcon="iqser:trash"></mat-icon>
<span
[innerHTML]="'dossier-template-stats.analyzed-pages' | translate: { count: dossierTemplate.numberOfPages }"
></span>
</div>
</div>
</div>
<div class="flex-3">
<redaction-simple-doughnut-chart
[config]="translateChartService.translateDossierStates(dossierTemplate.dossiersChartData, dossierTemplate.id)"
[radius]="63"
[strokeWidth]="15"
[subtitle]="'dossier-template-stats.active-dossiers' | translate: { count: dossierTemplate.numberOfActiveDossiers }"
direction="row"
totalType="sum"
></redaction-simple-doughnut-chart>
</div>
<div class="flex-3">
<redaction-simple-doughnut-chart
[config]="translateChartService.translateWorkflowStatus(dossierTemplate.documentsChartData)"
[radius]="63"
[strokeWidth]="15"
[subtitle]="'dossier-template-stats.total-documents' | translate"
direction="row"
totalType="sum"
></redaction-simple-doughnut-chart>
</div>
</ng-container>
<ng-template #empty>
<div class="text-muted">
<div class="heading mb-8">
{{ dossierTemplate.name }}
</div>
<div>
{{ 'dashboard.empty-template.description' | translate }}
</div>
</div>
<iqser-icon-button
(action)="newDossier()"
[label]="'dashboard.empty-template.new-dossier' | translate"
[type]="iconButtonTypes.primary"
icon="iqser:plus"
></iqser-icon-button>
</ng-template>
</a>

View File

@ -0,0 +1,53 @@
@use 'common-mixins';
.dialog {
@include common-mixins.clear-a;
flex-direction: row;
max-width: unset;
min-height: unset;
margin: 0 0 16px 0;
transition: background-color 0.2s;
&.empty {
justify-content: space-between;
align-items: center;
padding: 24px;
cursor: default;
}
&:not(.empty) {
&:hover {
background-color: var(--iqser-grey-2);
.heading {
text-decoration: underline;
}
}
> div {
padding: 24px;
display: flex;
flex-direction: column;
overflow: hidden;
&:not(:first-child) {
justify-content: center;
border-left: 1px solid var(--iqser-separator);
}
}
}
}
.stats-subtitle {
flex-direction: column;
> div {
margin-top: 10px;
}
mat-icon {
min-height: 14px;
min-width: 14px;
margin-right: 8px;
}
}

View File

@ -0,0 +1,28 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { DashboardStats } from '@red/domain';
import { IconButtonTypes } from '@iqser/common-ui';
import { DossiersDialogService } from '../../../dossier/shared/services/dossiers-dialog.service';
import { TranslateService } from '@ngx-translate/core';
import { TranslateChartService } from '@services/translate-chart.service';
@Component({
selector: 'redaction-template-stats [stats]',
templateUrl: './template-stats.component.html',
styleUrls: ['./template-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TemplateStatsComponent {
readonly iconButtonTypes = IconButtonTypes;
@Input() stats: DashboardStats;
constructor(
private readonly _dialogService: DossiersDialogService,
private readonly _translateService: TranslateService,
readonly translateChartService: TranslateChartService,
) {}
newDossier(): void {
this._dialogService.openDialog('addDossier', null, { dossierTemplateId: this.stats.dossierTemplateId });
}
}

View File

@ -0,0 +1,13 @@
<div class="overlay-shadow"></div>
<div class="container">
<div
[translateParams]="{ name: currentUser.firstName || currentUser.name }"
[translate]="'dashboard.greeting.title'"
class="heading-xl mb-8"
></div>
<div class="mb-32" translate="dashboard.greeting.subtitle"></div>
<redaction-template-stats *ngFor="let dossierTemplate of stats$ | async" [stats]="dossierTemplate"></redaction-template-stats>
</div>

View File

@ -0,0 +1,11 @@
:host {
align-items: center;
background-color: var(--iqser-grey-2);
.container {
padding: 32px;
width: 900px;
max-width: 100%;
box-sizing: border-box;
}
}

View File

@ -0,0 +1,25 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { UserService } from '@services/user.service';
import { DashboardStatsService } from '@services/dossier-templates/dashboard-stats.service';
import { UserPreferenceService } from '@services/user-preference.service';
@Component({
selector: 'redaction-dashboard-screen',
templateUrl: './dashboard-screen.component.html',
styleUrls: ['./dashboard-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DashboardScreenComponent implements OnInit {
readonly currentUser = this._userService.currentUser;
readonly stats$ = this._dashboardStatsService.all$;
constructor(
private readonly _userService: UserService,
private readonly _dashboardStatsService: DashboardStatsService,
private readonly _userPreferenceService: UserPreferenceService,
) {}
async ngOnInit(): Promise<void> {
await this._userPreferenceService.saveLastDossierTemplate(null);
}
}

View File

@ -0,0 +1,24 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DashboardScreenComponent } from './dashboard-screen/dashboard-screen.component';
import { RouterModule } from '@angular/router';
import { SharedModule } from '../shared/shared.module';
import { TemplateStatsComponent } from './components/template-stats/template-stats.component';
import { SharedDossiersModule } from '../dossier/shared/shared-dossiers.module';
import { BreadcrumbTypes } from '@red/domain';
const routes = [
{
path: '',
component: DashboardScreenComponent,
data: {
breadcrumbs: [BreadcrumbTypes.dashboard],
},
},
];
@NgModule({
declarations: [DashboardScreenComponent, TemplateStatsComponent],
imports: [RouterModule.forChild(routes), CommonModule, SharedModule, SharedDossiersModule],
})
export class DashboardModule {}

View File

@ -1,10 +1,10 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Dossier, DossierAttributeWithValue, DossierStats } from '@red/domain';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { FilesService } from '@services/entity-services/files.service';
import { firstValueFrom, Observable } from 'rxjs';
import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
import { DossiersDialogService } from '../../../dossier/services/dossiers-dialog.service';
import { DossiersDialogService } from '../../../dossier/shared/services/dossiers-dialog.service';
@Component({
selector: 'redaction-dossier-details-stats',
@ -28,14 +28,14 @@ export class DossierDetailsStatsComponent implements OnInit {
) {}
ngOnInit() {
this.dossierStats$ = this._dossierStatsService.watch$(this.dossier.dossierId);
this.dossierStats$ = this._dossierStatsService.watch$(this.dossier.id);
this.dossierTemplateName = this._dossierTemplatesService.find(this.dossier.dossierTemplateId)?.name || '-';
}
openEditDossierDialog(section: string): void {
const data = { dossierId: this.dossier.dossierId, section };
const data = { dossierId: this.dossier.id, section };
this._dialogService.openDialog('editDossier', null, data, async () => {
await firstValueFrom(this._filesService.loadAll(this.dossier.dossierId, this.dossier.routerPath));
await firstValueFrom(this._filesService.loadAll(this.dossier.id));
});
}
}

View File

@ -10,7 +10,7 @@ import { ActivatedRoute } from '@angular/router';
import { firstValueFrom, Observable } from 'rxjs';
import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
import { map, pluck, switchMap } from 'rxjs/operators';
import { DossiersDialogService } from '../../../dossier/services/dossiers-dialog.service';
import { DossiersDialogService } from '../../../dossier/shared/services/dossiers-dialog.service';
import { FilesService } from '@services/entity-services/files.service';
import { DOSSIER_ID } from '@utils/constants';
import { DossiersService } from '@services/dossiers/dossiers.service';
@ -80,7 +80,7 @@ export class DossierDetailsComponent {
key: status,
}));
documentsChartData.sort((a, b) => StatusSorter.byStatus(a.key, b.key));
return this.translateChartService.translateStatus(documentsChartData);
return this.translateChartService.translateLabels(documentsChartData);
}
#calculateStatusConfig(stats: DossierStats): ProgressBarConfigModel[] {

View File

@ -1,49 +1,51 @@
<iqser-page-header
(closeAction)="router.navigate(['/main', dossiersService.routerPath])"
(closeAction)="router.navigate([dossier.dossiersListRouterLink])"
[actionConfigs]="actionConfigs"
[helpModeKey]="'document'"
[showCloseButton]="true"
[viewModeSelection]="viewModeSelection"
>
<redaction-file-download-btn
[disabled]="listingService.areSomeSelected$ | async"
[dossier]="dossier"
[files]="entitiesService.all$ | async"
iqserHelpMode="edit_dossier_in_dossier"
tooltipPosition="below"
></redaction-file-download-btn>
<ng-container slot="right">
<redaction-file-download-btn
[disabled]="listingService.areSomeSelected$ | async"
[dossier]="dossier"
[files]="entitiesService.all$ | async"
iqserHelpMode="edit_dossier_in_dossier"
tooltipPosition="below"
></redaction-file-download-btn>
<iqser-circle-button
(action)="downloadDossierAsCSV()"
*ngIf="permissionsService.canDownloadCsvReport(dossier)"
[disabled]="listingService.areSomeSelected$ | async"
[tooltip]="'dossier-overview.header-actions.download-csv' | translate"
icon="iqser:csv"
iqserHelpMode="edit_dossier_in_dossier"
tooltipPosition="below"
></iqser-circle-button>
<iqser-circle-button
(action)="downloadDossierAsCSV()"
*ngIf="permissionsService.canDownloadCsvReport(dossier)"
[disabled]="listingService.areSomeSelected$ | async"
[tooltip]="'dossier-overview.header-actions.download-csv' | translate"
icon="iqser:csv"
iqserHelpMode="edit_dossier_in_dossier"
tooltipPosition="below"
></iqser-circle-button>
<iqser-circle-button
(action)="reanalyseDossier()"
*ngIf="permissionsService.displayReanalyseBtn(dossier)"
[disabled]="listingService.areSomeSelected$ | async"
[tooltipClass]="'small warn'"
[tooltip]="'dossier-overview.new-rule.toast.actions.reanalyse-all' | translate"
[type]="circleButtonTypes.warn"
icon="iqser:refresh"
tooltipPosition="below"
></iqser-circle-button>
<iqser-circle-button
(action)="reanalyseDossier()"
*ngIf="permissionsService.displayReanalyseBtn(dossier)"
[disabled]="listingService.areSomeSelected$ | async"
[tooltipClass]="'small warn'"
[tooltip]="'dossier-overview.new-rule.toast.actions.reanalyse-all' | translate"
[type]="circleButtonTypes.warn"
icon="iqser:refresh"
tooltipPosition="below"
></iqser-circle-button>
<iqser-circle-button
(action)="upload.emit()"
*ngIf="permissionsService.canUploadFiles(dossier)"
[tooltip]="'dossier-overview.header-actions.upload-document' | translate"
[type]="circleButtonTypes.primary"
class="ml-14"
icon="iqser:upload"
iqserHelpMode="edit_dossier_in_dossier"
tooltipPosition="below"
></iqser-circle-button>
<iqser-circle-button
(action)="upload.emit()"
*ngIf="permissionsService.canUploadFiles(dossier)"
[tooltip]="'dossier-overview.header-actions.upload-document' | translate"
[type]="circleButtonTypes.primary"
class="ml-14"
icon="iqser:upload"
iqserHelpMode="edit_dossier_in_dossier"
tooltipPosition="below"
></iqser-circle-button>
</ng-container>
</iqser-page-header>
<ng-template #viewModeSelection>

View File

@ -51,7 +51,7 @@ export class DossierOverviewScreenHeaderComponent implements OnInit {
) {}
ngOnInit() {
this.actionConfigs = this.configService.actionConfig(this.dossier.dossierId, this.listingService.areSomeSelected$);
this.actionConfigs = this.configService.actionConfig(this.dossier.id, this.listingService.areSomeSelected$);
}
async reanalyseDossier() {

View File

@ -17,7 +17,7 @@ import { PermissionsService } from '@services/permissions.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TranslateService } from '@ngx-translate/core';
import { UserService } from '@services/user.service';
import { DossiersDialogService } from '../dossier/services/dossiers-dialog.service';
import { DossiersDialogService } from '../dossier/shared/services/dossiers-dialog.service';
import { annotationFilterChecker, RedactionFilterSorter } from '../../utils';
import { workloadTranslations } from '../dossier/translations/workload-translations';
import { ConfigService as AppConfigService } from '@services/config.service';

View File

@ -1,4 +1,4 @@
import { Component, ElementRef, forwardRef, HostListener, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Component, ElementRef, forwardRef, HostListener, Injector, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Dossier, DossierAttributeWithValue, File, IFileAttributeConfig, WorkflowFileStatus } from '@red/domain';
import { FileDropOverlayService } from '@upload-download/services/file-drop-overlay.service';
import { FileUploadModel } from '@upload-download/model/file-upload.model';
@ -28,7 +28,7 @@ import { PermissionsService } from '@services/permissions.service';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { ConfigService } from '../config.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { UserPreferenceService } from '@services/user-preference.service';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { FilesService } from '@services/entity-services/files.service';
@ -126,7 +126,7 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
get #dossierFilesChange$() {
return this._dossiersService.dossierFileChanges$.pipe(
filter(dossierId => dossierId === this.dossierId),
switchMap(dossierId => this._filesService.loadAll(dossierId, this._dossiersService.routerPath)),
switchMap(dossierId => this._filesService.loadAll(dossierId)),
);
}

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { Dossier, File } from '@red/domain';
import { DossiersDialogService } from '../../dossier/services/dossiers-dialog.service';
import { DossiersDialogService } from '../../dossier/shared/services/dossiers-dialog.service';
import { ConfirmationDialogInput, LoadingService } from '@iqser/common-ui';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { FilesService } from '@services/entity-services/files.service';

View File

@ -1,21 +1,27 @@
import { Component, Injector } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { Component, Inject, Injector, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DownloadFileType, IDossierRequest, IDossierTemplate, IReportTemplate } from '@red/domain';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { downloadTypesTranslations } from '../../../../translations/download-types-translations';
import { BaseDialogComponent, IconButtonTypes, SaveOptions } from '@iqser/common-ui';
import { BaseDialogComponent, IconButtonTypes, LoadingService, SaveOptions } from '@iqser/common-ui';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { ReportTemplateService } from '@services/report-template.service';
import { firstValueFrom } from 'rxjs';
import { DOSSIER_TEMPLATE_ID } from '@utils/constants';
import dayjs from 'dayjs';
import { Router } from '@angular/router';
import { DossiersDialogService } from '../../shared/services/dossiers-dialog.service';
interface DialogData {
readonly dossierTemplateId?: string;
}
@Component({
templateUrl: './add-dossier-dialog.component.html',
styleUrls: ['./add-dossier-dialog.component.scss'],
})
export class AddDossierDialogComponent extends BaseDialogComponent {
export class AddDossierDialogComponent extends BaseDialogComponent implements OnInit {
readonly iconButtonTypes = IconButtonTypes;
hasDueDate = false;
@ -33,8 +39,12 @@ export class AddDossierDialogComponent extends BaseDialogComponent {
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _formBuilder: FormBuilder,
private readonly _reportTemplateController: ReportTemplateService,
private readonly _router: Router,
private readonly _dialogService: DossiersDialogService,
private readonly _loadingService: LoadingService,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddDossierDialogComponent>,
@Inject(MAT_DIALOG_DATA) readonly data: DialogData,
) {
super(_injector, _dialogRef);
this._getDossierTemplates();
@ -58,13 +68,26 @@ export class AddDossierDialogComponent extends BaseDialogComponent {
return this.form.invalid;
}
async ngOnInit(): Promise<void> {
await this.dossierTemplateChanged(this.form.get('dossierTemplateId').value);
}
reportTemplateValueMapper = (reportTemplate: IReportTemplate) => reportTemplate.templateId;
async save(options?: SaveOptions) {
this._loadingService.start();
const savedDossier = await firstValueFrom(this._activeDossiersService.createOrUpdate(this._formToObject()));
if (savedDossier) {
this._dialogRef.close({ dossier: savedDossier, addMembers: options?.addMembers });
await this._router.navigate([savedDossier.routerLink]);
if (options?.addMembers) {
this._dialogService.openDialog('editDossier', null, {
dossierId: savedDossier.id,
section: 'members',
});
}
this._dialogRef.close(savedDossier);
}
this._loadingService.stop();
}
async dossierTemplateChanged(dossierTemplateId) {
@ -98,7 +121,7 @@ export class AddDossierDialogComponent extends BaseDialogComponent {
return this._formBuilder.group(
{
dossierName: [null, Validators.required],
dossierTemplateId: [null, Validators.required],
dossierTemplateId: [this.data?.dossierTemplateId, Validators.required],
downloadFileTypes: [null],
reportTemplateIds: [null],
description: [null],

View File

@ -1,4 +1,4 @@
import { AfterViewInit, ChangeDetectorRef, Component, Inject, Injector, ViewChild } from '@angular/core';
import { AfterViewInit, Component, Inject, Injector, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Dossier } from '@red/domain';
import { EditDossierGeneralInfoComponent } from './general-info/edit-dossier-general-info.component';
@ -48,8 +48,7 @@ export class EditDossierDialogComponent extends BaseDialogComponent implements A
constructor(
private readonly _toaster: Toaster,
private readonly _activeDossiersService: DossiersService,
private readonly _changeRef: ChangeDetectorRef,
private readonly _dossiersService: DossiersService,
private readonly _loadingService: LoadingService,
private readonly _permissionsService: PermissionsService,
private readonly _userService: UserService,
@ -62,7 +61,7 @@ export class EditDossierDialogComponent extends BaseDialogComponent implements A
},
) {
super(_injector, _dialogRef, true);
this.dossier$ = this._activeDossiersService.getEntityChanged$(_data.dossierId).pipe(
this.dossier$ = this._dossiersService.getEntityChanged$(_data.dossierId).pipe(
tap(dossier => {
this._dossier = dossier;
this._initializeNavItems();

View File

@ -3,7 +3,7 @@
<mat-form-field floatLabel="always">
<mat-label>{{ 'assign-dossier-owner.dialog.single-user' | translate }}</mat-label>
<mat-select formControlName="owner" id="editDossierOwnerSelect">
<mat-option *ngFor="let userId of ownersSelectOptions; let index = index" [value]="userId" [id]="'mat-option-' + index">
<mat-option *ngFor="let userId of ownersSelectOptions; let index = index" [id]="'mat-option-' + index" [value]="userId">
{{ userId | name }}
</mat-option>
</mat-select>
@ -11,12 +11,12 @@
</div>
<ng-container *ngIf="selectedApproversList.length">
<div class="all-caps-label mt-16" translate="assign-dossier-owner.dialog.approvers" id="approversLabel"></div>
<div class="all-caps-label mt-16" id="approversLabel" translate="assign-dossier-owner.dialog.approvers"></div>
<redaction-team-members
(remove)="toggleSelected($event)"
[canAdd]="false"
[canRemove]="hasOwner && !disabled"
[dossierId]="dossier.dossierId"
[dossierId]="dossier.id"
[largeSpacing]="true"
[memberIds]="selectedApproversList"
[perLine]="13"
@ -25,12 +25,12 @@
</ng-container>
<ng-container *ngIf="(selectedReviewers$ | async)?.length">
<div class="all-caps-label mt-16" translate="assign-dossier-owner.dialog.reviewers" id="reviewersLabel"></div>
<div class="all-caps-label mt-16" id="reviewersLabel" translate="assign-dossier-owner.dialog.reviewers"></div>
<redaction-team-members
(remove)="toggleSelected($event)"
[canAdd]="false"
[canRemove]="hasOwner && !disabled"
[dossierId]="dossier.dossierId"
[dossierId]="dossier.id"
[largeSpacing]="true"
[memberIds]="selectedReviewers$ | async"
[perLine]="13"

View File

@ -2,14 +2,14 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Dossier, IDossierRequest, IDossierTemplate } from '@red/domain';
import { EditDossierSaveResult, EditDossierSectionInterface } from '../edit-dossier-section.interface';
import { DossiersDialogService } from '../../../services/dossiers-dialog.service';
import { DossiersDialogService } from '../../../shared/services/dossiers-dialog.service';
import { PermissionsService } from '@services/permissions.service';
import { Router } from '@angular/router';
import { MatDialogRef } from '@angular/material/dialog';
import { EditDossierDialogComponent } from '../edit-dossier-dialog.component';
import { ConfirmationDialogInput, ConfirmOptions, IconButtonTypes, LoadingService, TitleColors, Toaster } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
import { firstValueFrom } from 'rxjs';
import { DOSSIER_TEMPLATE_ID } from '@utils/constants';
@ -139,7 +139,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
this._loadingService.start();
await firstValueFrom(this._trashService.deleteDossier(this.dossier));
this._editDossierDialogRef.close();
await this._router.navigate(['main', 'dossiers']);
await this._router.navigate([this.dossier.dossiersListRouterLink]);
this._loadingService.stop();
this._toaster.success(_('edit-dossier-dialog.delete-successful'), { params: this.dossier });
});
@ -161,7 +161,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
if (result === ConfirmOptions.CONFIRM) {
this._loadingService.start();
await firstValueFrom(this._archivedDossiersService.archive([this.dossier]));
await this._router.navigate(['main', 'dossiers']);
await this._router.navigate([this.dossier.dossiersListRouterLink]);
this._toaster.success(_('dossier-listing.archive.archive-succeeded'), { params: this.dossier });
this._editDossierDialogRef.close();
this._loadingService.stop();
@ -187,7 +187,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
dossierTemplateId: [
{
value: this.dossier.dossierTemplateId,
disabled: this._dossierStatsService.get(this.dossier.dossierId).hasFiles || !this.dossier.isActive,
disabled: this._dossierStatsService.get(this.dossier.id).hasFiles || !this.dossier.isActive,
},
Validators.required,
],

View File

@ -12,7 +12,7 @@ const routes: Routes = [
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [DossierFilesGuard],
breadcrumbs: [BreadcrumbTypes.main, BreadcrumbTypes.dossier],
breadcrumbs: [BreadcrumbTypes.dossierTemplate, BreadcrumbTypes.dossier],
dossiersService: ACTIVE_DOSSIERS_SERVICE,
},
loadChildren: () => import('../dossier-overview/dossier-overview.module').then(m => m.DossierOverviewModule),
@ -22,7 +22,7 @@ const routes: Routes = [
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [DossierFilesGuard],
breadcrumbs: [BreadcrumbTypes.main, BreadcrumbTypes.dossier, BreadcrumbTypes.file],
breadcrumbs: [BreadcrumbTypes.dossierTemplate, BreadcrumbTypes.dossier, BreadcrumbTypes.file],
dossiersService: ACTIVE_DOSSIERS_SERVICE,
},
loadChildren: () => import('../file-preview/file-preview.module').then(m => m.FilePreviewModule),
@ -31,7 +31,7 @@ const routes: Routes = [
path: '',
pathMatch: 'full',
loadChildren: () => import('../dossiers-listing/dossiers-listing.module').then(m => m.DossiersListingModule),
data: { breadcrumbs: [BreadcrumbTypes.main] },
data: { breadcrumbs: [BreadcrumbTypes.dossierTemplate] },
},
];

View File

@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, OnChanges, Optional, ViewChild } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { Action, ActionTypes, Dossier, File } from '@red/domain';
import { DossiersDialogService } from '../../../services/dossiers-dialog.service';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import {
CircleButtonType,
CircleButtonTypes,
@ -24,7 +24,6 @@ import { ExcludedPagesService } from '../../../../file-preview/services/excluded
import { DocumentInfoService } from '../../../../file-preview/services/document-info.service';
import { ExpandableFileActionsComponent } from '@shared/components/expandable-file-actions/expandable-file-actions.component';
import { firstValueFrom, Observable } from 'rxjs';
import { RedactionImportService } from '../../services/redaction-import.service';
import { PageRotationService } from '../../../../file-preview/services/page-rotation.service';
@Component({

View File

@ -1,10 +1,10 @@
import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { AddDossierDialogComponent } from '../dialogs/add-dossier-dialog/add-dossier-dialog.component';
import { EditDossierDialogComponent } from '../dialogs/edit-dossier-dialog/edit-dossier-dialog.component';
import { AssignReviewerApproverDialogComponent } from '../dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component';
import { AddDossierDialogComponent } from '../../dialogs/add-dossier-dialog/add-dossier-dialog.component';
import { EditDossierDialogComponent } from '../../dialogs/edit-dossier-dialog/edit-dossier-dialog.component';
import { AssignReviewerApproverDialogComponent } from '../../dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component';
import { ConfirmationDialogComponent, DialogConfig, DialogService, largeDialogConfig } from '@iqser/common-ui';
import { ImportRedactionsDialogComponent } from '../../file-preview/dialogs/import-redactions-dialog/import-redactions-dialog';
import { ImportRedactionsDialogComponent } from '../../../file-preview/dialogs/import-redactions-dialog/import-redactions-dialog';
type DialogType = 'confirm' | 'editDossier' | 'addDossier' | 'assignFile' | 'importRedactions';

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { UserService } from '@services/user.service';
import { Dossier, File } from '@red/domain';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { DossiersDialogService } from './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';

View File

@ -4,7 +4,7 @@ import { FileAssignService } from './services/file-assign.service';
import { FileActionsComponent } from './components/file-actions/file-actions.component';
import { SharedModule } from '@shared/shared.module';
import { RedactionImportService } from './services/redaction-import.service';
import { DossiersDialogService } from '../services/dossiers-dialog.service';
import { DossiersDialogService } from './services/dossiers-dialog.service';
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';

View File

@ -1,6 +1,6 @@
<div (longPress)="forceReanalysisAction($event)" class="action-buttons" redactionLongPress>
<iqser-circle-button
(action)="openEditDossierDialog($event, dossier.dossierId)"
(action)="openEditDossierDialog($event, dossier.id)"
*ngIf="currentUser.isUser"
[icon]="currentUser.isManager ? 'iqser:edit' : 'red:info'"
[scrollableParentView]="scrollableParentView"

View File

@ -3,7 +3,7 @@ import { PermissionsService } from '@services/permissions.service';
import { CircleButtonTypes, List, ScrollableParentView, ScrollableParentViews, StatusBarConfig } from '@iqser/common-ui';
import { UserService } from '@services/user.service';
import { Dossier, DossierStats, File } from '@red/domain';
import { DossiersDialogService } from '../../../dossier/services/dossiers-dialog.service';
import { DossiersDialogService } from '../../../dossier/shared/services/dossiers-dialog.service';
import { LongPressEvent } from '@shared/directives/long-press.directive';
import { UserPreferenceService } from '@services/user-preference.service';
import { FilesMapService } from '@services/entity-services/files-map.service';
@ -44,7 +44,7 @@ export class DossiersListingActionsComponent implements OnChanges {
}
ngOnChanges() {
this.files = this.filesMapService.get(this.dossier.dossierId);
this.files = this.filesMapService.get(this.dossier.id);
this.displayReanalyseBtn = this.permissionsService.displayReanalyseBtn(this.dossier) && this.analysisForced;
}

View File

@ -1,25 +1,24 @@
<div>
<div *ngIf="stats$ | async as stats">
<redaction-simple-doughnut-chart
*ngIf="dossiersChartData$ | async as config"
[config]="config"
[config]="dossiersChartData$ | async"
[radius]="80"
[strokeWidth]="15"
[subtitle]="'dossier-listing.stats.charts.dossiers' | translate: { count: activeDossiersService.all.length }"
[subtitle]="'dossier-template-stats.active-dossiers' | translate: { count: stats.numberOfActiveDossiers }"
></redaction-simple-doughnut-chart>
<div *ngIf="activeDossiersService.generalStats$ | async as stats" class="dossier-stats-container">
<div class="dossier-stats-container">
<div class="dossier-stats-item">
<mat-icon svgIcon="red:needs-work"></mat-icon>
<div>
<div class="heading">{{ stats.totalAnalyzedPages | number }}</div>
<div [translateParams]="{ count: stats.totalAnalyzedPages }" [translate]="'dossier-listing.stats.analyzed-pages'"></div>
<div class="heading">{{ stats.numberOfPages | number }}</div>
<div [translateParams]="{ count: stats.numberOfPages }" [translate]="'dossier-listing.stats.analyzed-pages'"></div>
</div>
</div>
<div class="dossier-stats-item">
<mat-icon svgIcon="red:user"></mat-icon>
<div>
<div class="heading">{{ stats.totalPeople }}</div>
<div class="heading">{{ stats.numberOfPeople }}</div>
<div translate="dossier-listing.stats.total-people"></div>
</div>
</div>
@ -28,10 +27,9 @@
<div class="right-chart">
<redaction-simple-doughnut-chart
*ngIf="documentsChartData$ | async as config"
[config]="config"
[config]="documentsChartData$ | async"
[radius]="80"
[strokeWidth]="15"
[subtitle]="'dossier-listing.stats.charts.total-documents' | translate"
[subtitle]="'dossier-template-stats.total-documents' | translate"
></redaction-simple-doughnut-chart>
</div>

View File

@ -1,15 +1,12 @@
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 { 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 { Observable } from 'rxjs';
import { TranslateChartService } from '@services/translate-chart.service';
import { filter, map, switchMap } from 'rxjs/operators';
import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
import { TranslateService } from '@ngx-translate/core';
import { DossierStatesMapService } from '@services/entity-services/dossier-states-map.service';
import { map } from 'rxjs/operators';
import { DashboardStatsService } from '@services/dossier-templates/dashboard-stats.service';
import { ActivatedRoute } from '@angular/router';
import { DOSSIER_TEMPLATE_ID } from '@utils/constants';
import { DashboardStats } from '@red/domain';
@Component({
selector: 'redaction-dossiers-listing-details',
@ -18,59 +15,20 @@ import { DossierStatesMapService } from '@services/entity-services/dossier-state
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DossiersListingDetailsComponent {
readonly stats$: Observable<DashboardStats>;
readonly documentsChartData$: Observable<DoughnutChartConfig[]>;
readonly dossiersChartData$: Observable<DoughnutChartConfig[]>;
constructor(
readonly filterService: FilterService,
readonly activeDossiersService: ActiveDossiersService,
private readonly _dossierStatsMap: DossierStatsService,
private readonly _dashboardStatsService: DashboardStatsService,
private readonly _translateChartService: TranslateChartService,
private readonly _dossierStatesMapService: DossierStatesMapService,
private readonly _translateService: TranslateService,
private readonly _route: ActivatedRoute,
) {
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)),
const dossierTemplateId: string = this._route.snapshot.paramMap.get(DOSSIER_TEMPLATE_ID);
this.stats$ = this._dashboardStatsService.getEntityChanged$(dossierTemplateId);
this.dossiersChartData$ = this.stats$.pipe(
map(s => this._translateChartService.translateDossierStates(s.dossiersChartData, dossierTemplateId)),
);
this.dossiersChartData$ = this.activeDossiersService.all$.pipe(map(() => this._toDossierChartData()));
}
private _toDossierChartData(): DoughnutChartConfig[] {
const configArray: DoughnutChartConfig[] = this._dossierStatesMapService.stats;
const undefinedStateLength =
this.activeDossiersService.all.length - configArray.map(v => v.value).reduce((acc, val) => acc + val, 0);
configArray.push({
value: undefinedStateLength,
label: this._translateService.instant('edit-dossier-dialog.general-info.form.dossier-state.placeholder'),
color: '#E2E4E9',
});
return configArray;
}
private _toChartData(stats: DossierStats[]) {
const chartData: FileCountPerWorkflowStatus = {};
stats.forEach(stat => {
const statuses: FileCountPerWorkflowStatus = stat.fileCountPerWorkflowStatus;
Object.keys(statuses).forEach(status => {
chartData[status] = chartData[status] ? (chartData[status] as number) + (statuses[status] as number) : statuses[status];
});
});
const documentsChartData = Object.keys(chartData).map(
status =>
({
value: chartData[status],
color: status,
label: workflowFileStatusTranslations[status],
key: status,
} as DoughnutChartConfig),
);
documentsChartData.sort((a, b) => StatusSorter.byStatus(a.key, b.key));
return this._translateChartService.translateStatus(documentsChartData);
this.documentsChartData$ = this.stats$.pipe(map(s => this._translateChartService.translateWorkflowStatus(s.documentsChartData)));
}
}

View File

@ -22,7 +22,7 @@ export class TableItemComponent implements OnChanges {
ngOnChanges() {
if (this.dossier) {
this.#ngOnChanges$.next(this.dossier.dossierId);
this.#ngOnChanges$.next(this.dossier.id);
}
}
}

View File

@ -6,11 +6,11 @@ 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 { dossierMemberChecker, dossierStateChecker, dossierTemplateChecker, RedactionFilterSorter } from '@utils/index';
import { dossierMemberChecker, dossierStateChecker, RedactionFilterSorter } from '../../utils';
import { workloadTranslations } from '../dossier/translations/workload-translations';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
import { DossierStatesMapService } from '@services/entity-services/dossier-states-map.service';
import { DossiersDialogService } from '../dossier/shared/services/dossiers-dialog.service';
@Injectable()
export class ConfigService {
@ -18,9 +18,9 @@ export class ConfigService {
private readonly _translateService: TranslateService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _userService: UserService,
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dossierStatsService: DossierStatsService,
private readonly _dossierStatesMapService: DossierStatesMapService,
private readonly _dialogService: DossiersDialogService,
) {}
get tableConfig(): TableColumnConfig<Dossier>[] {
@ -38,19 +38,11 @@ export class ConfigService {
return this._userService.currentUser;
}
_myDossiersChecker = (dw: Dossier) => dw.ownerId === this._currentUser.id;
_toApproveChecker = (dw: Dossier) => dw.approverIds.includes(this._currentUser.id);
_toReviewChecker = (dw: Dossier) => dw.memberIds.includes(this._currentUser.id);
_otherChecker = (dw: Dossier) => !dw.memberIds.includes(this._currentUser.id);
buttonsConfig(addDossier: () => void): ButtonConfig[] {
buttonsConfig(dossierTemplateId: string): ButtonConfig[] {
return [
{
label: _('dossier-listing.add-new'),
action: addDossier,
action: () => this._openAddDossierDialog(dossierTemplateId),
hide: !this._currentUser.isManager,
icon: 'iqser:plus',
type: 'primary',
@ -63,7 +55,6 @@ export class ConfigService {
const allDistinctFileStatus = new Set<string>();
const allDistinctPeople = new Set<string>();
const allDistinctNeedsWork = new Set<string>();
const allDistinctDossierTemplates = new Set<string>();
const allDistinctDossierStates = new Set<string>();
const stateToTemplateMap = new Map<string, string>();
@ -72,7 +63,6 @@ export class ConfigService {
entities?.forEach(entry => {
entry.memberIds.forEach(f => allDistinctPeople.add(f));
allDistinctDossierTemplates.add(entry.dossierTemplateId);
if (entry.dossierStatusId) {
allDistinctDossierStates.add(entry.dossierStatusId);
stateToTemplateMap.set(entry.dossierStatusId, entry.dossierTemplateId);
@ -166,23 +156,6 @@ export class ConfigService {
matchAll: true,
});
const dossierTemplateFilters = [...allDistinctDossierTemplates].map(
id =>
new NestedFilter({
id: id,
label: this._dossierTemplatesService.find(id)?.name || '-',
}),
);
filterGroups.push({
slug: 'dossierTemplateFilters',
label: this._translateService.instant('filters.dossier-templates'),
icon: 'red:template',
hide: dossierTemplateFilters.length <= 1,
filters: dossierTemplateFilters,
checker: dossierTemplateChecker,
});
filterGroups.push({
slug: 'quickFilters',
filters: this._quickFilters(entities),
@ -208,6 +181,18 @@ export class ConfigService {
return filterGroups;
}
private _myDossiersChecker = (dw: Dossier) => dw.ownerId === this._currentUser.id;
private _toApproveChecker = (dw: Dossier) => dw.approverIds.includes(this._currentUser.id);
private _toReviewChecker = (dw: Dossier) => dw.memberIds.includes(this._currentUser.id);
private _otherChecker = (dw: Dossier) => !dw.memberIds.includes(this._currentUser.id);
private _openAddDossierDialog(dossierTemplateId: string): void {
this._dialogService.openDialog('addDossier', null, { dossierTemplateId });
}
private _quickFilters(entities: Dossier[]): NestedFilter[] {
const myDossiersLabel = this._translateService.instant('dossier-listing.quick-filters.my-dossiers');
const filters = [
@ -242,12 +227,12 @@ export class ConfigService {
}
private _dossierStatusChecker = (dossier: Dossier, filter: INestedFilter) => {
const stats = this._dossierStatsService.get(dossier.dossierId);
const stats = this._dossierStatsService.get(dossier.id);
return stats?.fileCountPerWorkflowStatus[filter.id];
};
private _annotationFilterChecker = (dossier: Dossier, filter: INestedFilter) => {
const stats = this._dossierStatsService.get(dossier.dossierId);
const stats = this._dossierStatsService.get(dossier.id);
switch (filter.id) {
// case 'analysis': {
// return stats.reanalysisRequired;

View File

@ -1,5 +1,9 @@
<section>
<iqser-page-header [buttonConfigs]="buttonConfigs" [helpModeKey]="'dossier'"></iqser-page-header>
<iqser-page-header [buttonConfigs]="buttonConfigs" [helpModeKey]="'dossier'">
<ng-container slot="beforeFilters">
<redaction-dossiers-type-switch></redaction-dossiers-type-switch>
</ng-container>
</iqser-page-header>
<div class="overlay-shadow"></div>
@ -25,7 +29,7 @@
</section>
<ng-template #needsWorkFilterTemplate let-filter="filter">
<redaction-type-filter [dossierTemplateId]="defaultDossierTemplateId" [filter]="filter"></redaction-type-filter>
<redaction-type-filter [dossierTemplateId]="dossierTemplateId" [filter]="filter"></redaction-type-filter>
</ng-template>
<ng-template #tableItemTemplate let-dossier="entity">

View File

@ -2,32 +2,28 @@ import { ChangeDetectionStrategy, Component, forwardRef, Injector, OnInit, Templ
import { Dossier } from '@red/domain';
import { UserService } from '@services/user.service';
import { PermissionsService } from '@services/permissions.service';
import { TranslateChartService } from '@services/translate-chart.service';
import { Router } from '@angular/router';
import { DossiersDialogService } from '../../dossier/services/dossiers-dialog.service';
import { DefaultListingServicesTmp, EntitiesService, ListingComponent, OnAttach, TableComponent } from '@iqser/common-ui';
import { ButtonConfig, DefaultListingServices, ListingComponent, OnAttach, TableComponent } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ConfigService } from '../config.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';
import { DossiersDialogService } from '../../dossier/shared/services/dossiers-dialog.service';
import { Router } from '@angular/router';
import { DOSSIER_TEMPLATE_ID } from '@utils/constants';
import { UserPreferenceService } from '@services/user-preference.service';
@Component({
templateUrl: './dossiers-listing-screen.component.html',
styleUrls: ['./dossiers-listing-screen.component.scss'],
providers: [
...DefaultListingServicesTmp,
{ provide: EntitiesService, useExisting: ActiveDossiersService },
{ provide: ListingComponent, useExisting: forwardRef(() => DossiersListingScreenComponent) },
],
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => DossiersListingScreenComponent) }],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DossiersListingScreenComponent extends ListingComponent<Dossier> implements OnInit, OnAttach {
readonly currentUser = this._userService.currentUser;
readonly tableColumnConfigs = this._configService.tableConfig;
readonly tableHeaderLabel = _('dossier-listing.table-header.title');
readonly buttonConfigs = this._configService.buttonsConfig(() => this.openAddDossierDialog());
readonly buttonConfigs: ButtonConfig[];
readonly dossierTemplateId: string;
@ViewChild('needsWorkFilterTemplate', {
read: TemplateRef,
static: true,
@ -36,44 +32,37 @@ export class DossiersListingScreenComponent extends ListingComponent<Dossier> im
@ViewChild(TableComponent) private readonly _tableComponent: TableComponent<Dossier>;
constructor(
private readonly _router: Router,
protected readonly _injector: Injector,
private readonly _userService: UserService,
readonly permissionsService: PermissionsService,
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _dialogService: DossiersDialogService,
private readonly _translateChartService: TranslateChartService,
private readonly _configService: ConfigService,
private readonly _filesService: FilesService,
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dialogService: DossiersDialogService,
private readonly _router: Router,
private readonly _userPreferenceService: UserPreferenceService,
) {
super(_injector);
this.dossierTemplateId = this._router.routerState.snapshot.root.firstChild.firstChild.paramMap.get(DOSSIER_TEMPLATE_ID);
this._router.routeReuseStrategy.shouldReuseRoute = () => false;
this.buttonConfigs = this._configService.buttonsConfig(this.dossierTemplateId);
}
get defaultDossierTemplateId(): string {
return this._dossierTemplatesService.all[0].id;
openAddDossierDialog(): void {
this._dialogService.openDialog('addDossier', null, { dossierTemplateId: this.dossierTemplateId });
}
ngOnInit(): void {
this.addSubscription = this._activeDossiersService.all$.pipe(tap(() => this._computeAllFilters())).subscribe();
async ngOnInit(): Promise<void> {
await this._userPreferenceService.saveLastDossierTemplate(this.dossierTemplateId);
this.addSubscription = this.entitiesService.all$.pipe(tap(() => this._computeAllFilters())).subscribe();
this.addSubscription = this._activeDossiersService.all$
.pipe(tap(dossiers => this.entitiesService.setEntities(dossiers.filter(d => d.dossierTemplateId === this.dossierTemplateId))))
.subscribe();
}
ngOnAttach(): void {
this._tableComponent?.scrollToLastIndex();
}
openAddDossierDialog(): void {
this._dialogService.openDialog('addDossier', null, null, async (addResponse: { dossier: Dossier; addMembers: boolean }) => {
await this._router.navigate([addResponse.dossier.routerLink]);
if (addResponse.addMembers) {
this._dialogService.openDialog('editDossier', null, {
dossierId: addResponse.dossier.dossierId,
section: 'members',
});
}
});
}
private _computeAllFilters() {
const filterGroups = this._configService.filterGroups(this.entitiesService.all, this._needsWorkFilterTemplate);
this.filterService.addFilterGroups(filterGroups);

View File

@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { DocumentInfoService } from '../../services/document-info.service';
import { combineLatest, Observable, switchMap } from 'rxjs';
import { PermissionsService } from '@services/permissions.service';

View File

@ -54,10 +54,7 @@ export class AcceptRecommendationDialogComponent extends BaseDialogComponent imp
async ngOnInit() {
super.ngOnInit();
this.possibleDictionaries = await this._dictionaryService.getDictionariesOptions(
this._dossier.dossierTemplateId,
this._dossier.dossierId,
);
this.possibleDictionaries = await this._dictionaryService.getDictionariesOptions(this._dossier.dossierTemplateId, this._dossier.id);
this.form.patchValue({
dictionary: this.possibleDictionaries.find(dict => dict.type === this.data.annotations[0].recommendationType).type,
});

View File

@ -76,10 +76,7 @@ export class ManualAnnotationDialogComponent extends BaseDialogComponent impleme
async ngOnInit() {
super.ngOnInit();
this.possibleDictionaries = await this._dictionaryService.getDictionariesOptions(
this._dossier.dossierTemplateId,
this._dossier.dossierId,
);
this.possibleDictionaries = await this._dictionaryService.getDictionariesOptions(this._dossier.dossierTemplateId, this._dossier.id);
const data = await firstValueFrom(this._justificationsService.getForDossierTemplate(this._dossier.dossierTemplateId));
this.legalOptions = data.map(lbm => ({
legalBasis: lbm.reason,

View File

@ -427,7 +427,7 @@ export class AnnotationActionsService {
value: text,
};
this._processObsAndEmit(this._manualRedactionService.resizeOrSuggestResize([resizeRequest], data.dossier.dossierId, fileId));
this._processObsAndEmit(this._manualRedactionService.resizeOrSuggestResize([resizeRequest], data.dossier.id, fileId));
});
}

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { BehaviorSubject, merge, Observable } from 'rxjs';
import { shareLast } from '@iqser/common-ui';
import { filter, map, startWith, tap, withLatestFrom } from 'rxjs/operators';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { File, IFileAttributeConfig } from '@red/domain';

View File

@ -1,7 +1,7 @@
import { Injectable, Injector } from '@angular/core';
import { combineLatest, firstValueFrom, from, merge, Observable, of, pairwise, Subject, switchMap } from 'rxjs';
import { Dossier, File } from '@red/domain';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { PermissionsService } from '@services/permissions.service';
import { boolFactory } from '@iqser/common-ui';
@ -39,8 +39,9 @@ export class FilePreviewStateService {
private readonly _filesService: FilesService,
private readonly _dossiersService: DossiersService,
private readonly _fileManagementService: FileManagementService,
private readonly _router: Router,
) {
const dossiersService = dossiersServiceResolver(_injector);
const dossiersService = dossiersServiceResolver(_injector, _router);
this.fileId = route.snapshot.paramMap.get(FILE_ID);
this.dossierId = route.snapshot.paramMap.get(DOSSIER_ID);
@ -81,7 +82,7 @@ export class FilePreviewStateService {
#dossierFilesChange$() {
return this._dossiersService.dossierFileChanges$.pipe(
filter(dossierId => dossierId === this.dossierId),
switchMap(dossierId => this._filesService.loadAll(dossierId, this._dossiersService.routerPath)),
switchMap(dossierId => this._filesService.loadAll(dossierId)),
);
}

View File

@ -20,10 +20,9 @@ import { RouterHistoryService } from '@services/router-history.service';
import { Dossier, 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';
import { FeaturesService } from '@services/features.service';
import { DOSSIERS_ARCHIVE } from '@utils/constants';
import { DossiersCacheService } from '../../../services/dossiers/dossiers-cache.service';
@Component({
templateUrl: './search-screen.component.html',
@ -66,8 +65,7 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
protected readonly _injector: Injector,
private readonly _activatedRoute: ActivatedRoute,
private readonly _loadingService: LoadingService,
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _archivedDossiersService: ArchivedDossiersService,
private readonly _dossiersCacheService: DossiersCacheService,
readonly routerHistoryService: RouterHistoryService,
private readonly _translateService: TranslateService,
private readonly _filesMapService: FilesMapService,
@ -83,13 +81,12 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> 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: allDossiers.map(dossierToFilter),
filters: this._dossiersCacheService.all.map(dossierToFilter),
checker: keyChecker('dossierId'),
};
this.filterService.addFilterGroups([dossierNameFilter]);
@ -166,10 +163,11 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
}: IMatchedDocument): ISearchListItem {
const file = this._filesMapService.get(dossierId, fileId);
if (!file) {
console.error('Missing file');
return undefined;
}
const dossier = (dossierArchived ? this._archivedDossiersService : this._activeDossiersService).find(dossierId);
const dossier = this._dossiersCacheService.get(dossierId);
return {
id: fileId,

View File

@ -6,7 +6,7 @@ import { Dictionary, DICTIONARY_TYPE_KEY_MAP, DictionaryType, Dossier, DossierTe
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { DictionaryService } from '@services/entity-services/dictionary.service';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { EditorComponent } from '@shared/components/editor/editor.component';
import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service';
import IModelDeltaDecoration = monaco.editor.IModelDeltaDecoration;
@ -82,7 +82,7 @@ export class DictionaryManagerComponent implements OnChanges {
return;
}
this._onDossierChanged(dossier.dossierTemplateId, dossier.dossierId)
this._onDossierChanged(dossier.dossierTemplateId, dossier.id)
.pipe(take(1))
.subscribe(entries => {
this.diffEditorText = entries;

View File

@ -1,6 +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 { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { List } from '@iqser/common-ui';
import dayjs from 'dayjs';

View File

@ -0,0 +1,9 @@
<a [routerLinkActive]="'active'" [routerLink]="['..', DOSSIERS_ROUTE]" class="red-tab">
{{ 'dossiers-type-switch.active' | translate }}
</a>
<a [routerLinkActive]="'active'" [routerLink]="['..', ARCHIVE_ROUTE]" class="red-tab">
{{ 'dossiers-type-switch.archive' | translate }}
</a>
<div class="separator"></div>

View File

@ -0,0 +1,11 @@
:host {
display: flex;
}
.separator {
background-color: var(--iqser-separator);
width: 1px;
height: 30px;
margin-left: 8px;
margin-right: 16px;
}

View File

@ -0,0 +1,13 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ARCHIVE_ROUTE, DOSSIERS_ROUTE } from '@utils/constants';
@Component({
selector: 'redaction-dossiers-type-switch',
templateUrl: './dossiers-type-switch.component.html',
styleUrls: ['./dossiers-type-switch.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DossiersTypeSwitchComponent {
readonly DOSSIERS_ROUTE = DOSSIERS_ROUTE;
readonly ARCHIVE_ROUTE = ARCHIVE_ROUTE;
}

View File

@ -47,6 +47,7 @@
> div {
border-radius: 4px;
padding: 3px 8px;
width: 100%;
&:not(:last-child) {
margin-bottom: 8px;

View File

@ -1,4 +1,4 @@
import { Component, Input, OnChanges, OnInit } from '@angular/core';
import { Component, Input, OnChanges, OnInit, Optional } from '@angular/core';
import { Color } from '@red/domain';
import { FilterService, INestedFilter } from '@iqser/common-ui';
import { Observable, of } from 'rxjs';
@ -30,17 +30,20 @@ export class SimpleDoughnutChartComponent implements OnChanges, OnInit {
filtersEnabled: boolean;
chartData: any[] = [];
perimeter: number;
cx = 0;
cy = 0;
size = 0;
filters$: Observable<INestedFilter[]>;
constructor(readonly filterService: FilterService) {
this.filterService.filterGroups$.subscribe(() => {
this.filtersEnabled = !!this.filterService.filterGroups.find(g => g.slug === this.filterKey);
});
constructor(@Optional() readonly filterService: FilterService) {
if (filterService) {
this.filterService.filterGroups$.subscribe(() => {
this.filtersEnabled = !!this.filterService.filterGroups.find(g => g.slug === this.filterKey);
});
} else {
this.filtersEnabled = false;
}
}
get circumference(): number {
@ -56,7 +59,7 @@ export class SimpleDoughnutChartComponent implements OnChanges, OnInit {
}
ngOnInit() {
this.filters$ = this.filterService.getFilterModels$(this.filterKey) ?? of([]);
this.filters$ = (this.filtersEnabled && this.filterService.getFilterModels$(this.filterKey)) ?? of([]);
}
ngOnChanges(): void {
@ -67,7 +70,7 @@ export class SimpleDoughnutChartComponent implements OnChanges, OnInit {
}
filterChecked$(key: string): Observable<boolean> {
return this.filters$.pipe(map(all => all?.find(e => e.id === key)?.checked));
return this.filtersEnabled ? this.filters$.pipe(map(all => all?.find(e => e.id === key)?.checked)) : of(false);
}
calculateChartData() {

View File

@ -1,7 +1,7 @@
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { CircleButtonTypes, List } from '@iqser/common-ui';
import { UserService } from '@services/user.service';
import { DossiersDialogService } from '../../../dossier/services/dossiers-dialog.service';
import { DossiersDialogService } from '../../../dossier/shared/services/dossiers-dialog.service';
@Component({
selector: 'redaction-team-members',

View File

@ -31,6 +31,9 @@ import { FileNameColumnComponent } from '@shared/components/file-name-column/fil
import { DossierNameColumnComponent } from '@shared/components/dossier-name-column/dossier-name-column.component';
import { MAT_DATE_FORMATS } from '@angular/material/core';
import { DragDropFileUploadDirective } from '@shared/directives/drag-drop-file-upload.directive';
import { DossiersTypeSwitchComponent } from '@shared/components/dossiers-type-switch/dossiers-type-switch.component';
import { TranslateModule } from '@ngx-translate/core';
import { RouterModule } from '@angular/router';
const buttons = [FileDownloadBtnComponent, UserButtonComponent];
@ -51,6 +54,7 @@ const components = [
DossierNameColumnComponent,
FileStatsComponent,
FileNameColumnComponent,
DossiersTypeSwitchComponent,
...buttons,
];
@ -61,7 +65,7 @@ const modules = [MatConfigModule, ScrollingModule, IconsModule, FormsModule, Rea
@NgModule({
declarations: [...components, ...utils, EditorComponent],
imports: [CommonModule, ...modules, MonacoEditorModule],
imports: [CommonModule, ...modules, MonacoEditorModule, TranslateModule, RouterModule],
exports: [...modules, ...components, ...utils],
providers: [
{

View File

@ -42,7 +42,7 @@ export class FileUploadService extends GenericService<IFileUploadResult> impleme
super(_injector, 'upload');
const fileFetch$ = this._fetchFiles$.pipe(
throttleTime(250),
switchMap(dossierId => this._filesService.loadAll(dossierId, 'dossiers')),
switchMap(dossierId => this._filesService.loadAll(dossierId)),
);
this._subscriptions.add(fileFetch$.subscribe());
const interval$ = interval(2500).pipe(tap(() => this._handleUploads()));

View File

@ -6,10 +6,11 @@ import { filter, pluck } from 'rxjs/operators';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { TranslateService } from '@ngx-translate/core';
import { BreadcrumbTypes } from '@red/domain';
import { DOSSIER_ID, DOSSIERS_ARCHIVE, FILE_ID } from '@utils/constants';
import { DOSSIER_ID, DOSSIER_TEMPLATE_ID, DOSSIERS_ARCHIVE, FILE_ID } from '@utils/constants';
import { DossiersService } from '@services/dossiers/dossiers.service';
import { dossiersServiceResolver } from '@services/entity-services/dossiers.service.provider';
import { FeaturesService } from '@services/features.service';
import { DashboardStatsService } from '@services/dossier-templates/dashboard-stats.service';
export type RouterLinkActiveOptions = { exact: boolean } | IsActiveMatchOptions;
export type BreadcrumbDisplayType = 'text' | 'dropdown';
@ -40,6 +41,7 @@ export class BreadcrumbsService {
private readonly _router: Router,
private readonly _translateService: TranslateService,
private readonly _filesMapService: FilesMapService,
private readonly _dashboardStatsService: DashboardStatsService,
private readonly _featuresService: FeaturesService,
) {
this.breadcrumbs$ = this._store$.asObservable();
@ -47,7 +49,7 @@ export class BreadcrumbsService {
this._router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
const root = this._router.routerState.snapshot.root;
this._clear();
this._addBreadcrumbs(root.firstChild);
this._addBreadcrumbs(root);
});
}
@ -56,26 +58,15 @@ export class BreadcrumbsService {
}
private get _dossiersService(): DossiersService {
return dossiersServiceResolver(this._injector);
return dossiersServiceResolver(this._injector, this._router);
}
private get _mainBreadcrumb(): Breadcrumb {
private get _dashboardBreadcrumb(): Breadcrumb {
return {
name$: of(this._translateService.instant('top-bar.navigation-items.dossiers')),
name$: of(this._translateService.instant('top-bar.navigation-items.dashboard')),
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'],
routerLink: ['/main', 'dashboard'],
routerLinkActiveOptions: { exact: true },
},
};
@ -89,81 +80,92 @@ export class BreadcrumbsService {
this._store$.next([]);
}
private _addBreadcrumbs(route: ActivatedRouteSnapshot) {
private _addBreadcrumbs(route: ActivatedRouteSnapshot, params: Record<string, string> = {}) {
if (route.firstChild) {
this._addBreadcrumbs(route.firstChild);
this._addBreadcrumbs(route.firstChild, { ...params, ...route.params });
return;
}
const breadcrumbs = route.data.breadcrumbs || [];
if (breadcrumbs.length === 1 && this._featuresService.isEnabled(DOSSIERS_ARCHIVE)) {
if (breadcrumbs[0] === BreadcrumbTypes.main) {
this._addMainDropdownBreadcrumb('active');
if (breadcrumbs[0] === BreadcrumbTypes.dossierTemplate) {
this._addDossierTemplateDropdown(params);
return;
}
if (breadcrumbs[0] === BreadcrumbTypes.archive) {
this._addMainDropdownBreadcrumb('archived');
if (breadcrumbs[0] === BreadcrumbTypes.dashboard) {
this._append(this._dashboardBreadcrumb);
return;
}
}
for (const breadcrumb of breadcrumbs) {
switch (breadcrumb) {
case BreadcrumbTypes.main:
this._append(this._mainBreadcrumb);
break;
case BreadcrumbTypes.archive:
this._append(this._archiveBreadcrumb);
case BreadcrumbTypes.dossierTemplate:
this._append(this._dossierTemplateBreadcrumb(params));
break;
case BreadcrumbTypes.dossier:
this._addDossierBreadcrumb(route);
this._addDossierBreadcrumb(params);
break;
case BreadcrumbTypes.file:
this._addFileBreadcrumb(route);
this._addFileBreadcrumb(params);
break;
}
}
}
private _addMainDropdownBreadcrumb(type: 'active' | 'archived'): void {
const activeDossiers: Breadcrumb = this._mainBreadcrumb;
const archivedDossiers: Breadcrumb = this._archiveBreadcrumb;
const activeOption = type === 'active' ? activeDossiers : archivedDossiers;
private _addDossierTemplateDropdown(params: Record<string, string>): void {
const breadcrumbs = this._dashboardStatsService.all
.filter(dt => !dt.isEmpty)
.map(dt => this._dossierTemplateBreadcrumb({ dossierTemplateId: dt.id }));
const activeOption = breadcrumbs.find(b => b.options.routerLink[1] === params[DOSSIER_TEMPLATE_ID]);
this._append({
name$: activeOption.name$,
type: 'dropdown' as BreadcrumbDisplayType,
options: {
options: [activeDossiers, archivedDossiers],
options: breadcrumbs,
activeOption,
},
});
}
private _addDossierBreadcrumb(route: ActivatedRouteSnapshot): void {
private _dossierTemplateBreadcrumb(params: Record<string, string>): Breadcrumb {
const dossierTemplateId: string = params[DOSSIER_TEMPLATE_ID];
return {
name$: this._dashboardStatsService.getEntityChanged$(dossierTemplateId).pipe(pluck('name')),
type: 'text' as BreadcrumbDisplayType,
options: {
routerLink: ['/main', dossierTemplateId, this._dossiersService.routerPath],
routerLinkActiveOptions: { exact: true },
clamp: true,
},
};
}
private _addDossierBreadcrumb(params: Record<string, string>): void {
const dossiersService = this._dossiersService;
const dossierId = route.paramMap.get(DOSSIER_ID);
const dossierId: string = params[DOSSIER_ID];
this._append({
name$: dossiersService.getEntityChanged$(dossierId).pipe(pluck('dossierName')),
type: 'text' as BreadcrumbDisplayType,
options: {
routerLink: ['/main', dossiersService.routerPath, dossierId],
routerLink: [dossiersService.find(dossierId).routerLink],
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;
private _addFileBreadcrumb(params: Record<string, string>): void {
const dossierId: string = params[DOSSIER_ID];
const fileId: string = params[FILE_ID];
this._append({
name$: this._filesMapService.watch$(dossierId, fileId).pipe(pluck('filename')),
type: 'text' as BreadcrumbDisplayType,
options: {
routerLink: ['/main', dossiersService.routerPath, dossierId, 'file', fileId],
routerLink: [this._filesMapService.get(dossierId, fileId).routerLink],
clamp: true,
},
});

View File

@ -0,0 +1,29 @@
import { EntitiesService, mapEach } from '@iqser/common-ui';
import { DashboardStats, IDashboardStats } from '@red/domain';
import { Injectable, Injector } from '@angular/core';
import { NGXLogger } from 'ngx-logger';
import { Observable } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators';
import { DossierStatesService } from '@services/entity-services/dossier-states.service';
@Injectable({
providedIn: 'root',
})
export class DashboardStatsService extends EntitiesService<DashboardStats, IDashboardStats> {
constructor(
protected readonly _injector: Injector,
private readonly _dossierStatesService: DossierStatesService,
private readonly _logger: NGXLogger,
) {
super(_injector, DashboardStats, 'dossier-template/stats');
}
loadAll(): Observable<DashboardStats[]> {
return this.getAll(this._defaultModelPath).pipe(
mapEach(entity => new DashboardStats(entity)),
switchMap(entities => this._dossierStatesService.loadAllForAllTemplates().pipe(map(() => entities))),
tap(entities => entities.sort((a, b) => (a.numberOfActiveDossiers > 0 && b.numberOfActiveDossiers === 0 ? -1 : 1))),
tap(entities => this.setEntities(entities)),
);
}
}

View File

@ -2,12 +2,12 @@ import { EntitiesService, List, mapEach, RequiredParam, Toaster, Validate } from
import { DossierTemplate, IDossierTemplate } from '@red/domain';
import { Injectable, Injector } from '@angular/core';
import { forkJoin, Observable, of } from 'rxjs';
import { FileAttributesService } from './file-attributes.service';
import { FileAttributesService } from '../entity-services/file-attributes.service';
import { catchError, mapTo, switchMap, tap } from 'rxjs/operators';
import { DossierTemplateStatsService } from '@services/entity-services/dossier-template-stats.service';
import { DossierTemplateStatsService } from '../entity-services/dossier-template-stats.service';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { DictionaryService } from '@services/entity-services/dictionary.service';
import { DictionaryService } from '../entity-services/dictionary.service';
const DOSSIER_TEMPLATE_CONFLICT_MSG = _('dossier-templates-listing.error.conflict');
const GENERIC_MSG = _('dossier-templates-listing.error.generic');

View File

@ -1,21 +1,25 @@
import { Injectable, Injector } from '@angular/core';
import { switchMap, tap } from 'rxjs/operators';
import { timer } from 'rxjs';
import { CHANGED_CHECK_INTERVAL } from '@utils/constants';
import { CHANGED_CHECK_INTERVAL, DOSSIERS_ROUTE } from '@utils/constants';
import { DossiersService } from './dossiers.service';
export interface IDossiersStats {
totalPeople: number;
totalAnalyzedPages: number;
}
import { Observable, timer } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
import { Dossier } from '@red/domain';
@Injectable({
providedIn: 'root',
})
export class ActiveDossiersService extends DossiersService {
constructor(protected readonly _injector: Injector) {
super(_injector, 'dossier', 'dossiers');
private _initializedRefresh = false;
constructor(protected readonly _injector: Injector) {
super(_injector, 'dossier', DOSSIERS_ROUTE);
}
initializeRefresh() {
if (this._initializedRefresh) {
return;
}
this._initializedRefresh = true;
timer(CHANGED_CHECK_INTERVAL, CHANGED_CHECK_INTERVAL)
.pipe(
switchMap(() => this.loadOnlyChanged()),
@ -24,7 +28,8 @@ export class ActiveDossiersService extends DossiersService {
.subscribe();
}
getCountWithState(dossierStatusId: string): number {
return this.all.filter(dossier => dossier.dossierStatusId === dossierStatusId).length;
loadAll(): Observable<Dossier[]> {
this.initializeRefresh();
return super.loadAll();
}
}

View File

@ -7,7 +7,7 @@ import { ActiveDossiersService } from './active-dossiers.service';
import { DossiersService } from './dossiers.service';
import { FilesMapService } from '../entity-services/files-map.service';
import { FeaturesService } from '@services/features.service';
import { DOSSIERS_ARCHIVE } from '@utils/constants';
import { ARCHIVE_ROUTE, DOSSIERS_ARCHIVE } from '@utils/constants';
@Injectable({ providedIn: 'root' })
export class ArchivedDossiersService extends DossiersService {
@ -17,7 +17,7 @@ export class ArchivedDossiersService extends DossiersService {
private readonly _filesMapService: FilesMapService,
private readonly _featuresService: FeaturesService,
) {
super(_injector, 'archived-dossiers', 'archive');
super(_injector, 'archived-dossiers', ARCHIVE_ROUTE);
}
archive(dossiers: Dossier[]): Observable<unknown> {

View File

@ -0,0 +1,52 @@
import { EventEmitter, Injectable } from '@angular/core';
import { ActiveDossiersService } from './active-dossiers.service';
import { ArchivedDossiersService } from './archived-dossiers.service';
import { firstValueFrom, forkJoin, merge } from 'rxjs';
import { map, skip, take } from 'rxjs/operators';
import { Dossier } from '@red/domain';
@Injectable({
providedIn: 'root',
})
export class DossiersCacheService {
readonly changed$ = new EventEmitter<void>();
private _dossiers: Dossier[] = JSON.parse(localStorage.getItem('dossiers')) || [];
constructor(
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _archivedDossiersService: ArchivedDossiersService,
) {
// Skip 1 to avoid clearing the cache when the dossier services are initialized
merge(_activeDossiersService.all$.pipe(skip(1)), _archivedDossiersService.all$.pipe(skip(1))).subscribe(() => {
this.set();
});
}
get empty(): boolean {
return !localStorage.getItem('dossiers');
}
get all(): Dossier[] {
return this._dossiers;
}
async load(): Promise<void> {
await firstValueFrom(
forkJoin([this._activeDossiersService.loadAll().pipe(take(1)), this._archivedDossiersService.loadAll().pipe(take(1))]).pipe(
map(list => list.flat()),
),
);
this.set();
}
set(): void {
const dossiers = [this._activeDossiersService.all, this._archivedDossiersService.all].flat();
this._dossiers = dossiers;
localStorage.setItem('dossiers', JSON.stringify(dossiers));
this.changed$.emit();
}
get(dossierId: string) {
return this._dossiers.find(dossier => dossier.id === dossierId);
}
}

View File

@ -1,23 +1,21 @@
import { EntitiesService, List, mapEach, QueryParam, RequiredParam, shareLast, Toaster, Validate } from '@iqser/common-ui';
import { EntitiesService, List, mapEach, QueryParam, RequiredParam, Toaster, Validate } from '@iqser/common-ui';
import { Dossier, DossierStats, IDossier, IDossierChanges, IDossierRequest } from '@red/domain';
import { combineLatest, forkJoin, Observable, of, Subject, throwError } from 'rxjs';
import { forkJoin, Observable, of, Subject, throwError } from 'rxjs';
import { catchError, filter, map, switchMap, tap } from 'rxjs/operators';
import { Injector } from '@angular/core';
import { DossierStatesService } from '../entity-services/dossier-states.service';
import { DossierStatsService } from './dossier-stats.service';
import { IDossiersStats } from './active-dossiers.service';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { NGXLogger } from 'ngx-logger';
import { DashboardStatsService } from '@services/dossier-templates/dashboard-stats.service';
const CONFLICT_MSG = _('add-dossier-dialog.errors.dossier-already-exists');
const GENERIC_MSG = _('add-dossier-dialog.errors.generic');
export abstract class DossiersService extends EntitiesService<Dossier, IDossier> {
readonly dossierFileChanges$ = new Subject<string>();
readonly generalStats$ = this.all$.pipe(switchMap(entities => this.#generalStats$(entities)));
protected readonly _dossierStatsService = this._injector.get(DossierStatsService);
protected readonly _dossierStateService = this._injector.get(DossierStatesService);
protected readonly _dashboardStatsService = this._injector.get(DashboardStatsService);
protected readonly _toaster = this._injector.get(Toaster);
protected readonly _logger = this._injector.get(NGXLogger);
@ -61,10 +59,10 @@ export abstract class DossiersService extends EntitiesService<Dossier, IDossier>
loadAll(): Observable<Dossier[]> {
const dossierIds = (dossiers: Dossier[]) => dossiers.map(d => d.id);
return this.getAll().pipe(
mapEach(entity => new Dossier(entity, this.routerPath)),
mapEach(entity => new Dossier(entity)),
/* Load stats before updating entities */
switchMap(dossiers => this._dossierStatsService.getFor(dossierIds(dossiers)).pipe(map(() => dossiers))),
switchMap(dossiers => this._dossierStateService.loadAllForAllTemplates().pipe(map(() => dossiers))),
switchMap(dossiers => this._dashboardStatsService.loadAll().pipe(map(() => dossiers))),
tap(dossiers => this.setEntities(dossiers)),
);
}
@ -81,33 +79,9 @@ export abstract class DossiersService extends EntitiesService<Dossier, IDossier>
private _load(id: string): Observable<DossierStats[]> {
const queryParams: List<QueryParam> = [{ key: 'includeArchived', value: this._path === 'archived-dossiers' }];
return super._getOne([id], this._defaultModelPath, queryParams).pipe(
map(entity => new Dossier(entity, 'dossiers')),
map(entity => new Dossier(entity)),
tap(dossier => this.replace(dossier)),
switchMap(dossier => this._dossierStatsService.getFor([dossier.dossierId])),
);
}
#computeStats(entities: List<Dossier>): IDossiersStats {
let totalAnalyzedPages = 0;
const totalPeople = new Set<string>();
entities.forEach(dossier => {
dossier.memberIds?.forEach(m => totalPeople.add(m));
totalAnalyzedPages += this._dossierStatsService.get(dossier.dossierId).numberOfPages;
});
return {
totalPeople: totalPeople.size,
totalAnalyzedPages,
};
}
#generalStats$(entities: List<Dossier>): Observable<IDossiersStats> {
const stats$ = entities.map(entity => this._dossierStatsService.watch$(entity.dossierId));
return combineLatest(stats$).pipe(
filter(stats => stats.every(s => !!s)),
map(() => this.#computeStats(entities)),
shareLast(),
);
}
}

View File

@ -2,7 +2,7 @@ import { Injectable, Injector } from '@angular/core';
import { EntitiesService, mapEach, RequiredParam, Toaster, Validate } from '@iqser/common-ui';
import { DossierState, IDossierState } from '@red/domain';
import { EMPTY, forkJoin, Observable, switchMap } from 'rxjs';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { catchError, defaultIfEmpty, tap } from 'rxjs/operators';
import { DossierStatesMapService } from '@services/entity-services/dossier-states-map.service';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';

View File

@ -1,11 +1,11 @@
import { ActivatedRoute } from '@angular/router';
import { Router } from '@angular/router';
import { Injector, ProviderToken } from '@angular/core';
import { DossiersService } from '../dossiers/dossiers.service';
/** Usage in components or services is only allowed in guards or in constructors.
* Otherwise, it causes errors on navigation to other screens if the component is reused. */
export const dossiersServiceResolver = (injector: Injector) => {
let route: ActivatedRoute = injector.get<ActivatedRoute>(ActivatedRoute);
export const dossiersServiceResolver = (injector: Injector, router: Router) => {
let route = router.routerState.root;
while (route.firstChild) {
route = route.firstChild;
}
@ -16,5 +16,5 @@ export const dossiersServiceResolver = (injector: Injector) => {
export const dossiersServiceProvider = {
provide: DossiersService,
useFactory: dossiersServiceResolver,
deps: [Injector],
deps: [Injector, Router],
};

View File

@ -1,11 +1,11 @@
import { GenericService, HeadersConfiguration, IRouterPath, List, QueryParam, RequiredParam, Validate } from '@iqser/common-ui';
import { GenericService, HeadersConfiguration, List, QueryParam, RequiredParam, Validate } from '@iqser/common-ui';
import { Injectable, Injector } from '@angular/core';
import { HttpHeaders, HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { FilesService } from '@services/entity-services/files.service';
import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
import { IPageRotationRequest } from '@red/domain';
import { File, IPageRotationRequest } from '@red/domain';
@Injectable({
providedIn: 'root',
@ -20,10 +20,9 @@ export class FileManagementService extends GenericService<unknown> {
}
@Validate()
delete(@RequiredParam() files: List<IRouterPath>, @RequiredParam() dossierId: string) {
delete(@RequiredParam() files: List<File>, @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)));
return super._post(fileIds, `delete/${dossierId}`).pipe(switchMap(() => this._filesService.loadAll(dossierId)));
}
@Validate()

View File

@ -12,11 +12,7 @@ export class FilesMapService extends EntitiesMapService<File, IFile> {
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,
file.routerPath,
),
new File({ ...file, [property]: generateValue(file[property]), lastUpdated: new Date().toISOString() }, file.reviewerName),
);
this.replace(newFiles);
}

View File

@ -1,5 +1,5 @@
import { Injectable, Injector } from '@angular/core';
import { EntitiesService, IRouterPath, List, mapEach, RequiredParam, Validate } from '@iqser/common-ui';
import { EntitiesService, List, mapEach, RequiredParam, Validate } from '@iqser/common-ui';
import { File, IFile } from '@red/domain';
import { Observable } from 'rxjs';
import { UserService } from '../user.service';
@ -23,9 +23,9 @@ export class FilesService extends EntitiesService<File, IFile> {
}
/** Reload dossier files + stats. */
loadAll(dossierId: string, routerPath: string) {
loadAll(dossierId: string) {
const files$ = this.getFor(dossierId).pipe(
mapEach(file => new File(file, this._userService.getNameForId(file.assignee), routerPath)),
mapEach(file => new File(file, this._userService.getNameForId(file.assignee))),
tap(() => this._logger.info('[FILE] Loaded')),
);
const loadStats$ = files$.pipe(switchMap(files => this._dossierStatsService.getFor([dossierId]).pipe(map(() => files))));
@ -34,7 +34,7 @@ export class FilesService extends EntitiesService<File, IFile> {
reload(dossierId: string, file: File): Observable<boolean> {
return super._getOne([dossierId, file.id]).pipe(
map(_file => new File(_file, this._userService.getNameForId(_file.assignee), file.routerPath)),
map(_file => new File(_file, this._userService.getNameForId(_file.assignee))),
switchMap(_file => this._dossierStatsService.getFor([dossierId]).pipe(map(() => _file))),
map(_file => this._filesMapService.replace([_file])),
tap(() => this._logger.info('[FILE] Reloaded')),
@ -42,56 +42,46 @@ export class FilesService extends EntitiesService<File, IFile> {
}
@Validate()
setUnassigned(@RequiredParam() files: List<IRouterPath>, @RequiredParam() dossierId: string) {
setUnassigned(@RequiredParam() files: List<File>, @RequiredParam() dossierId: string) {
const url = `${this._defaultModelPath}/set-assignee/${dossierId}/bulk`;
const fileIds = files.map(f => f.id);
const routerPath: string = files[0].routerPath;
return this._post<unknown>(fileIds, url).pipe(switchMap(() => this.loadAll(dossierId, routerPath)));
return this._post<unknown>(fileIds, url).pipe(switchMap(() => this.loadAll(dossierId)));
}
@Validate()
setToNewFor(@RequiredParam() files: List<IRouterPath>, @RequiredParam() dossierId: string) {
setToNewFor(@RequiredParam() files: List<File>, @RequiredParam() dossierId: string) {
const url = `${this._defaultModelPath}/new/${dossierId}/bulk`;
const fileIds = files.map(f => f.id);
const routerPath: string = files[0].routerPath;
return this._post<unknown>(fileIds, url).pipe(switchMap(() => this.loadAll(dossierId, routerPath)));
return this._post<unknown>(fileIds, url).pipe(switchMap(() => this.loadAll(dossierId)));
}
@Validate()
setUnderApprovalFor(@RequiredParam() files: List<IRouterPath>, @RequiredParam() dossierId: string, assigneeId: string) {
setUnderApprovalFor(@RequiredParam() files: List<File>, @RequiredParam() dossierId: string, assigneeId: string) {
const url = `${this._defaultModelPath}/under-approval/${dossierId}/bulk`;
const fileIds = files.map(f => f.id);
const routerPath: string = files[0].routerPath;
return this._post<unknown>(fileIds, url, [{ key: 'assigneeId', value: assigneeId }]).pipe(
switchMap(() => this.loadAll(dossierId, routerPath)),
);
return this._post<unknown>(fileIds, url, [{ key: 'assigneeId', value: assigneeId }]).pipe(switchMap(() => this.loadAll(dossierId)));
}
@Validate()
setReviewerFor(@RequiredParam() files: List<IRouterPath>, @RequiredParam() dossierId: string, assigneeId: string) {
setReviewerFor(@RequiredParam() files: List<File>, @RequiredParam() dossierId: string, assigneeId: string) {
const url = `${this._defaultModelPath}/under-review/${dossierId}/bulk`;
const fileIds = files.map(f => f.id);
const routerPath: string = files[0].routerPath;
return this._post<unknown>(fileIds, url, [{ key: 'assigneeId', value: assigneeId }]).pipe(
switchMap(() => this.loadAll(dossierId, routerPath)),
);
return this._post<unknown>(fileIds, url, [{ key: 'assigneeId', value: assigneeId }]).pipe(switchMap(() => this.loadAll(dossierId)));
}
@Validate()
setApprovedFor(@RequiredParam() files: List<IRouterPath>, @RequiredParam() dossierId: string) {
setApprovedFor(@RequiredParam() files: List<File>, @RequiredParam() dossierId: string) {
const fileIds = files.map(f => f.id);
const routerPath: string = files[0].routerPath;
return this._post<unknown>(fileIds, `${this._defaultModelPath}/approved/${dossierId}/bulk`).pipe(
switchMap(() => this.loadAll(dossierId, routerPath)),
switchMap(() => this.loadAll(dossierId)),
);
}
@Validate()
setUnderReviewFor(@RequiredParam() files: List<IRouterPath>, @RequiredParam() dossierId: string) {
setUnderReviewFor(@RequiredParam() files: List<File>, @RequiredParam() dossierId: string) {
const fileIds = files.map(f => f.id);
const routerPath: string = files[0].routerPath;
return this._post<unknown>(fileIds, `${this._defaultModelPath}/under-review/${dossierId}/bulk`).pipe(
switchMap(() => this.loadAll(dossierId, routerPath)),
switchMap(() => this.loadAll(dossierId)),
);
}
}

View File

@ -2,20 +2,17 @@ import { Injectable, Injector } from '@angular/core';
import { GenericService } from '@iqser/common-ui';
import { Dossier, IMatchedDocument, ISearchInput, ISearchRequest, ISearchResponse } from '@red/domain';
import { Observable, of, zip } from 'rxjs';
import { mapTo, switchMap } from 'rxjs/operators';
import { ActiveDossiersService } from '../dossiers/active-dossiers.service';
import { map, switchMap } from 'rxjs/operators';
import { FilesMapService } from './files-map.service';
import { FilesService } from './files.service';
import { ArchivedDossiersService } from '../dossiers/archived-dossiers.service';
import { DossiersService } from '../dossiers/dossiers.service';
import { DossiersCacheService } from '../dossiers/dossiers-cache.service';
@Injectable({ providedIn: 'root' })
export class PlatformSearchService extends GenericService<ISearchResponse> {
constructor(
protected readonly _injector: Injector,
private readonly _filesService: FilesService,
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _archivedDossiersService: ArchivedDossiersService,
private readonly _dossiersCacheService: DossiersCacheService,
private readonly _filesMapService: FilesMapService,
) {
super(_injector, 'search-v2');
@ -42,20 +39,19 @@ export class PlatformSearchService extends GenericService<ISearchResponse> {
return this._post(body).pipe(switchMap(searchValue => this._loadMissingFiles$(searchValue)));
}
private _dossiersWithMissingFiles(searchResponse: ISearchResponse, service: DossiersService): Dossier[] {
const documentsOfType = searchResponse.matchedDocuments.filter(document => service.has(document.dossierId));
private _dossiersWithMissingFiles(searchResponse: ISearchResponse): Dossier[] {
const documentsOfType = searchResponse.matchedDocuments.filter(document => this._dossiersCacheService.get(document.dossierId));
const fileNotLoaded = ({ dossierId, fileId }: IMatchedDocument) => !this._filesMapService.get(dossierId, fileId);
const dossiersWithNotLoadedFiles = documentsOfType.filter(fileNotLoaded).map(document => document.dossierId);
return Array.from(new Set(dossiersWithNotLoadedFiles)).map(dossierId => service.find(dossierId));
return Array.from(new Set(dossiersWithNotLoadedFiles)).map(dossierId => this._dossiersCacheService.get(dossierId));
}
private _loadMissingFiles$(searchResponse: ISearchResponse): Observable<ISearchResponse> {
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);
const dossiers = this._dossiersWithMissingFiles(searchResponse);
return dossiers.length ? this._loadFilesFor$(dossiers).pipe(map(() => searchResponse)) : of(searchResponse);
}
private _loadFilesFor$(dossiers: Dossier[]) {
return zip(...dossiers.map(dossier => this._filesService.loadAll(dossier.id, dossier.routerPath)));
return zip(...dossiers.map(dossier => this._filesService.loadAll(dossier.id)));
}
}

View File

@ -12,11 +12,6 @@ import { flatMap } from 'lodash-es';
import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
import { FilesService } from '@services/entity-services/files.service';
export interface IDossiersStats {
totalPeople: number;
totalAnalyzedPages: number;
}
@Injectable({
providedIn: 'root',
})
@ -39,7 +34,7 @@ export class TrashService extends GenericService<TrashItem> {
this._toaster.error(_('dossier-listing.delete.delete-failed'), { params: dossier });
return of({});
};
return this.delete(dossier.dossierId, 'dossier').pipe(
return this.delete(dossier.id, 'dossier').pipe(
switchMap(() => this._activeDossiersService.loadAll()),
catchError(showToast),
);
@ -81,7 +76,7 @@ export class TrashService extends GenericService<TrashItem> {
getFiles(dossierIds = this._activeDossiersService.all.map(d => d.id)): Observable<TrashFile[]> {
return this._post<Record<string, IFile[]>>(dossierIds, 'status/softdeleted').pipe(
map(res => flatMap(Object.values(res))),
mapEach(file => new File(file, this._userService.getNameForId(file.assignee), this._activeDossiersService.routerPath)),
mapEach(file => new File(file, this._userService.getNameForId(file.assignee))),
mapEach(file => {
const dossier = this._activeDossiersService.find(file.dossierId);
return new TrashFile(
@ -133,6 +128,6 @@ export class TrashService extends GenericService<TrashItem> {
}
private _restoreFiles(@RequiredParam() dossierId: string, @RequiredParam() fileIds: List) {
return this._post(fileIds, `delete/restore/${dossierId}`).pipe(switchMap(() => this._filesService.loadAll(dossierId, 'dossiers')));
return this._post(fileIds, `delete/restore/${dossierId}`).pipe(switchMap(() => this._filesService.loadAll(dossierId)));
}
}

View File

@ -6,12 +6,11 @@ import { Dossier, INotification, Notification, NotificationTypes } from '@red/do
import { map, switchMap, tap } from 'rxjs/operators';
import { notificationsTranslations } from '../translations/notifications-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ActiveDossiersService } from './dossiers/active-dossiers.service';
import { UserService } from '@services/user.service';
import dayjs from 'dayjs';
import { CHANGED_CHECK_INTERVAL } from '@utils/constants';
import { BASE_HREF } from '../tokens';
import { ArchivedDossiersService } from '@services/dossiers/archived-dossiers.service';
import { DossiersCacheService } from '@services/dossiers/dossiers-cache.service';
const INCLUDE_SEEN = false;
@ -23,19 +22,22 @@ export class NotificationsService extends EntitiesService<Notification, INotific
@Inject(BASE_HREF) private readonly _baseHref: string,
protected readonly _injector: Injector,
private readonly _translateService: TranslateService,
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _archivedDossiersService: ArchivedDossiersService,
private readonly _userService: UserService,
private readonly _dossiersCacheService: DossiersCacheService,
) {
super(_injector, Notification, 'notification');
timer(0, CHANGED_CHECK_INTERVAL)
.pipe(
switchMap(() => (this._activeDossiersService.all.length ? of(null) : this._activeDossiersService.loadAll())),
switchMap(() => (this._archivedDossiersService.all.length ? of(null) : this._archivedDossiersService.loadAll())),
switchMap(() => (this._dossiersCacheService.empty ? this._dossiersCacheService.load() : of(null))),
switchMap(() => this.#loadNotificationsIfChanged()),
)
.subscribe();
// Rebuild notifications when cached dossiers are updated
this._dossiersCacheService.changed$.subscribe(() => {
this.setEntities(this.all.map(e => this._new(e)));
});
}
@Validate()
@ -75,7 +77,7 @@ export class NotificationsService extends EntitiesService<Notification, INotific
private _translate(notification: INotification, translation: string): string {
const fileId = notification.target.fileId;
const dossierId = notification.target.dossierId;
const dossier = this._activeDossiersService.find(dossierId) || this._archivedDossiersService.find(dossierId);
const dossier = this._dossiersCacheService.get(dossierId);
const fileName = notification.target.fileName;
return this._translateService.instant(translation, {

View File

@ -33,7 +33,7 @@ export class PermissionsService {
}
displayReanalyseBtn(dossier: Dossier): boolean {
return this.isApprover(dossier) && !!this._filesMapService.get(dossier.dossierId).find(f => f.analysisRequired);
return this.isApprover(dossier) && !!this._filesMapService.get(dossier.id).find(f => f.analysisRequired);
}
canUploadFiles(dossier: Dossier): boolean {

View File

@ -1,5 +1,5 @@
import { Injectable, Injector } from '@angular/core';
import { GenericService, IRouterPath, List, QueryParam, RequiredParam, Toaster, Validate } from '@iqser/common-ui';
import { GenericService, 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';
@ -36,9 +36,8 @@ export class ReanalysisService extends GenericService<unknown> {
}
@Validate()
reanalyzeFilesForDossier(@RequiredParam() files: List<IRouterPath>, @RequiredParam() dossierId: string, params?: ReanalyzeQueryParams) {
reanalyzeFilesForDossier(@RequiredParam() files: List<File>, @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 });
@ -47,22 +46,19 @@ export class ReanalysisService extends GenericService<unknown> {
queryParams.push({ key: 'triggeredByUser', value: true });
}
return this._post(fileIds, `reanalyze/${dossierId}/bulk`, queryParams).pipe(
switchMap(() => this._filesService.loadAll(dossierId, routerPath)),
);
return this._post(fileIds, `reanalyze/${dossierId}/bulk`, queryParams).pipe(switchMap(() => this._filesService.loadAll(dossierId)));
}
@Validate()
toggleAnalysis(@RequiredParam() dossierId: string, @RequiredParam() files: List<IRouterPath>, excluded?: boolean) {
toggleAnalysis(@RequiredParam() dossierId: string, @RequiredParam() files: List<File>, 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, routerPath)),
switchMap(() => this._filesService.loadAll(dossierId)),
);
}
@ -86,24 +82,19 @@ export class ReanalysisService extends GenericService<unknown> {
}
@Validate()
ocrFiles(@RequiredParam() files: List<IRouterPath>, @RequiredParam() dossierId: string) {
ocrFiles(@RequiredParam() files: List<File>, @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)),
);
return this._post(fileIds, `ocr/reanalyze/${dossierId}/bulk`).pipe(switchMap(() => this._filesService.loadAll(dossierId)));
}
@Validate()
reanalyzeDossier(@RequiredParam() dossier: Dossier, force?: boolean) {
const { dossierId, routerPath } = dossier;
const { dossierId } = dossier;
const queryParams: QueryParam[] = [];
if (force) {
queryParams.push({ key: 'force', value: force });
}
return this._post({}, `reanalyze/${dossierId}`, queryParams).pipe(
switchMap(() => this._filesService.loadAll(dossierId, routerPath)),
);
return this._post({}, `reanalyze/${dossierId}`, queryParams).pipe(switchMap(() => this._filesService.loadAll(dossierId)));
}
}

View File

@ -6,11 +6,11 @@ import { filter } from 'rxjs/operators';
providedIn: 'root',
})
export class RouterHistoryService {
private _lastDossiersScreen = '/main/dossiers';
private _lastDossiersScreen = '/';
constructor(private readonly _router: Router) {
this._router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe((event: NavigationEnd) => {
if (event.url.startsWith('/main/dossiers')) {
if (event.url.includes('/dossiers') || event.url.includes('/archive')) {
this._lastDossiersScreen = event.url;
}
});
@ -18,7 +18,7 @@ export class RouterHistoryService {
navigateToLastDossiersScreen(): void {
if (this._router.url === this._lastDossiersScreen) {
this._router.navigate(['/main/dossiers']);
this._router.navigate(['/']);
} else {
this._router.navigate([this._lastDossiersScreen]);
}

Some files were not shown because too many files have changed in this diff Show More