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">
<div
[translateParams]="{
type: data.dossierState ? 'edit' : 'create',
type: type,
name: data.dossierState?.name
}"
[translate]="'add-edit-dossier-state.title'"

View File

@ -1,8 +1,11 @@
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 { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
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 {
readonly dossierState: IDossierState;
@ -18,6 +21,9 @@ interface DialogData {
export class AddEditDossierStateDialogComponent extends BaseDialogComponent {
constructor(
private readonly _formBuilder: FormBuilder,
private readonly _loadingService: LoadingService,
private readonly _toaster: Toaster,
private readonly _dossierStateService: DossierStatesService,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddEditDossierStateDialogComponent>,
@Inject(MAT_DIALOG_DATA) readonly data: DialogData,
@ -27,13 +33,23 @@ export class AddEditDossierStateDialogComponent extends BaseDialogComponent {
this.initialFormValue = this.form.getRawValue();
}
save(): void {
get type(): 'edit' | 'create' {
return this.data.dossierState ? 'edit' : 'create';
}
async save(): Promise<void> {
const dossierState: IDossierState = {
dossierStatusId: this.data.dossierState?.dossierStatusId,
dossierTemplateId: this.data.dossierTemplateId,
...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 {

View File

@ -12,12 +12,12 @@
<form [formGroup]="form">
<div class="flex">
<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
[placeholder]="'confirm-delete-dossier-state.form.status-placeholder' | translate"
[placeholder]="'confirm-delete-dossier-state.form.state-placeholder' | translate"
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">
{{ state.name }}
</mat-option>
@ -29,10 +29,10 @@
</div>
<div class="dialog-actions">
<button (click)="dialogRef.close(afterCloseValue)" color="primary" mat-flat-button>
<button (click)="save()" color="primary" mat-flat-button>
{{ label | translate }}
</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>
<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 { IDossierState } from '@red/domain';
import { DossierState } from '@red/domain';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup } from '@angular/forms';
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 {
readonly toBeDeletedState: IDossierState;
readonly otherStates: IDossierState[];
readonly toBeDeletedState: DossierState;
readonly otherStates: DossierState[];
readonly dossierCount: number;
}
@ -21,7 +27,12 @@ export class ConfirmDeleteDossierStateDialogComponent {
constructor(
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,
) {
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');
}
get afterCloseValue(): string | true {
return this.replaceDossierStatusId ?? true;
async save(): Promise<void> {
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 {

View File

@ -1,101 +1,98 @@
<ng-container *ngIf="dossierStateService.all">
<section>
<div class="page-header">
<redaction-dossier-template-breadcrumbs class="flex-1"></redaction-dossier-template-breadcrumbs>
<section>
<div class="page-header">
<redaction-dossier-template-breadcrumbs class="flex-1"></redaction-dossier-template-breadcrumbs>
<div class="actions flex-1">
<redaction-dossier-template-actions></redaction-dossier-template-actions>
<div class="actions flex-1">
<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
[routerLink]="['../..']"
[tooltip]="'common.close' | translate"
icon="iqser:close"
tooltipPosition="below"
(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 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"
[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>
</div>
</ng-template>

View File

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

View File

@ -4,22 +4,19 @@ import {
DefaultListingServices,
IconButtonTypes,
ListingComponent,
LoadingService,
SortingOrders,
TableColumnConfig,
Toaster,
} from '@iqser/common-ui';
import { DossierState, IDossierState } from '@red/domain';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { DossierStateService } from '@services/entity-services/dossier-state.service';
import { firstValueFrom } from 'rxjs';
import { firstValueFrom, map, Observable } from 'rxjs';
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 { 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({
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 {
readonly iconButtonTypes = IconButtonTypes;
readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser;
readonly tableHeaderLabel = _('dossier-states-listing.table-header.title');
readonly tableColumnConfigs: TableColumnConfig<DossierState>[] = [
{ 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.dossiers-count') },
];
chartData: DoughnutChartConfig[];
chartData$: Observable<DoughnutChartConfig[]>;
readonly #dossierTemplateId: string;
constructor(
protected readonly _injector: Injector,
private readonly _loadingService: LoadingService,
private readonly _activeDossiersService: ActiveDossiersService,
readonly dossierStateService: DossierStateService,
private readonly _dialogService: AdminDialogService,
private readonly _userService: UserService,
private readonly _toaster: Toaster,
private readonly _route: ActivatedRoute,
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dossierStatesMapService: DossierStatesMapService,
private readonly _dossierStatesService: DossierStatesService,
readonly permissionsService: PermissionsService,
) {
super(_injector);
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> {
@ -63,7 +60,7 @@ export class DossierStatesListingScreenComponent extends ListingComponent<Dossie
column: 'rank',
order: SortingOrders.asc,
});
await this.#loadData();
await firstValueFrom(this._dossierStatesService.loadAllForTemplate(this.#dossierTemplateId));
}
openAddEditStateDialog($event: MouseEvent, dossierState?: IDossierState) {
@ -71,66 +68,24 @@ export class DossierStatesListingScreenComponent extends ListingComponent<Dossie
dossierState,
dossierTemplateId: this.#dossierTemplateId,
};
this._dialogService.openDialog('addEditDossierState', $event, data, async (newValue: IDossierState) => {
await this.#createNewDossierStateAndRefreshView(newValue);
});
this._dialogService.openDialog('addEditDossierState', $event, data);
}
openConfirmDeleteStateDialog($event: MouseEvent, dossierState: IDossierState) {
const templateId = this.#dossierTemplateId;
openConfirmDeleteStateDialog($event: MouseEvent, dossierState: DossierState) {
const data = {
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,
};
this._dialogService.openDialog('deleteDossierState', $event, data, async (value: string | true) => {
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();
});
this._dialogService.openDialog('deleteDossierState', $event, data);
}
async #createNewDossierStateAndRefreshView(newValue: IDossierState): Promise<void> {
this._loadingService.start();
await firstValueFrom(this.dossierStateService.updateDossierState(newValue)).catch(error => {
if (error.status === HttpStatusCode.Conflict) {
this._toaster.error(_('dossier-states-listing.error.conflict'));
} else {
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)));
#chartData(states: DossierState[]): DoughnutChartConfig[] {
return states.map(state => ({
value: state.dossierCount,
label: state.name,
key: state.name,
color: state.color,
}));
}
}

View File

@ -9,5 +9,5 @@
</div>
<div class="cell">
<redaction-dossier-status [dossier]="dossier"></redaction-dossier-status>
<redaction-dossier-state [dossier]="dossier"></redaction-dossier-state>
</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.last-modified'), sortByKey: 'archivedTime' },
{ 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="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-option *ngFor="let stateId of states" [value]="stateId">
<div class="flex-align-items-center">

View File

@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
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 { DossiersDialogService } from '../../../services/dossiers-dialog.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 { DossierStatsService } from '@services/dossiers/dossier-stats.service';
import { firstValueFrom } from 'rxjs';
import { DossierStateService } from '@services/entity-services/dossier-state.service';
import { DOSSIER_TEMPLATE_ID } from '@utils/constants';
import { TranslateService } from '@ngx-translate/core';
import { DossiersService } from '@services/dossiers/dossiers.service';
import { TrashDossiersService } from '@services/entity-services/trash-dossiers.service';
import { ArchivedDossiersService } from '@services/dossiers/archived-dossiers.service';
import { DossierStatesMapService } from '@services/entity-services/dossier-states-map.service';
@Component({
selector: 'redaction-edit-dossier-general-info',
@ -36,11 +36,10 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
hasDueDate: boolean;
dossierTemplates: IDossierTemplate[];
states: string[];
currentStatus: DossierState;
constructor(
readonly permissionsService: PermissionsService,
private readonly _dossierStateService: DossierStateService,
private readonly _dossierStatesMapService: DossierStatesMapService,
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dossiersService: DossiersService,
private readonly _trashDossiersService: TrashDossiersService,
@ -80,20 +79,22 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
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() {
this.states = [
null,
...this._dossierStateService.all.filter(s => s.dossierTemplateId === this.dossier.dossierTemplateId).map(s => s.id),
];
this.states = [null, ...this._dossierStatesMapService.get(this.dossier.dossierTemplateId).map(s => s.id)];
this.statusPlaceholder = this.#statusPlaceholder;
this.#filterInvalidDossierTemplates();
this.form = this.#getForm();
if (!this.permissionsService.canEditDossier(this.dossier)) {
this.form.disable();
}
if (this.dossier.dossierStatusId) {
this.currentStatus = this._dossierStateService.find(this.dossier.dossierStatusId);
}
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 {
const formFieldWithArchivedCheck = value => ({ value, disabled: !this.dossier.isActive });
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() {
this.dossierTemplates = this._dossierTemplatesService.all.filter(r => {
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 { filter, map, switchMap } from 'rxjs/operators';
import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
import { DossierStateService } from '@services/entity-services/dossier-state.service';
import { TranslateService } from '@ngx-translate/core';
import { DossierStatesMapService } from '@services/entity-services/dossier-states-map.service';
@Component({
selector: 'redaction-dossiers-listing-details',
@ -26,7 +26,7 @@ export class DossiersListingDetailsComponent {
readonly activeDossiersService: ActiveDossiersService,
private readonly _dossierStatsMap: DossierStatsService,
private readonly _translateChartService: TranslateChartService,
private readonly _dossierStateService: DossierStateService,
private readonly _dossierStatesMapService: DossierStatesMapService,
private readonly _translateService: TranslateService,
) {
this.documentsChartData$ = this.activeDossiersService.all$.pipe(
@ -40,24 +40,12 @@ export class DossiersListingDetailsComponent {
}
private _toDossierChartData(): DoughnutChartConfig[] {
this._dossierStateService.all.forEach(
state => (state.dossierCount = this.activeDossiersService.getCountWithState(state.dossierStatusId)),
);
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);
const configArray: DoughnutChartConfig[] = this._dossierStatesMapService.stats;
const undefinedStateLength =
this.activeDossiersService.all.length - configArray.map(v => v.value).reduce((acc, val) => acc + val, 0);
configArray.push({
value: notAssignedLength,
label: this._translateService.instant('edit-dossier-dialog.general-info.form.dossier-status.placeholder'),
value: undefinedStateLength,
label: this._translateService.instant('edit-dossier-dialog.general-info.form.dossier-state.placeholder'),
color: '#E2E4E9',
});

View File

@ -21,7 +21,7 @@
</div>
<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>
</div>

View File

@ -1,6 +1,6 @@
import { Injectable, TemplateRef } from '@angular/core';
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 { TranslateService } from '@ngx-translate/core';
import { UserPreferenceService } from '@services/user-preference.service';
@ -10,7 +10,7 @@ import { dossierMemberChecker, dossierStateChecker, dossierTemplateChecker, Reda
import { workloadTranslations } from '../../translations/workload-translations';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.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()
export class ConfigService {
@ -20,7 +20,7 @@ export class ConfigService {
private readonly _userService: UserService,
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dossierStatsService: DossierStatsService,
private readonly _dossierStateService: DossierStateService,
private readonly _dossierStatesMapService: DossierStatesMapService,
) {}
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.owner'), class: 'user-column' },
{ 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 allDistinctDossierStates = new Set<string>();
const stateToTemplateMap = new Map<string, string>();
const filterGroups: IFilterGroup[] = [];
entities?.forEach(entry => {
@ -73,6 +75,7 @@ export class ConfigService {
allDistinctDossierTemplates.add(entry.dossierTemplateId);
if (entry.dossierStatusId) {
allDistinctDossierStates.add(entry.dossierStatusId);
stateToTemplateMap.set(entry.dossierStatusId, entry.dossierTemplateId);
}
const stats = this._dossierStatsService.get(entry.dossierId);
@ -100,13 +103,13 @@ export class ConfigService {
id =>
new NestedFilter({
id: id,
label: this._dossierStateService.find(id).name,
label: this._dossierStatesMapService.get(stateToTemplateMap.get(id), id).name,
}),
);
filterGroups.push({
slug: 'dossierStatesFilters',
label: this._translateService.instant('filters.dossier-status'),
label: this._translateService.instant('filters.dossier-state'),
icon: 'red:status',
hide: dossierStatesFilters.length <= 1,
filters: dossierStatesFilters,
@ -114,7 +117,7 @@ export class ConfigService {
});
const statusFilters = [...allDistinctFileStatus].map(
status =>
(status: WorkflowFileStatus) =>
new NestedFilter({
id: 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';
.dossier-status-container {
.dossier-state-container {
justify-content: flex-end;
width: 100%;
}
@ -9,7 +9,7 @@ redaction-small-chip {
margin-left: 8px;
}
.dossier-status-text {
.dossier-state-text {
font-size: 13px;
line-height: 16px;
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 { ExpandableFileActionsComponent } from './components/expandable-file-actions/expandable-file-actions.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';
const buttons = [FileDownloadBtnComponent, UserButtonComponent];
@ -45,7 +45,7 @@ const components = [
TeamMembersComponent,
ExpandableFileActionsComponent,
ProcessingIndicatorComponent,
DossierStatusComponent,
DossierStateComponent,
DossiersListingDossierNameComponent,
...buttons,

View File

@ -1,7 +1,7 @@
import { Injectable, Injector } from '@angular/core';
import { switchMap, tap } from 'rxjs/operators';
import { timer } from 'rxjs';
import { CHANGED_CHECK_INTERVAL } from '../../utils/constants';
import { CHANGED_CHECK_INTERVAL } from '@utils/constants';
import { DossiersService } from './dossiers.service';
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 { catchError, filter, map, mapTo, pluck, switchMap, tap } from 'rxjs/operators';
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 { IDossiersStats } from './active-dossiers.service';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
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');
export abstract class DossiersService extends EntitiesService<Dossier, IDossier> {
readonly dossierFileChanges$ = new Subject<string>();
readonly generalStats$ = this.all$.pipe(switchMap(entities => this.#generalStats$(entities)));
protected readonly _dossierStatsService = this._injector.get(DossierStatsService);
protected readonly _dossierStateService = this._injector.get(DossierStateService);
protected readonly _dossierStateService = this._injector.get(DossierStatesService);
protected readonly _toaster = this._injector.get(Toaster);
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()
createOrUpdate(@RequiredParam() dossier: IDossierRequest): Observable<Dossier> {
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;
};

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);
}
canPerformDossierStatesActions(user = this._userService.currentUser): boolean {
return user.isAdmin;
}
isReviewerOrApprover(file: File): boolean {
const dossier = this._getDossier(file);
return this.isFileAssignee(file) || this.isApprover(dossier);

View File

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

View File

@ -83,8 +83,9 @@
"name-placeholder": "Enter Name",
"rank": "Rank"
},
"save": "Save Status",
"title": "{type, select, edit{Edit {name}} create{Create} other{}} Dossier Status"
"save": "Save State",
"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": {
"error": {
@ -327,7 +328,7 @@
"title": "No archived dossiers match your current filters."
},
"table-col-names": {
"dossier-status": "Dossier Status",
"dossier-state": "Dossier State",
"last-modified": "Archived Time",
"name": "Name",
"owner": "Owner"
@ -460,12 +461,13 @@
"delete": "Delete",
"delete-replace": "Delete and Replace",
"form": {
"status": "Replace Status",
"status-placeholder": "Choose another status"
"state": "Replace State",
"state-placeholder": "Choose another state"
},
"suggestion": "Would you like to replace the states of the Dossiers with another status?",
"title": "Delete Dossier Status",
"warning": "The {name} status is assigned to {count} {count, plural, one{Dossier} other{Dossiers}}."
"success": "Successfully deleted state!",
"suggestion": "Would you like to replace the dossiers' states with another state?",
"title": "Delete Dossier State",
"warning": "The {name} state is assigned to {count} {count, plural, one{dossier} other{dossiers}}."
},
"confirm-delete-users": {
"cancel": "Keep {usersCount, plural, one{User} other{Users}}",
@ -769,7 +771,7 @@
},
"table-col-names": {
"documents-status": "Documents Status",
"dossier-status": "Dossier Status",
"dossier-state": "Dossier State",
"name": "Name",
"needs-work": "Workload",
"owner": "Owner"
@ -881,16 +883,16 @@
"dossier-states": "Dossier States",
"dossier-states-listing": {
"action": {
"delete": "Delete Status",
"edit": "Edit Status"
"delete": "Delete State",
"edit": "Edit State"
},
"add-new": "New Status",
"add-new": "New State",
"chart": {
"dossier-states": "{count, plural, one{Dossier State} other{Dossier States}}"
},
"error": {
"conflict": "Dossier State with this name already exists!",
"generic": "Failed to add Dossier State"
"conflict": "Dossier state with this name already exists!",
"generic": "Failed to save dossier state!"
},
"no-data": {
"title": "There are no dossier states."
@ -1065,9 +1067,9 @@
"label": "Description",
"placeholder": "Enter Description"
},
"dossier-status": {
"label": "Dossier Status",
"no-status-placeholder": "This dossier template has no states",
"dossier-state": {
"label": "Dossier State",
"no-state-placeholder": "This dossier template has no states",
"placeholder": "Undefined"
},
"due-date": "Due Date",
@ -1359,7 +1361,7 @@
"filters": {
"assigned-people": "Assignee(s)",
"documents-status": "Documents Status",
"dossier-status": "Dossier Status",
"dossier-state": "Dossier State",
"dossier-templates": "Dossier Templates",
"empty": "Empty",
"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';
export class DossierState implements IDossierState, IListable {
export class DossierState extends Entity<IDossierState> {
readonly description: string;
readonly dossierStatusId: string;
readonly dossierTemplateId: string;
readonly name: string;
readonly color: string;
readonly rank?: number;
dossierCount?: number;
readonly routerLink = undefined;
readonly dossierCount: number;
constructor(dossierState: IDossierState) {
super(dossierState);
this.description = dossierState.description;
this.dossierStatusId = dossierState.dossierStatusId;
this.dossierTemplateId = dossierState.dossierTemplateId;
this.name = dossierState.name;
this.color = dossierState.color;
this.dossierCount = dossierState.dossierCount;
this.rank = dossierState.rank;
this.dossierCount = dossierState.dossierCount || 0;
}
get id(): string {