diff --git a/apps/red-ui/src/app/app-routing.module.ts b/apps/red-ui/src/app/app-routing.module.ts index ed3ddb7b1..7f0060461 100644 --- a/apps/red-ui/src/app/app-routing.module.ts +++ b/apps/red-ui/src/app/app-routing.module.ts @@ -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', }, ]; diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts index 90bd1aba0..07c42e3e1 100644 --- a/apps/red-ui/src/app/app.module.ts +++ b/apps/red-ui/src/app/app.module.ts @@ -163,6 +163,7 @@ const components = [AppComponent, AuthErrorComponent, NotificationsComponent, Sp multi: true, useFactory: configurationInitializer, deps: [ + BASE_HREF, KeycloakService, Title, ConfigService, diff --git a/apps/red-ui/src/app/components/base-screen/base-screen.component.html b/apps/red-ui/src/app/components/base-screen/base-screen.component.html index 7040eba5c..2f6675b9e 100644 --- a/apps/red-ui/src/app/components/base-screen/base-screen.component.html +++ b/apps/red-ui/src/app/components/base-screen/base-screen.component.html @@ -7,12 +7,12 @@ -
diff --git a/apps/red-ui/src/app/components/base-screen/base-screen.component.ts b/apps/red-ui/src/app/components/base-screen/base-screen.component.ts index 12e8a0df5..943d9e36c 100644 --- a/apps/red-ui/src/app/components/base-screen/base-screen.component.ts +++ b/apps/red-ui/src/app/components/base-screen/base-screen.component.ts @@ -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; } diff --git a/apps/red-ui/src/app/components/notifications/notifications.component.ts b/apps/red-ui/src/app/components/notifications/notifications.component.ts index e86ba6b10..ea129b453 100644 --- a/apps/red-ui/src/app/components/notifications/notifications.component.ts +++ b/apps/red-ui/src/app/components/notifications/notifications.component.ts @@ -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; readonly groupedNotifications$: Observable; - 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$; } diff --git a/apps/red-ui/src/app/guards/dashboard-guard.service.ts b/apps/red-ui/src/app/guards/dashboard-guard.service.ts new file mode 100644 index 000000000..a9eec07ae --- /dev/null +++ b/apps/red-ui/src/app/guards/dashboard-guard.service.ts @@ -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 { + await firstValueFrom(this._dashboardStatsService.loadAll()); + return true; + } +} diff --git a/apps/red-ui/src/app/guards/dossier-files-guard.ts b/apps/red-ui/src/app/guards/dossier-files-guard.ts index b6229360b..9aab2878a 100644 --- a/apps/red-ui/src/app/guards/dossier-files-guard.ts +++ b/apps/red-ui/src/app/guards/dossier-files-guard.ts @@ -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 { const dossierId = route.paramMap.get(DOSSIER_ID); + const dossierTemplateId = route.paramMap.get(DOSSIER_TEMPLATE_ID); const token: ProviderToken = route.data.dossiersService; const dossiersService: DossiersService = this._injector.get(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; } diff --git a/apps/red-ui/src/app/guards/dossier-template-exists.guard.ts b/apps/red-ui/src/app/guards/dossier-template-exists.guard.ts index 850ba72ca..b53592a16 100644 --- a/apps/red-ui/src/app/guards/dossier-template-exists.guard.ts +++ b/apps/red-ui/src/app/guards/dossier-template-exists.guard.ts @@ -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 { 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; } diff --git a/apps/red-ui/src/app/guards/dossier-templates.guard.ts b/apps/red-ui/src/app/guards/dossier-templates.guard.ts index 9a163ee8f..7495d23ff 100644 --- a/apps/red-ui/src/app/guards/dossier-templates.guard.ts +++ b/apps/red-ui/src/app/guards/dossier-templates.guard.ts @@ -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 { await firstValueFrom(this._dossierTemplatesService.loadAll()); diff --git a/apps/red-ui/src/app/guards/entity-exists-guard.service.ts b/apps/red-ui/src/app/guards/entity-exists-guard.service.ts index f363829ff..012583808 100644 --- a/apps/red-ui/src/app/guards/entity-exists-guard.service.ts +++ b/apps/red-ui/src/app/guards/entity-exists-guard.service.ts @@ -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 { diff --git a/apps/red-ui/src/app/modules/admin/base-admin-screen/base-admin-screen.component.scss b/apps/red-ui/src/app/modules/admin/base-admin-screen/base-admin-screen.component.scss index e7a72018d..333a85d49 100644 --- a/apps/red-ui/src/app/modules/admin/base-admin-screen/base-admin-screen.component.scss +++ b/apps/red-ui/src/app/modules/admin/base-admin-screen/base-admin-screen.component.scss @@ -1,3 +1,3 @@ :host { - display: flex; + flex-direction: row; } diff --git a/apps/red-ui/src/app/modules/admin/base-entity-screen/base-entity-screen.component.ts b/apps/red-ui/src/app/modules/admin/base-entity-screen/base-entity-screen.component.ts index 3551ba542..e76e6980e 100644 --- a/apps/red-ui/src/app/modules/admin/base-entity-screen/base-entity-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/base-entity-screen/base-entity-screen.component.ts @@ -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'; diff --git a/apps/red-ui/src/app/modules/admin/components/dossier-template-breadcrumbs/dossier-template-breadcrumbs.component.ts b/apps/red-ui/src/app/modules/admin/components/dossier-template-breadcrumbs/dossier-template-breadcrumbs.component.ts index da0e9ac58..b5108a29e 100644 --- a/apps/red-ui/src/app/modules/admin/components/dossier-template-breadcrumbs/dossier-template-breadcrumbs.component.ts +++ b/apps/red-ui/src/app/modules/admin/components/dossier-template-breadcrumbs/dossier-template-breadcrumbs.component.ts @@ -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'; diff --git a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-state-dialog/add-edit-dossier-state-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-state-dialog/add-edit-dossier-state-dialog.component.ts index f9ad50bd4..c3c8dab56 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-state-dialog/add-edit-dossier-state-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-state-dialog/add-edit-dossier-state-dialog.component.ts @@ -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, @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) {} diff --git a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-template-dialog/add-edit-dossier-template-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-template-dialog/add-edit-dossier-template-dialog.component.ts index 8a5534542..e98272044 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-template-dialog/add-edit-dossier-template-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-template-dialog/add-edit-dossier-template-dialog.component.ts @@ -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'; diff --git a/apps/red-ui/src/app/modules/admin/dialogs/clone-dossier-template-dialog/clone-dossier-template-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/clone-dossier-template-dialog/clone-dossier-template-dialog.component.ts index 51437e070..c26e9b629 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/clone-dossier-template-dialog/clone-dossier-template-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/clone-dossier-template-dialog/clone-dossier-template-dialog.component.ts @@ -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(); - } } diff --git a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-dossier-state-dialog/confirm-delete-dossier-state-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-dossier-state-dialog/confirm-delete-dossier-state-dialog.component.ts index 910b6e977..5d32ec0c7 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-dossier-state-dialog/confirm-delete-dossier-state-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-dossier-state-dialog/confirm-delete-dossier-state-dialog.component.ts @@ -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, private readonly _activeDossiersService: ActiveDossiersService, private readonly _archivedDossiersService: ArchivedDossiersService, @@ -55,7 +55,7 @@ export class ConfirmDeleteDossierStateDialogComponent { async save(): Promise { 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))]), ); diff --git a/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-configurations-dialog/file-attributes-configurations-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-configurations-dialog/file-attributes-configurations-dialog.component.ts index 6c20af995..45334ed9c 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-configurations-dialog/file-attributes-configurations-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-configurations-dialog/file-attributes-configurations-dialog.component.ts @@ -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'; diff --git a/apps/red-ui/src/app/modules/admin/screens/default-colors/default-colors-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/default-colors/default-colors-screen.component.ts index d93f3e956..97643322c 100644 --- a/apps/red-ui/src/app/modules/admin/screens/default-colors/default-colors-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/default-colors/default-colors-screen.component.ts @@ -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'; diff --git a/apps/red-ui/src/app/modules/admin/screens/dossier-templates-listing/dossier-templates-listing-screen/dossier-templates-listing-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/dossier-templates-listing/dossier-templates-listing-screen/dossier-templates-listing-screen.component.ts index 896cd6ed9..1059bbc20 100644 --- a/apps/red-ui/src/app/modules/admin/screens/dossier-templates-listing/dossier-templates-listing-screen/dossier-templates-listing-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/dossier-templates-listing/dossier-templates-listing-screen/dossier-templates-listing-screen.component.ts @@ -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({ diff --git a/apps/red-ui/src/app/modules/admin/screens/file-attributes-listing/file-attributes-listing-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/file-attributes-listing/file-attributes-listing-screen.component.ts index a06e24ba3..36f8ee278 100644 --- a/apps/red-ui/src/app/modules/admin/screens/file-attributes-listing/file-attributes-listing-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/file-attributes-listing/file-attributes-listing-screen.component.ts @@ -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({ diff --git a/apps/red-ui/src/app/modules/admin/screens/info/info-screen/dossier-template-info-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/info/info-screen/dossier-template-info-screen.component.ts index e1cfc90a0..c953e8a70 100644 --- a/apps/red-ui/src/app/modules/admin/screens/info/info-screen/dossier-template-info-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/info/info-screen/dossier-template-info-screen.component.ts @@ -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'; diff --git a/apps/red-ui/src/app/modules/admin/screens/justifications/add-edit-justification-dialog/add-edit-justification-dialog.component.ts b/apps/red-ui/src/app/modules/admin/screens/justifications/add-edit-justification-dialog/add-edit-justification-dialog.component.ts index 5a53c1d98..4d61f4f73 100644 --- a/apps/red-ui/src/app/modules/admin/screens/justifications/add-edit-justification-dialog/add-edit-justification-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/justifications/add-edit-justification-dialog/add-edit-justification-dialog.component.ts @@ -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'; diff --git a/apps/red-ui/src/app/modules/admin/screens/justifications/justifications-dialog.service.ts b/apps/red-ui/src/app/modules/admin/screens/justifications/justifications-dialog.service.ts index 6fcf1f246..668de1d80 100644 --- a/apps/red-ui/src/app/modules/admin/screens/justifications/justifications-dialog.service.ts +++ b/apps/red-ui/src/app/modules/admin/screens/justifications/justifications-dialog.service.ts @@ -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'; diff --git a/apps/red-ui/src/app/modules/admin/screens/reports/reports-screen/reports-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/reports/reports-screen/reports-screen.component.ts index 04ddca911..1742a103a 100644 --- a/apps/red-ui/src/app/modules/admin/screens/reports/reports-screen/reports-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/reports/reports-screen/reports-screen.component.ts @@ -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'; diff --git a/apps/red-ui/src/app/modules/admin/screens/trash/trash-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/trash/trash-screen.component.ts index 335e5c128..8f77182f1 100644 --- a/apps/red-ui/src/app/modules/admin/screens/trash/trash-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/trash/trash-screen.component.ts @@ -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', diff --git a/apps/red-ui/src/app/modules/admin/shared/components/dossier-template-actions/dossier-template-actions.component.ts b/apps/red-ui/src/app/modules/admin/shared/components/dossier-template-actions/dossier-template-actions.component.ts index f43070a56..172e66f72 100644 --- a/apps/red-ui/src/app/modules/admin/shared/components/dossier-template-actions/dossier-template-actions.component.ts +++ b/apps/red-ui/src/app/modules/admin/shared/components/dossier-template-actions/dossier-template-actions.component.ts @@ -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); } diff --git a/apps/red-ui/src/app/modules/archive/archive-routing.module.ts b/apps/red-ui/src/app/modules/archive/archive-routing.module.ts index 9a2774a25..d9b734f59 100644 --- a/apps/red-ui/src/app/modules/archive/archive-routing.module.ts +++ b/apps/red-ui/src/app/modules/archive/archive-routing.module.ts @@ -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), diff --git a/apps/red-ui/src/app/modules/archive/components/table-item/table-item.component.ts b/apps/red-ui/src/app/modules/archive/components/table-item/table-item.component.ts index 84c4bcd65..b978de713 100644 --- a/apps/red-ui/src/app/modules/archive/components/table-item/table-item.component.ts +++ b/apps/red-ui/src/app/modules/archive/components/table-item/table-item.component.ts @@ -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); } } } diff --git a/apps/red-ui/src/app/modules/archive/screens/archived-dossiers-screen/archived-dossiers-screen.component.html b/apps/red-ui/src/app/modules/archive/screens/archived-dossiers-screen/archived-dossiers-screen.component.html index 5786e6f44..520f31b7f 100644 --- a/apps/red-ui/src/app/modules/archive/screens/archived-dossiers-screen/archived-dossiers-screen.component.html +++ b/apps/red-ui/src/app/modules/archive/screens/archived-dossiers-screen/archived-dossiers-screen.component.html @@ -1,5 +1,9 @@
- + + + + +
diff --git a/apps/red-ui/src/app/modules/archive/screens/archived-dossiers-screen/archived-dossiers-screen.component.ts b/apps/red-ui/src/app/modules/archive/screens/archived-dossiers-screen/archived-dossiers-screen.component.ts index 2efab9ccc..f535c5b58 100644 --- a/apps/red-ui/src/app/modules/archive/screens/archived-dossiers-screen/archived-dossiers-screen.component.ts +++ b/apps/red-ui/src/app/modules/archive/screens/archived-dossiers-screen/archived-dossiers-screen.component.ts @@ -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 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() { diff --git a/apps/red-ui/src/app/modules/archive/services/config.service.ts b/apps/red-ui/src/app/modules/archive/services/config.service.ts index 227fecad7..29ebf051e 100644 --- a/apps/red-ui/src/app/modules/archive/services/config.service.ts +++ b/apps/red-ui/src/app/modules/archive/services/config.service.ts @@ -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 { diff --git a/apps/red-ui/src/app/modules/auth/auth.guard.ts b/apps/red-ui/src/app/modules/auth/auth.guard.ts index 12803b3b0..6701e12cc 100644 --- a/apps/red-ui/src/app/modules/auth/auth.guard.ts +++ b/apps/red-ui/src/app/modules/auth/auth.guard.ts @@ -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 { + async isAccessAllowed(route: ActivatedRouteSnapshot): Promise { 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; } diff --git a/apps/red-ui/src/app/modules/auth/red-role.guard.ts b/apps/red-ui/src/app/modules/auth/red-role.guard.ts index f9d43d465..e9f33c587 100644 --- a/apps/red-ui/src/app/modules/auth/red-role.guard.ts +++ b/apps/red-ui/src/app/modules/auth/red-role.guard.ts @@ -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(); } diff --git a/apps/red-ui/src/app/modules/dashboard/components/template-stats/template-stats.component.html b/apps/red-ui/src/app/modules/dashboard/components/template-stats/template-stats.component.html new file mode 100644 index 000000000..be41704eb --- /dev/null +++ b/apps/red-ui/src/app/modules/dashboard/components/template-stats/template-stats.component.html @@ -0,0 +1,77 @@ + + +
+
{{ dossierTemplate.name }}
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+
+ +
+
+ + +
+
+ {{ dossierTemplate.name }} +
+
+ {{ 'dashboard.empty-template.description' | translate }} +
+
+ +
+
diff --git a/apps/red-ui/src/app/modules/dashboard/components/template-stats/template-stats.component.scss b/apps/red-ui/src/app/modules/dashboard/components/template-stats/template-stats.component.scss new file mode 100644 index 000000000..4160bf43b --- /dev/null +++ b/apps/red-ui/src/app/modules/dashboard/components/template-stats/template-stats.component.scss @@ -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; + } +} diff --git a/apps/red-ui/src/app/modules/dashboard/components/template-stats/template-stats.component.ts b/apps/red-ui/src/app/modules/dashboard/components/template-stats/template-stats.component.ts new file mode 100644 index 000000000..c8181b4e4 --- /dev/null +++ b/apps/red-ui/src/app/modules/dashboard/components/template-stats/template-stats.component.ts @@ -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 }); + } +} diff --git a/apps/red-ui/src/app/modules/dashboard/dashboard-screen/dashboard-screen.component.html b/apps/red-ui/src/app/modules/dashboard/dashboard-screen/dashboard-screen.component.html new file mode 100644 index 000000000..b87902860 --- /dev/null +++ b/apps/red-ui/src/app/modules/dashboard/dashboard-screen/dashboard-screen.component.html @@ -0,0 +1,13 @@ +
+ +
+
+ +
+ + +
diff --git a/apps/red-ui/src/app/modules/dashboard/dashboard-screen/dashboard-screen.component.scss b/apps/red-ui/src/app/modules/dashboard/dashboard-screen/dashboard-screen.component.scss new file mode 100644 index 000000000..f646df137 --- /dev/null +++ b/apps/red-ui/src/app/modules/dashboard/dashboard-screen/dashboard-screen.component.scss @@ -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; + } +} diff --git a/apps/red-ui/src/app/modules/dashboard/dashboard-screen/dashboard-screen.component.ts b/apps/red-ui/src/app/modules/dashboard/dashboard-screen/dashboard-screen.component.ts new file mode 100644 index 000000000..94c2046d1 --- /dev/null +++ b/apps/red-ui/src/app/modules/dashboard/dashboard-screen/dashboard-screen.component.ts @@ -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 { + await this._userPreferenceService.saveLastDossierTemplate(null); + } +} diff --git a/apps/red-ui/src/app/modules/dashboard/dashboard.module.ts b/apps/red-ui/src/app/modules/dashboard/dashboard.module.ts new file mode 100644 index 000000000..db67fc124 --- /dev/null +++ b/apps/red-ui/src/app/modules/dashboard/dashboard.module.ts @@ -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 {} diff --git a/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details-stats/dossier-details-stats.component.ts b/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details-stats/dossier-details-stats.component.ts index 171927d46..3263d871c 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details-stats/dossier-details-stats.component.ts +++ b/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details-stats/dossier-details-stats.component.ts @@ -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)); }); } } diff --git a/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.ts b/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.ts index b7cd35d50..9214c1424 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.ts +++ b/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.ts @@ -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[] { diff --git a/apps/red-ui/src/app/modules/dossier-overview/components/screen-header/dossier-overview-screen-header.component.html b/apps/red-ui/src/app/modules/dossier-overview/components/screen-header/dossier-overview-screen-header.component.html index 15b9a88a0..c8e337ebf 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/components/screen-header/dossier-overview-screen-header.component.html +++ b/apps/red-ui/src/app/modules/dossier-overview/components/screen-header/dossier-overview-screen-header.component.html @@ -1,49 +1,51 @@ - + + - + - + - + + diff --git a/apps/red-ui/src/app/modules/dossier-overview/components/screen-header/dossier-overview-screen-header.component.ts b/apps/red-ui/src/app/modules/dossier-overview/components/screen-header/dossier-overview-screen-header.component.ts index 666a68e48..ced3ba7d5 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/components/screen-header/dossier-overview-screen-header.component.ts +++ b/apps/red-ui/src/app/modules/dossier-overview/components/screen-header/dossier-overview-screen-header.component.ts @@ -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() { diff --git a/apps/red-ui/src/app/modules/dossier-overview/config.service.ts b/apps/red-ui/src/app/modules/dossier-overview/config.service.ts index ba0fa6602..4c81d4ecb 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/config.service.ts +++ b/apps/red-ui/src/app/modules/dossier-overview/config.service.ts @@ -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'; diff --git a/apps/red-ui/src/app/modules/dossier-overview/screen/dossier-overview-screen.component.ts b/apps/red-ui/src/app/modules/dossier-overview/screen/dossier-overview-screen.component.ts index eb5c2d8cc..04f5092f3 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/screen/dossier-overview-screen.component.ts +++ b/apps/red-ui/src/app/modules/dossier-overview/screen/dossier-overview-screen.component.ts @@ -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 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)), ); } diff --git a/apps/red-ui/src/app/modules/dossier-overview/services/bulk-actions.service.ts b/apps/red-ui/src/app/modules/dossier-overview/services/bulk-actions.service.ts index 018dfd6b4..7877dd3c2 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/services/bulk-actions.service.ts +++ b/apps/red-ui/src/app/modules/dossier-overview/services/bulk-actions.service.ts @@ -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'; diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/add-dossier-dialog/add-dossier-dialog.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/add-dossier-dialog/add-dossier-dialog.component.ts index cb220082f..c8d84feeb 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/add-dossier-dialog/add-dossier-dialog.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/add-dossier-dialog/add-dossier-dialog.component.ts @@ -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, + @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 { + 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], diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.ts index a723bc52d..ed5407b64 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.ts @@ -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(); diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-team/edit-dossier-team.component.html b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-team/edit-dossier-team.component.html index de53985b0..c446cf066 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-team/edit-dossier-team.component.html +++ b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-team/edit-dossier-team.component.html @@ -3,7 +3,7 @@ {{ 'assign-dossier-owner.dialog.single-user' | translate }} - + {{ userId | name }} @@ -11,12 +11,12 @@
-
+
-
+
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] }, }, ]; diff --git a/apps/red-ui/src/app/modules/dossier/shared/components/file-actions/file-actions.component.ts b/apps/red-ui/src/app/modules/dossier/shared/components/file-actions/file-actions.component.ts index 406183cad..cd2d059e1 100644 --- a/apps/red-ui/src/app/modules/dossier/shared/components/file-actions/file-actions.component.ts +++ b/apps/red-ui/src/app/modules/dossier/shared/components/file-actions/file-actions.component.ts @@ -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({ diff --git a/apps/red-ui/src/app/modules/dossier/services/dossiers-dialog.service.ts b/apps/red-ui/src/app/modules/dossier/shared/services/dossiers-dialog.service.ts similarity index 71% rename from apps/red-ui/src/app/modules/dossier/services/dossiers-dialog.service.ts rename to apps/red-ui/src/app/modules/dossier/shared/services/dossiers-dialog.service.ts index 41ce299fc..18811f5d5 100644 --- a/apps/red-ui/src/app/modules/dossier/services/dossiers-dialog.service.ts +++ b/apps/red-ui/src/app/modules/dossier/shared/services/dossiers-dialog.service.ts @@ -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'; diff --git a/apps/red-ui/src/app/modules/dossier/shared/services/file-assign.service.ts b/apps/red-ui/src/app/modules/dossier/shared/services/file-assign.service.ts index 63718d1fe..b346509a5 100644 --- a/apps/red-ui/src/app/modules/dossier/shared/services/file-assign.service.ts +++ b/apps/red-ui/src/app/modules/dossier/shared/services/file-assign.service.ts @@ -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'; diff --git a/apps/red-ui/src/app/modules/dossier/shared/shared-dossiers.module.ts b/apps/red-ui/src/app/modules/dossier/shared/shared-dossiers.module.ts index 57cb8d531..99ad2410c 100644 --- a/apps/red-ui/src/app/modules/dossier/shared/shared-dossiers.module.ts +++ b/apps/red-ui/src/app/modules/dossier/shared/shared-dossiers.module.ts @@ -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'; diff --git a/apps/red-ui/src/app/modules/dossiers-listing/components/dossiers-listing-actions/dossiers-listing-actions.component.html b/apps/red-ui/src/app/modules/dossiers-listing/components/dossiers-listing-actions/dossiers-listing-actions.component.html index 8ea5f9bc5..68a0473cf 100644 --- a/apps/red-ui/src/app/modules/dossiers-listing/components/dossiers-listing-actions/dossiers-listing-actions.component.html +++ b/apps/red-ui/src/app/modules/dossiers-listing/components/dossiers-listing-actions/dossiers-listing-actions.component.html @@ -1,6 +1,6 @@
+
-
+
-
{{ stats.totalAnalyzedPages | number }}
-
+
{{ stats.numberOfPages | number }}
+
-
{{ stats.totalPeople }}
+
{{ stats.numberOfPeople }}
@@ -28,10 +27,9 @@
diff --git a/apps/red-ui/src/app/modules/dossiers-listing/components/dossiers-listing-details/dossiers-listing-details.component.ts b/apps/red-ui/src/app/modules/dossiers-listing/components/dossiers-listing-details/dossiers-listing-details.component.ts index d2033d0f1..4974c67f9 100644 --- a/apps/red-ui/src/app/modules/dossiers-listing/components/dossiers-listing-details/dossiers-listing-details.component.ts +++ b/apps/red-ui/src/app/modules/dossiers-listing/components/dossiers-listing-details/dossiers-listing-details.component.ts @@ -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; readonly documentsChartData$: Observable; readonly dossiersChartData$: Observable; 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))); } } diff --git a/apps/red-ui/src/app/modules/dossiers-listing/components/table-item/table-item.component.ts b/apps/red-ui/src/app/modules/dossiers-listing/components/table-item/table-item.component.ts index 410becc74..5495b6946 100644 --- a/apps/red-ui/src/app/modules/dossiers-listing/components/table-item/table-item.component.ts +++ b/apps/red-ui/src/app/modules/dossiers-listing/components/table-item/table-item.component.ts @@ -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); } } } diff --git a/apps/red-ui/src/app/modules/dossiers-listing/config.service.ts b/apps/red-ui/src/app/modules/dossiers-listing/config.service.ts index 3efed55d8..f57e33fe0 100644 --- a/apps/red-ui/src/app/modules/dossiers-listing/config.service.ts +++ b/apps/red-ui/src/app/modules/dossiers-listing/config.service.ts @@ -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[] { @@ -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(); const allDistinctPeople = new Set(); const allDistinctNeedsWork = new Set(); - const allDistinctDossierTemplates = new Set(); const allDistinctDossierStates = new Set(); const stateToTemplateMap = new Map(); @@ -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; diff --git a/apps/red-ui/src/app/modules/dossiers-listing/screen/dossiers-listing-screen.component.html b/apps/red-ui/src/app/modules/dossiers-listing/screen/dossiers-listing-screen.component.html index 23835ba78..a1051a9f8 100644 --- a/apps/red-ui/src/app/modules/dossiers-listing/screen/dossiers-listing-screen.component.html +++ b/apps/red-ui/src/app/modules/dossiers-listing/screen/dossiers-listing-screen.component.html @@ -1,5 +1,9 @@
- + + + + +
@@ -25,7 +29,7 @@
- + diff --git a/apps/red-ui/src/app/modules/dossiers-listing/screen/dossiers-listing-screen.component.ts b/apps/red-ui/src/app/modules/dossiers-listing/screen/dossiers-listing-screen.component.ts index 97361b639..895ac8c2a 100644 --- a/apps/red-ui/src/app/modules/dossiers-listing/screen/dossiers-listing-screen.component.ts +++ b/apps/red-ui/src/app/modules/dossiers-listing/screen/dossiers-listing-screen.component.ts @@ -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 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 im @ViewChild(TableComponent) private readonly _tableComponent: TableComponent; 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 { + 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); diff --git a/apps/red-ui/src/app/modules/file-preview/components/document-info/document-info.component.ts b/apps/red-ui/src/app/modules/file-preview/components/document-info/document-info.component.ts index c84117b21..70a2777ea 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/document-info/document-info.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/document-info/document-info.component.ts @@ -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'; diff --git a/apps/red-ui/src/app/modules/file-preview/dialogs/accept-recommendation-dialog/accept-recommendation-dialog.component.ts b/apps/red-ui/src/app/modules/file-preview/dialogs/accept-recommendation-dialog/accept-recommendation-dialog.component.ts index a945a3430..a63b328a3 100644 --- a/apps/red-ui/src/app/modules/file-preview/dialogs/accept-recommendation-dialog/accept-recommendation-dialog.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/dialogs/accept-recommendation-dialog/accept-recommendation-dialog.component.ts @@ -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, }); diff --git a/apps/red-ui/src/app/modules/file-preview/dialogs/manual-redaction-dialog/manual-annotation-dialog.component.ts b/apps/red-ui/src/app/modules/file-preview/dialogs/manual-redaction-dialog/manual-annotation-dialog.component.ts index 4fc46241b..de35d8d0b 100644 --- a/apps/red-ui/src/app/modules/file-preview/dialogs/manual-redaction-dialog/manual-annotation-dialog.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/dialogs/manual-redaction-dialog/manual-annotation-dialog.component.ts @@ -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, diff --git a/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts b/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts index 5d33b65f1..8a3ee9bc8 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts @@ -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)); }); } diff --git a/apps/red-ui/src/app/modules/file-preview/services/document-info.service.ts b/apps/red-ui/src/app/modules/file-preview/services/document-info.service.ts index 60023ef3a..db0e9ed95 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/document-info.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/document-info.service.ts @@ -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'; diff --git a/apps/red-ui/src/app/modules/file-preview/services/file-preview-state.service.ts b/apps/red-ui/src/app/modules/file-preview/services/file-preview-state.service.ts index 34e546838..5ce48cb69 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/file-preview-state.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/file-preview-state.service.ts @@ -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)), ); } diff --git a/apps/red-ui/src/app/modules/search/search-screen/search-screen.component.ts b/apps/red-ui/src/app/modules/search/search-screen/search-screen.component.ts index eed89ea85..6276490ab 100644 --- a/apps/red-ui/src/app/modules/search/search-screen/search-screen.component.ts +++ b/apps/red-ui/src/app/modules/search/search-screen/search-screen.component.ts @@ -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 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 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 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, diff --git a/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts index 02ec11ae4..004482d7f 100644 --- a/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts @@ -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; diff --git a/apps/red-ui/src/app/modules/shared/components/dossier-name-column/dossier-name-column.component.ts b/apps/red-ui/src/app/modules/shared/components/dossier-name-column/dossier-name-column.component.ts index 6b7635966..94b423232 100644 --- a/apps/red-ui/src/app/modules/shared/components/dossier-name-column/dossier-name-column.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/dossier-name-column/dossier-name-column.component.ts @@ -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'; diff --git a/apps/red-ui/src/app/modules/shared/components/dossiers-type-switch/dossiers-type-switch.component.html b/apps/red-ui/src/app/modules/shared/components/dossiers-type-switch/dossiers-type-switch.component.html new file mode 100644 index 000000000..885248299 --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/components/dossiers-type-switch/dossiers-type-switch.component.html @@ -0,0 +1,9 @@ + + {{ 'dossiers-type-switch.active' | translate }} + + + + {{ 'dossiers-type-switch.archive' | translate }} + + +
diff --git a/apps/red-ui/src/app/modules/shared/components/dossiers-type-switch/dossiers-type-switch.component.scss b/apps/red-ui/src/app/modules/shared/components/dossiers-type-switch/dossiers-type-switch.component.scss new file mode 100644 index 000000000..0f3831df2 --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/components/dossiers-type-switch/dossiers-type-switch.component.scss @@ -0,0 +1,11 @@ +:host { + display: flex; +} + +.separator { + background-color: var(--iqser-separator); + width: 1px; + height: 30px; + margin-left: 8px; + margin-right: 16px; +} diff --git a/apps/red-ui/src/app/modules/shared/components/dossiers-type-switch/dossiers-type-switch.component.ts b/apps/red-ui/src/app/modules/shared/components/dossiers-type-switch/dossiers-type-switch.component.ts new file mode 100644 index 000000000..7488e01da --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/components/dossiers-type-switch/dossiers-type-switch.component.ts @@ -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; +} diff --git a/apps/red-ui/src/app/modules/shared/components/simple-doughnut-chart/simple-doughnut-chart.component.scss b/apps/red-ui/src/app/modules/shared/components/simple-doughnut-chart/simple-doughnut-chart.component.scss index 2e474e4dd..51fdd8722 100644 --- a/apps/red-ui/src/app/modules/shared/components/simple-doughnut-chart/simple-doughnut-chart.component.scss +++ b/apps/red-ui/src/app/modules/shared/components/simple-doughnut-chart/simple-doughnut-chart.component.scss @@ -47,6 +47,7 @@ > div { border-radius: 4px; padding: 3px 8px; + width: 100%; &:not(:last-child) { margin-bottom: 8px; diff --git a/apps/red-ui/src/app/modules/shared/components/simple-doughnut-chart/simple-doughnut-chart.component.ts b/apps/red-ui/src/app/modules/shared/components/simple-doughnut-chart/simple-doughnut-chart.component.ts index b7bf8c3a2..94ffb130e 100644 --- a/apps/red-ui/src/app/modules/shared/components/simple-doughnut-chart/simple-doughnut-chart.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/simple-doughnut-chart/simple-doughnut-chart.component.ts @@ -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; - 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 { - 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() { diff --git a/apps/red-ui/src/app/modules/shared/components/team-members/team-members.component.ts b/apps/red-ui/src/app/modules/shared/components/team-members/team-members.component.ts index fc3d0f8e0..2ec1357f0 100644 --- a/apps/red-ui/src/app/modules/shared/components/team-members/team-members.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/team-members/team-members.component.ts @@ -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', diff --git a/apps/red-ui/src/app/modules/shared/shared.module.ts b/apps/red-ui/src/app/modules/shared/shared.module.ts index 40152677f..e77823834 100644 --- a/apps/red-ui/src/app/modules/shared/shared.module.ts +++ b/apps/red-ui/src/app/modules/shared/shared.module.ts @@ -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: [ { diff --git a/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts b/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts index 5a7e86f1c..9d669a7f3 100644 --- a/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts +++ b/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts @@ -42,7 +42,7 @@ export class FileUploadService extends GenericService 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())); diff --git a/apps/red-ui/src/app/services/breadcrumbs.service.ts b/apps/red-ui/src/app/services/breadcrumbs.service.ts index 12238cd6b..fb0c23760 100644 --- a/apps/red-ui/src/app/services/breadcrumbs.service.ts +++ b/apps/red-ui/src/app/services/breadcrumbs.service.ts @@ -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 = {}) { 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): 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): 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): 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): 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, }, }); diff --git a/apps/red-ui/src/app/services/dossier-templates/dashboard-stats.service.ts b/apps/red-ui/src/app/services/dossier-templates/dashboard-stats.service.ts new file mode 100644 index 000000000..79819f682 --- /dev/null +++ b/apps/red-ui/src/app/services/dossier-templates/dashboard-stats.service.ts @@ -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 { + constructor( + protected readonly _injector: Injector, + private readonly _dossierStatesService: DossierStatesService, + private readonly _logger: NGXLogger, + ) { + super(_injector, DashboardStats, 'dossier-template/stats'); + } + + loadAll(): Observable { + 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)), + ); + } +} diff --git a/apps/red-ui/src/app/services/entity-services/dossier-templates.service.ts b/apps/red-ui/src/app/services/dossier-templates/dossier-templates.service.ts similarity index 93% rename from apps/red-ui/src/app/services/entity-services/dossier-templates.service.ts rename to apps/red-ui/src/app/services/dossier-templates/dossier-templates.service.ts index a6479d814..979e3d9e5 100644 --- a/apps/red-ui/src/app/services/entity-services/dossier-templates.service.ts +++ b/apps/red-ui/src/app/services/dossier-templates/dossier-templates.service.ts @@ -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'); diff --git a/apps/red-ui/src/app/services/dossiers/active-dossiers.service.ts b/apps/red-ui/src/app/services/dossiers/active-dossiers.service.ts index 0112b0ac6..817d9e97f 100644 --- a/apps/red-ui/src/app/services/dossiers/active-dossiers.service.ts +++ b/apps/red-ui/src/app/services/dossiers/active-dossiers.service.ts @@ -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 { + this.initializeRefresh(); + return super.loadAll(); } } diff --git a/apps/red-ui/src/app/services/dossiers/archived-dossiers.service.ts b/apps/red-ui/src/app/services/dossiers/archived-dossiers.service.ts index 0f496c809..739933d98 100644 --- a/apps/red-ui/src/app/services/dossiers/archived-dossiers.service.ts +++ b/apps/red-ui/src/app/services/dossiers/archived-dossiers.service.ts @@ -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 { diff --git a/apps/red-ui/src/app/services/dossiers/dossiers-cache.service.ts b/apps/red-ui/src/app/services/dossiers/dossiers-cache.service.ts new file mode 100644 index 000000000..76570444b --- /dev/null +++ b/apps/red-ui/src/app/services/dossiers/dossiers-cache.service.ts @@ -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(); + 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 { + 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); + } +} diff --git a/apps/red-ui/src/app/services/dossiers/dossiers.service.ts b/apps/red-ui/src/app/services/dossiers/dossiers.service.ts index 9f8f9d5a2..2a576a40f 100644 --- a/apps/red-ui/src/app/services/dossiers/dossiers.service.ts +++ b/apps/red-ui/src/app/services/dossiers/dossiers.service.ts @@ -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 { readonly dossierFileChanges$ = new Subject(); - 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 loadAll(): Observable { 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 private _load(id: string): Observable { const queryParams: List = [{ 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): IDossiersStats { - let totalAnalyzedPages = 0; - const totalPeople = new Set(); - - 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): Observable { - 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(), - ); - } } diff --git a/apps/red-ui/src/app/services/entity-services/dossier-states.service.ts b/apps/red-ui/src/app/services/entity-services/dossier-states.service.ts index 7e2659262..1fa21f731 100644 --- a/apps/red-ui/src/app/services/entity-services/dossier-states.service.ts +++ b/apps/red-ui/src/app/services/entity-services/dossier-states.service.ts @@ -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'; diff --git a/apps/red-ui/src/app/services/entity-services/dossiers.service.provider.ts b/apps/red-ui/src/app/services/entity-services/dossiers.service.provider.ts index 42b0de04a..ddc69712a 100644 --- a/apps/red-ui/src/app/services/entity-services/dossiers.service.provider.ts +++ b/apps/red-ui/src/app/services/entity-services/dossiers.service.provider.ts @@ -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); +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], }; diff --git a/apps/red-ui/src/app/services/entity-services/file-management.service.ts b/apps/red-ui/src/app/services/entity-services/file-management.service.ts index 0d26bb4cd..a37a33a07 100644 --- a/apps/red-ui/src/app/services/entity-services/file-management.service.ts +++ b/apps/red-ui/src/app/services/entity-services/file-management.service.ts @@ -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 { } @Validate() - delete(@RequiredParam() files: List, @RequiredParam() dossierId: string) { + delete(@RequiredParam() files: List, @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() diff --git a/apps/red-ui/src/app/services/entity-services/files-map.service.ts b/apps/red-ui/src/app/services/entity-services/files-map.service.ts index bfe6587aa..d71f717d1 100644 --- a/apps/red-ui/src/app/services/entity-services/files-map.service.ts +++ b/apps/red-ui/src/app/services/entity-services/files-map.service.ts @@ -12,11 +12,7 @@ export class FilesMapService extends EntitiesMapService { 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); } diff --git a/apps/red-ui/src/app/services/entity-services/files.service.ts b/apps/red-ui/src/app/services/entity-services/files.service.ts index 3569e678b..9d8b48ef2 100644 --- a/apps/red-ui/src/app/services/entity-services/files.service.ts +++ b/apps/red-ui/src/app/services/entity-services/files.service.ts @@ -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 { } /** 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 { reload(dossierId: string, file: File): Observable { 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 { } @Validate() - setUnassigned(@RequiredParam() files: List, @RequiredParam() dossierId: string) { + setUnassigned(@RequiredParam() files: List, @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(fileIds, url).pipe(switchMap(() => this.loadAll(dossierId, routerPath))); + return this._post(fileIds, url).pipe(switchMap(() => this.loadAll(dossierId))); } @Validate() - setToNewFor(@RequiredParam() files: List, @RequiredParam() dossierId: string) { + setToNewFor(@RequiredParam() files: List, @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(fileIds, url).pipe(switchMap(() => this.loadAll(dossierId, routerPath))); + return this._post(fileIds, url).pipe(switchMap(() => this.loadAll(dossierId))); } @Validate() - setUnderApprovalFor(@RequiredParam() files: List, @RequiredParam() dossierId: string, assigneeId: string) { + setUnderApprovalFor(@RequiredParam() files: List, @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(fileIds, url, [{ key: 'assigneeId', value: assigneeId }]).pipe( - switchMap(() => this.loadAll(dossierId, routerPath)), - ); + return this._post(fileIds, url, [{ key: 'assigneeId', value: assigneeId }]).pipe(switchMap(() => this.loadAll(dossierId))); } @Validate() - setReviewerFor(@RequiredParam() files: List, @RequiredParam() dossierId: string, assigneeId: string) { + setReviewerFor(@RequiredParam() files: List, @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(fileIds, url, [{ key: 'assigneeId', value: assigneeId }]).pipe( - switchMap(() => this.loadAll(dossierId, routerPath)), - ); + return this._post(fileIds, url, [{ key: 'assigneeId', value: assigneeId }]).pipe(switchMap(() => this.loadAll(dossierId))); } @Validate() - setApprovedFor(@RequiredParam() files: List, @RequiredParam() dossierId: string) { + setApprovedFor(@RequiredParam() files: List, @RequiredParam() dossierId: string) { const fileIds = files.map(f => f.id); - const routerPath: string = files[0].routerPath; return this._post(fileIds, `${this._defaultModelPath}/approved/${dossierId}/bulk`).pipe( - switchMap(() => this.loadAll(dossierId, routerPath)), + switchMap(() => this.loadAll(dossierId)), ); } @Validate() - setUnderReviewFor(@RequiredParam() files: List, @RequiredParam() dossierId: string) { + setUnderReviewFor(@RequiredParam() files: List, @RequiredParam() dossierId: string) { const fileIds = files.map(f => f.id); - const routerPath: string = files[0].routerPath; return this._post(fileIds, `${this._defaultModelPath}/under-review/${dossierId}/bulk`).pipe( - switchMap(() => this.loadAll(dossierId, routerPath)), + switchMap(() => this.loadAll(dossierId)), ); } } diff --git a/apps/red-ui/src/app/services/entity-services/platform-search.service.ts b/apps/red-ui/src/app/services/entity-services/platform-search.service.ts index d729373d4..671a4fdd1 100644 --- a/apps/red-ui/src/app/services/entity-services/platform-search.service.ts +++ b/apps/red-ui/src/app/services/entity-services/platform-search.service.ts @@ -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 { 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 { 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 { - 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))); } } diff --git a/apps/red-ui/src/app/services/entity-services/trash.service.ts b/apps/red-ui/src/app/services/entity-services/trash.service.ts index 06153fc4c..506a44315 100644 --- a/apps/red-ui/src/app/services/entity-services/trash.service.ts +++ b/apps/red-ui/src/app/services/entity-services/trash.service.ts @@ -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 { 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 { getFiles(dossierIds = this._activeDossiersService.all.map(d => d.id)): Observable { return this._post>(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 { } 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))); } } diff --git a/apps/red-ui/src/app/services/notifications.service.ts b/apps/red-ui/src/app/services/notifications.service.ts index e68359f12..2356cd6c1 100644 --- a/apps/red-ui/src/app/services/notifications.service.ts +++ b/apps/red-ui/src/app/services/notifications.service.ts @@ -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 (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 f.analysisRequired); + return this.isApprover(dossier) && !!this._filesMapService.get(dossier.id).find(f => f.analysisRequired); } canUploadFiles(dossier: Dossier): boolean { diff --git a/apps/red-ui/src/app/services/reanalysis.service.ts b/apps/red-ui/src/app/services/reanalysis.service.ts index d58a2e406..243c98b21 100644 --- a/apps/red-ui/src/app/services/reanalysis.service.ts +++ b/apps/red-ui/src/app/services/reanalysis.service.ts @@ -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 { } @Validate() - reanalyzeFilesForDossier(@RequiredParam() files: List, @RequiredParam() dossierId: string, params?: ReanalyzeQueryParams) { + reanalyzeFilesForDossier(@RequiredParam() files: List, @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 { 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, excluded?: boolean) { + toggleAnalysis(@RequiredParam() dossierId: string, @RequiredParam() files: List, 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 { } @Validate() - ocrFiles(@RequiredParam() files: List, @RequiredParam() dossierId: string) { + ocrFiles(@RequiredParam() files: List, @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))); } } diff --git a/apps/red-ui/src/app/services/router-history.service.ts b/apps/red-ui/src/app/services/router-history.service.ts index 5e87a8b82..62fc4177a 100644 --- a/apps/red-ui/src/app/services/router-history.service.ts +++ b/apps/red-ui/src/app/services/router-history.service.ts @@ -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]); } diff --git a/apps/red-ui/src/app/services/translate-chart.service.ts b/apps/red-ui/src/app/services/translate-chart.service.ts index 97c14b8e6..335022e91 100644 --- a/apps/red-ui/src/app/services/translate-chart.service.ts +++ b/apps/red-ui/src/app/services/translate-chart.service.ts @@ -2,17 +2,34 @@ import { Injectable } from '@angular/core'; import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component'; import { TranslateService } from '@ngx-translate/core'; import { rolesTranslations } from '../translations/roles-translations'; +import { workflowFileStatusTranslations } from '../translations/file-status-translations'; +import { DossierStatesMapService } from './entity-services/dossier-states-map.service'; @Injectable({ providedIn: 'root', }) export class TranslateChartService { - constructor(private readonly _translateService: TranslateService) {} + constructor(private readonly _translateService: TranslateService, private readonly _dossierStatesMapService: DossierStatesMapService) {} - translateStatus(config: DoughnutChartConfig[]): DoughnutChartConfig[] { + translateLabels(config: DoughnutChartConfig[]): DoughnutChartConfig[] { return config.map(val => ({ ...val, label: this._translateService.instant(val.label) })); } + translateDossierStates(config: DoughnutChartConfig[], dossierTemplateId: string): DoughnutChartConfig[] { + return config.map(val => { + if (!val.key) { + return { ...val, label: this._translateService.instant(val.label) }; + } else { + const dossierState = this._dossierStatesMapService.get(dossierTemplateId, val.key); + return { ...val, key: null, label: dossierState.name, color: dossierState.color }; + } + }); + } + + translateWorkflowStatus(config: DoughnutChartConfig[]): DoughnutChartConfig[] { + return config.map(val => ({ ...val, label: this._translateService.instant(workflowFileStatusTranslations[val.label] as string) })); + } + translateRoles(config: DoughnutChartConfig[]): DoughnutChartConfig[] { return config.map(val => ({ ...val, diff --git a/apps/red-ui/src/app/services/user-preference.service.ts b/apps/red-ui/src/app/services/user-preference.service.ts index 7ae1d333d..5df934cac 100644 --- a/apps/red-ui/src/app/services/user-preference.service.ts +++ b/apps/red-ui/src/app/services/user-preference.service.ts @@ -8,6 +8,7 @@ const KEYS = { language: 'Language', dossierRecent: 'Dossier-Recent', filePreviewTooltips: 'File-Preview-Tooltips', + lastDossierTemplate: 'Last-Dossier-Template', } as const; @Injectable({ @@ -30,38 +31,36 @@ export class UserPreferenceService extends GenericService { } getLastOpenedFileForDossier(dossierId: string): string { - const key = `${KEYS.dossierRecent}-${dossierId}`; - return this._getAttribute(key); + return this._getAttribute(`${KEYS.dossierRecent}-${dossierId}`); } async saveLastOpenedFileForDossier(dossierId: string, fileId: string): Promise { - const key = `${KEYS.dossierRecent}-${dossierId}`; - this.userAttributes[key] = [fileId]; - await firstValueFrom(this.savePreferences([fileId], key)); + await this._save(`${KEYS.dossierRecent}-${dossierId}`, fileId); + } + + getLastDossierTemplate(): string { + return this._getAttribute(KEYS.lastDossierTemplate); + } + + async saveLastDossierTemplate(dossierTemplateId: string): Promise { + await this._save(KEYS.lastDossierTemplate, dossierTemplateId); } getLanguage(): string { - const key = KEYS.language; - return this._getAttribute(key); + return this._getAttribute(KEYS.language); } async saveLanguage(language: string): Promise { - const key = KEYS.language; - this.userAttributes[key] = [language]; - await firstValueFrom(this.savePreferences([language], key)); + await this._save(KEYS.language, language); } getFilePreviewTooltipsPreference(): boolean { - const key = KEYS.filePreviewTooltips; - return this._getAttribute(key, 'false') === 'true'; + return this._getAttribute(KEYS.filePreviewTooltips, 'false') === 'true'; } async toggleFilePreviewTooltipsPreference(): Promise { - const key = KEYS.filePreviewTooltips; - const currentValue = this.getFilePreviewTooltipsPreference(); - const nextValue = [(!currentValue).toString()]; - this.userAttributes[key] = nextValue; - await firstValueFrom(this.savePreferences(nextValue, key)); + const nextValue = (!this.getFilePreviewTooltipsPreference()).toString(); + await this._save(KEYS.filePreviewTooltips, nextValue); } toggleDevFeatures(): void { @@ -79,6 +78,11 @@ export class UserPreferenceService extends GenericService { return this._put(body, `${this._defaultModelPath}/${key}`); } + private async _save(key: string, value: string): Promise { + this.userAttributes[key] = [value]; + await firstValueFrom(this.savePreferences([value], key)); + } + private _getAttribute(key: string, defaultValue = ''): string { if (this.userAttributes[key]?.length > 0) { return this.userAttributes[key][0]; diff --git a/apps/red-ui/src/app/utils/configuration.initializer.ts b/apps/red-ui/src/app/utils/configuration.initializer.ts index bbd074151..849145c01 100644 --- a/apps/red-ui/src/app/utils/configuration.initializer.ts +++ b/apps/red-ui/src/app/utils/configuration.initializer.ts @@ -9,7 +9,16 @@ import { UserPreferenceService } from '@services/user-preference.service'; import { UserService } from '@services/user.service'; import { FeaturesService } from '@services/features.service'; +function lastDossierTemplateRedirect(baseHref: string, userPreferenceService: UserPreferenceService) { + const url = window.location.href.split('/').filter(s => s.length > 0); + const lastDossierTemplate = userPreferenceService.getLastDossierTemplate(); + if (lastDossierTemplate && [baseHref, 'main'].includes(url[url.length - 1])) { + window.location.href = `/${baseHref}/main/${lastDossierTemplate}`; + } +} + export function configurationInitializer( + baseHref: string, keycloakService: KeycloakService, title: Title, configService: ConfigService, @@ -36,6 +45,9 @@ export function configurationInitializer( title.setTitle('RedactManager'); return of({}); }), + tap(() => { + lastDossierTemplateRedirect(baseHref.replace('/', ''), userPreferenceService); + }), switchMap(() => languageService.chooseAndSetInitialLanguage()), tap(() => userService.initialize()), take(1), diff --git a/apps/red-ui/src/app/utils/constants.ts b/apps/red-ui/src/app/utils/constants.ts index 1f808d01d..4fb7381f9 100644 --- a/apps/red-ui/src/app/utils/constants.ts +++ b/apps/red-ui/src/app/utils/constants.ts @@ -5,3 +5,6 @@ export const FILE_ID = 'fileId'; export const DOSSIER_TEMPLATE_ID = 'dossierTemplateId'; export const ENTITY_TYPE = 'entity'; export const DOSSIERS_ARCHIVE = 'DOSSIERS_ARCHIVE'; + +export const ARCHIVE_ROUTE = 'archive'; +export const DOSSIERS_ROUTE = 'dossiers'; diff --git a/apps/red-ui/src/assets/i18n/de.json b/apps/red-ui/src/assets/i18n/de.json index 607e2a984..9b8ce8cdb 100644 --- a/apps/red-ui/src/assets/i18n/de.json +++ b/apps/red-ui/src/assets/i18n/de.json @@ -599,6 +599,16 @@ } }, "content": "Begründung", + "dashboard": { + "empty-template": { + "description": "", + "new-dossier": "" + }, + "greeting": { + "subtitle": "", + "title": "" + } + }, "default-colors-screen": { "action": { "edit": "Farbe bearbeiten" @@ -766,10 +776,6 @@ }, "stats": { "analyzed-pages": "Seiten", - "charts": { - "dossiers": "Dossiers", - "total-documents": "Anzahl der Dokumente" - }, "total-people": "Anzahl der Benutzer" }, "table-col-names": { @@ -924,6 +930,14 @@ "valid-from": "", "valid-to": "" }, + "dossier-template-stats": { + "active-dossiers": "Aktive Dossiers", + "analyzed-pages": "", + "archived-dossiers": "", + "deleted-dossiers": "", + "total-documents": "Anzahl der Dokumente", + "total-people": "" + }, "dossier-templates-listing": { "action": { "clone": "", @@ -967,6 +981,10 @@ "incomplete": "" } }, + "dossiers-type-switch": { + "active": "", + "archive": "" + }, "download-includes": "Wählen Sie die Dokumente für Ihr Download-Paket aus", "download-status": { "queued": "Ihr Download wurde zur Warteschlange hinzugefügt. Hier finden Sie alle angeforderten Downloads: My Downloads." @@ -1873,9 +1891,8 @@ }, "top-bar": { "navigation-items": { - "archived-dossiers": "", "back": "Zurück", - "dossiers": "Aktives Dossier", + "dashboard": "", "my-account": { "children": { "account": "Konto", diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index d532ecb08..2df6a0b00 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -599,6 +599,16 @@ } }, "content": "Reason", + "dashboard": { + "empty-template": { + "description": "This template does not contain any dossiers. Start by creating a dossier to use it on.", + "new-dossier": "New Dossier" + }, + "greeting": { + "subtitle": "Here's what's happening in your redaction teams today.", + "title": "Welcome, {name}!" + } + }, "default-colors-screen": { "action": { "edit": "Edit Color" @@ -766,10 +776,6 @@ }, "stats": { "analyzed-pages": "{count, plural, one{Page} other{Pages}}", - "charts": { - "dossiers": "{count, plural, one{Dossier} other{Dossiers}}", - "total-documents": "Total Documents" - }, "total-people": "Total users" }, "table-col-names": { @@ -924,6 +930,14 @@ "valid-from": "Valid from: {date}", "valid-to": "Valid to: {date}" }, + "dossier-template-stats": { + "active-dossiers": "Active {count, plural, one{Dossier} other{Dossiers}}", + "analyzed-pages": "{count} {count, plural, one{Page} other {Pages}} Analysed", + "archived-dossiers": "{count} {count, plural, one{Dossier} other {Dossiers}} in Archive", + "deleted-dossiers": "{count} {count, plural, one{Dossier} other {Dossiers}} in Trash", + "total-documents": "Total Documents", + "total-people": "{count} {count, plural, one{User} other {Users}}" + }, "dossier-templates-listing": { "action": { "clone": "Clone Template", @@ -967,6 +981,10 @@ "incomplete": "Incomplete" } }, + "dossiers-type-switch": { + "active": "Active", + "archive": "Archived" + }, "download-includes": "Choose what is included at download:", "download-status": { "queued": "Your download has been queued, you can see all your requested downloads here: My Downloads." @@ -1384,7 +1402,7 @@ "dossier-state": "Dossier State", "dossier-templates": "Dossier Templates", "empty": "Empty", - "filter-by": "Filter:", + "filter-by": "Filter by:", "needs-work": "Workload", "people": "Dossier Member(s)" }, @@ -1873,9 +1891,8 @@ }, "top-bar": { "navigation-items": { - "archived-dossiers": "Archived Dossiers", "back": "Back", - "dossiers": "Active Dossiers", + "dashboard": "Dashboard", "my-account": { "children": { "account": "Account", diff --git a/libs/common-ui b/libs/common-ui index 46f19f216..37ab9a6ed 160000 --- a/libs/common-ui +++ b/libs/common-ui @@ -1 +1 @@ -Subproject commit 46f19f216fb158d90439cfaa0dcd94f7d9f71f72 +Subproject commit 37ab9a6ed7fb9f0a70a2226f75a86611405f907d diff --git a/libs/red-domain/src/lib/dossier-templates/dashboard-stats.model.ts b/libs/red-domain/src/lib/dossier-templates/dashboard-stats.model.ts new file mode 100644 index 000000000..d99a9b03c --- /dev/null +++ b/libs/red-domain/src/lib/dossier-templates/dashboard-stats.model.ts @@ -0,0 +1,89 @@ +import { IDashboardStats, ProcessingFileStatus, StatusSorter, WorkflowFileStatus } from '@red/domain'; +import { IListable } from '@iqser/common-ui'; +import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component'; + +export class DashboardStats implements IListable, IDashboardStats { + readonly dossierCountByStatus: [ + { + count: number; + statusId: string; + }, + ]; + readonly dossierTemplateId: string; + readonly dossiersInTemplate: [string]; + readonly fileCountPerProcessingStatus: [ + { + readonly count: number; + readonly processingStatus: ProcessingFileStatus; + }, + ]; + readonly fileCountPerWorkflowStatus: [ + { + readonly count: number; + readonly workflowStatus: WorkflowFileStatus; + }, + ]; + readonly name: string; + readonly numberOfActiveDossiers: number; + readonly numberOfActiveFiles: number; + readonly numberOfArchivedDossiers: number; + readonly numberOfDeletedDossiers: number; + readonly numberOfExcludedPages: number; + readonly numberOfPages: number; + readonly numberOfPeople: number; + readonly numberOfSoftDeletedFiles: number; + readonly dossiersChartData: DoughnutChartConfig[]; + readonly documentsChartData: DoughnutChartConfig[]; + + constructor(stats: IDashboardStats) { + this.dossierCountByStatus = stats.dossierCountByStatus; + this.dossierTemplateId = stats.dossierTemplateId; + this.dossiersInTemplate = stats.dossiersInTemplate; + this.fileCountPerProcessingStatus = stats.fileCountPerProcessingStatus; + this.fileCountPerWorkflowStatus = stats.fileCountPerWorkflowStatus; + this.name = stats.name; + this.numberOfActiveDossiers = stats.numberOfActiveDossiers; + this.numberOfActiveFiles = stats.numberOfActiveFiles; + this.numberOfArchivedDossiers = stats.numberOfArchivedDossiers; + this.numberOfDeletedDossiers = stats.numberOfDeletedDossiers; + this.numberOfExcludedPages = stats.numberOfExcludedPages; + this.numberOfPages = stats.numberOfPages; + this.numberOfPeople = stats.numberOfPeople; + this.numberOfSoftDeletedFiles = stats.numberOfSoftDeletedFiles; + + this.dossiersChartData = this._dossiersChartData; + this.documentsChartData = this._documentsChartData; + } + + get isEmpty(): boolean { + return this.numberOfActiveDossiers === 0; + } + + get id(): string { + return this.dossierTemplateId; + } + + get searchKey(): string { + return this.name; + } + + private get _dossiersChartData(): DoughnutChartConfig[] { + return this.dossierCountByStatus.map(d => ({ + value: d.count, + color: '#e2e4e9', + label: 'edit-dossier-dialog.general-info.form.dossier-state.placeholder', + key: d.statusId, + })); + } + + private get _documentsChartData(): DoughnutChartConfig[] { + const configArray: DoughnutChartConfig[] = this.fileCountPerWorkflowStatus.map(d => ({ + value: d.count, + color: d.workflowStatus, + label: d.workflowStatus, + key: d.workflowStatus, + })); + configArray.sort((a: DoughnutChartConfig, b) => StatusSorter.byStatus(a.label, b.label)); + return configArray; + } +} diff --git a/libs/red-domain/src/lib/dossier-templates/dashboard-stats.ts b/libs/red-domain/src/lib/dossier-templates/dashboard-stats.ts new file mode 100644 index 000000000..b7db72f1d --- /dev/null +++ b/libs/red-domain/src/lib/dossier-templates/dashboard-stats.ts @@ -0,0 +1,33 @@ +import { ProcessingFileStatus, WorkflowFileStatus } from '@red/domain'; + +export interface IDashboardStats { + readonly dossierCountByStatus: [ + { + count: number; + statusId: string; + }, + ]; + readonly dossierTemplateId: string; + readonly dossiersInTemplate: [string]; + readonly fileCountPerProcessingStatus: [ + { + readonly count: number; + readonly processingStatus: ProcessingFileStatus; + }, + ]; + readonly fileCountPerWorkflowStatus: [ + { + readonly count: number; + readonly workflowStatus: WorkflowFileStatus; + }, + ]; + readonly name: string; + readonly numberOfActiveDossiers: number; + readonly numberOfActiveFiles: number; + readonly numberOfArchivedDossiers: number; + readonly numberOfDeletedDossiers: number; + readonly numberOfExcludedPages: number; + readonly numberOfPages: number; + readonly numberOfPeople: number; + readonly numberOfSoftDeletedFiles: number; +} diff --git a/libs/red-domain/src/lib/dossier-templates/dossier-template.model.ts b/libs/red-domain/src/lib/dossier-templates/dossier-template.model.ts index 703853f39..82f5cc5d3 100644 --- a/libs/red-domain/src/lib/dossier-templates/dossier-template.model.ts +++ b/libs/red-domain/src/lib/dossier-templates/dossier-template.model.ts @@ -47,4 +47,8 @@ export class DossierTemplate implements IDossierTemplate, IListable { get routerLink(): string { return `/main/admin/dossier-templates/${this.dossierTemplateId}`; } + + get dossiersRouterLink(): string { + return `/main/${this.dossierTemplateId}/dossiers`; + } } diff --git a/libs/red-domain/src/lib/dossier-templates/index.ts b/libs/red-domain/src/lib/dossier-templates/index.ts index cf815ad22..8af5fc988 100644 --- a/libs/red-domain/src/lib/dossier-templates/index.ts +++ b/libs/red-domain/src/lib/dossier-templates/index.ts @@ -2,4 +2,6 @@ export * from './dossier-template'; export * from './dossier-template.model'; export * from './dossier-template-stats'; export * from './dossier-template-stats.model'; +export * from './dashboard-stats'; +export * from './dashboard-stats.model'; export * from './types'; diff --git a/libs/red-domain/src/lib/dossiers/dossier.model.ts b/libs/red-domain/src/lib/dossiers/dossier.model.ts index 8d3eb5db6..d84139928 100644 --- a/libs/red-domain/src/lib/dossiers/dossier.model.ts +++ b/libs/red-domain/src/lib/dossiers/dossier.model.ts @@ -1,8 +1,9 @@ -import { IListable, IRouterPath, List } from '@iqser/common-ui'; +import { IListable, List } from '@iqser/common-ui'; import { IDossier } from './dossier'; import { DownloadFileType } from '../shared'; +import { ARCHIVE_ROUTE, DOSSIERS_ROUTE } from '@utils/constants'; -export class Dossier implements IDossier, IListable, IRouterPath { +export class Dossier implements IDossier, IListable { readonly dossierId: string; readonly dossierTemplateId: string; readonly ownerId: string; @@ -22,8 +23,11 @@ export class Dossier implements IDossier, IListable, IRouterPath { readonly watermarkPreviewEnabled: boolean; readonly archivedTime: string; readonly hasReviewers: boolean; + readonly routerLink: string; + readonly dossiersListRouterLink: string; + readonly id: string; - constructor(dossier: IDossier, readonly routerPath: string) { + constructor(dossier: IDossier) { this.dossierId = dossier.dossierId; this.approverIds = dossier.approverIds; this.date = dossier.date; @@ -43,14 +47,11 @@ export class Dossier implements IDossier, IListable, IRouterPath { this.watermarkPreviewEnabled = dossier.watermarkPreviewEnabled; this.archivedTime = dossier.archivedTime; this.hasReviewers = !!this.memberIds && this.memberIds.length > 1; - } - get id(): string { - return this.dossierId; - } - - get routerLink(): string { - return `/main/${this.routerPath}/${this.dossierId}`; + this.id = this.dossierId; + const routerPath = (this.isArchived ? ARCHIVE_ROUTE : DOSSIERS_ROUTE) as string; + this.dossiersListRouterLink = `/main/${this.dossierTemplateId}/${routerPath}`; + this.routerLink = `${this.dossiersListRouterLink}/${this.dossierId}`; } get searchKey(): string { diff --git a/libs/red-domain/src/lib/files/file.model.ts b/libs/red-domain/src/lib/files/file.model.ts index c47c6e092..764ea776e 100644 --- a/libs/red-domain/src/lib/files/file.model.ts +++ b/libs/red-domain/src/lib/files/file.model.ts @@ -1,4 +1,4 @@ -import { Entity, IRouterPath } from '@iqser/common-ui'; +import { Entity } from '@iqser/common-ui'; import { StatusSorter } from '../shared'; import { isFullProcessingStatuses, @@ -10,8 +10,9 @@ import { } from './types'; import { IFile } from './file'; import { FileAttributes } from '../file-attributes'; +import { ARCHIVE_ROUTE, DOSSIERS_ROUTE } from '@utils/constants'; -export class File extends Entity implements IFile, IRouterPath { +export class File extends Entity implements IFile { readonly added?: string; readonly allManualRedactionsApplied: boolean; readonly analysisDuration?: number; @@ -51,6 +52,8 @@ export class File extends Entity implements IFile, IRouterPath { readonly redactionModificationDate: string; readonly lastManualChangeDate?: string; readonly hasHighlights: boolean; + readonly dossierArchived: boolean; + readonly dossierTemplateId: string; readonly statusSort: number; readonly cacheIdentifier?: string; @@ -70,7 +73,7 @@ export class File extends Entity implements IFile, IRouterPath { readonly canBeOpened: boolean; readonly canBeOCRed: boolean; - constructor(file: IFile, readonly reviewerName: string, readonly routerPath: string) { + constructor(file: IFile, readonly reviewerName: string) { super(file); this.added = file.added; this.allManualRedactionsApplied = !!file.allManualRedactionsApplied; @@ -112,6 +115,8 @@ export class File extends Entity implements IFile, IRouterPath { this.redactionModificationDate = file.redactionModificationDate ?? ''; this.lastManualChangeDate = file.lastManualChangeDate; this.hasHighlights = file.hasHighlights; + this.dossierArchived = file.dossierArchived; + this.dossierTemplateId = file.dossierTemplateId; this.statusSort = StatusSorter[this.workflowStatus]; this.cacheIdentifier = btoa(this.fileManipulationDate ?? ''); @@ -146,6 +151,7 @@ export class File extends Entity implements IFile, IRouterPath { } get routerLink(): string | undefined { - return this.canBeOpened ? `/main/${this.routerPath}/${this.dossierId}/file/${this.fileId}` : undefined; + const routerPath = this.dossierArchived ? ARCHIVE_ROUTE : DOSSIERS_ROUTE; + return this.canBeOpened ? `/main/${this.dossierTemplateId}/${routerPath}/${this.dossierId}/file/${this.fileId}` : undefined; } } diff --git a/libs/red-domain/src/lib/files/file.ts b/libs/red-domain/src/lib/files/file.ts index cbdd0e21e..59ba00cd8 100644 --- a/libs/red-domain/src/lib/files/file.ts +++ b/libs/red-domain/src/lib/files/file.ts @@ -149,6 +149,10 @@ export interface IFile { readonly hasHighlights: boolean; + readonly dossierArchived: boolean; + + readonly dossierTemplateId: string; + /** * Last time the actual file was touched */ diff --git a/libs/red-domain/src/lib/shared/breadcrumb-types.ts b/libs/red-domain/src/lib/shared/breadcrumb-types.ts index 74ed8ae69..727c9afee 100644 --- a/libs/red-domain/src/lib/shared/breadcrumb-types.ts +++ b/libs/red-domain/src/lib/shared/breadcrumb-types.ts @@ -1,8 +1,8 @@ -export type BreadcrumbType = 'main' | 'dossier' | 'file' | 'archive'; +export type BreadcrumbType = 'dossierTemplate' | 'dossier' | 'file' | 'dashboard'; export const BreadcrumbTypes = { - main: 'main' as BreadcrumbType, + dossierTemplate: 'dossierTemplate' as BreadcrumbType, dossier: 'dossier' as BreadcrumbType, file: 'file' as BreadcrumbType, - archive: 'archive' as BreadcrumbType, + dashboard: 'dashboard' as BreadcrumbType, }; diff --git a/libs/red-domain/src/lib/trash/trash-dossier.model.ts b/libs/red-domain/src/lib/trash/trash-dossier.model.ts index fc0d78981..db1b61104 100644 --- a/libs/red-domain/src/lib/trash/trash-dossier.model.ts +++ b/libs/red-domain/src/lib/trash/trash-dossier.model.ts @@ -16,6 +16,7 @@ export class TrashDossier extends TrashItem { readonly dueDate?: string; readonly ownerId: string; readonly softDeletedTime: string; + readonly id: string; constructor( dossier: IDossier, @@ -35,10 +36,8 @@ export class TrashDossier extends TrashItem { // Because of migrations, for some this is not set this.softDeletedTime = dossier.softDeletedTime || '-'; this.canRestore = this.canRestore && this._hasRestoreRights; - } - get id(): string { - return this.dossierId; + this.id = this.dossierId; } get searchKey(): string { diff --git a/paligo-theme/paligo-styles/redacto-theme.css b/paligo-theme/paligo-styles/redacto-theme.css index 520e846ea..84db8fc87 100644 --- a/paligo-theme/paligo-styles/redacto-theme.css +++ b/paligo-theme/paligo-styles/redacto-theme.css @@ -1,480 +1,480 @@ @charset "UTF-8"; -@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&display=swap"); +@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&display=swap'); .portal-header { - z-index: 1; - height: 450px; + z-index: 1; + height: 450px; } .portal-header::before { - background-color: #283241; + background-color: #283241; } .portal-header h1 { - font-size: 64px; - font-weight: 300; - line-height: 87px; - margin-top: 68px; - margin-bottom: 40px; + font-size: 64px; + font-weight: 300; + line-height: 87px; + margin-top: 68px; + margin-bottom: 40px; } .portal-header .portal-search { - max-width: 600px; - margin: auto; - position: relative; + max-width: 600px; + margin: auto; + position: relative; } .portal-header .portal-search .search-field { - width: 100%; - border: 1px solid #d3d5da; - border-radius: 8px; - background-color: #fff; + width: 100%; + border: 1px solid #d3d5da; + border-radius: 8px; + background-color: #fff; } .portal-header .portal-search .search-field::placeholder { - opacity: 0.7; + opacity: 0.7; } .portal-header .portal-search .search-field, .portal-header .portal-search .search-field::placeholder { - color: #283241; - font-size: 14px; - line-height: 18px; + color: #283241; + font-size: 14px; + line-height: 18px; } .portal-header .portal-search .search-field { - padding: 12px 17px; + padding: 12px 17px; } .portal-header .portal-search .btn { - position: absolute; - right: 0; - padding: 11px 18px; - background-color: transparent; - color: #283241; - cursor: pointer; - border-radius: 0 8px 8px 0; + position: absolute; + right: 0; + padding: 11px 18px; + background-color: transparent; + color: #283241; + cursor: pointer; + border-radius: 0 8px 8px 0; } .portal-header .portal-search .btn:hover { - background-color: #dd4d50; + background-color: #dd4d50; } @media only screen and (max-width: 768px) { - .portal-header h1 { - font-size: 42px; - font-weight: 300; - line-height: 57px; - } + .portal-header h1 { + font-size: 42px; + font-weight: 300; + line-height: 57px; + } } .portal-single-publication { - background-color: transparent; - width: 280px; + background-color: transparent; + width: 280px; } .portal-single-publication > a { - border-radius: 4px; + border-radius: 4px; } .portal-single-publication .publication-icon { - background-color: #dd4d50; + background-color: #dd4d50; } .featured-content-label { - margin-top: 24px; - text-align: center; + margin-top: 24px; + text-align: center; } .featured-content { - margin-top: 24px; - margin-bottom: 0; + margin-top: 24px; + margin-bottom: 0; } .featured-content .inner { - margin: 0; - justify-content: center; + margin: 0; + justify-content: center; } .featured-content .publication-contents { - padding: 24px 40px; - border: 1px solid #e2e4e9; - width: 100%; - margin: 0; - background-color: #fff; - border-radius: 4px; - width: 250px; - margin: 0 15px !important; + padding: 24px 40px; + border: 1px solid #e2e4e9; + width: 100%; + margin: 0; + background-color: #fff; + border-radius: 4px; + width: 250px; + margin: 0 15px !important; } .featured-content .publication-contents h4.featured-title, .featured-content .publication-contents .section-toc-title { - margin: 0; + margin: 0; } .featured-content .publication-contents h4.featured-title a, .featured-content .publication-contents .section-toc-title a { - color: #283241; + color: #283241; } .featured-content .publication-contents h4.featured-title a:hover, .featured-content .publication-contents .section-toc-title a:hover { - color: #283241; - text-decoration: underline; + color: #283241; + text-decoration: underline; } .featured-content .publication-contents .section-toc-title { - font-size: 28px; - font-weight: 300; - line-height: 36px; + font-size: 28px; + font-weight: 300; + line-height: 36px; } .featured-content .publication-contents ul { - margin: 0; - padding: 0; + margin: 0; + padding: 0; } .featured-content .publication-contents li { - margin: 4px 0; + margin: 4px 0; } .featured-content .publication-contents li:first-child { - margin-top: 20px; + margin-top: 20px; } .featured-content .publication-contents li:last-child { - margin-bottom: 40px; + margin-bottom: 40px; } .featured-content .publication-contents li a { - color: #dd4d50; - font-size: 16px; - line-height: 24px; + color: #dd4d50; + font-size: 16px; + line-height: 24px; } .featured-content .publication-contents li a:hover { - color: #dd4d50; - text-decoration: underline; + color: #dd4d50; + text-decoration: underline; } .featured-content .publication-contents h4 span, .featured-content .publication-contents li::before { - display: none; + display: none; } .featured-content .publication-contents li a { - font-size: inherit; - line-height: inherit; + font-size: inherit; + line-height: inherit; } /* Einleitung */ .cat-panel-1:before { - content: "\f277"; + content: '\f277'; } /* Workflow */ .cat-panel-2:before { - content: "\f0c1"; + content: '\f0c1'; } /* Voraussetzungen */ .cat-panel-3:before { - content: "\f109"; + content: '\f109'; } /* Benutzermenü und -profil */ .cat-panel-4:before { - content: "\f007"; + content: '\f007'; } /* Benachrichtigungen */ .cat-panel-5:before { - content: "\f0f3"; + content: '\f0f3'; } /* Suchfunktion */ .cat-panel-6:before { - content: "\f002"; + content: '\f002'; } /* Ebenen in der Benutzeroberfläche des RedactManagers */ .cat-panel-7:before { - content: "\f248"; + content: '\f248'; } /* Rollen und Berechtigungen */ .cat-panel-8:before { - content: "\f084"; + content: '\f084'; } /* Dossier erstellen und verwalten */ .cat-panel-9:before { - content: "\f07c"; + content: '\f07c'; } /* Dokumente bearbeiten im Editor */ .cat-panel-10:before { - content: "\f15c"; + content: '\f15c'; } /* Dossier abschließen und herunterladen */ .cat-panel-11:before { - content: "\f019"; + content: '\f019'; } /* Funktionsübersicht */ .cat-panel-12:before { - content: "\f03a"; + content: '\f03a'; } /* Glossar */ .cat-panel-13:before { - content: "\f02d"; + content: '\f02d'; } /* FAQ’s (häufige Fragen) */ .cat-panel-14:before { - content: "\f059"; + content: '\f059'; } .portal-search-result { - background-color: #f5f5f7; + background-color: #f5f5f7; } .search-container { - padding-bottom: 100px; + padding-bottom: 100px; } .portal-search-result { - padding: 80px 0 0 0; + padding: 80px 0 0 0; } ul.searchresults { - border: 1px solid #e2e4e9; - background-color: #fff; - border-radius: 4px; - margin-top: 32px; + border: 1px solid #e2e4e9; + background-color: #fff; + border-radius: 4px; + margin-top: 32px; } ul.searchresults .search-highlight { - font-style: normal; + font-style: normal; } li.searchresultitem { - margin: 0 32px; - border-bottom: 1px solid #e2e4e9; - padding: 32px 8px; + margin: 0 32px; + border-bottom: 1px solid #e2e4e9; + padding: 32px 8px; } .searchresultitem.selected-searchresultitem { - background-color: transparent; - border-radius: 0; + background-color: transparent; + border-radius: 0; } .searchresulttitle { - font-size: 28px; - font-weight: 300; - line-height: 36px; - color: #283241; + font-size: 28px; + font-weight: 300; + line-height: 36px; + color: #283241; } .searchresultsnippet { - margin: 16px 0; - color: #283241; + margin: 16px 0; + color: #283241; } .search-result-breadcrumbs { - color: #dd4d50; + color: #dd4d50; } .portal-footer, .site-footer { - border-top: 1px solid #e2e4e9; - padding: 0; + border-top: 1px solid #e2e4e9; + padding: 0; } .portal-footer.portal-footer, .site-footer.portal-footer { - margin-top: 100px; + margin-top: 100px; } .portal-footer .inner, .site-footer .inner { - margin: 0; - padding: 8px 0 64px 0; - font-size: 16px; - line-height: 24px; + margin: 0; + padding: 8px 0 64px 0; + font-size: 16px; + line-height: 24px; } .portal-footer .inner > *, .site-footer .inner > * { - padding: 0; + padding: 0; } .portal-footer .inner .copyright, .site-footer .inner .copyright { - width: 50%; + width: 50%; } :root { - --iqser-primary: lightblue; - --iqser-primary-rgb: 220, 230, 234; - --iqser-primary-2: orange; - --iqser-accent: blue; - --iqser-accent-rgb: 123, 234, 111; - --iqser-disabled: #9398a0; - --iqser-not-disabled-table-item: #f9fafb; - --iqser-btn-bg-hover: #e2e4e9; - --iqser-btn-bg: #f0f1f4; - --iqser-warn: #fdbd00; - --iqser-white: white; - --iqser-black: black; - --iqser-light: white; - --iqser-separator: rgba(226, 228, 233, 0.9); - --iqser-quick-filter-border: #d3d5da; - --iqser-grey-1: #283241; - --iqser-grey-2: #f4f5f7; - --iqser-grey-3: #aaacb3; - --iqser-grey-4: #e2e4e9; - --iqser-grey-5: #d3d5da; - --iqser-grey-6: #f0f1f4; - --iqser-grey-7: #9398a0; - --iqser-grey-8: #f9fafb; - --iqser-grey-9: #f5f5f7; - --iqser-grey-10: #313d4e; - --iqser-grey-11: #ecedf0; - --iqser-green-1: #00ff00; - --iqser-green-2: #5ce594; - --iqser-orange-1: #ff801a; - --iqser-yellow-1: #ffb83b; - --iqser-yellow-2: #fdbd00; - --iqser-yellow-rgb: 253, 189, 0; - --iqser-red-1: #dd4d50; - --iqser-red-2: #f16164; - --iqser-blue-1: #4875f7; - --iqser-blue-2: #48c9f7; - --iqser-blue-3: #5b97db; - --iqser-blue-4: #374c81; - --iqser-blue-5: #c5d3eb; - --iqser-pink-1: #f125de; - --iqser-helpmode-primary: green; + --iqser-primary: lightblue; + --iqser-primary-rgb: 220, 230, 234; + --iqser-primary-2: orange; + --iqser-accent: blue; + --iqser-accent-rgb: 123, 234, 111; + --iqser-disabled: #9398a0; + --iqser-not-disabled-table-item: #f9fafb; + --iqser-btn-bg-hover: #e2e4e9; + --iqser-btn-bg: #f0f1f4; + --iqser-warn: #fdbd00; + --iqser-white: white; + --iqser-black: black; + --iqser-light: white; + --iqser-separator: rgba(226, 228, 233, 0.9); + --iqser-quick-filter-border: #d3d5da; + --iqser-grey-1: #283241; + --iqser-grey-2: #f4f5f7; + --iqser-grey-3: #aaacb3; + --iqser-grey-4: #e2e4e9; + --iqser-grey-5: #d3d5da; + --iqser-grey-6: #f0f1f4; + --iqser-grey-7: #9398a0; + --iqser-grey-8: #f9fafb; + --iqser-grey-9: #f5f5f7; + --iqser-grey-10: #313d4e; + --iqser-grey-11: #ecedf0; + --iqser-green-1: #00ff00; + --iqser-green-2: #5ce594; + --iqser-orange-1: #ff801a; + --iqser-yellow-1: #ffb83b; + --iqser-yellow-2: #fdbd00; + --iqser-yellow-rgb: 253, 189, 0; + --iqser-red-1: #dd4d50; + --iqser-red-2: #f16164; + --iqser-blue-1: #4875f7; + --iqser-blue-2: #48c9f7; + --iqser-blue-3: #5b97db; + --iqser-blue-4: #374c81; + --iqser-blue-5: #c5d3eb; + --iqser-pink-1: #f125de; + --iqser-helpmode-primary: green; } .site-sidebar { - background-color: #283241; - scrollbar-color: var(--iqser-quick-filter-border) var(--iqser-grey-2); - scrollbar-width: thin; - /* Track */ - /* Handle */ + background-color: #283241; + scrollbar-color: var(--iqser-quick-filter-border) var(--iqser-grey-2); + scrollbar-width: thin; + /* Track */ + /* Handle */ } .site-sidebar .logo { - padding: 24px 0 30px 0 !important; + padding: 24px 0 30px 0 !important; } .site-sidebar::-webkit-scrollbar { - width: 11px; + width: 11px; } .site-sidebar::-webkit-scrollbar-track { - background: var(--iqser-grey-2); + background: var(--iqser-grey-2); } .site-sidebar::-webkit-scrollbar-thumb { - background: var(--iqser-quick-filter-border); + background: var(--iqser-quick-filter-border); } .site-sidebar-search { - padding: 0 24px; + padding: 0 24px; } .site-sidebar-search .search-field { - width: 100%; - border: 1px solid #d3d5da; - border-radius: 8px; - background-color: #fff; + width: 100%; + border: 1px solid #d3d5da; + border-radius: 8px; + background-color: #fff; } .site-sidebar-search .search-field::placeholder { - opacity: 0.7; + opacity: 0.7; } .site-sidebar-search .search-field, .site-sidebar-search .search-field::placeholder { - color: #283241; - font-size: 14px; - line-height: 18px; + color: #283241; + font-size: 14px; + line-height: 18px; } .site-sidebar-search .search-field { - padding: 7px 13px; + padding: 7px 13px; } .nav-site-sidebar { - margin-top: 16px; + margin-top: 16px; } .nav-site-sidebar .topic-link { - padding-top: 11px; - padding-bottom: 11px; - font-size: 14px; - line-height: 18px; - color: #d3d5da; + padding-top: 11px; + padding-bottom: 11px; + font-size: 14px; + line-height: 18px; + color: #d3d5da; } .nav-site-sidebar .topic-link:hover { - background-color: #313d4e; + background-color: #313d4e; } .nav-site-sidebar .active > .topic-link { - background-color: #313d4e; + background-color: #313d4e; } .nav-site-sidebar .active > a { - color: #fff; - font-weight: 600; + color: #fff; + font-weight: 600; } .nav-site-sidebar > li > a { - padding-left: 24px; + padding-left: 24px; } .nav-site-sidebar > li > ul > li > a { - padding-left: 32px; + padding-left: 32px; } .nav-site-sidebar > li > ul > li > ul > li > a { - padding-left: 40px; + padding-left: 40px; } .toc .glyphicon { - top: 5px; + top: 5px; } .toc > li > .topic-link > .glyphicon { - margin-top: 4px; + margin-top: 4px; } .toolbar { - box-shadow: none; - padding: 21px 24px; - margin-bottom: 50px; + box-shadow: none; + padding: 21px 24px; + margin-bottom: 50px; } .topic-content .breadcrumb-container { - margin-top: 40px; + margin-top: 40px; } .topic-content .breadcrumb { - font-size: 14px; - line-height: 18px; - font-weight: 600; + font-size: 14px; + line-height: 18px; + font-weight: 600; } .topic-content .breadcrumb a { - color: #283241; + color: #283241; } .topic-content .breadcrumb a:hover { - color: #dd4d50; - text-decoration: underline; + color: #dd4d50; + text-decoration: underline; } .topic-content .breadcrumb .breadcrumb-node { - color: #dd4d50; + color: #dd4d50; } main article { - margin-bottom: 0; - padding: 0; + margin-bottom: 0; + padding: 0; } section > .titlepage .title { - margin: 24px 0 16px 0; + margin: 24px 0 16px 0; } #topic-content > section > .titlepage h2.title { - margin: 0 0 24px; + margin: 0 0 24px; } .image-viewport { - margin: auto; + margin: auto; } .image-viewport img { - margin: 16px auto; + margin: 16px auto; } .pager { - margin-top: 30px; - margin-bottom: 30px; - padding: 0; + margin-top: 30px; + margin-bottom: 30px; + padding: 0; } .pager li > a, .pager li > span { - color: #dd4d50; - font-size: 14px; - font-weight: 600; - line-height: 19px; - text-transform: uppercase; - padding: 0; - background-color: transparent; - border: none; - border-radius: 0; + color: #dd4d50; + font-size: 14px; + font-weight: 600; + line-height: 19px; + text-transform: uppercase; + padding: 0; + background-color: transparent; + border: none; + border-radius: 0; } .pager li > a:hover, .pager li > span:hover { - text-decoration: underline; - background-color: transparent; - color: #dd4d50; + text-decoration: underline; + background-color: transparent; + color: #dd4d50; } .warning, @@ -482,229 +482,240 @@ section > .titlepage .title { .important, .caution, .tip { - margin-top: 32px; - margin-bottom: 32px; - padding: 16px 24px 16px 68px; - background-color: #fff; - border-left: 4px solid #dd4d50; - border-radius: 4px; + margin-top: 32px; + margin-bottom: 32px; + padding: 16px 24px 16px 68px; + background-color: #fff; + border-left: 4px solid #dd4d50; + border-radius: 4px; } .warning:before, .note:before, .important:before, .caution:before, .tip:before { - color: #dd4d50; - width: 20px; - height: 20px; - text-align: center; - left: 24px; - top: calc(50% - 15px); + color: #dd4d50; + width: 20px; + height: 20px; + text-align: center; + left: 24px; + top: calc(50% - 15px); } .warning h3, .note h3, .important h3, .caution h3, .tip h3 { - padding: 0; - font-size: 18px; - font-weight: 600; - line-height: 24px; - margin-bottom: 8px; + padding: 0; + font-size: 18px; + font-weight: 600; + line-height: 24px; + margin-bottom: 8px; } .warning p, .note p, .important p, .caution p, .tip p { - line-height: 20px; + line-height: 20px; } .topic-content > section > p { - margin: 12px 0; + margin: 12px 0; } .panel { - padding: 12px 0; - border-radius: 4px; - border: none; + padding: 12px 0; + border-radius: 4px; + border: none; } .panel .panel-body > p { - margin-bottom: 12px; + margin-bottom: 12px; } .panel .panel-body > p:not(:first-of-type) { - margin-top: 18px; + margin-top: 18px; } .mediaobject { - margin-top: 20px; + margin-top: 20px; } .mediaobject img { - border-radius: 4px; - margin: 0; - box-shadow: 0 3px 12px 5px rgba(40, 50, 65, 0.14); + border-radius: 4px; + margin: 0; + box-shadow: 0 3px 12px 5px rgba(40, 50, 65, 0.14); } .mediaobject .caption > p { - font-size: 14px; - text-align: center; - font-style: italic; - margin: 0; + font-size: 14px; + text-align: center; + font-style: italic; + margin: 0; } .inlinemediaobject { - vertical-align: unset; + vertical-align: unset; } main ol, main ul { - margin: 0 0 24px; + margin: 0 0 24px; } .section-toc { - padding: 24px 40px; - border: 1px solid #e2e4e9; - width: 100%; - margin: 0; - background-color: #fff; - border-radius: 4px; + padding: 24px 40px; + border: 1px solid #e2e4e9; + width: 100%; + margin: 0; + background-color: #fff; + border-radius: 4px; } .section-toc h4.featured-title, .section-toc .section-toc-title { - margin: 0; + margin: 0; } .section-toc h4.featured-title a, .section-toc .section-toc-title a { - color: #283241; + color: #283241; } .section-toc h4.featured-title a:hover, .section-toc .section-toc-title a:hover { - color: #283241; - text-decoration: underline; + color: #283241; + text-decoration: underline; } .section-toc .section-toc-title { - font-size: 28px; - font-weight: 300; - line-height: 36px; + font-size: 28px; + font-weight: 300; + line-height: 36px; } .section-toc ul { - margin: 0; - padding: 0; + margin: 0; + padding: 0; } .section-toc li { - margin: 4px 0; + margin: 4px 0; } .section-toc li:first-child { - margin-top: 20px; + margin-top: 20px; } .section-toc li:last-child { - margin-bottom: 40px; + margin-bottom: 40px; } .section-toc li a { - color: #dd4d50; - font-size: 16px; - line-height: 24px; + color: #dd4d50; + font-size: 16px; + line-height: 24px; } .section-toc li a:hover { - color: #dd4d50; - text-decoration: underline; + color: #dd4d50; + text-decoration: underline; } .section-toc h4 span, .section-toc li::before { - display: none; + display: none; } .section-toc li:first-child { - margin-top: 16px; + margin-top: 16px; } .section-toc li:last-child { - margin-bottom: 8px; + margin-bottom: 8px; } .procedure > li.step::before { - background-color: transparent; - border: 1px solid #dd4d50; - color: #dd4d50; - line-height: 23px; + background-color: transparent; + border: 1px solid #dd4d50; + color: #dd4d50; + line-height: 23px; } .question { - font-weight: 600; + font-weight: 600; } .question > td > p { - margin: 32px 0 18px 0; + margin: 32px 0 18px 0; } .question > td:first-child { - padding-right: 4px; + padding-right: 4px; } .fixed-toolbar article.topic :target.question:before { - content: none; + content: none; } body { - color: #283241; - background-color: #f5f5f7; - font-family: "Open Sans", sans-serif; - scrollbar-color: var(--iqser-quick-filter-border) var(--iqser-grey-2); - scrollbar-width: thin; - /* Track */ - /* Handle */ + color: #283241; + background-color: #f5f5f7; + font-family: 'Open Sans', sans-serif; + scrollbar-color: var(--iqser-quick-filter-border) var(--iqser-grey-2); + scrollbar-width: thin; + /* Track */ + /* Handle */ } -body h1, body .h1, -body h2, body .h2, -body h3, body .h3, -body h4, body .h4, -body h5, body .h5, -body h6, body .h6, +body h1, +body .h1, +body h2, +body .h2, +body h3, +body .h3, +body h4, +body .h4, +body h5, +body .h5, +body h6, +body .h6, body p, body pre { - margin: 0; - font-family: "Open Sans", sans-serif; + margin: 0; + font-family: 'Open Sans', sans-serif; } body::-webkit-scrollbar { - width: 11px; + width: 11px; } body::-webkit-scrollbar-track { - background: var(--iqser-grey-2); + background: var(--iqser-grey-2); } body::-webkit-scrollbar-thumb { - background: var(--iqser-quick-filter-border); + background: var(--iqser-quick-filter-border); } -body h1, body .h1 { - font-size: 64px; - font-weight: 300; - line-height: 87px; +body h1, +body .h1 { + font-size: 64px; + font-weight: 300; + line-height: 87px; } -body h2, body .h2 { - font-size: 42px; - font-weight: 300; - line-height: 57px; +body h2, +body .h2 { + font-size: 42px; + font-weight: 300; + line-height: 57px; } -body h3, body .h3 { - font-size: 32px; - font-weight: 300; - line-height: 43px; +body h3, +body .h3 { + font-size: 32px; + font-weight: 300; + line-height: 43px; } -body h4, body .h4 { - font-size: 28px; - font-weight: 300; - line-height: 36px; +body h4, +body .h4 { + font-size: 28px; + font-weight: 300; + line-height: 36px; } -body h5, body .h5 { - font-size: 18px; - font-weight: 600; - line-height: 24px; +body h5, +body .h5 { + font-size: 18px; + font-weight: 600; + line-height: 24px; } body p { - font-size: 16px; - line-height: 24px; + font-size: 16px; + line-height: 24px; } body a { - color: #dd4d50; + color: #dd4d50; } body a:hover { - text-decoration: underline; - color: #dd4d50; + text-decoration: underline; + color: #dd4d50; } body a:focus { - color: #dd4d50; + color: #dd4d50; }