RED-10139: refactoring dossier listing.

This commit is contained in:
Nicoleta Panaghiu 2024-10-21 17:15:53 +03:00
parent b7ff80ecac
commit a2870531c9
26 changed files with 471 additions and 401 deletions

View File

@ -1,82 +1,84 @@
<a
*ngIf="stats as dossierTemplate"
[attr.help-mode-key]="!dossierTemplate.isEmpty ? 'open_dossier_template' : null"
[class.empty]="dossierTemplate.isEmpty"
[routerLink]="dossierTemplate.isEmpty ? null : ['..', dossierTemplate.dossierTemplateId, 'dossiers']"
class="dialog"
>
<ng-container *ngIf="!dossierTemplate.isEmpty; else empty">
<div class="flex-2">
<div class="heading mb-6">{{ dossierTemplate.name }}</div>
<div class="stats-subtitle">
<div>
<mat-icon svgIcon="red:archive"></mat-icon>
<span
[innerHTML]="
'dossier-template-stats.archived-dossiers' | translate: { count: dossierTemplate.numberOfArchivedDossiers }
"
></span>
</div>
<div>
<mat-icon svgIcon="iqser:trash"></mat-icon>
<span
[innerHTML]="
'dossier-template-stats.deleted-dossiers' | translate: { count: dossierTemplate.numberOfDeletedDossiers }
"
></span>
</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
<span [innerHTML]="'dossier-template-stats.total-people' | translate: { count: dossierTemplate.numberOfPeople }"></span>
</div>
<div>
<mat-icon svgIcon="iqser:pages"></mat-icon>
<span
[innerHTML]="'dossier-template-stats.analyzed-pages' | translate: { count: dossierTemplate.numberOfPages }"
></span>
@if (stats(); as dossierTemplate) {
<a
[attr.help-mode-key]="!isTemplateEmpty() ? 'open_dossier_template' : null"
[class.empty]="isTemplateEmpty()"
[routerLink]="isTemplateEmpty() ? null : ['..', dossierTemplate.dossierTemplateId, 'dossiers']"
class="dialog"
>
@if (!isTemplateEmpty()) {
<div class="flex-2">
<div class="heading mb-6">{{ dossierTemplate.name }}</div>
<div class="stats-subtitle">
<div>
<mat-icon svgIcon="red:archive"></mat-icon>
<span
[innerHTML]="
'dossier-template-stats.archived-dossiers' | translate: { count: dossierTemplate.numberOfArchivedDossiers }
"
></span>
</div>
<div>
<mat-icon svgIcon="iqser:trash"></mat-icon>
<span
[innerHTML]="
'dossier-template-stats.deleted-dossiers' | translate: { count: dossierTemplate.numberOfDeletedDossiers }
"
></span>
</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
<span
[innerHTML]="'dossier-template-stats.total-people' | translate: { count: dossierTemplate.numberOfPeople }"
></span>
</div>
<div>
<mat-icon svgIcon="iqser:pages"></mat-icon>
<span
[innerHTML]="'dossier-template-stats.analyzed-pages' | translate: { count: dossierTemplate.numberOfPages }"
></span>
</div>
</div>
</div>
<div class="flex-3">
<redaction-donut-chart
[config]="dossierStates()"
[radius]="63"
[strokeWidth]="15"
[subtitles]="['dossier-template-stats.active-dossiers' | translate: { count: dossierTemplate.numberOfActiveDossiers }]"
direction="row"
totalType="sum"
></redaction-donut-chart>
</div>
<div class="flex-3">
<redaction-donut-chart
[config]="workflowStatuses()"
[radius]="63"
[strokeWidth]="15"
[subtitles]="['dossier-template-stats.total-documents' | translate]"
direction="row"
totalType="sum"
></redaction-donut-chart>
</div>
} @else {
<div class="text-muted">
<div class="heading mb-8">
{{ dossierTemplate.name }}
</div>
<div>
{{ 'dashboard.empty-template.description' | translate }}
</div>
</div>
</div>
<div class="flex-3">
<redaction-donut-chart
[config]="translateChartService.translateAndSortDossierStates(dossierTemplate.dossiersChartConfig, dossierTemplate.id)"
[radius]="63"
[strokeWidth]="15"
[subtitles]="['dossier-template-stats.active-dossiers' | translate: { count: dossierTemplate.numberOfActiveDossiers }]"
direction="row"
totalType="sum"
></redaction-donut-chart>
</div>
<div class="flex-3">
<redaction-donut-chart
[config]="translateChartService.translateWorkflowStatus(dossierTemplate.documentsChartConfig)"
[radius]="63"
[strokeWidth]="15"
[subtitles]="['dossier-template-stats.total-documents' | translate]"
direction="row"
totalType="sum"
></redaction-donut-chart>
</div>
</ng-container>
<ng-template #empty>
<div class="text-muted">
<div class="heading mb-8">
{{ dossierTemplate.name }}
</div>
<div>
{{ 'dashboard.empty-template.description' | translate }}
</div>
</div>
<iqser-icon-button
(action)="newDossier()"
*ngIf="permissionsService.canCreateDossier(dossierTemplate)"
[attr.help-mode-key]="'new_dossier'"
[label]="'dashboard.empty-template.new-dossier' | translate"
[type]="iconButtonTypes.primary"
[buttonId]="(dossierTemplate.name | snakeCase) + '-icon-button'"
icon="iqser:plus"
></iqser-icon-button>
</ng-template>
</a>
@if (canCreateDossier()) {
<iqser-icon-button
(action)="newDossier()"
[attr.help-mode-key]="'new_dossier'"
[label]="'dashboard.empty-template.new-dossier' | translate"
[type]="iconButtonTypes.primary"
[buttonId]="(dossierTemplate.name | snakeCase) + '-icon-button'"
icon="iqser:plus"
></iqser-icon-button>
}
}
</a>
}

View File

@ -1,11 +1,10 @@
import { Component, Input } from '@angular/core';
import { Component, computed, input } from '@angular/core';
import { DashboardStats } from '@red/domain';
import { IconButtonComponent, IconButtonTypes } from '@iqser/common-ui';
import { TranslateChartService } from '@services/translate-chart.service';
import { SharedDialogService } from '@shared/services/dialog.service';
import { Roles } from '@users/roles';
import { PermissionsService } from '@services/permissions.service';
import { NgIf } from '@angular/common';
import { RouterLink } from '@angular/router';
import { MatIcon } from '@angular/material/icon';
import { TranslateModule } from '@ngx-translate/core';
@ -17,13 +16,20 @@ import { SnakeCasePipe } from '@common-ui/pipes/snake-case.pipe';
templateUrl: './template-stats.component.html',
styleUrls: ['./template-stats.component.scss'],
standalone: true,
imports: [NgIf, RouterLink, MatIcon, TranslateModule, DonutChartComponent, IconButtonComponent, SnakeCasePipe],
imports: [RouterLink, MatIcon, TranslateModule, DonutChartComponent, IconButtonComponent, SnakeCasePipe],
})
export class TemplateStatsComponent {
readonly iconButtonTypes = IconButtonTypes;
readonly roles = Roles;
@Input() stats: DashboardStats;
readonly stats = input<DashboardStats>();
readonly dossierStates = computed(() =>
this.translateChartService.translateAndSortDossierStates(this.stats().dossiersChartConfig, this.stats().id),
);
readonly workflowStatuses = computed(() => this.translateChartService.translateWorkflowStatus(this.stats().documentsChartConfig));
readonly isTemplateEmpty = computed(() => this.stats().isEmpty);
readonly canCreateDossier = computed(() => this.permissionsService.canCreateDossier(this.stats()));
constructor(
private readonly _dialogService: SharedDialogService,
@ -32,6 +38,6 @@ export class TemplateStatsComponent {
) {}
newDossier(): void {
this._dialogService.openDialog('addDossier', { dossierTemplateId: this.stats.dossierTemplateId });
this._dialogService.openDialog('addDossier', { dossierTemplateId: this.stats().dossierTemplateId });
}
}

View File

@ -1 +1,3 @@
<iqser-status-bar *ngIf="stats" [configs]="statusBarConfig"></iqser-status-bar>
@if (stats()) {
<iqser-status-bar [configs]="statusBarConfig()"></iqser-status-bar>
}

View File

@ -1,27 +1,22 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
import { DossierStats, StatusSorter } from '@red/domain';
import { List } from '@iqser/common-ui/lib/utils';
import { StatusBarComponent, StatusBarConfig } from '@iqser/common-ui/lib/shared';
import { NgIf } from '@angular/common';
@Component({
selector: 'redaction-dossier-documents-status',
templateUrl: './dossier-documents-status.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [StatusBarComponent, NgIf],
imports: [StatusBarComponent],
})
export class DossierDocumentsStatusComponent implements OnChanges {
@Input() stats: DossierStats;
statusBarConfig: List<StatusBarConfig<string>>;
export class DossierDocumentsStatusComponent {
readonly stats = input<DossierStats>();
readonly statusBarConfig = computed(() => this.#statusConfig);
private get _statusConfig(): List<StatusBarConfig<string>> {
const { fileCountPerWorkflowStatus } = this.stats;
get #statusConfig(): List<StatusBarConfig<string>> {
const { fileCountPerWorkflowStatus } = this.stats();
const statuses = Object.keys(fileCountPerWorkflowStatus).sort(StatusSorter.byStatus);
return statuses.map(status => ({ length: fileCountPerWorkflowStatus[status], color: status }));
}
ngOnChanges(): void {
this.statusBarConfig = this._statusConfig;
}
}

View File

@ -1,15 +1,13 @@
<div class="needs-work">
<redaction-annotation-icon
*ngIf="dossierStats.hasRedactionsFilePresent"
[color]="redactionColor$ | async"
[label]="'redaction-abbreviation' | translate"
type="square"
></redaction-annotation-icon>
@if (dossierStats().hasRedactionsFilePresent) {
<redaction-annotation-icon
[color]="redactionColor$ | async"
[label]="'redaction-abbreviation' | translate"
type="square"
></redaction-annotation-icon>
}
<redaction-annotation-icon
*ngIf="dossierStats.hasHintsNoRedactionsFilePresent"
[color]="hintColor$ | async"
label="H"
type="circle"
></redaction-annotation-icon>
@if (dossierStats().hasHintsNoRedactionsFilePresent) {
<redaction-annotation-icon [color]="hintColor$ | async" label="H" type="circle"></redaction-annotation-icon>
}
</div>

View File

@ -1,9 +1,9 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { ChangeDetectionStrategy, Component, input, OnChanges, SimpleChanges } from '@angular/core';
import { DefaultColorType, Dossier, DossierStats } from '@red/domain';
import { DefaultColorsService } from '@services/entity-services/default-colors.service';
import { BehaviorSubject, Observable, switchMap } from 'rxjs';
import { AnnotationIconComponent } from '@shared/components/annotation-icon/annotation-icon.component';
import { AsyncPipe, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
@Component({
@ -12,12 +12,12 @@ import { TranslateModule } from '@ngx-translate/core';
styleUrls: ['./dossier-workload-column.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [AnnotationIconComponent, AsyncPipe, TranslateModule, NgIf],
imports: [AnnotationIconComponent, AsyncPipe, TranslateModule],
})
export class DossierWorkloadColumnComponent implements OnChanges {
readonly #dossierTemplateId$ = new BehaviorSubject<string>(null);
@Input() dossier: Dossier;
@Input() dossierStats: DossierStats;
readonly dossier = input<Dossier>();
readonly dossierStats = input<DossierStats>();
readonly hintColor$: Observable<string>;
readonly redactionColor$: Observable<string>;
@ -30,7 +30,7 @@ export class DossierWorkloadColumnComponent implements OnChanges {
ngOnChanges(changes: SimpleChanges): void {
if (changes.dossier) {
this.#dossierTemplateId$.next(this.dossier.dossierTemplateId);
this.#dossierTemplateId$.next(this.dossier().dossierTemplateId);
}
}

View File

@ -1,30 +1,32 @@
<div *ngIf="stats$ | async as stats">
<redaction-donut-chart
[config]="dossiersChartConfig$ | async"
[radius]="80"
[strokeWidth]="15"
[subtitles]="['dossier-template-stats.active-dossiers' | translate : { count: stats.numberOfActiveDossiers }]"
filterKey="dossierStatesFilters"
></redaction-donut-chart>
@if (stats$ | async; as stats) {
<div>
<redaction-donut-chart
[config]="dossiersChartConfig$ | async"
[radius]="80"
[strokeWidth]="15"
[subtitles]="['dossier-template-stats.active-dossiers' | translate: { count: stats.numberOfActiveDossiers }]"
filterKey="dossierStatesFilters"
></redaction-donut-chart>
<div class="dossier-stats-container">
<div class="dossier-stats-item">
<mat-icon svgIcon="red:needs-work"></mat-icon>
<div>
<div class="heading">{{ stats.numberOfPages | number }}</div>
<div [translateParams]="{ count: stats.numberOfPages }" [translate]="'dossier-listing.stats.analyzed-pages'"></div>
<div class="dossier-stats-container">
<div class="dossier-stats-item">
<mat-icon svgIcon="red:needs-work"></mat-icon>
<div>
<div class="heading">{{ stats.numberOfPages | number }}</div>
<div [translateParams]="{ count: stats.numberOfPages }" [translate]="'dossier-listing.stats.analyzed-pages'"></div>
</div>
</div>
</div>
<div class="dossier-stats-item">
<mat-icon svgIcon="red:user"></mat-icon>
<div>
<div class="heading">{{ stats.numberOfPeople }}</div>
<div translate="dossier-listing.stats.total-people"></div>
<div class="dossier-stats-item">
<mat-icon svgIcon="red:user"></mat-icon>
<div>
<div class="heading">{{ stats.numberOfPeople }}</div>
<div translate="dossier-listing.stats.total-people"></div>
</div>
</div>
</div>
</div>
</div>
}
<div class="right-chart">
<redaction-donut-chart

View File

@ -6,7 +6,7 @@ import { map } from 'rxjs/operators';
import { DashboardStatsService } from '@services/dossier-templates/dashboard-stats.service';
import { getParam } from '@iqser/common-ui/lib/utils';
import { DonutChartComponent } from '@shared/components/donut-chart/donut-chart.component';
import { AsyncPipe, DecimalPipe, NgIf } from '@angular/common';
import { AsyncPipe, DecimalPipe } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { MatIcon } from '@angular/material/icon';
@ -16,7 +16,7 @@ import { MatIcon } from '@angular/material/icon';
styleUrls: ['./dossiers-listing-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [DonutChartComponent, AsyncPipe, NgIf, TranslateModule, MatIcon, DecimalPipe],
imports: [DonutChartComponent, AsyncPipe, TranslateModule, MatIcon, DecimalPipe],
})
export class DossiersListingDetailsComponent {
readonly stats$: Observable<DashboardStats>;

View File

@ -1,18 +1,20 @@
<ng-container *ngIf="stats$ | async as stats">
@if (stats$ | async; as stats) {
<div class="cell">
<redaction-dossier-name-column [dossierStats]="stats" [dossier]="dossier"></redaction-dossier-name-column>
<redaction-dossier-name-column [dossierStats]="stats" [dossier]="dossier()"></redaction-dossier-name-column>
</div>
<div class="cell">
<redaction-date-column [date]="stats.fileManipulationDate"></redaction-date-column>
</div>
<div class="cell" *ngIf="!isDocumine">
<redaction-dossier-workload-column [dossierStats]="stats" [dossier]="dossier"></redaction-dossier-workload-column>
</div>
@if (!isDocumine) {
<div class="cell">
<redaction-dossier-workload-column [dossierStats]="stats" [dossier]="dossier()"></redaction-dossier-workload-column>
</div>
}
<div class="cell user-column">
<iqser-initials-avatar [user]="dossier.ownerId" [withName]="true"></iqser-initials-avatar>
<iqser-initials-avatar [user]="dossier().ownerId" [withName]="true"></iqser-initials-avatar>
</div>
<div class="cell status-container">
@ -20,8 +22,8 @@
</div>
<div class="cell">
<redaction-dossier-state [dossier]="dossier"></redaction-dossier-state>
<redaction-dossier-state [dossier]="dossier()"></redaction-dossier-state>
<redaction-dossiers-listing-actions [dossier]="dossier"></redaction-dossiers-listing-actions>
<redaction-dossiers-listing-actions [dossier]="dossier()"></redaction-dossiers-listing-actions>
</div>
</ng-container>
}

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { ChangeDetectionStrategy, Component, input, OnChanges } from '@angular/core';
import { Dossier, DossierStats } from '@red/domain';
import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
import { BehaviorSubject, Observable } from 'rxjs';
@ -6,7 +6,7 @@ import { switchMap, tap } from 'rxjs/operators';
import { getConfig } from '@iqser/common-ui';
import { DossierNameColumnComponent } from '@shared/components/dossier-name-column/dossier-name-column.component';
import { DateColumnComponent } from '../../../shared-dossiers/components/date-column/date-column.component';
import { AsyncPipe, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { DossierWorkloadColumnComponent } from '../dossier-workload-column/dossier-workload-column.component';
import { InitialsAvatarComponent } from '@common-ui/users';
import { DossierDocumentsStatusComponent } from '../dossier-documents-status/dossier-documents-status.component';
@ -23,7 +23,6 @@ import { DossiersListingActionsComponent } from '../../../shared-dossiers/compon
DossierNameColumnComponent,
DateColumnComponent,
AsyncPipe,
NgIf,
DossierWorkloadColumnComponent,
InitialsAvatarComponent,
DossierDocumentsStatusComponent,
@ -32,7 +31,7 @@ import { DossiersListingActionsComponent } from '../../../shared-dossiers/compon
],
})
export class TableItemComponent implements OnChanges {
@Input() dossier!: Dossier;
readonly dossier = input.required<Dossier>();
readonly stats$: Observable<DossierStats>;
readonly isDocumine = getConfig().IS_DOCUMINE;
@ -43,14 +42,14 @@ export class TableItemComponent implements OnChanges {
switchMap(dossierId => this.dossierStatsService.watch$(dossierId)),
// TODO required for sorting the dossier table - fix me Baby one more time!
tap(stats => {
this.dossier.changedDate = stats.fileManipulationDate;
this.dossier().changedDate = stats.fileManipulationDate;
}),
);
}
ngOnChanges() {
if (this.dossier) {
this.#ngOnChanges$.next(this.dossier.id);
if (this.dossier()) {
this.#ngOnChanges$.next(this.dossier().id);
}
}
}

View File

@ -18,7 +18,7 @@
[noDataButtonLabel]="'dossier-listing.no-data.action' | translate"
[noDataText]="'dossier-listing.no-data.title' | translate"
[noMatchText]="'dossier-listing.no-match.title' | translate"
[showNoDataButton]="permissionsService.canCreateDossier(dossierTemplate)"
[showNoDataButton]="canCreateDossier()"
[tableColumnConfigs]="tableColumnConfigs"
[rowIdPrefix]="'dossier'"
[namePropertyKey]="'dossierName'"
@ -27,7 +27,9 @@
</div>
<div class="right-container" iqserHasScrollbar>
<redaction-dossiers-listing-details *ngIf="(entitiesService.noData$ | async) === false"></redaction-dossiers-listing-details>
@if ((entitiesService.noData$ | async) === false) {
<redaction-dossiers-listing-details></redaction-dossiers-listing-details>
}
</div>
</div>
</section>

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Dossier, DOSSIER_TEMPLATE_ID, DossierTemplate } from '@red/domain';
import { PermissionsService } from '@services/permissions.service';
import {
@ -49,7 +49,8 @@ export class DossiersListingScreenComponent extends ListingComponent<Dossier> im
readonly tableHeaderLabel = _('dossier-listing.table-header.title');
readonly buttonConfigs: ButtonConfig[];
readonly dossierTemplate: DossierTemplate;
readonly computeFilters$ = this._activeDossiersService.all$.pipe(tap(() => this._computeAllFilters()));
readonly canCreateDossier = computed(() => this.permissionsService.canCreateDossier(this.dossierTemplate));
readonly computeFilters$ = this._activeDossiersService.all$.pipe(tap(() => this.#computeAllFilters()));
@ViewChild('needsWorkFilterTemplate', {
read: TemplateRef,
static: true,
@ -91,7 +92,7 @@ export class DossiersListingScreenComponent extends ListingComponent<Dossier> im
this._loadingService.stop();
}
private _computeAllFilters() {
#computeAllFilters() {
const filterGroups = this._configService.filterGroups(
this.entitiesService.all,
this._needsWorkFilterTemplate,

View File

@ -1,33 +1,30 @@
<div (longPress)="forceReanalysisAction($event)" class="action-buttons" redactionLongPress>
<iqser-circle-button
(action)="openEditDossierDialog(dossier.id)"
(action)="openEditDossierDialog(dossier().id)"
*allow="roles.dossiers.read; if: currentUser.isUser"
[attr.help-mode-key]="'edit_dossier'"
[icon]="
((iqserPermissionsService.has$(roles.dossiers.edit) | async) && currentUser.isManager) || canEditDossierDictionary
? 'iqser:edit'
: 'red:info'
"
[icon]="(hasEditDossierRole() && currentUser.isManager) || canEditDossierDictionary() ? 'iqser:edit' : 'red:info'"
[tooltip]="
(((iqserPermissionsService.has$(roles.dossiers.edit) | async) && currentUser.isManager) || canEditDossierDictionary
((hasEditDossierRole() && currentUser.isManager) || canEditDossierDictionary()
? 'dossier-listing.edit.action'
: 'dossier-listing.dossier-info.action'
) | translate
"
></iqser-circle-button>
<iqser-circle-button
(action)="reanalyseDossier(dossier)"
*ngIf="displayReanalyseBtn"
[tooltip]="'dossier-listing.reanalyse.action' | translate"
icon="iqser:refresh"
></iqser-circle-button>
@if (displayReanalyseBtn) {
<iqser-circle-button
(action)="reanalyseDossier(dossier())"
[tooltip]="'dossier-listing.reanalyse.action' | translate"
icon="iqser:refresh"
></iqser-circle-button>
}
<redaction-file-download-btn
[attr.help-mode-key]="isDocumine ? 'template_download_dossier' : 'download_dossier'"
[buttonId]="'download-dossier-files-' + dossier.id"
[buttonId]="'download-dossier-files-' + dossier().id"
[disabled]="downloadBtnDisabled"
[dossier]="dossier"
[dossier]="dossier()"
[files]="files"
dossierDownload
></redaction-file-download-btn>

View File

@ -1,5 +1,5 @@
import { AsyncPipe, NgIf } from '@angular/common';
import { Component, Input, OnChanges } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { Component, computed, input, OnChanges } from '@angular/core';
import { CircleButtonComponent, getConfig, IqserAllowDirective, IqserPermissionsService, largeDialogConfig } from '@iqser/common-ui';
import { getCurrentUser } from '@iqser/common-ui/lib/users';
import { TranslateModule } from '@ngx-translate/core';
@ -13,12 +13,13 @@ import { Roles } from '@users/roles';
import { UserPreferenceService } from '@users/user-preference.service';
import { EditDossierDialogComponent } from '../../dialogs/edit-dossier-dialog/edit-dossier-dialog.component';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
selector: 'redaction-dossiers-listing-actions [dossier]',
templateUrl: './dossiers-listing-actions.component.html',
standalone: true,
imports: [LongPressDirective, CircleButtonComponent, IqserAllowDirective, TranslateModule, NgIf, FileDownloadBtnComponent, AsyncPipe],
imports: [LongPressDirective, CircleButtonComponent, IqserAllowDirective, TranslateModule, FileDownloadBtnComponent, AsyncPipe],
})
export class DossiersListingActionsComponent implements OnChanges {
readonly roles = Roles;
@ -30,7 +31,9 @@ export class DossiersListingActionsComponent implements OnChanges {
displayReanalyseBtn = false;
downloadBtnDisabled = false;
@Input() dossier: Dossier;
readonly dossier = input<Dossier>();
readonly canEditDossierDictionary = computed(() => this.permissionsService.canEditDossierDictionary(this.dossier()));
readonly hasEditDossierRole = toSignal(this.iqserPermissionsService.has$(this.roles.dossiers.edit));
constructor(
private readonly _reanalysisService: ReanalysisService,
@ -41,14 +44,10 @@ export class DossiersListingActionsComponent implements OnChanges {
private readonly _userPreferenceService: UserPreferenceService,
) {}
get canEditDossierDictionary() {
return this.permissionsService.canEditDossierDictionary(this.dossier);
}
ngOnChanges() {
this.files = this.filesMapService.get(this.dossier.id);
this.files = this.filesMapService.get(this.dossier().id);
this.downloadBtnDisabled = this.files.some(file => !file.lastProcessed);
this.displayReanalyseBtn = this.permissionsService.displayReanalyseBtn(this.dossier) && this.analysisForced;
this.displayReanalyseBtn = this.permissionsService.displayReanalyseBtn(this.dossier()) && this.analysisForced;
}
forceReanalysisAction($event: LongPressEvent) {

View File

@ -0,0 +1,13 @@
<svg:circle
[attr.cx]="cx()"
[attr.cy]="cy()"
[attr.r]="radius()"
[attr.stroke-dasharray]="circumference()"
[attr.stroke-dashoffset]="strokeDashOffset()"
[attr.stroke-width]="strokeWidth()"
[attr.stroke]="stroke()"
[attr.transform]="circleTransformValue()"
[class]="config().color"
fill="transparent"
xmlns:svg="http://www.w3.org/2000/svg"
/>

After

Width:  |  Height:  |  Size: 387 B

View File

@ -0,0 +1,4 @@
:host {
display: contents;
height: fit-content;
}

View File

@ -0,0 +1,33 @@
import { Component, computed, input } from '@angular/core';
import { DonutChartConfig } from '@red/domain';
@Component({
selector: '[redaction-chart-circle-config]',
standalone: true,
imports: [],
templateUrl: './chart-circle-config.component.html',
styleUrl: './chart-circle-config.component.scss',
})
export class ChartCircleConfigComponent {
readonly index = input<number>();
readonly config = input<DonutChartConfig>();
readonly cx = input<number>();
readonly cy = input<number>();
readonly radius = input<number>();
readonly circumference = input<number>();
readonly strokeWidth = input<number>();
readonly chartData = input<{ degrees: number }>();
readonly dataTotal = input<number>();
readonly stroke = computed(() => (this.config().color.includes('#') ? this.config().color : ''));
readonly percentage = computed(() => this.config().value / this.dataTotal());
readonly strokeDashOffset = computed(() => {
const strokeDiff = this.percentage() * this.circumference();
return this.circumference() - strokeDiff;
});
readonly circleTransformValue = computed(() => {
return `rotate(${this.chartData().degrees}, ${this.cx()}, ${this.cy()})`;
});
}

View File

@ -0,0 +1,8 @@
<div
(click)="config().key && selectValue(config().key)"
[class.active]="filterChecked()"
[class.filter-disabled]="!config().key || !filters().length"
[id]="config().id"
>
<iqser-status-bar [configs]="statusBarConfigs()" [small]="true"></iqser-status-bar>
</div>

View File

@ -0,0 +1,23 @@
@use 'variables';
div {
border-radius: 4px;
padding: 3px 8px;
width: 100%;
&:not(:last-child) {
margin-bottom: 8px;
}
&:not(.filter-disabled) {
cursor: pointer;
}
&:hover:not(.active):not(.filter-disabled) {
background-color: var(--iqser-btn-bg);
}
&.active {
background-color: rgba(variables.$primary, 0.1);
}
}

View File

@ -0,0 +1,47 @@
import { Component, computed, input, Optional } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { StatusBarComponent } from '@common-ui/shared';
import { DonutChartConfig } from '@red/domain';
import { FilterService, INestedFilter } from '@common-ui/filtering';
@Component({
selector: 'redaction-chart-filters',
standalone: true,
imports: [AsyncPipe, StatusBarComponent],
templateUrl: './chart-filters.component.html',
styleUrl: './chart-filters.component.scss',
})
export class ChartFiltersComponent {
readonly config = input<DonutChartConfig>();
readonly totalType = input<'sum' | 'count' | 'simpleLabel'>('sum');
readonly counterText = input<string>();
readonly filterKey = input<string>();
readonly valueFormatter = input<(value: number) => string>();
readonly filters = input<INestedFilter[]>();
readonly formattedValue = computed(() =>
this.valueFormatter() ? this.valueFormatter()(this.config().value) : this.config().value.toString(),
);
readonly filterChecked = computed(() => this.filters().find(item => item.id === this.config().key)?.checked);
readonly label = computed(() => {
return this.totalType() === 'simpleLabel'
? `${this.config().label}`
: this.totalType() === 'sum'
? `${this.formattedValue()} ${this.config().label}`
: `${this.config().label} (${this.formattedValue()} ${this.counterText()})`;
});
readonly statusBarConfigs = computed(() => [
{
length: this.config().value,
color: this.config().color,
label: this.label(),
cssClass: this.config().color === 'PROCESSING' || this.config().color === 'OCR_PROCESSING' ? 'loading' : '',
},
]);
constructor(@Optional() readonly filterService: FilterService) {}
selectValue(key: string): void {
this.filterService?.toggleFilter(this.filterKey(), key);
}
}

View File

@ -1,59 +1,63 @@
<div [class]="'container flex ' + direction">
<svg [attr.height]="size" [attr.width]="size" [style.min-width]="size" attr.viewBox="0 0 {{ size }} {{ size }}" class="donut-chart">
<g *ngFor="let value of config; let i = index">
<circle
*ngIf="!!chartData[i]"
[attr.cx]="cx"
[attr.cy]="cy"
[attr.r]="radius"
[attr.stroke-dasharray]="circumference"
[attr.stroke-dashoffset]="calculateStrokeDashOffset(value.value)"
[attr.stroke-width]="strokeWidth"
[attr.stroke]="value.color.includes('#') ? value.color : ''"
[attr.transform]="returnCircleTransformValue(i)"
[class]="value.color"
fill="transparent"
/>
</g>
<div [class]="'container flex ' + direction()">
<svg
[attr.height]="size()"
[attr.width]="size()"
[style.min-width]="size()"
attr.viewBox="0 0 {{ size() }} {{ size() }}"
class="donut-chart"
>
@for (value of config(); track value.label) {
@if (!!chartData()[$index]) {
<g
redaction-chart-circle-config
[config]="value"
[index]="$index"
[cx]="cx()"
[cy]="cy()"
[circumference]="circumference()"
[radius]="radius()"
[strokeWidth]="strokeWidth()"
[chartData]="chartData()[$index]"
[dataTotal]="dataTotal()"
></g>
}
}
</svg>
<div [style]="'height: ' + size + 'px; width: ' + size + 'px; padding: ' + (strokeWidth + 5) + 'px;'" class="text-container">
<div class="heading-xl">{{ getFormattedValue(displayedDataTotal) }}</div>
<div *ngIf="subtitles.length === 1" class="mt-5">{{ subtitles[0] }}</div>
<div [style]="'height: ' + size() + 'px; width: ' + size() + 'px; padding: ' + (strokeWidth() + 5) + 'px;'" class="text-container">
<div class="heading-xl">{{ formatedValue() }}</div>
@if (subtitles().length === 1) {
<div class="mt-5">{{ subtitles()[0] }}</div>
}
<div *ngIf="subtitleTemplate as t" class="mt-5">
<ng-container *ngTemplateOutlet="t"></ng-container>
</div>
@if (subtitleTemplate(); as t) {
<div class="mt-5">
<ng-container *ngTemplateOutlet="t"></ng-container>
</div>
}
<mat-select
(selectionChange)="subtitleChanged.emit(subtitles.indexOf($event.value))"
*ngIf="subtitles.length > 1"
[value]="subtitles[0]"
class="mt-5 ml-10"
>
<mat-option *ngFor="let subtitle of subtitles" [value]="subtitle"> {{ subtitle }} </mat-option>
</mat-select>
@if (subtitles().length > 1) {
<mat-select
(selectionChange)="subtitleChanged.emit(subtitles().indexOf($event.value))"
[value]="subtitles()[0]"
class="mt-5 ml-10"
>
@for (subtitle of subtitles(); track subtitle) {
<mat-option [value]="subtitle"> {{ subtitle }}</mat-option>
}
</mat-select>
}
</div>
<div [attr.help-mode-key]="helpModeKey" class="breakdown-container">
<div
(click)="val.key && selectValue(val.key)"
*ngFor="let val of config"
[class.active]="filterChecked$(val.key) | async"
[class.filter-disabled]="!val.key || !(filters$ | async).length"
[id]="val.id"
>
<iqser-status-bar
[configs]="[
{
length: val.value,
color: val.color,
label: getLabel(val),
cssClass: val.color === 'PROCESSING' || val.color === 'OCR_PROCESSING' ? 'loading' : ''
}
]"
[small]="true"
></iqser-status-bar>
</div>
<div [attr.help-mode-key]="helpModeKey()" class="breakdown-container">
@for (value of config(); track value.label) {
<redaction-chart-filters
[config]="value"
[filterKey]="filterKey()"
[totalType]="totalType()"
[counterText]="counterText()"
[filters]="filters()"
></redaction-chart-filters>
}
</div>
</div>

View File

@ -1,5 +1,3 @@
@use 'variables';
:host {
height: fit-content;
}
@ -47,28 +45,6 @@
flex-direction: column;
align-items: flex-start;
margin-left: -8px;
> div {
border-radius: 4px;
padding: 3px 8px;
width: 100%;
&:not(:last-child) {
margin-bottom: 8px;
}
&:not(.filter-disabled) {
cursor: pointer;
}
&:hover:not(.active):not(.filter-disabled) {
background-color: var(--iqser-btn-bg);
}
&.active {
background-color: rgba(variables.$primary, 0.1);
}
}
}
mat-select {

View File

@ -1,87 +1,49 @@
import { Component, EventEmitter, Input, OnChanges, OnInit, Optional, Output, TemplateRef } from '@angular/core';
import { Component, computed, input, Optional, output, TemplateRef } from '@angular/core';
import { DonutChartConfig } from '@red/domain';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { AsyncPipe, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
import { filter, map, switchMap } from 'rxjs/operators';
import { AsyncPipe, NgTemplateOutlet } from '@angular/common';
import { MatSelectModule } from '@angular/material/select';
import { FilterService, INestedFilter } from '@iqser/common-ui/lib/filtering';
import { get, shareLast } from '@iqser/common-ui/lib/utils';
import { FilterService } from '@iqser/common-ui/lib/filtering';
import { shareLast } from '@iqser/common-ui/lib/utils';
import { StatusBarComponent } from '@iqser/common-ui/lib/shared';
import { ChartCircleConfigComponent } from '@shared/components/donut-chart/chart-circle-config/chart-circle-config.component';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { ChartFiltersComponent } from '@shared/components/donut-chart/chart-filters/chart-filters.component';
@Component({
selector: 'redaction-donut-chart',
templateUrl: './donut-chart.component.html',
styleUrls: ['./donut-chart.component.scss'],
standalone: true,
imports: [NgForOf, NgIf, MatSelectModule, StatusBarComponent, AsyncPipe, NgTemplateOutlet],
imports: [MatSelectModule, StatusBarComponent, AsyncPipe, NgTemplateOutlet, ChartCircleConfigComponent, ChartFiltersComponent],
})
export class DonutChartComponent implements OnChanges, OnInit {
@Input() subtitles: string[] = [];
@Input() config: DonutChartConfig[] = [];
@Input() radius = 85;
@Input() strokeWidth = 20;
@Input() direction: 'row' | 'column' = 'column';
@Input() totalType: 'sum' | 'count' | 'simpleLabel' = 'sum';
@Input() counterText: string;
@Input() filterKey;
@Input() helpModeKey;
@Input() valueFormatter?: (value: number) => string;
@Input() subtitleTemplate?: TemplateRef<any>;
export class DonutChartComponent {
readonly subtitles = input<string[]>([]);
readonly config = input<DonutChartConfig[]>([]);
readonly radius = input(85);
readonly strokeWidth = input(20);
readonly direction = input<'row' | 'column'>('column');
readonly totalType = input<'sum' | 'count' | 'simpleLabel'>('sum');
readonly counterText = input<string>();
readonly filterKey = input<string>();
readonly helpModeKey = input();
readonly valueFormatter = input<(value: number) => string>();
readonly subtitleTemplate = input<TemplateRef<any>>();
@Output() readonly subtitleChanged = new EventEmitter<number>();
readonly subtitleChanged = output<number>();
chartData: any[] = [];
cx = 0;
cy = 0;
size = 0;
filters$: Observable<INestedFilter[]>;
readonly dataTotal = computed(() => {
return this.config()
.map(v => v.value)
.reduce((acc, val) => acc + val, 0);
});
readonly displayedDataTotal = computed(() => (this.totalType() === 'sum' ? this.dataTotal() : this.config().length));
readonly formatedValue = computed(() => this.getFormattedValue(this.displayedDataTotal()));
readonly circumference = computed(() => 2 * Math.PI * this.radius());
get circumference(): number {
return 2 * Math.PI * this.radius;
}
get dataTotal(): number {
return this.config.map(v => v.value).reduce((acc, val) => acc + val, 0);
}
get displayedDataTotal() {
return this.totalType === 'sum' ? this.dataTotal : this.config.length;
}
constructor(@Optional() readonly filterService: FilterService) {
// TODO: move this component to a separate module, split into smaller components, improve filters
}
ngOnInit() {
const filterModels$ = this.filterService?.getFilterModels$(this.filterKey).pipe(
map(filters => filters ?? []),
shareLast(),
);
this.filters$ = filterModels$ ?? of([]);
}
ngOnChanges(): void {
this.calculateChartData();
this.cx = this.radius + this.strokeWidth / 2;
this.cy = this.radius + this.strokeWidth / 2;
this.size = this.strokeWidth + this.radius * 2;
}
filterChecked$(key: string): Observable<boolean> {
return this.filters$.pipe(
get(filter => filter.id === key),
map(filter => !!filter?.checked),
);
}
getFormattedValue(value: number): string {
return this.valueFormatter ? this.valueFormatter(value) : value.toString();
}
calculateChartData() {
readonly chartData = computed(() => {
let angleOffset = -90;
this.chartData = this.config.map(dataVal => {
return this.config().map(dataVal => {
if (dataVal.value === 0) {
return null;
}
@ -90,30 +52,33 @@ export class DonutChartComponent implements OnChanges, OnInit {
angleOffset = this.dataPercentage(dataVal.value) * 360 + angleOffset;
return res;
});
});
readonly cx = computed(() => this.radius() + this.strokeWidth() / 2);
readonly cy = computed(() => this.radius() + this.strokeWidth() / 2);
readonly size = computed(() => this.strokeWidth() + this.radius() * 2);
readonly filterKey$ = toObservable(this.filterKey);
readonly filters = toSignal(
this.filterKey$.pipe(
filter(Boolean),
switchMap(filterKey => {
return this.filterService?.getFilterModels$(filterKey).pipe(
map(filters => [...filters] ?? []),
shareLast(),
);
}),
),
{ initialValue: [] },
);
constructor(@Optional() readonly filterService: FilterService) {
// TODO: move this component to a separate module, split into smaller components, improve filters
}
calculateStrokeDashOffset(dataVal: number): number {
const strokeDiff = this.dataPercentage(dataVal) * this.circumference;
return this.circumference - strokeDiff;
getFormattedValue(value: number): string {
return this.valueFormatter() ? this.valueFormatter()(value) : value.toString();
}
dataPercentage(dataVal: number): number {
return dataVal / this.dataTotal;
}
returnCircleTransformValue(index: number) {
return `rotate(${this.chartData[index].degrees}, ${this.cx}, ${this.cy})`;
}
getLabel({ label, value }: DonutChartConfig): string {
return this.totalType === 'simpleLabel'
? `${label}`
: this.totalType === 'sum'
? `${this.getFormattedValue(value)} ${label}`
: `${label} (${this.getFormattedValue(value)} ${this.counterText})`;
}
selectValue(key: string): void {
this.filterService?.toggleFilter(this.filterKey, key);
return dataVal / this.dataTotal();
}
}

View File

@ -1,37 +1,43 @@
<div [attr.help-mode-key]="'dossier'" [matTooltip]="dossier.dossierName" class="table-item-title heading mb-6" matTooltipPosition="above">
{{ dossier.dossierName }}
<div [attr.help-mode-key]="'dossier'" [matTooltip]="dossier().dossierName" class="table-item-title heading mb-6" matTooltipPosition="above">
{{ dossier().dossierName }}
</div>
<div class="small-label stats-subtitle mb-6">
<div>
<mat-icon svgIcon="red:template"></mat-icon>
{{ getDossierTemplateNameFor(dossier.dossierTemplateId) }}
{{ dossierTemplateName() }}
</div>
</div>
<div *ngIf="dossierStats" class="stats-subtitle">
<div class="small-label">
<mat-icon svgIcon="iqser:document"></mat-icon>
{{ isSoftDeleted ? dossierStats.numberOfSoftDeletedFiles : dossierStats.numberOfFiles }}
</div>
@if (dossierStats()) {
<div class="stats-subtitle">
<div class="small-label">
<mat-icon svgIcon="iqser:document"></mat-icon>
{{ isSoftDeleted() ? dossierStats().numberOfSoftDeletedFiles : dossierStats().numberOfFiles }}
</div>
<div *ngIf="!isSoftDeleted" class="small-label">
<mat-icon svgIcon="iqser:pages"></mat-icon>
{{ dossierStats.numberOfPages }}
</div>
@if (!isSoftDeleted()) {
<div class="small-label">
<mat-icon svgIcon="iqser:pages"></mat-icon>
{{ dossierStats().numberOfPages }}
</div>
}
<div class="small-label">
<mat-icon svgIcon="red:user"></mat-icon>
{{ dossier.memberIds.length }}
</div>
<div class="small-label">
<mat-icon svgIcon="red:user"></mat-icon>
{{ dossier().memberIds.length }}
</div>
<div class="small-label">
<mat-icon svgIcon="iqser:calendar"></mat-icon>
{{ dossier.date | date: 'mediumDate' }}
</div>
<div class="small-label">
<mat-icon svgIcon="iqser:calendar"></mat-icon>
{{ dossier().date | date: 'mediumDate' }}
</div>
<div *ngIf="dossier.dueDate" [class.error]="passedDueDate" [class.warn]="approachingDueDate" class="small-label">
<mat-icon svgIcon="red:lightning"></mat-icon>
{{ dossier.dueDate | date: 'mediumDate' }}
@if (dossier().dueDate) {
<div [class.error]="passedDueDate()" [class.warn]="approachingDueDate()" class="small-label">
<mat-icon svgIcon="red:lightning"></mat-icon>
{{ dossier().dueDate | date: 'mediumDate' }}
</div>
}
</div>
</div>
}

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
import { List } from '@iqser/common-ui/lib/utils';
import { DossierStats, IDossier } from '@red/domain';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
@ -6,7 +6,7 @@ import { dateWithoutTime } from '@utils/functions';
import dayjs from 'dayjs';
import { MatTooltip } from '@angular/material/tooltip';
import { MatIcon } from '@angular/material/icon';
import { DatePipe, NgIf } from '@angular/common';
import { DatePipe } from '@angular/common';
const DUE_DATE_WARN_DAYS = 14;
@ -24,31 +24,17 @@ export interface PartialDossier extends Partial<IDossier> {
templateUrl: './dossier-name-column.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [MatTooltip, MatIcon, NgIf, DatePipe],
imports: [MatTooltip, MatIcon, DatePipe],
})
export class DossierNameColumnComponent {
@Input() dossier: PartialDossier;
@Input() dossierStats: DossierStats;
readonly dossier = input<PartialDossier>();
readonly dossierStats = input<DossierStats>();
readonly dossierTemplateName = computed(() => this._dossierTemplatesService.find(this.dossier().dossierTemplateId)?.name);
readonly approachingDueDate = computed(() => this.#dueDateDaysDiff() >= 0 && this.#dueDateDaysDiff() <= DUE_DATE_WARN_DAYS);
readonly passedDueDate = computed(() => this.#dueDateDaysDiff() < 0);
readonly isSoftDeleted = computed(() => !!this.dossier().softDeletedTime);
readonly #dueDateDaysDiff = computed(() => dateWithoutTime(dayjs(this.dossier().dueDate)).diff(dateWithoutTime(dayjs()), 'day'));
constructor(private readonly _dossierTemplatesService: DossierTemplatesService) {}
get approachingDueDate(): boolean {
return this.#dueDateDaysDiff >= 0 && this.#dueDateDaysDiff <= DUE_DATE_WARN_DAYS;
}
get passedDueDate(): boolean {
return this.#dueDateDaysDiff < 0;
}
get isSoftDeleted(): boolean {
return !!this.dossier.softDeletedTime;
}
get #dueDateDaysDiff(): number {
return dateWithoutTime(dayjs(this.dossier.dueDate)).diff(dateWithoutTime(dayjs()), 'day');
}
getDossierTemplateNameFor(dossierTemplateId: string): string {
return this._dossierTemplatesService.find(dossierTemplateId)?.name || '-';
}
}

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { ChangeDetectionStrategy, Component, input, OnChanges } from '@angular/core';
import { Dossier, DossierState } from '@red/domain';
import { DossierStatesMapService } from '@services/entity-services/dossier-states-map.service';
import { Observable } from 'rxjs';
@ -15,12 +15,12 @@ import { TranslateModule } from '@ngx-translate/core';
imports: [SmallChipComponent, AsyncPipe, TranslateModule],
})
export class DossierStateComponent implements OnChanges {
@Input() dossier: Dossier;
readonly dossier = input<Dossier>();
dossierState$: Observable<DossierState>;
constructor(private readonly _dossierStatesMapService: DossierStatesMapService) {}
ngOnChanges(): void {
this.dossierState$ = this._dossierStatesMapService.watch$(this.dossier.dossierTemplateId, this.dossier.dossierStatusId);
this.dossierState$ = this._dossierStatesMapService.watch$(this.dossier().dossierTemplateId, this.dossier().dossierStatusId);
}
}