RED-3586, RED-3593: Redo dossier states

This commit is contained in:
Adina Țeudan 2022-03-14 21:01:33 +02:00
parent 61ecb52969
commit 460701a17b
29 changed files with 372 additions and 353 deletions

View File

@ -1,7 +1,7 @@
<section class="dialog"> <section class="dialog">
<div <div
[translateParams]="{ [translateParams]="{
type: data.dossierState ? 'edit' : 'create', type: type,
name: data.dossierState?.name name: data.dossierState?.name
}" }"
[translate]="'add-edit-dossier-state.title'" [translate]="'add-edit-dossier-state.title'"

View File

@ -1,8 +1,11 @@
import { ChangeDetectionStrategy, Component, Inject, Injector } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject, Injector } from '@angular/core';
import { BaseDialogComponent } from '@iqser/common-ui'; import { BaseDialogComponent, LoadingService, Toaster } from '@iqser/common-ui';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { IDossierState } from '@red/domain'; import { IDossierState } from '@red/domain';
import { firstValueFrom } from 'rxjs';
import { DossierStatesService } from '@services/entity-services/dossier-states.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
interface DialogData { interface DialogData {
readonly dossierState: IDossierState; readonly dossierState: IDossierState;
@ -18,6 +21,9 @@ interface DialogData {
export class AddEditDossierStateDialogComponent extends BaseDialogComponent { export class AddEditDossierStateDialogComponent extends BaseDialogComponent {
constructor( constructor(
private readonly _formBuilder: FormBuilder, private readonly _formBuilder: FormBuilder,
private readonly _loadingService: LoadingService,
private readonly _toaster: Toaster,
private readonly _dossierStateService: DossierStatesService,
protected readonly _injector: Injector, protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddEditDossierStateDialogComponent>, protected readonly _dialogRef: MatDialogRef<AddEditDossierStateDialogComponent>,
@Inject(MAT_DIALOG_DATA) readonly data: DialogData, @Inject(MAT_DIALOG_DATA) readonly data: DialogData,
@ -27,13 +33,23 @@ export class AddEditDossierStateDialogComponent extends BaseDialogComponent {
this.initialFormValue = this.form.getRawValue(); this.initialFormValue = this.form.getRawValue();
} }
save(): void { get type(): 'edit' | 'create' {
return this.data.dossierState ? 'edit' : 'create';
}
async save(): Promise<void> {
const dossierState: IDossierState = { const dossierState: IDossierState = {
dossierStatusId: this.data.dossierState?.dossierStatusId, dossierStatusId: this.data.dossierState?.dossierStatusId,
dossierTemplateId: this.data.dossierTemplateId, dossierTemplateId: this.data.dossierTemplateId,
...this.form.getRawValue(), ...this.form.getRawValue(),
}; };
this._dialogRef.close(dossierState); this._loadingService.start();
try {
await firstValueFrom(this._dossierStateService.createOrUpdate(dossierState));
this._toaster.success(_('add-edit-dossier-state.success'), { params: { type: this.type } });
this._dialogRef.close();
} catch (e) {}
this._loadingService.stop();
} }
#getForm(): FormGroup { #getForm(): FormGroup {

View File

@ -12,12 +12,12 @@
<form [formGroup]="form"> <form [formGroup]="form">
<div class="flex"> <div class="flex">
<div class="iqser-input-group w-300"> <div class="iqser-input-group w-300">
<label translate="confirm-delete-dossier-state.form.status"></label> <label translate="confirm-delete-dossier-state.form.state"></label>
<mat-select <mat-select
[placeholder]="'confirm-delete-dossier-state.form.status-placeholder' | translate" [placeholder]="'confirm-delete-dossier-state.form.state-placeholder' | translate"
formControlName="replaceDossierStatusId" formControlName="replaceDossierStatusId"
> >
<mat-option>{{ 'confirm-delete-dossier-state.form.status-placeholder' | translate }}</mat-option> <mat-option>{{ 'confirm-delete-dossier-state.form.state-placeholder' | translate }}</mat-option>
<mat-option *ngFor="let state of data.otherStates" [value]="state.dossierStatusId"> <mat-option *ngFor="let state of data.otherStates" [value]="state.dossierStatusId">
{{ state.name }} {{ state.name }}
</mat-option> </mat-option>
@ -29,10 +29,10 @@
</div> </div>
<div class="dialog-actions"> <div class="dialog-actions">
<button (click)="dialogRef.close(afterCloseValue)" color="primary" mat-flat-button> <button (click)="save()" color="primary" mat-flat-button>
{{ label | translate }} {{ label | translate }}
</button> </button>
<div (click)="dialogRef.close()" [translate]="'confirm-delete-dossier-state.cancel'" class="all-caps-label cancel"></div> <div [translate]="'confirm-delete-dossier-state.cancel'" class="all-caps-label cancel" mat-dialog-close></div>
</div> </div>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button> <iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>

View File

@ -1,12 +1,18 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { IDossierState } from '@red/domain'; import { DossierState } from '@red/domain';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { firstValueFrom, forkJoin } from 'rxjs';
import { DossierStatesService } from '@services/entity-services/dossier-states.service';
import { LoadingService, Toaster } from '@iqser/common-ui';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { ArchivedDossiersService } from '@services/dossiers/archived-dossiers.service';
import { take } from 'rxjs/operators';
interface DialogData { interface DialogData {
readonly toBeDeletedState: IDossierState; readonly toBeDeletedState: DossierState;
readonly otherStates: IDossierState[]; readonly otherStates: DossierState[];
readonly dossierCount: number; readonly dossierCount: number;
} }
@ -21,7 +27,12 @@ export class ConfirmDeleteDossierStateDialogComponent {
constructor( constructor(
private readonly _formBuilder: FormBuilder, private readonly _formBuilder: FormBuilder,
readonly dialogRef: MatDialogRef<ConfirmDeleteDossierStateDialogComponent>, private readonly _loadingService: LoadingService,
private readonly _toaster: Toaster,
private readonly _dossierStateService: DossierStatesService,
private readonly _dialogRef: MatDialogRef<ConfirmDeleteDossierStateDialogComponent>,
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _archivedDossiersService: ArchivedDossiersService,
@Inject(MAT_DIALOG_DATA) readonly data: DialogData, @Inject(MAT_DIALOG_DATA) readonly data: DialogData,
) { ) {
this.form = this.#getForm(); this.form = this.#getForm();
@ -42,8 +53,15 @@ export class ConfirmDeleteDossierStateDialogComponent {
return this.replaceDossierStatusId ? _('confirm-delete-dossier-state.delete-replace') : _('confirm-delete-dossier-state.delete'); return this.replaceDossierStatusId ? _('confirm-delete-dossier-state.delete-replace') : _('confirm-delete-dossier-state.delete');
} }
get afterCloseValue(): string | true { async save(): Promise<void> {
return this.replaceDossierStatusId ?? true; this._loadingService.start();
await firstValueFrom(this._dossierStateService.deleteState(this.data.toBeDeletedState, this.replaceDossierStatusId));
await firstValueFrom(
forkJoin([this._activeDossiersService.loadAll().pipe(take(1)), this._archivedDossiersService.loadAll().pipe(take(1))]),
);
this._toaster.success(_('confirm-delete-dossier-state.success'));
this._dialogRef.close();
this._loadingService.stop();
} }
#getForm(): FormGroup { #getForm(): FormGroup {

View File

@ -1,101 +1,98 @@
<ng-container *ngIf="dossierStateService.all"> <section>
<section> <div class="page-header">
<div class="page-header"> <redaction-dossier-template-breadcrumbs class="flex-1"></redaction-dossier-template-breadcrumbs>
<redaction-dossier-template-breadcrumbs class="flex-1"></redaction-dossier-template-breadcrumbs>
<div class="actions flex-1"> <div class="actions flex-1">
<redaction-dossier-template-actions></redaction-dossier-template-actions> <redaction-dossier-template-actions></redaction-dossier-template-actions>
<iqser-circle-button
[routerLink]="['../..']"
[tooltip]="'common.close' | translate"
icon="iqser:close"
tooltipPosition="below"
></iqser-circle-button>
</div>
</div>
<div class="content-inner">
<div class="overlay-shadow"></div>
<redaction-admin-side-nav type="dossierTemplates"></redaction-admin-side-nav>
<div class="content-container">
<iqser-table
[headerTemplate]="headerTemplate"
[itemSize]="80"
[noDataText]="'dossier-states-listing.no-data.title' | translate"
[noMatchText]="'dossier-states-listing.no-match.title' | translate"
[tableColumnConfigs]="tableColumnConfigs"
emptyColumnWidth="1fr"
noDataIcon="red:attribute"
></iqser-table>
</div>
<div class="right-container">
<redaction-simple-doughnut-chart
*ngIf="chartData$ | async as chartData"
[config]="chartData"
[radius]="80"
[strokeWidth]="15"
[subtitle]="'dossier-states-listing.chart.dossier-states' | translate: { count: chartData.length }"
[totalType]="'simpleLabel'"
></redaction-simple-doughnut-chart>
</div>
</div>
</section>
<ng-template #headerTemplate>
<div class="table-header-actions">
<iqser-input-with-action
[(value)]="searchService.searchValue"
[placeholder]="'dossier-states-listing.search' | translate"
></iqser-input-with-action>
<iqser-icon-button
(action)="openAddEditStateDialog($event)"
*ngIf="permissionsService.canPerformDossierStatesActions"
[label]="'dossier-states-listing.add-new' | translate"
[type]="iconButtonTypes.primary"
icon="iqser:plus"
></iqser-icon-button>
</div>
</ng-template>
<ng-template #tableItemTemplate let-entity="entity">
<div *ngIf="cast(entity) as state">
<div class="cell">
<div class="flex-align-items-center">
<div [style.background-color]="state.color" class="dossier-state-square"></div>
<div class="state-name">{{ state.name }}</div>
</div>
</div>
<div class="cell small-label">
<span>{{ state.rank }}</span>
</div>
<div class="cell small-label">
<span>{{ state.dossierCount }}</span>
</div>
<div class="cell">
<div *ngIf="permissionsService.canPerformDossierStatesActions" class="action-buttons">
<iqser-circle-button <iqser-circle-button
[routerLink]="['../..']" (action)="openAddEditStateDialog($event, state)"
[tooltip]="'common.close' | translate" [tooltip]="'dossier-states-listing.action.edit' | translate"
icon="iqser:close" [type]="circleButtonTypes.dark"
tooltipPosition="below" icon="iqser:edit"
></iqser-circle-button>
<iqser-circle-button
(action)="openConfirmDeleteStateDialog($event, state)"
[tooltip]="'dossier-states-listing.action.delete' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:trash"
></iqser-circle-button> ></iqser-circle-button>
</div> </div>
</div> </div>
</div>
<div class="content-inner"> </ng-template>
<div class="overlay-shadow"></div>
<redaction-admin-side-nav type="dossierTemplates"></redaction-admin-side-nav>
<div class="content-container">
<iqser-table
[headerTemplate]="headerTemplate"
[itemSize]="80"
[noDataText]="'dossier-states-listing.no-data.title' | translate"
[noMatchText]="'dossier-states-listing.no-match.title' | translate"
[selectionEnabled]="true"
[tableColumnConfigs]="tableColumnConfigs"
emptyColumnWidth="1fr"
noDataIcon="red:attribute"
></iqser-table>
</div>
<div class="right-container">
<redaction-simple-doughnut-chart
*ngIf="chartData"
[config]="chartData"
[radius]="80"
[strokeWidth]="15"
[subtitle]="'dossier-states-listing.chart.dossier-states' | translate: { count: chartData.length }"
[totalType]="'simpleLabel'"
></redaction-simple-doughnut-chart>
</div>
</div>
</section>
<ng-template #headerTemplate>
<div class="table-header-actions">
<iqser-input-with-action
[(value)]="searchService.searchValue"
[placeholder]="'dossier-states-listing.search' | translate"
></iqser-input-with-action>
<iqser-icon-button
(action)="openAddEditStateDialog($event)"
*ngIf="currentUser.isAdmin"
[label]="'dossier-states-listing.add-new' | translate"
[type]="iconButtonTypes.primary"
icon="iqser:plus"
></iqser-icon-button>
</div>
</ng-template>
<ng-template #tableItemTemplate let-entity="entity">
<div *ngIf="cast(entity) as state">
<div class="cell">
<div class="flex-align-items-center">
<div [style.background-color]="state.color" class="dossier-state-square"></div>
<div class="state-name">{{ state.name }}</div>
</div>
</div>
<div class="cell small-label">
<span>{{ state.rank }}</span>
</div>
<div class="cell small-label">
<span>{{ state.dossierCount }}</span>
</div>
<div class="cell">
<div *ngIf="currentUser.isAdmin" class="action-buttons">
<iqser-circle-button
(action)="openAddEditStateDialog($event, state)"
[tooltip]="'dossier-states-listing.action.edit' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:edit"
></iqser-circle-button>
<iqser-circle-button
(action)="openConfirmDeleteStateDialog($event, state)"
[tooltip]="'dossier-states-listing.action.delete' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:trash"
></iqser-circle-button>
</div>
</div>
</div>
</ng-template>
</ng-container>

View File

@ -3,6 +3,7 @@
.dossier-state-square { .dossier-state-square {
height: 16px; height: 16px;
width: 16px; width: 16px;
min-width: 16px;
margin-right: 16px; margin-right: 16px;
} }

View File

@ -4,22 +4,19 @@ import {
DefaultListingServices, DefaultListingServices,
IconButtonTypes, IconButtonTypes,
ListingComponent, ListingComponent,
LoadingService,
SortingOrders, SortingOrders,
TableColumnConfig, TableColumnConfig,
Toaster,
} from '@iqser/common-ui'; } from '@iqser/common-ui';
import { DossierState, IDossierState } from '@red/domain'; import { DossierState, IDossierState } from '@red/domain';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service'; import { firstValueFrom, map, Observable } from 'rxjs';
import { DossierStateService } from '@services/entity-services/dossier-state.service';
import { firstValueFrom } from 'rxjs';
import { AdminDialogService } from '../../services/admin-dialog.service'; import { AdminDialogService } from '../../services/admin-dialog.service';
import { UserService } from '@services/user.service';
import { HttpStatusCode } from '@angular/common/http';
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component'; import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service'; import { DossierStatesMapService } from '@services/entity-services/dossier-states-map.service';
import { tap } from 'rxjs/operators';
import { PermissionsService } from '@services/permissions.service';
import { DossierStatesService } from '@services/entity-services/dossier-states.service';
@Component({ @Component({
templateUrl: './dossier-states-listing-screen.component.html', templateUrl: './dossier-states-listing-screen.component.html',
@ -33,29 +30,29 @@ import { DossierTemplatesService } from '@services/entity-services/dossier-templ
export class DossierStatesListingScreenComponent extends ListingComponent<DossierState> implements OnInit, OnDestroy { export class DossierStatesListingScreenComponent extends ListingComponent<DossierState> implements OnInit, OnDestroy {
readonly iconButtonTypes = IconButtonTypes; readonly iconButtonTypes = IconButtonTypes;
readonly circleButtonTypes = CircleButtonTypes; readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser;
readonly tableHeaderLabel = _('dossier-states-listing.table-header.title'); readonly tableHeaderLabel = _('dossier-states-listing.table-header.title');
readonly tableColumnConfigs: TableColumnConfig<DossierState>[] = [ readonly tableColumnConfigs: TableColumnConfig<DossierState>[] = [
{ label: _('dossier-states-listing.table-col-names.name'), sortByKey: 'name' }, { label: _('dossier-states-listing.table-col-names.name'), sortByKey: 'name' },
{ label: _('dossier-states-listing.table-col-names.rank'), sortByKey: 'rank' }, { label: _('dossier-states-listing.table-col-names.rank'), sortByKey: 'rank' },
{ label: _('dossier-states-listing.table-col-names.dossiers-count') }, { label: _('dossier-states-listing.table-col-names.dossiers-count') },
]; ];
chartData: DoughnutChartConfig[]; chartData$: Observable<DoughnutChartConfig[]>;
readonly #dossierTemplateId: string; readonly #dossierTemplateId: string;
constructor( constructor(
protected readonly _injector: Injector, protected readonly _injector: Injector,
private readonly _loadingService: LoadingService,
private readonly _activeDossiersService: ActiveDossiersService,
readonly dossierStateService: DossierStateService,
private readonly _dialogService: AdminDialogService, private readonly _dialogService: AdminDialogService,
private readonly _userService: UserService,
private readonly _toaster: Toaster,
private readonly _route: ActivatedRoute, private readonly _route: ActivatedRoute,
private readonly _dossierTemplatesService: DossierTemplatesService, private readonly _dossierStatesMapService: DossierStatesMapService,
private readonly _dossierStatesService: DossierStatesService,
readonly permissionsService: PermissionsService,
) { ) {
super(_injector); super(_injector);
this.#dossierTemplateId = _route.snapshot.paramMap.get('dossierTemplateId'); this.#dossierTemplateId = _route.snapshot.paramMap.get('dossierTemplateId');
this.chartData$ = this._dossierStatesMapService.get$(this.#dossierTemplateId).pipe(
tap(states => this.entitiesService.setEntities(states)),
map(states => this.#chartData(states)),
);
} }
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
@ -63,7 +60,7 @@ export class DossierStatesListingScreenComponent extends ListingComponent<Dossie
column: 'rank', column: 'rank',
order: SortingOrders.asc, order: SortingOrders.asc,
}); });
await this.#loadData(); await firstValueFrom(this._dossierStatesService.loadAllForTemplate(this.#dossierTemplateId));
} }
openAddEditStateDialog($event: MouseEvent, dossierState?: IDossierState) { openAddEditStateDialog($event: MouseEvent, dossierState?: IDossierState) {
@ -71,66 +68,24 @@ export class DossierStatesListingScreenComponent extends ListingComponent<Dossie
dossierState, dossierState,
dossierTemplateId: this.#dossierTemplateId, dossierTemplateId: this.#dossierTemplateId,
}; };
this._dialogService.openDialog('addEditDossierState', $event, data, async (newValue: IDossierState) => { this._dialogService.openDialog('addEditDossierState', $event, data);
await this.#createNewDossierStateAndRefreshView(newValue);
});
} }
openConfirmDeleteStateDialog($event: MouseEvent, dossierState: IDossierState) { openConfirmDeleteStateDialog($event: MouseEvent, dossierState: DossierState) {
const templateId = this.#dossierTemplateId;
const data = { const data = {
toBeDeletedState: dossierState, toBeDeletedState: dossierState,
otherStates: this.entitiesService.all.filter(state => state.dossierStatusId !== dossierState.dossierStatusId), otherStates: this.entitiesService.all.filter(state => state.id !== dossierState.id),
dossierCount: dossierState.dossierCount, dossierCount: dossierState.dossierCount,
}; };
this._dialogService.openDialog('deleteDossierState', $event, data, async (value: string | true) => { this._dialogService.openDialog('deleteDossierState', $event, data);
if (value) {
if (typeof value === 'string') {
await firstValueFrom(this.dossierStateService.deleteAndReplace(dossierState.dossierStatusId, value));
} else {
await firstValueFrom(this.dossierStateService.delete(dossierState.dossierStatusId));
}
}
await firstValueFrom(this._dossierTemplatesService.refreshDossierTemplate(templateId));
await this.#loadData();
});
} }
async #createNewDossierStateAndRefreshView(newValue: IDossierState): Promise<void> { #chartData(states: DossierState[]): DoughnutChartConfig[] {
this._loadingService.start(); return states.map(state => ({
await firstValueFrom(this.dossierStateService.updateDossierState(newValue)).catch(error => { value: state.dossierCount,
if (error.status === HttpStatusCode.Conflict) { label: state.name,
this._toaster.error(_('dossier-states-listing.error.conflict')); key: state.name,
} else { color: state.color,
this._toaster.error(_('dossier-states-listing.error.generic')); }));
}
});
await firstValueFrom(this._dossierTemplatesService.refreshDossierTemplate(this.#dossierTemplateId));
await this.#loadData();
}
async #loadData(): Promise<void> {
this._loadingService.start();
// TODO: Move this in service; dossiers states service should be a mapping service
await firstValueFrom(this._activeDossiersService.loadAll());
try {
const dossierStates = this.dossierStateService.all.filter(d => d.dossierTemplateId === this.#dossierTemplateId);
this.#setStatesCount(dossierStates);
this.chartData = dossierStates.map(state => ({
value: state.dossierCount,
label: state.name,
key: state.name,
color: state.color,
}));
this.entitiesService.setEntities(dossierStates || []);
} catch (e) {}
this._loadingService.stop();
}
#setStatesCount(dossierStates: DossierState[]): void {
dossierStates.forEach(state => (state.dossierCount = this._activeDossiersService.getCountWithState(state.dossierStatusId)));
} }
} }

View File

@ -9,5 +9,5 @@
</div> </div>
<div class="cell"> <div class="cell">
<redaction-dossier-status [dossier]="dossier"></redaction-dossier-status> <redaction-dossier-state [dossier]="dossier"></redaction-dossier-state>
</div> </div>

View File

@ -10,7 +10,7 @@ export class ConfigService {
{ label: _('archived-dossiers-listing.table-col-names.name'), sortByKey: 'searchKey', width: '2fr' }, { label: _('archived-dossiers-listing.table-col-names.name'), sortByKey: 'searchKey', width: '2fr' },
{ label: _('archived-dossiers-listing.table-col-names.last-modified'), sortByKey: 'archivedTime' }, { label: _('archived-dossiers-listing.table-col-names.last-modified'), sortByKey: 'archivedTime' },
{ label: _('archived-dossiers-listing.table-col-names.owner'), class: 'user-column' }, { label: _('archived-dossiers-listing.table-col-names.owner'), class: 'user-column' },
{ label: _('archived-dossiers-listing.table-col-names.dossier-status'), class: 'flex-end', width: '2fr' }, { label: _('archived-dossiers-listing.table-col-names.dossier-state'), class: 'flex-end', width: '2fr' },
]; ];
} }
} }

View File

@ -42,7 +42,7 @@
<div class="flex fields-container"> <div class="flex fields-container">
<div class="iqser-input-group w-300"> <div class="iqser-input-group w-300">
<label translate="edit-dossier-dialog.general-info.form.dossier-status.label"></label> <label translate="edit-dossier-dialog.general-info.form.dossier-state.label"></label>
<mat-select [placeholder]="statusPlaceholder" formControlName="dossierStatusId"> <mat-select [placeholder]="statusPlaceholder" formControlName="dossierStatusId">
<mat-option *ngFor="let stateId of states" [value]="stateId"> <mat-option *ngFor="let stateId of states" [value]="stateId">
<div class="flex-align-items-center"> <div class="flex-align-items-center">

View File

@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import * as moment from 'moment'; import * as moment from 'moment';
import { Dossier, DossierState, IDossierRequest, IDossierTemplate } from '@red/domain'; import { Dossier, IDossierRequest, IDossierTemplate } from '@red/domain';
import { EditDossierSaveResult, EditDossierSectionInterface } from '../edit-dossier-section.interface'; import { EditDossierSaveResult, EditDossierSectionInterface } from '../edit-dossier-section.interface';
import { DossiersDialogService } from '../../../services/dossiers-dialog.service'; import { DossiersDialogService } from '../../../services/dossiers-dialog.service';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
@ -13,12 +13,12 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service'; import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierStatsService } from '@services/dossiers/dossier-stats.service'; import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { DossierStateService } from '@services/entity-services/dossier-state.service';
import { DOSSIER_TEMPLATE_ID } from '@utils/constants'; import { DOSSIER_TEMPLATE_ID } from '@utils/constants';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DossiersService } from '@services/dossiers/dossiers.service'; import { DossiersService } from '@services/dossiers/dossiers.service';
import { TrashDossiersService } from '@services/entity-services/trash-dossiers.service'; import { TrashDossiersService } from '@services/entity-services/trash-dossiers.service';
import { ArchivedDossiersService } from '@services/dossiers/archived-dossiers.service'; import { ArchivedDossiersService } from '@services/dossiers/archived-dossiers.service';
import { DossierStatesMapService } from '@services/entity-services/dossier-states-map.service';
@Component({ @Component({
selector: 'redaction-edit-dossier-general-info', selector: 'redaction-edit-dossier-general-info',
@ -36,11 +36,10 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
hasDueDate: boolean; hasDueDate: boolean;
dossierTemplates: IDossierTemplate[]; dossierTemplates: IDossierTemplate[];
states: string[]; states: string[];
currentStatus: DossierState;
constructor( constructor(
readonly permissionsService: PermissionsService, readonly permissionsService: PermissionsService,
private readonly _dossierStateService: DossierStateService, private readonly _dossierStatesMapService: DossierStatesMapService,
private readonly _dossierTemplatesService: DossierTemplatesService, private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dossiersService: DossiersService, private readonly _dossiersService: DossiersService,
private readonly _trashDossiersService: TrashDossiersService, private readonly _trashDossiersService: TrashDossiersService,
@ -80,20 +79,22 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
return this.hasDueDate && this.form.get('dueDate').value === null; return this.hasDueDate && this.form.get('dueDate').value === null;
} }
get #statusPlaceholder(): string {
return this._translateService.instant(
this.states.length === 1
? 'edit-dossier-dialog.general-info.form.dossier-state.no-state-placeholder'
: 'edit-dossier-dialog.general-info.form.dossier-state.placeholder',
);
}
ngOnInit() { ngOnInit() {
this.states = [ this.states = [null, ...this._dossierStatesMapService.get(this.dossier.dossierTemplateId).map(s => s.id)];
null,
...this._dossierStateService.all.filter(s => s.dossierTemplateId === this.dossier.dossierTemplateId).map(s => s.id),
];
this.statusPlaceholder = this.#statusPlaceholder; this.statusPlaceholder = this.#statusPlaceholder;
this.#filterInvalidDossierTemplates(); this.#filterInvalidDossierTemplates();
this.form = this.#getForm(); this.form = this.#getForm();
if (!this.permissionsService.canEditDossier(this.dossier)) { if (!this.permissionsService.canEditDossier(this.dossier)) {
this.form.disable(); this.form.disable();
} }
if (this.dossier.dossierStatusId) {
this.currentStatus = this._dossierStateService.find(this.dossier.dossierStatusId);
}
this.hasDueDate = !!this.dossier.dueDate; this.hasDueDate = !!this.dossier.dueDate;
} }
@ -168,6 +169,17 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
}); });
} }
getStateName(stateId: string): string {
return (
this._dossierStatesMapService.get(this.dossier.dossierTemplateId, stateId)?.name ||
this._translateService.instant('edit-dossier-dialog.general-info.form.dossier-state.placeholder')
);
}
getStateColor(stateId: string): string {
return this._dossierStatesMapService.get(this.dossier.dossierTemplateId, stateId).color;
}
#getForm(): FormGroup { #getForm(): FormGroup {
const formFieldWithArchivedCheck = value => ({ value, disabled: !this.dossier.isActive }); const formFieldWithArchivedCheck = value => ({ value, disabled: !this.dossier.isActive });
return this._formBuilder.group({ return this._formBuilder.group({
@ -185,25 +197,6 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
}); });
} }
get #statusPlaceholder(): string {
return this._translateService.instant(
this.states.length === 1
? 'edit-dossier-dialog.general-info.form.dossier-status.no-status-placeholder'
: 'edit-dossier-dialog.general-info.form.dossier-status.placeholder',
);
}
getStateName(stateId: string): string {
return (
this._dossierStateService.find(stateId)?.name ||
this._translateService.instant('edit-dossier-dialog.general-info.form.dossier-status.placeholder')
);
}
getStateColor(stateId: string): string {
return this._dossierStateService.find(stateId).color;
}
#filterInvalidDossierTemplates() { #filterInvalidDossierTemplates() {
this.dossierTemplates = this._dossierTemplatesService.all.filter(r => { this.dossierTemplates = this._dossierTemplatesService.all.filter(r => {
if (this.dossier?.dossierTemplateId === r.dossierTemplateId) { if (this.dossier?.dossierTemplateId === r.dossierTemplateId) {

View File

@ -8,8 +8,8 @@ import { workflowFileStatusTranslations } from '../../../../../../translations/f
import { TranslateChartService } from '@services/translate-chart.service'; import { TranslateChartService } from '@services/translate-chart.service';
import { filter, map, switchMap } from 'rxjs/operators'; import { filter, map, switchMap } from 'rxjs/operators';
import { DossierStatsService } from '@services/dossiers/dossier-stats.service'; import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
import { DossierStateService } from '@services/entity-services/dossier-state.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DossierStatesMapService } from '@services/entity-services/dossier-states-map.service';
@Component({ @Component({
selector: 'redaction-dossiers-listing-details', selector: 'redaction-dossiers-listing-details',
@ -26,7 +26,7 @@ export class DossiersListingDetailsComponent {
readonly activeDossiersService: ActiveDossiersService, readonly activeDossiersService: ActiveDossiersService,
private readonly _dossierStatsMap: DossierStatsService, private readonly _dossierStatsMap: DossierStatsService,
private readonly _translateChartService: TranslateChartService, private readonly _translateChartService: TranslateChartService,
private readonly _dossierStateService: DossierStateService, private readonly _dossierStatesMapService: DossierStatesMapService,
private readonly _translateService: TranslateService, private readonly _translateService: TranslateService,
) { ) {
this.documentsChartData$ = this.activeDossiersService.all$.pipe( this.documentsChartData$ = this.activeDossiersService.all$.pipe(
@ -40,24 +40,12 @@ export class DossiersListingDetailsComponent {
} }
private _toDossierChartData(): DoughnutChartConfig[] { private _toDossierChartData(): DoughnutChartConfig[] {
this._dossierStateService.all.forEach( const configArray: DoughnutChartConfig[] = this._dossierStatesMapService.stats;
state => (state.dossierCount = this.activeDossiersService.getCountWithState(state.dossierStatusId)), const undefinedStateLength =
); this.activeDossiersService.all.length - configArray.map(v => v.value).reduce((acc, val) => acc + val, 0);
const configArray: DoughnutChartConfig[] = [
...this._dossierStateService.all
.reduce((acc, { color, dossierCount, name }) => {
const key = name + '-' + color;
const item = acc.get(key) ?? Object.assign({}, { value: 0, label: name, color: color });
return acc.set(key, { ...item, value: item.value + dossierCount });
}, new Map<string, DoughnutChartConfig>())
.values(),
];
const notAssignedLength = this.activeDossiersService.all.length - configArray.map(v => v.value).reduce((acc, val) => acc + val, 0);
configArray.push({ configArray.push({
value: notAssignedLength, value: undefinedStateLength,
label: this._translateService.instant('edit-dossier-dialog.general-info.form.dossier-status.placeholder'), label: this._translateService.instant('edit-dossier-dialog.general-info.form.dossier-state.placeholder'),
color: '#E2E4E9', color: '#E2E4E9',
}); });

View File

@ -21,7 +21,7 @@
</div> </div>
<div class="cell"> <div class="cell">
<redaction-dossier-status [dossier]="dossier"></redaction-dossier-status> <redaction-dossier-state [dossier]="dossier"></redaction-dossier-state>
<redaction-dossiers-listing-actions [dossier]="dossier" [stats]="stats"></redaction-dossiers-listing-actions> <redaction-dossiers-listing-actions [dossier]="dossier" [stats]="stats"></redaction-dossiers-listing-actions>
</div> </div>

View File

@ -1,6 +1,6 @@
import { Injectable, TemplateRef } from '@angular/core'; import { Injectable, TemplateRef } from '@angular/core';
import { ButtonConfig, IFilterGroup, INestedFilter, keyChecker, NestedFilter, TableColumnConfig } from '@iqser/common-ui'; import { ButtonConfig, IFilterGroup, INestedFilter, keyChecker, NestedFilter, TableColumnConfig } from '@iqser/common-ui';
import { Dossier, StatusSorter, User } from '@red/domain'; import { Dossier, StatusSorter, User, WorkflowFileStatus } from '@red/domain';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { UserPreferenceService } from '@services/user-preference.service'; import { UserPreferenceService } from '@services/user-preference.service';
@ -10,7 +10,7 @@ import { dossierMemberChecker, dossierStateChecker, dossierTemplateChecker, Reda
import { workloadTranslations } from '../../translations/workload-translations'; import { workloadTranslations } from '../../translations/workload-translations';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service'; import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierStatsService } from '@services/dossiers/dossier-stats.service'; import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
import { DossierStateService } from '@services/entity-services/dossier-state.service'; import { DossierStatesMapService } from '@services/entity-services/dossier-states-map.service';
@Injectable() @Injectable()
export class ConfigService { export class ConfigService {
@ -20,7 +20,7 @@ export class ConfigService {
private readonly _userService: UserService, private readonly _userService: UserService,
private readonly _dossierTemplatesService: DossierTemplatesService, private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dossierStatsService: DossierStatsService, private readonly _dossierStatsService: DossierStatsService,
private readonly _dossierStateService: DossierStateService, private readonly _dossierStatesMapService: DossierStatesMapService,
) {} ) {}
get tableConfig(): TableColumnConfig<Dossier>[] { get tableConfig(): TableColumnConfig<Dossier>[] {
@ -30,7 +30,7 @@ export class ConfigService {
{ label: _('dossier-listing.table-col-names.needs-work') }, { label: _('dossier-listing.table-col-names.needs-work') },
{ label: _('dossier-listing.table-col-names.owner'), class: 'user-column' }, { label: _('dossier-listing.table-col-names.owner'), class: 'user-column' },
{ label: _('dossier-listing.table-col-names.documents-status'), class: 'flex-end', width: 'auto' }, { label: _('dossier-listing.table-col-names.documents-status'), class: 'flex-end', width: 'auto' },
{ label: _('dossier-listing.table-col-names.dossier-status'), class: 'flex-end' }, { label: _('dossier-listing.table-col-names.dossier-state'), class: 'flex-end' },
]; ];
} }
@ -66,6 +66,8 @@ export class ConfigService {
const allDistinctDossierTemplates = new Set<string>(); const allDistinctDossierTemplates = new Set<string>();
const allDistinctDossierStates = new Set<string>(); const allDistinctDossierStates = new Set<string>();
const stateToTemplateMap = new Map<string, string>();
const filterGroups: IFilterGroup[] = []; const filterGroups: IFilterGroup[] = [];
entities?.forEach(entry => { entities?.forEach(entry => {
@ -73,6 +75,7 @@ export class ConfigService {
allDistinctDossierTemplates.add(entry.dossierTemplateId); allDistinctDossierTemplates.add(entry.dossierTemplateId);
if (entry.dossierStatusId) { if (entry.dossierStatusId) {
allDistinctDossierStates.add(entry.dossierStatusId); allDistinctDossierStates.add(entry.dossierStatusId);
stateToTemplateMap.set(entry.dossierStatusId, entry.dossierTemplateId);
} }
const stats = this._dossierStatsService.get(entry.dossierId); const stats = this._dossierStatsService.get(entry.dossierId);
@ -100,13 +103,13 @@ export class ConfigService {
id => id =>
new NestedFilter({ new NestedFilter({
id: id, id: id,
label: this._dossierStateService.find(id).name, label: this._dossierStatesMapService.get(stateToTemplateMap.get(id), id).name,
}), }),
); );
filterGroups.push({ filterGroups.push({
slug: 'dossierStatesFilters', slug: 'dossierStatesFilters',
label: this._translateService.instant('filters.dossier-status'), label: this._translateService.instant('filters.dossier-state'),
icon: 'red:status', icon: 'red:status',
hide: dossierStatesFilters.length <= 1, hide: dossierStatesFilters.length <= 1,
filters: dossierStatesFilters, filters: dossierStatesFilters,
@ -114,7 +117,7 @@ export class ConfigService {
}); });
const statusFilters = [...allDistinctFileStatus].map( const statusFilters = [...allDistinctFileStatus].map(
status => (status: WorkflowFileStatus) =>
new NestedFilter({ new NestedFilter({
id: status, id: status,
label: this._translateService.instant(workflowFileStatusTranslations[status]), label: this._translateService.instant(workflowFileStatusTranslations[status]),

View File

@ -0,0 +1,6 @@
<div class="flex-align-items-center dossier-state-container">
<div class="dossier-state-text">
{{ (dossierState$ | async)?.name || ('edit-dossier-dialog.general-info.form.dossier-state.placeholder' | translate) }}
</div>
<redaction-small-chip [color]="(dossierState$ | async)?.color || '#E2E4E9'"></redaction-small-chip>
</div>

View File

@ -1,6 +1,6 @@
@use 'variables'; @use 'variables';
.dossier-status-container { .dossier-state-container {
justify-content: flex-end; justify-content: flex-end;
width: 100%; width: 100%;
} }
@ -9,7 +9,7 @@ redaction-small-chip {
margin-left: 8px; margin-left: 8px;
} }
.dossier-status-text { .dossier-state-text {
font-size: 13px; font-size: 13px;
line-height: 16px; line-height: 16px;
color: variables.$grey-1; color: variables.$grey-1;

View File

@ -0,0 +1,21 @@
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';
@Component({
selector: 'redaction-dossier-state [dossier]',
templateUrl: './dossier-state.component.html',
styleUrls: ['./dossier-state.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DossierStateComponent implements OnChanges {
@Input() dossier: Dossier;
dossierState$: Observable<DossierState>;
constructor(private readonly _dossierStatesMapService: DossierStatesMapService) {}
ngOnChanges(): void {
this.dossierState$ = this._dossierStatesMapService.watch$(this.dossier.dossierTemplateId, this.dossier.dossierStatusId);
}
}

View File

@ -1,6 +0,0 @@
<div class="flex-align-items-center dossier-status-container">
<div class="dossier-status-text">
{{ currentState?.name || ('edit-dossier-dialog.general-info.form.dossier-status.placeholder' | translate) }}
</div>
<redaction-small-chip [color]="currentState?.color || '#E2E4E9'"></redaction-small-chip>
</div>

View File

@ -1,28 +0,0 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
import { Dossier, DossierState } from '@red/domain';
import { DossierStateService } from '@services/entity-services/dossier-state.service';
@Component({
selector: 'redaction-dossier-status [dossier]',
templateUrl: './dossier-status.component.html',
styleUrls: ['./dossier-status.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DossierStatusComponent implements OnInit, OnChanges {
@Input() dossier: Dossier;
currentState: DossierState;
constructor(private readonly _dossierStateService: DossierStateService) {}
ngOnInit(): void {
this.#setState();
}
ngOnChanges(): void {
this.#setState();
}
#setState(): void {
this.currentState = this._dossierStateService.find(this.dossier.dossierStatusId);
}
}

View File

@ -27,7 +27,7 @@ import { TeamMembersComponent } from './components/team-members/team-members.com
import { EditorComponent } from './components/editor/editor.component'; import { EditorComponent } from './components/editor/editor.component';
import { ExpandableFileActionsComponent } from './components/expandable-file-actions/expandable-file-actions.component'; import { ExpandableFileActionsComponent } from './components/expandable-file-actions/expandable-file-actions.component';
import { ProcessingIndicatorComponent } from '@shared/components/processing-indicator/processing-indicator.component'; import { ProcessingIndicatorComponent } from '@shared/components/processing-indicator/processing-indicator.component';
import { DossierStatusComponent } from '@shared/components/dossier-status/dossier-status.component'; import { DossierStateComponent } from '@shared/components/dossier-state/dossier-state.component';
import { DossiersListingDossierNameComponent } from '@shared/components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component'; import { DossiersListingDossierNameComponent } from '@shared/components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component';
const buttons = [FileDownloadBtnComponent, UserButtonComponent]; const buttons = [FileDownloadBtnComponent, UserButtonComponent];
@ -45,7 +45,7 @@ const components = [
TeamMembersComponent, TeamMembersComponent,
ExpandableFileActionsComponent, ExpandableFileActionsComponent,
ProcessingIndicatorComponent, ProcessingIndicatorComponent,
DossierStatusComponent, DossierStateComponent,
DossiersListingDossierNameComponent, DossiersListingDossierNameComponent,
...buttons, ...buttons,

View File

@ -1,7 +1,7 @@
import { Injectable, Injector } from '@angular/core'; import { Injectable, Injector } from '@angular/core';
import { switchMap, tap } from 'rxjs/operators'; import { switchMap, tap } from 'rxjs/operators';
import { timer } from 'rxjs'; import { timer } from 'rxjs';
import { CHANGED_CHECK_INTERVAL } from '../../utils/constants'; import { CHANGED_CHECK_INTERVAL } from '@utils/constants';
import { DossiersService } from './dossiers.service'; import { DossiersService } from './dossiers.service';
export interface IDossiersStats { export interface IDossiersStats {

View File

@ -3,20 +3,20 @@ import { Dossier, DossierStats, IChangesDetails, IDossier, IDossierChanges, IDos
import { combineLatest, EMPTY, forkJoin, Observable, of, Subject, throwError } from 'rxjs'; import { combineLatest, EMPTY, forkJoin, Observable, of, Subject, throwError } from 'rxjs';
import { catchError, filter, map, mapTo, pluck, switchMap, tap } from 'rxjs/operators'; import { catchError, filter, map, mapTo, pluck, switchMap, tap } from 'rxjs/operators';
import { Injector } from '@angular/core'; import { Injector } from '@angular/core';
import { DossierStateService } from '../entity-services/dossier-state.service'; import { DossierStatesService } from '../entity-services/dossier-states.service';
import { DossierStatsService } from './dossier-stats.service'; import { DossierStatsService } from './dossier-stats.service';
import { IDossiersStats } from './active-dossiers.service'; import { IDossiersStats } from './active-dossiers.service';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
const DOSSIER_EXISTS_MSG = _('add-dossier-dialog.errors.dossier-already-exists'); const CONFLICT_MSG = _('add-dossier-dialog.errors.dossier-already-exists');
const GENERIC_MSG = _('add-dossier-dialog.errors.generic'); const GENERIC_MSG = _('add-dossier-dialog.errors.generic');
export abstract class DossiersService extends EntitiesService<Dossier, IDossier> { export abstract class DossiersService extends EntitiesService<Dossier, IDossier> {
readonly dossierFileChanges$ = new Subject<string>(); readonly dossierFileChanges$ = new Subject<string>();
readonly generalStats$ = this.all$.pipe(switchMap(entities => this.#generalStats$(entities))); readonly generalStats$ = this.all$.pipe(switchMap(entities => this.#generalStats$(entities)));
protected readonly _dossierStatsService = this._injector.get(DossierStatsService); protected readonly _dossierStatsService = this._injector.get(DossierStatsService);
protected readonly _dossierStateService = this._injector.get(DossierStateService); protected readonly _dossierStateService = this._injector.get(DossierStatesService);
protected readonly _toaster = this._injector.get(Toaster); protected readonly _toaster = this._injector.get(Toaster);
protected constructor(protected readonly _injector: Injector, protected readonly _path: string, readonly routerPath: string) { protected constructor(protected readonly _injector: Injector, protected readonly _path: string, readonly routerPath: string) {
@ -26,7 +26,7 @@ export abstract class DossiersService extends EntitiesService<Dossier, IDossier>
@Validate() @Validate()
createOrUpdate(@RequiredParam() dossier: IDossierRequest): Observable<Dossier> { createOrUpdate(@RequiredParam() dossier: IDossierRequest): Observable<Dossier> {
const showToast = (error: HttpErrorResponse) => { const showToast = (error: HttpErrorResponse) => {
this._toaster.error(error.status === HttpStatusCode.Conflict ? DOSSIER_EXISTS_MSG : GENERIC_MSG); this._toaster.error(error.status === HttpStatusCode.Conflict ? CONFLICT_MSG : GENERIC_MSG);
return EMPTY; return EMPTY;
}; };

View File

@ -1,41 +0,0 @@
import { Injectable, Injector } from '@angular/core';
import { EntitiesService, mapEach, RequiredParam, Validate } from '@iqser/common-ui';
import { DossierState, IDossierState } from '@red/domain';
import { forkJoin, Observable, switchMap } from 'rxjs';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { defaultIfEmpty, map, tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class DossierStateService extends EntitiesService<DossierState, IDossierState> {
constructor(protected readonly _injector: Injector, private readonly _dossierTemplatesService: DossierTemplatesService) {
super(_injector, DossierState, 'dossier-status');
}
@Validate()
updateDossierState(@RequiredParam() body: IDossierState) {
return this._post<unknown>(body, this._defaultModelPath);
}
@Validate()
loadAllForTemplate(@RequiredParam() templateId: string) {
return this.loadAll(`${this._defaultModelPath}/dossier-template/${templateId}`);
}
loadAllForAllTemplates(): Observable<DossierState[]> {
return this._dossierTemplatesService.all$.pipe(
mapEach(template => template.dossierTemplateId),
mapEach(id => this.loadAllForTemplate(id)),
switchMap(all => forkJoin(all).pipe(defaultIfEmpty([] as DossierState[][]))),
map(value => value.flatMap(item => item)),
tap(value => this.setEntities(value)),
);
}
@Validate()
deleteAndReplace(@RequiredParam() dossierStatusId: string, @RequiredParam() replaceDossierStatusId: string) {
const url = `${this._defaultModelPath}/${dossierStatusId}?replaceDossierStatusId=${replaceDossierStatusId}`;
return this.delete({}, url);
}
}

View File

@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import { DossierState, IDossierState } from '@red/domain';
import { EntitiesMapService } from '@iqser/common-ui';
import { DOSSIER_TEMPLATE_ID } from '@utils/constants';
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
import { flatMap } from 'lodash';
@Injectable({ providedIn: 'root' })
export class DossierStatesMapService extends EntitiesMapService<DossierState, IDossierState> {
constructor() {
super(DOSSIER_TEMPLATE_ID);
}
get stats(): DoughnutChartConfig[] {
const allStates = flatMap(Array.from(this._map.values()).map(obs => obs.value));
return Array.from(
allStates
.reduce((acc, { color, name, dossierCount }) => {
const key = name + '-' + color;
const item = acc.get(key) ?? Object.assign({}, { value: 0, label: name, color: color });
return acc.set(key, { ...item, value: item.value + dossierCount });
}, new Map<string, DoughnutChartConfig>())
.values(),
);
}
}

View File

@ -0,0 +1,60 @@
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 { catchError, defaultIfEmpty, tap } from 'rxjs/operators';
import { DossierStatesMapService } from '@services/entity-services/dossier-states-map.service';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
const CONFLICT_MSG = _('dossier-states-listing.error.conflict');
const GENERIC_MSG = _('dossier-states-listing.error.generic');
@Injectable({
providedIn: 'root',
})
export class DossierStatesService extends EntitiesService<DossierState, IDossierState> {
constructor(
protected readonly _injector: Injector,
private readonly _toaster: Toaster,
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dossierStatesMapService: DossierStatesMapService,
) {
super(_injector, DossierState, 'dossier-status');
}
@Validate()
createOrUpdate(@RequiredParam() state: IDossierState): Observable<DossierState[]> {
const showToast = (error: HttpErrorResponse) => {
this._toaster.error(error.status === HttpStatusCode.Conflict ? CONFLICT_MSG : GENERIC_MSG);
return EMPTY;
};
return this._post<unknown>(state, this._defaultModelPath).pipe(
catchError(showToast),
switchMap(() => this.loadAllForTemplate(state.dossierTemplateId)),
);
}
@Validate()
loadAllForTemplate(@RequiredParam() templateId: string) {
return this.loadAll(`${this._defaultModelPath}/dossier-template/${templateId}`).pipe(
tap(states => this._dossierStatesMapService.set(templateId, states)),
);
}
loadAllForAllTemplates(): Observable<DossierState[][]> {
return this._dossierTemplatesService.all$.pipe(
mapEach(template => template.dossierTemplateId),
mapEach(id => this.loadAllForTemplate(id)),
switchMap(all => forkJoin(all).pipe(defaultIfEmpty([] as DossierState[][]))),
);
}
deleteState(dossierState: IDossierState, replaceDossierStatusId?: string): Observable<unknown> {
const queryParams = replaceDossierStatusId ? [{ key: 'replaceDossierStatusId', value: replaceDossierStatusId }] : null;
return super
.delete(dossierState.dossierStatusId, this._defaultModelPath, queryParams)
.pipe(switchMap(() => this.loadAllForTemplate(dossierState.dossierTemplateId)));
}
}

View File

@ -19,6 +19,10 @@ export class PermissionsService {
return dossiersServiceResolver(this._injector); return dossiersServiceResolver(this._injector);
} }
canPerformDossierStatesActions(user = this._userService.currentUser): boolean {
return user.isAdmin;
}
isReviewerOrApprover(file: File): boolean { isReviewerOrApprover(file: File): boolean {
const dossier = this._getDossier(file); const dossier = this._getDossier(file);
return this.isFileAssignee(file) || this.isApprover(dossier); return this.isFileAssignee(file) || this.isApprover(dossier);

View File

@ -84,6 +84,7 @@
"rank": "" "rank": ""
}, },
"save": "", "save": "",
"success": "",
"title": "" "title": ""
}, },
"add-edit-dossier-template": { "add-edit-dossier-template": {
@ -327,7 +328,7 @@
"title": "" "title": ""
}, },
"table-col-names": { "table-col-names": {
"dossier-status": "", "dossier-state": "",
"last-modified": "", "last-modified": "",
"name": "", "name": "",
"owner": "" "owner": ""
@ -460,9 +461,10 @@
"delete": "", "delete": "",
"delete-replace": "", "delete-replace": "",
"form": { "form": {
"status": "", "state": "",
"status-placeholder": "" "state-placeholder": ""
}, },
"success": "",
"suggestion": "", "suggestion": "",
"title": "", "title": "",
"warning": "" "warning": ""
@ -769,7 +771,7 @@
}, },
"table-col-names": { "table-col-names": {
"documents-status": "", "documents-status": "",
"dossier-status": "", "dossier-state": "",
"name": "Name", "name": "Name",
"needs-work": "Arbeitsvorrat", "needs-work": "Arbeitsvorrat",
"owner": "Besitzer" "owner": "Besitzer"
@ -1065,9 +1067,9 @@
"label": "Beschreibung", "label": "Beschreibung",
"placeholder": "Beschreibung eingeben" "placeholder": "Beschreibung eingeben"
}, },
"dossier-status": { "dossier-state": {
"label": "", "label": "",
"no-status-placeholder": "", "no-state-placeholder": "",
"placeholder": "" "placeholder": ""
}, },
"due-date": "Termin", "due-date": "Termin",
@ -1359,7 +1361,7 @@
"filters": { "filters": {
"assigned-people": "Beauftragt", "assigned-people": "Beauftragt",
"documents-status": "", "documents-status": "",
"dossier-status": "", "dossier-state": "",
"dossier-templates": "Regelsätze", "dossier-templates": "Regelsätze",
"empty": "Leer", "empty": "Leer",
"filter-by": "Filter:", "filter-by": "Filter:",

View File

@ -83,8 +83,9 @@
"name-placeholder": "Enter Name", "name-placeholder": "Enter Name",
"rank": "Rank" "rank": "Rank"
}, },
"save": "Save Status", "save": "Save State",
"title": "{type, select, edit{Edit {name}} create{Create} other{}} Dossier Status" "success": "Successfully {type, select, edit{updated} create{created} other{}} the dossier state!",
"title": "{type, select, edit{Edit {name}} create{Create} other{}} Dossier State"
}, },
"add-edit-dossier-template": { "add-edit-dossier-template": {
"error": { "error": {
@ -327,7 +328,7 @@
"title": "No archived dossiers match your current filters." "title": "No archived dossiers match your current filters."
}, },
"table-col-names": { "table-col-names": {
"dossier-status": "Dossier Status", "dossier-state": "Dossier State",
"last-modified": "Archived Time", "last-modified": "Archived Time",
"name": "Name", "name": "Name",
"owner": "Owner" "owner": "Owner"
@ -460,12 +461,13 @@
"delete": "Delete", "delete": "Delete",
"delete-replace": "Delete and Replace", "delete-replace": "Delete and Replace",
"form": { "form": {
"status": "Replace Status", "state": "Replace State",
"status-placeholder": "Choose another status" "state-placeholder": "Choose another state"
}, },
"suggestion": "Would you like to replace the states of the Dossiers with another status?", "success": "Successfully deleted state!",
"title": "Delete Dossier Status", "suggestion": "Would you like to replace the dossiers' states with another state?",
"warning": "The {name} status is assigned to {count} {count, plural, one{Dossier} other{Dossiers}}." "title": "Delete Dossier State",
"warning": "The {name} state is assigned to {count} {count, plural, one{dossier} other{dossiers}}."
}, },
"confirm-delete-users": { "confirm-delete-users": {
"cancel": "Keep {usersCount, plural, one{User} other{Users}}", "cancel": "Keep {usersCount, plural, one{User} other{Users}}",
@ -769,7 +771,7 @@
}, },
"table-col-names": { "table-col-names": {
"documents-status": "Documents Status", "documents-status": "Documents Status",
"dossier-status": "Dossier Status", "dossier-state": "Dossier State",
"name": "Name", "name": "Name",
"needs-work": "Workload", "needs-work": "Workload",
"owner": "Owner" "owner": "Owner"
@ -881,16 +883,16 @@
"dossier-states": "Dossier States", "dossier-states": "Dossier States",
"dossier-states-listing": { "dossier-states-listing": {
"action": { "action": {
"delete": "Delete Status", "delete": "Delete State",
"edit": "Edit Status" "edit": "Edit State"
}, },
"add-new": "New Status", "add-new": "New State",
"chart": { "chart": {
"dossier-states": "{count, plural, one{Dossier State} other{Dossier States}}" "dossier-states": "{count, plural, one{Dossier State} other{Dossier States}}"
}, },
"error": { "error": {
"conflict": "Dossier State with this name already exists!", "conflict": "Dossier state with this name already exists!",
"generic": "Failed to add Dossier State" "generic": "Failed to save dossier state!"
}, },
"no-data": { "no-data": {
"title": "There are no dossier states." "title": "There are no dossier states."
@ -1065,9 +1067,9 @@
"label": "Description", "label": "Description",
"placeholder": "Enter Description" "placeholder": "Enter Description"
}, },
"dossier-status": { "dossier-state": {
"label": "Dossier Status", "label": "Dossier State",
"no-status-placeholder": "This dossier template has no states", "no-state-placeholder": "This dossier template has no states",
"placeholder": "Undefined" "placeholder": "Undefined"
}, },
"due-date": "Due Date", "due-date": "Due Date",
@ -1359,7 +1361,7 @@
"filters": { "filters": {
"assigned-people": "Assignee(s)", "assigned-people": "Assignee(s)",
"documents-status": "Documents Status", "documents-status": "Documents Status",
"dossier-status": "Dossier Status", "dossier-state": "Dossier State",
"dossier-templates": "Dossier Templates", "dossier-templates": "Dossier Templates",
"empty": "Empty", "empty": "Empty",
"filter-by": "Filter:", "filter-by": "Filter:",

View File

@ -1,23 +1,25 @@
import { IListable } from '@iqser/common-ui'; import { Entity } from '@iqser/common-ui';
import { IDossierState } from './dossier-state'; import { IDossierState } from './dossier-state';
export class DossierState implements IDossierState, IListable { export class DossierState extends Entity<IDossierState> {
readonly description: string; readonly description: string;
readonly dossierStatusId: string; readonly dossierStatusId: string;
readonly dossierTemplateId: string; readonly dossierTemplateId: string;
readonly name: string; readonly name: string;
readonly color: string; readonly color: string;
readonly rank?: number; readonly rank?: number;
dossierCount?: number; readonly routerLink = undefined;
readonly dossierCount: number;
constructor(dossierState: IDossierState) { constructor(dossierState: IDossierState) {
super(dossierState);
this.description = dossierState.description; this.description = dossierState.description;
this.dossierStatusId = dossierState.dossierStatusId; this.dossierStatusId = dossierState.dossierStatusId;
this.dossierTemplateId = dossierState.dossierTemplateId; this.dossierTemplateId = dossierState.dossierTemplateId;
this.name = dossierState.name; this.name = dossierState.name;
this.color = dossierState.color; this.color = dossierState.color;
this.dossierCount = dossierState.dossierCount;
this.rank = dossierState.rank; this.rank = dossierState.rank;
this.dossierCount = dossierState.dossierCount || 0;
} }
get id(): string { get id(): string {