RED-7324: Backport license screen updates

This commit is contained in:
Adina Țeudan 2023-10-02 11:41:37 +03:00
parent 953d9253eb
commit 7edfec222b
50 changed files with 1022 additions and 1555 deletions

View File

@ -73,7 +73,7 @@
"stylePreprocessorOptions": {
"includePaths": ["./apps/red-ui/src/assets/styles", "./libs/common-ui/src/assets/styles"]
},
"scripts": ["node_modules/@pdftron/webviewer/webviewer.min.js"],
"scripts": ["node_modules/@pdftron/webviewer/webviewer.min.js", "node_modules/chart.js/dist/chart.js"],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,

View File

@ -60,6 +60,7 @@ import {
} from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
import { AuditInfoDialogComponent } from './dialogs/audit-info-dialog/audit-info-dialog.component';
import { DonutChartComponent } from '@shared/components/donut-chart/donut-chart.component';
const dialogs = [
AddEditCloneDossierTemplateDialogComponent,
@ -131,6 +132,7 @@ const components = [
IqserSharedModule,
IqserHelpModeModule,
IqserPermissionsModule,
DonutChartComponent,
],
})
export class AdminModule {}

View File

@ -1,116 +0,0 @@
<ngx-charts-chart
(legendLabelActivate)="onActivate($event)"
(legendLabelClick)="onClick($event)"
(legendLabelDeactivate)="onDeactivate($event)"
[activeEntries]="activeEntries"
[animations]="animations"
[legendOptions]="legendOptions"
[showLegend]="legend"
[view]="[width + legendSpacing, height]"
xmlns:svg="http://www.w3.org/2000/svg"
>
<svg:g [attr.transform]="transform" class="bar-chart chart">
<svg:g
(dimensionsChanged)="updateXAxisHeight($event)"
*ngIf="xAxis"
[dims]="dims"
[labelText]="xAxisLabel"
[rotateTicks]="true"
[showLabel]="showXAxisLabel"
[tickFormatting]="xAxisTickFormatting"
[xScale]="xScale"
ngx-charts-x-axis
></svg:g>
<svg:g
(dimensionsChanged)="updateYAxisWidth($event)"
*ngIf="yAxis"
[dims]="dims"
[labelText]="yAxisLabel"
[showGridLines]="showGridLines"
[showLabel]="showYAxisLabel"
[tickFormatting]="yAxisTickFormatting"
[yOrient]="yOrientLeft"
[yScale]="yScale"
[valuesMaxLength]="getValuesMaxLength(1)"
red-ngx-charts-y-axis
></svg:g>
<svg:g
(dimensionsChanged)="updateYAxisWidth($event)"
*ngIf="yAxis"
[dims]="dims"
[labelText]="yAxisLabelRight"
[showGridLines]="showGridLines"
[showLabel]="showRightYAxisLabel"
[tickFormatting]="yRightAxisTickFormatting"
[yOrient]="yOrientRight"
[yScale]="yScaleLine"
[valuesMaxLength]="getValuesMaxLength(0)"
red-ngx-charts-y-axis
></svg:g>
<svg:g
(activate)="onActivate($event)"
(bandwidth)="updateLineWidth($event)"
(deactivate)="onDeactivate($event)"
[activeEntries]="activeEntries"
[animations]="animations"
[colors]="colors"
[dims]="dims"
[gradient]="gradient"
[noBarWhenZero]="noBarWhenZero"
[seriesLine]="lineChart"
[series]="results"
[tooltipDisabled]="true"
[xScale]="xScale"
[yScale]="yScale"
ngx-combo-charts-series-vertical
></svg:g>
</svg:g>
<svg:g [attr.transform]="transform" class="line-chart chart">
<svg:g>
<svg:g *ngFor="let series of lineChart; trackBy: trackBy">
<svg:g
[activeEntries]="activeEntries"
[animations]="animations"
[colors]="colorsLine"
[curve]="curve"
[data]="series"
[rangeFillOpacity]="rangeFillOpacity"
[scaleType]="scaleType"
[xScale]="xScaleLine"
[yScale]="yScaleLine"
ngx-charts-line-series
></svg:g>
</svg:g>
<svg:g
(hover)="updateHoveredVertical($event)"
*ngIf="!tooltipDisabled"
[colors]="colorsLine"
[dims]="dims"
[results]="combinedSeries"
[tooltipDisabled]="tooltipDisabled"
[xScale]="xScaleLine"
[xSet]="xSet"
[yScale]="yScaleLine"
ngx-charts-tooltip-area
></svg:g>
<svg:g *ngFor="let series of lineChart">
<svg:g
(activate)="onActivate($event)"
(deactivate)="onDeactivate($event)"
[activeEntries]="activeEntries"
[colors]="colorsLine"
[data]="series"
[scaleType]="scaleType"
[tooltipDisabled]="tooltipDisabled"
[visibleValue]="hoveredVertical"
[xScale]="xScaleLine"
[yScale]="yScaleLine"
ngx-charts-circle-series
></svg:g>
</svg:g>
</svg:g>
</svg:g>
</ngx-charts-chart>

View File

@ -1,103 +0,0 @@
.ngx-charts {
float: left;
overflow: visible;
.circle,
.bar,
.arc {
cursor: pointer;
}
.bar,
.cell,
.arc,
.card {
&.active,
&:hover {
opacity: 0.8;
transition: opacity 100ms ease-in-out;
}
&:focus {
outline: none;
}
&.hidden {
display: none;
}
}
g {
&:focus {
outline: none;
}
}
.line-series,
.line-series-range,
.area-series {
&.inactive {
transition: opacity 100ms ease-in-out;
opacity: 0.2;
}
}
.line-highlight {
display: none;
&.active {
display: block;
}
}
.area {
opacity: 0.6;
}
.circle {
&:hover {
cursor: pointer;
}
}
.label {
font-size: 12px;
font-weight: normal;
}
.tooltip-anchor {
fill: var(--iqser-text);
}
.gridline-path {
stroke: #ddd;
stroke-width: 1;
fill: none;
}
.grid-panel {
rect {
fill: none;
}
&.odd {
rect {
fill: rgba(0, 0, 0, 0.05);
}
}
}
fill: var(--iqser-text);
}
.chart-legend .legend-labels {
background: var(--iqser-alt-background) !important;
.legend-label .legend-label-text {
color: var(--iqser-text) !important;
&:hover {
color: rgba(var(--iqser-text-rgb), 0.8) !important;
}
}
}

View File

@ -1,384 +0,0 @@
import { Component, EventEmitter, HostListener, Input, Output, ViewEncapsulation } from '@angular/core';
import { curveLinear } from 'd3-shape';
import { scaleBand, scaleLinear, scalePoint, scaleTime } from 'd3-scale';
import {
BaseChartComponent,
calculateViewDimensions,
Color,
ColorHelper,
LegendPosition,
Orientation,
ScaleType,
Series,
StringOrNumberOrDate,
ViewDimensions,
} from '@swimlane/ngx-charts';
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'combo-chart-component',
templateUrl: './combo-chart.component.html',
styleUrls: ['./combo-chart.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class ComboChartComponent extends BaseChartComponent {
@Input() curve: any = curveLinear;
@Input() legend = false;
@Input() legendTitle = 'Legend';
@Input() legendPosition: LegendPosition = LegendPosition.Right;
@Input() xAxis;
@Input() yAxis;
@Input() showXAxisLabel;
@Input() showYAxisLabel;
@Input() showRightYAxisLabel;
@Input() xAxisLabel;
@Input() yAxisLabel;
@Input() yAxisLabelRight;
@Input() tooltipDisabled = false;
@Input() gradient: boolean;
@Input() showGridLines = true;
@Input() activeEntries: any[] = [];
@Input() schemeType: ScaleType;
@Input() yAxisTickFormatting: any;
@Input() yRightAxisTickFormatting: any;
@Input() roundDomains = false;
@Input() colorSchemeLine: Color;
@Input() autoScale;
@Input() lineChart: Series[];
@Input() yLeftAxisScaleFactor: any;
@Input() yRightAxisScaleFactor: any;
@Input() rangeFillOpacity: number;
@Input() animations = true;
@Input() noBarWhenZero = true;
@Output() activate = new EventEmitter<{ value; entries: unknown[] }>();
@Output() deactivate = new EventEmitter<{ value; entries: unknown[] }>();
dims: ViewDimensions;
xScale: any;
yScale: any;
xDomain: string[] | number[];
yDomain: string[] | number[];
transform: string;
colors: ColorHelper;
colorsLine: ColorHelper;
margin = [10, 20, 10, 20];
xAxisHeight = 0;
yAxisWidth = 0;
legendOptions: any;
scaleType: ScaleType = ScaleType.Linear;
xScaleLine;
yScaleLine;
xDomainLine;
yDomainLine;
seriesDomain;
combinedSeries: Series[];
xSet;
filteredDomain;
hoveredVertical;
yOrientLeft: Orientation = Orientation.Left;
yOrientRight: Orientation = Orientation.Right;
legendSpacing = 0;
bandwidth: number;
barPadding = 8;
@Input() xAxisTickFormatting: any;
trackBy(_index, item): string {
return item.name;
}
update(): void {
super.update();
this.dims = calculateViewDimensions({
width: this.width,
height: this.height,
margins: this.margin,
showXAxis: this.xAxis,
showYAxis: this.yAxis,
xAxisHeight: this.xAxisHeight,
yAxisWidth: this.yAxisWidth,
showXLabel: this.showXAxisLabel,
showYLabel: this.showYAxisLabel,
showLegend: this.legend,
legendType: this.schemeType,
legendPosition: this.legendPosition,
});
if (!this.yAxis) {
this.legendSpacing = 0;
} else if (this.showYAxisLabel && this.yAxis) {
this.legendSpacing = 150;
} else {
this.legendSpacing = 40;
}
this.xScale = this.getXScale();
this.yScale = this.getYScale();
// line chart
this.xDomainLine = this.getXDomainLine();
if (this.filteredDomain) {
this.xDomainLine = this.filteredDomain;
}
this.yDomainLine = this.getYDomainLine();
this.seriesDomain = this.getSeriesDomain();
this.scaleLines();
this.setColors();
this.legendOptions = this.getLegendOptions();
this.transform = `translate(${this.dims.xOffset} , ${this.margin[0]})`;
}
deactivateAll() {
this.activeEntries = [...this.activeEntries];
for (const entry of this.activeEntries) {
this.deactivate.emit({ value: entry, entries: [] });
}
this.activeEntries = [];
}
@HostListener('mouseleave')
hideCircles(): void {
this.hoveredVertical = null;
this.deactivateAll();
}
updateHoveredVertical(item): void {
this.hoveredVertical = item.value;
this.deactivateAll();
}
scaleLines() {
this.xScaleLine = this.getXScaleLine(this.xDomainLine, this.dims.width);
this.yScaleLine = this.getYScaleLine(this.yDomainLine, this.dims.height);
}
getSeriesDomain(): any[] {
this.combinedSeries = this.lineChart.slice(0);
this.combinedSeries.push({
name: this.yAxisLabel,
series: this.results,
});
return this.combinedSeries.map(d => d.name);
}
isDate(value): value is Date {
return value instanceof Date;
}
getScaleType(values): ScaleType {
let date = true;
let num = true;
for (const value of values) {
if (!this.isDate(value)) {
date = false;
}
if (typeof value !== 'number') {
num = false;
}
}
if (date) {
return ScaleType.Time;
}
if (num) {
return ScaleType.Linear;
}
return ScaleType.Ordinal;
}
getXDomainLine(): any[] {
let values: StringOrNumberOrDate[] = [];
for (const results of this.lineChart) {
for (const d of results.series) {
if (!values.includes(d.name)) {
values.push(d.name);
}
}
}
this.scaleType = this.getScaleType(values);
let domain = [];
if (this.scaleType === 'time') {
const min = Math.min(...(values as number[]));
const max = Math.max(...(values as number[]));
domain = [min, max];
} else if (this.scaleType === 'linear') {
values = values.map(v => Number(v));
const min = Math.min(...(values as number[]));
const max = Math.max(...(values as number[]));
domain = [min, max];
} else {
domain = values;
}
this.xSet = values;
return domain;
}
getYDomainLine(): any[] {
const domain: number[] = [];
for (const results of this.lineChart) {
for (const d of results.series) {
if (domain.indexOf(d.value) < 0) {
domain.push(d.value);
}
if (d.min !== undefined) {
if (domain.indexOf(d.min) < 0) {
domain.push(d.min);
}
}
if (d.max !== undefined) {
if (domain.indexOf(d.max) < 0) {
domain.push(d.max);
}
}
}
}
let min = Math.min(...domain);
const max = Math.max(...domain);
if (this.yRightAxisScaleFactor) {
const minMax = this.yRightAxisScaleFactor(min, max);
return [Math.min(0, minMax.min as number), minMax.max];
}
min = Math.min(0, min);
return [min, max];
}
getXScaleLine(domain, width: number): any {
let scale;
if (this.bandwidth === undefined) {
this.bandwidth = width - this.barPadding;
}
const offset = Math.floor((width + this.barPadding - (this.bandwidth + this.barPadding) * domain.length) / 2);
if (this.scaleType === 'time') {
scale = scaleTime().range([0, width]).domain(domain);
} else if (this.scaleType === 'linear') {
scale = scaleLinear().range([0, width]).domain(domain);
if (this.roundDomains) {
scale = scale.nice();
}
} else if (this.scaleType === 'ordinal') {
scale = scalePoint()
.range([offset + this.bandwidth / 2, width - offset - this.bandwidth / 2])
.domain(domain);
}
return scale;
}
getYScaleLine(domain, height): any {
const scale = scaleLinear().range([height, 0]).domain(domain);
return this.roundDomains ? scale.nice() : scale;
}
getXScale(): any {
this.xDomain = this.getXDomain();
const spacing = this.xDomain.length / (this.dims.width / this.barPadding + 1);
return scaleBand().range([0, this.dims.width]).paddingInner(spacing).domain(this.xDomain);
}
getYScale(): any {
this.yDomain = this.getYDomain();
const scale = scaleLinear().range([this.dims.height, 0]).domain(this.yDomain);
return this.roundDomains ? scale.nice() : scale;
}
getXDomain(): any[] {
return this.results.map(d => d.name);
}
getYDomain() {
const values: number[] = this.results.map(d => d.value);
const min = Math.min(0, ...values);
const max = Math.max(...values);
if (this.yLeftAxisScaleFactor) {
const minMax = this.yLeftAxisScaleFactor(min, max);
return [Math.min(0, minMax.min as number), minMax.max];
}
return [min, max];
}
onClick(data) {
this.select.emit(data);
}
setColors(): void {
let domain: number[] | string[];
if (this.schemeType === 'ordinal') {
domain = this.xDomain;
} else {
domain = this.yDomain;
}
this.colors = new ColorHelper(this.scheme, this.schemeType, domain, this.customColors);
this.colorsLine = new ColorHelper(this.colorSchemeLine, this.schemeType, domain, this.customColors);
}
getLegendOptions() {
const opts = {
scaleType: this.schemeType,
colors: undefined,
domain: [],
title: undefined,
position: this.legendPosition,
};
if (opts.scaleType === 'ordinal') {
opts.domain = this.seriesDomain;
opts.colors = this.colorsLine;
opts.title = this.legendTitle;
} else {
opts.domain = this.seriesDomain;
opts.colors = this.colors.scale;
}
return opts;
}
getValuesMaxLength(index: number): number {
const values = this.lineChart[index].series.map(s => s.value);
return Math.max(...values).toString().length;
}
updateLineWidth(width): void {
this.bandwidth = width;
this.scaleLines();
}
updateYAxisWidth({ width }: { width: number }): void {
this.yAxisWidth = width + 20;
this.update();
}
updateXAxisHeight({ height }): void {
this.xAxisHeight = height;
this.update();
}
onActivate(item) {
const idx = this.activeEntries.findIndex(d => d.name === item.name && d.value === item.value && d.series === item.series);
if (idx > -1) {
return;
}
this.activeEntries = [item, ...this.activeEntries];
this.activate.emit({ value: item, entries: this.activeEntries });
}
onDeactivate(item) {
const idx = this.activeEntries.findIndex(d => d.name === item.name && d.value === item.value && d.series === item.series);
this.activeEntries.splice(idx, 1);
this.activeEntries = [...this.activeEntries];
this.deactivate.emit({ value: item, entries: this.activeEntries });
}
}

View File

@ -1,199 +0,0 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { animate, style, transition, trigger } from '@angular/animations';
import { Bar, BarOrientation, formatLabel, PlacementTypes, StyleTypes } from '@swimlane/ngx-charts';
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'g[ngx-combo-charts-series-vertical]',
template: `
<svg:g
ngx-charts-bar
*ngFor="let bar of bars; trackBy: trackBy"
[@animationState]="'active'"
[width]="bar.width"
[height]="bar.height"
[x]="bar.x"
[y]="bar.y"
[fill]="bar.color"
[stops]="bar.gradientStops"
[data]="bar.data"
[orientation]="orientations.Vertical"
[roundEdges]="bar.roundEdges"
[gradient]="gradient"
[isActive]="isActive(bar.data)"
[animations]="animations"
[noBarWhenZero]="noBarWhenZero"
(activate)="activate.emit($event)"
(deactivate)="deactivate.emit($event)"
ngx-tooltip
[tooltipDisabled]="tooltipDisabled"
[tooltipPlacement]="tooltipPlacements.Top"
[tooltipType]="tooltipTypes.tooltip"
[tooltipTitle]="bar.tooltipText"
></svg:g>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
trigger('animationState', [
transition('* => void', [
style({
opacity: 1,
transform: '*',
}),
animate(500, style({ opacity: 0, transform: 'scale(0)' })),
]),
]),
],
})
export class ComboSeriesVerticalComponent implements OnChanges {
@Input() dims;
@Input() type = 'standard';
@Input() series;
@Input() seriesLine;
@Input() xScale;
@Input() yScale;
@Input() colors;
@Input() tooltipDisabled = false;
@Input() gradient: boolean;
@Input() activeEntries: any[];
@Input() seriesName: string;
@Input() animations = true;
@Input() noBarWhenZero = true;
@Output() activate = new EventEmitter();
@Output() deactivate = new EventEmitter();
@Output() bandwidth = new EventEmitter();
bars: any;
x: any;
y: any;
readonly tooltipTypes = StyleTypes;
readonly tooltipPlacements = PlacementTypes;
readonly orientations = BarOrientation;
ngOnChanges(): void {
this.update();
}
update(): void {
let width;
if (this.series.length) {
width = this.xScale.bandwidth();
this.bandwidth.emit(width);
}
let d0 = 0;
let total;
if (this.type === 'normalized') {
total = this.series.map(d => d.value).reduce((sum: number, d: number) => sum + d, 0);
}
this.bars = this.series.map((d, index) => {
let value: number = d.value;
const label = d.name;
const formattedLabel = formatLabel(label);
const roundEdges = this.type === 'standard';
const bar: Bar = {
value,
label,
roundEdges,
data: d,
width,
formattedLabel,
height: 0,
x: 0,
y: 0,
ariaLabel: label,
tooltipText: label,
color: undefined,
gradientStops: undefined,
};
let offset0 = d0;
let offset1 = offset0 + value;
if (this.type === 'standard') {
bar.height = Math.abs(this.yScale(value) - this.yScale(0));
bar.x = this.xScale(label);
if (value < 0) {
bar.y = this.yScale(0);
} else {
bar.y = this.yScale(value);
}
} else if (this.type === 'stacked') {
d0 += value;
bar.height = this.yScale(offset0) - this.yScale(offset1);
bar.x = 0;
bar.y = this.yScale(offset1);
// bar.offset0 = offset0;
// bar.offset1 = offset1;
} else if (this.type === 'normalized') {
d0 += value;
if (total > 0) {
offset0 = (offset0 * 100) / total;
offset1 = (offset1 * 100) / total;
} else {
offset0 = 0;
offset1 = 0;
}
bar.height = this.yScale(offset0) - this.yScale(offset1);
bar.x = 0;
bar.y = this.yScale(offset1);
// bar.offset0 = offset0;
// bar.offset1 = offset1;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
value = (offset1 - offset0).toFixed(2) + '%';
}
if (this.colors.scaleType === 'ordinal') {
bar.color = this.colors.getColor(label);
} else {
if (this.type === 'standard') {
bar.color = this.colors.getColor(value);
bar.gradientStops = this.colors.getLinearGradientStops(value);
} else {
bar.color = this.colors.getColor(offset1);
bar.gradientStops = this.colors.getLinearGradientStops(offset1, offset0);
}
}
let tooltipLabel = formattedLabel;
if (this.seriesName) {
tooltipLabel = `${this.seriesName}${formattedLabel}`;
}
this.getSeriesTooltips(this.seriesLine, index);
const lineValue: string = this.seriesLine[0].series[index].value;
bar.tooltipText = `
<span class="tooltip-label">${tooltipLabel}</span>
<span class="tooltip-val">
Y1 - ${value.toLocaleString()} Y2 - ${lineValue.toLocaleString()}%
</span>
`;
return bar;
});
}
getSeriesTooltips(seriesLine, index) {
return seriesLine.map(d => d.series[index]);
}
isActive(entry): boolean {
if (!this.activeEntries) {
return false;
}
const item = this.activeEntries.find(d => entry.name === d.name && entry.series === d.series);
return item !== undefined;
}
trackBy(_index, bar): string {
return bar.label;
}
}

View File

@ -1,3 +0,0 @@
export * from './combo-chart.component';
export * from './combo-series-vertical.component';
export * from './y-axis.component';

View File

@ -1,103 +0,0 @@
import { Component, Input, Output, EventEmitter, OnChanges, ViewChild, SimpleChanges, ChangeDetectionStrategy } from '@angular/core';
import { Orientation, ViewDimensions, YAxisTicksComponent } from '@swimlane/ngx-charts';
@Component({
selector: 'g[red-ngx-charts-y-axis]',
template: `
<svg:g [attr.class]="yAxisClassName" [attr.transform]="transform">
<svg:g
ngx-charts-y-axis-ticks
*ngIf="yScale"
[trimTicks]="trimTicks"
[maxTickLength]="maxTickLength"
[tickFormatting]="tickFormatting"
[tickArguments]="tickArguments"
[tickValues]="ticks"
[tickStroke]="tickStroke"
[scale]="yScale"
[orient]="yOrient"
[showGridLines]="showGridLines"
[gridLineWidth]="dims.width"
[referenceLines]="referenceLines"
[showRefLines]="showRefLines"
[showRefLabels]="showRefLabels"
[height]="dims.height"
(dimensionsChanged)="emitTicksWidth($event)"
/>
<svg:g
ngx-charts-axis-label
*ngIf="showLabel"
[label]="labelText"
[offset]="labelOffset"
[orient]="yOrient"
[height]="dims.height"
[width]="dims.width"
></svg:g>
</svg:g>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class YAxisComponent implements OnChanges {
@Input() yScale;
@Input() dims: ViewDimensions;
@Input() trimTicks: boolean;
@Input() maxTickLength: number;
@Input() tickFormatting;
@Input() ticks: any[];
@Input() showGridLines = false;
@Input() showLabel: boolean;
@Input() labelText: string;
@Input() yAxisTickCount: any;
@Input() yOrient: Orientation = Orientation.Left;
@Input() referenceLines;
@Input() showRefLines: boolean;
@Input() showRefLabels: boolean;
@Input() yAxisOffset = 0;
@Input() valuesMaxLength = 0;
@Output() dimensionsChanged = new EventEmitter();
yAxisClassName = 'y axis';
tickArguments: number[];
offset: number;
transform: string;
labelOffset = 15;
fill = 'none';
stroke = '#CCC';
tickStroke = '#CCC';
strokeWidth = 1;
padding = 5;
@ViewChild(YAxisTicksComponent) ticksComponent: YAxisTicksComponent;
ngOnChanges(changes: SimpleChanges): void {
this.update();
}
update(): void {
this.offset = -(this.yAxisOffset + this.padding);
if (this.yOrient === Orientation.Right) {
this.labelOffset = 65 + (this.valuesMaxLength + this.valuesMaxLength / 4) * 5;
this.transform = `translate(${this.offset + this.dims.width} , 0)`;
} else {
this.transform = `translate(${this.offset} , 0)`;
}
if (this.yAxisTickCount !== undefined) {
this.tickArguments = [this.yAxisTickCount];
}
}
emitTicksWidth({ width }): void {
if (width !== this.labelOffset && this.yOrient === Orientation.Right) {
this.labelOffset = width + this.labelOffset + 300;
setTimeout(() => {
this.dimensionsChanged.emit({ width });
}, 0);
} else if (width !== this.labelOffset) {
this.labelOffset = width + (this.valuesMaxLength + this.valuesMaxLength / 4) * 5;
setTimeout(() => {
this.dimensionsChanged.emit({ width });
}, 0);
}
}
}

View File

@ -0,0 +1,3 @@
<canvas height="400px" id="{{ chartId }}" width="1000px">
{{ chart }}
</canvas>

View File

@ -0,0 +1,89 @@
import { Component, Input, OnChanges } from '@angular/core';
import { Chart, ChartConfiguration, ChartDataset } from 'chart.js';
import { Debounce } from '@iqser/common-ui';
@Component({
selector: 'redaction-chart [datasets] [labels] [chartId]',
templateUrl: './chart.component.html',
styleUrls: ['./chart.component.scss'],
})
export class ChartComponent implements OnChanges {
@Input() datasets: ChartDataset[];
@Input() labels: string[];
@Input() valueFormatter?: (value: number) => string;
@Input() secondaryAxis = false;
@Input() yAxisLabel?: string;
@Input() yAxisLabelRight?: string;
@Input() reverseLegend = false;
@Input() chartId: string;
chartData: ChartConfiguration['data'];
chartOptions: ChartConfiguration<'line'>['options'] = {};
chart: Chart;
@Debounce(0)
ngOnChanges() {
this.chartData = {
labels: this.labels,
datasets: this.datasets,
};
this.#setChartOptions();
}
#setChartOptions(): void {
this.chartOptions = {
responsive: false,
scales: {
y: {
stacked: true,
ticks: {
callback: this.valueFormatter ? (value: number) => this.valueFormatter(value) : undefined,
count: 9,
},
title: {
display: !!this.yAxisLabel,
text: this.yAxisLabel,
padding: { bottom: 20 },
},
min: 0,
},
y1: {
display: this.secondaryAxis,
position: 'right',
ticks: {
callback: this.valueFormatter ? (value: number) => this.valueFormatter(value) : undefined,
count: 9,
},
title: {
display: !!this.yAxisLabelRight,
text: this.yAxisLabelRight,
padding: { bottom: 20 },
},
min: 0,
},
},
plugins: {
legend: { position: 'right', reverse: this.reverseLegend, maxWidth: 280 },
tooltip: {
callbacks: {
label: this.valueFormatter ? item => `${item.dataset.label}: ${this.valueFormatter(item.parsed.y)}` : undefined,
},
},
},
layout: {
padding: {
top: 50,
bottom: 50,
},
},
datasets: { bar: { barPercentage: 0.7 } },
aspectRatio: 2.5,
};
this.chart = new Chart(this.chartId, {
type: 'line',
data: this.chartData,
options: this.chartOptions,
});
}
}

View File

@ -0,0 +1,35 @@
<div class="grid-container">
<div class="section-title all-caps-label" translate="license-info-screen.analysis-capacity-usage.section-title"></div>
<div class="row">
<div translate="license-info-screen.analysis-capacity-usage.used-in-period"></div>
<div>
{{ licenseService.selectedLicenseReport.analysedFilesBytes | size }}
<ng-container *ngIf="licenseService.analysisCapacityBytesForSelectedLicensePercentage >= 0">
({{ licenseService.analysisCapacityBytesForSelectedLicensePercentage | number : '1.0-2' }}%)
</ng-container>
</div>
</div>
<div class="row">
<div translate="license-info-screen.analysis-capacity-usage.used-in-total"></div>
<div>
{{ licenseService.allLicensesReport.analysedFilesBytes | size }}
</div>
</div>
<div class="row chart-row">
<div>
<redaction-chart
*ngIf="data$ | async as data"
[datasets]="data.datasets"
[labels]="data.labels"
[secondaryAxis]="true"
[valueFormatter]="data.valueFormatter"
[yAxisLabelRight]="data.yAxisLabelRight"
[yAxisLabel]="data.yAxisLabel"
chartId="analysis"
></redaction-chart>
</div>
</div>
</div>

View File

@ -0,0 +1,48 @@
:host {
display: contents;
}
.grid-container {
display: grid;
grid-template-columns: 250px 300px 1fr;
margin: 20px;
.row {
display: contents;
> div {
padding: 8px 20px;
&:first-of-type {
font-weight: 600;
}
&:nth-child(2) {
grid-column: span 2;
}
}
&:hover {
> div {
background-color: var(--iqser-alt-background);
}
}
}
.section-title {
grid-column: span 3;
padding: 20px 20px 8px;
margin-bottom: 8px;
border-bottom: 1px solid var(--iqser-separator);
}
.chart-row {
> div {
grid-column: span 3;
}
&:hover > div {
background-color: var(--iqser-background);
}
}
}

View File

@ -0,0 +1,71 @@
import { Component } from '@angular/core';
import { LicenseService } from '@services/license.service';
import { map } from 'rxjs/operators';
import { ChartDataset } from 'chart.js';
import { ChartBlue, ChartGreen, ChartRed } from '../../utils/constants';
import { getLabelsFromLicense, getLineConfig } from '../../utils/functions';
import { TranslateService } from '@ngx-translate/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { size } from '@iqser/common-ui';
@Component({
selector: 'red-license-analysis-capacity-usage',
templateUrl: './license-analysis-capacity-usage.component.html',
styleUrls: ['./license-analysis-capacity-usage.component.scss'],
})
export class LicenseAnalysisCapacityUsageComponent {
readonly data$ = this.licenseService.selectedLicense$.pipe(map(() => this.#getData()));
constructor(readonly licenseService: LicenseService, private readonly _translateService: TranslateService) {}
#getData() {
const yAxisLabel = _('license-info-screen.analysis-capacity-usage.analyzed-per-month');
const yAxisLabelRight = _('license-info-screen.analysis-capacity-usage.total-analyzed-data');
return {
datasets: this.#getCapacityDatasets(),
labels: getLabelsFromLicense(this.licenseService.selectedLicenseReport),
yAxisLabel: this._translateService.instant(yAxisLabel),
yAxisLabelRight: this._translateService.instant(yAxisLabelRight),
valueFormatter: (value: number) => size(value),
};
}
#getCapacityDatasets(): ChartDataset[] {
const monthlyData = this.licenseService.selectedLicenseReport.monthlyData;
const datasets: ChartDataset[] = [
{
data: monthlyData.flatMap(d => d.analysedFilesBytes),
label: this._translateService.instant('license-info-screen.analysis-capacity-usage.analyzed-per-month'),
type: 'bar',
backgroundColor: ChartBlue,
borderColor: ChartBlue,
order: 2,
},
{
data: monthlyData.map(
(month, monthIndex) =>
month.analysedFilesBytes + monthlyData.slice(0, monthIndex).reduce((acc, curr) => acc + curr.analysedFilesBytes, 0),
),
label: this._translateService.instant('license-info-screen.analysis-capacity-usage.analyzed-cumulative'),
yAxisID: 'y1',
order: 1,
...getLineConfig(ChartGreen, true, false),
},
];
if (this.licenseService.analysisCapacityBytes > 0) {
datasets.push({
data: monthlyData.flatMap(() => this.licenseService.analysisCapacityBytes),
label: this._translateService.instant('license-info-screen.analysis-capacity-usage.licensed'),
...getLineConfig(ChartRed, true, false),
yAxisID: 'y1',
order: 1,
});
}
return datasets;
}
}

View File

@ -0,0 +1,47 @@
<div class="grid-container">
<div class="section-title all-caps-label" translate="license-info-screen.page-usage.section-title"></div>
<div class="row">
<div translate="license-info-screen.page-usage.current-analyzed-pages"></div>
<div>
{{ licenseService.selectedLicenseReport.numberOfAnalyzedPages }}
<ng-container *ngIf="licenseService.analyzedPagesPercentageForSelectedLicensePercentage > 0">
({{ licenseService.analyzedPagesPercentageForSelectedLicensePercentage | number : '1.0-2' }}%)
</ng-container>
</div>
</div>
<div class="row">
<div translate="license-info-screen.page-usage.ocr-analyzed-pages"></div>
<div>{{ licenseService.selectedLicenseReport.numberOfOcrPages }}</div>
</div>
<div *ngIf="licenseService.unlicensedPages" class="row">
<div translate="license-info-screen.page-usage.unlicensed-analyzed"></div>
<div>{{ licenseService.unlicensedPages }}</div>
</div>
<div class="row">
<div [innerHTML]="'license-info-screen.page-usage.total-analyzed' | translate"></div>
<div>{{ licenseService.allLicensesReport.numberOfAnalyzedPages }}</div>
</div>
<div class="row">
<div [innerHTML]="'license-info-screen.page-usage.total-ocr-analyzed' | translate"></div>
<div>{{ licenseService.allLicensesReport.numberOfOcrPages }}</div>
</div>
<div class="row chart-row">
<div>
<redaction-chart
*ngIf="data$ | async as data"
[datasets]="data.datasets"
[labels]="data.labels"
[secondaryAxis]="true"
[yAxisLabelRight]="data.yAxisLabelRight"
[yAxisLabel]="data.yAxisLabel"
chartId="pages"
></redaction-chart>
</div>
</div>
</div>

View File

@ -0,0 +1,48 @@
:host {
display: contents;
}
.grid-container {
display: grid;
grid-template-columns: 250px 300px 1fr;
margin: 20px;
.row {
display: contents;
> div {
padding: 8px 20px;
&:first-of-type {
font-weight: 600;
}
&:nth-child(2) {
grid-column: span 2;
}
}
&:hover {
> div {
background-color: var(--iqser-alt-background);
}
}
}
.section-title {
grid-column: span 3;
padding: 20px 20px 8px;
margin-bottom: 8px;
border-bottom: 1px solid var(--iqser-separator);
}
.chart-row {
> div {
grid-column: span 3;
}
&:hover > div {
background-color: var(--iqser-background);
}
}
}

View File

@ -0,0 +1,64 @@
import { Component } from '@angular/core';
import { LicenseService } from '@services/license.service';
import { map } from 'rxjs/operators';
import { ChartDataset } from 'chart.js';
import { ChartBlue, ChartGreen, ChartRed } from '../../utils/constants';
import { getLabelsFromLicense, getLineConfig } from '../../utils/functions';
import { TranslateService } from '@ngx-translate/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@Component({
selector: 'red-license-page-usage',
templateUrl: './license-page-usage.component.html',
styleUrls: ['./license-page-usage.component.scss'],
})
export class LicensePageUsageComponent {
readonly data$ = this.licenseService.selectedLicense$.pipe(map(() => this.#getData()));
constructor(readonly licenseService: LicenseService, private readonly _translateService: TranslateService) {}
#getData() {
const yAxisLabel = _('license-info-screen.page-usage.pages-per-month');
const yAxisLabelRight = _('license-info-screen.page-usage.total-pages');
return {
datasets: this.#getPagesDatasets(),
labels: getLabelsFromLicense(this.licenseService.selectedLicenseReport),
yAxisLabel: this._translateService.instant(yAxisLabel),
yAxisLabelRight: this._translateService.instant(yAxisLabelRight),
};
}
#getPagesDatasets(): ChartDataset[] {
const monthlyData = this.licenseService.selectedLicenseReport.monthlyData;
return [
{
data: monthlyData.flatMap(d => d.numberOfAnalyzedPages),
label: this._translateService.instant('license-info-screen.page-usage.pages-per-month'),
type: 'bar',
backgroundColor: ChartBlue,
borderColor: ChartBlue,
order: 2,
},
{
data: monthlyData.flatMap(() => this.licenseService.totalLicensedNumberOfPages),
label: this._translateService.instant('license-info-screen.page-usage.total-pages'),
...getLineConfig(ChartRed, true, false),
yAxisID: 'y1',
order: 1,
},
{
data: monthlyData.map(
(month, monthIndex) =>
month.numberOfAnalyzedPages +
monthlyData.slice(0, monthIndex).reduce((acc, curr) => acc + curr.numberOfAnalyzedPages, 0),
),
label: this._translateService.instant('license-info-screen.page-usage.cumulative-pages'),
yAxisID: 'y1',
order: 1,
...getLineConfig(ChartGreen, true, false),
},
];
}
}

View File

@ -0,0 +1,65 @@
<div class="grid-container">
<div class="section-title all-caps-label" translate="license-info-screen.retention-capacity-usage.section-title"></div>
<div class="row">
<div translate="license-info-screen.retention-capacity-usage.active-documents"></div>
<div>{{ licenseService.selectedLicenseReport.activeFilesUploadedBytes | size }}</div>
</div>
<div class="row">
<div translate="license-info-screen.retention-capacity-usage.archived-documents"></div>
<div>{{ licenseService.selectedLicenseReport.archivedFilesUploadedBytes | size }}</div>
</div>
<div class="row">
<div translate="license-info-screen.retention-capacity-usage.trash-documents"></div>
<div>{{ licenseService.selectedLicenseReport.trashFilesUploadedBytes | size }}</div>
</div>
<div class="row">
<div translate="license-info-screen.retention-capacity-usage.used-capacity"></div>
<div>
{{ licenseService.selectedLicenseReport.totalFilesUploadedBytes | size }}
<ng-container *ngIf="licenseService.retentionCapacityBytesForSelectedLicensePercentage >= 0">
({{ licenseService.retentionCapacityBytesForSelectedLicensePercentage | number : '1.0-2' }}%)
</ng-container>
</div>
</div>
<div class="donut-chart-wrapper pl-20">
<redaction-donut-chart
*ngIf="licenseService.retentionCapacityBytes > 0"
[config]="donutChartConfig$ | async"
[direction]="'row'"
[radius]="80"
[strokeWidth]="15"
[subtitleTemplate]="capacitySubtitles"
[totalType]="'sum'"
[valueFormatter]="size"
></redaction-donut-chart>
</div>
<div class="row chart-row">
<div>
<redaction-chart
*ngIf="data$ | async as data"
[datasets]="data.datasets"
[labels]="data.labels"
[reverseLegend]="true"
[valueFormatter]="formatSize"
chartId="retention"
></redaction-chart>
</div>
</div>
</div>
<ng-template #capacitySubtitles>
<span *ngIf="licenseService.retentionCapacityBytesForSelectedLicensePercentage <= 100; else exceeded">
{{ 'license-info-screen.retention-capacity-usage.storage-capacity' | translate }}
</span>
<ng-template #exceeded>
<span class="exceeded-capacity">
{{ 'license-info-screen.retention-capacity-usage.exceeded-capacity' | translate }}
</span>
</ng-template>
</ng-template>

View File

@ -0,0 +1,48 @@
:host {
display: contents;
}
.grid-container {
display: grid;
grid-template-columns: 250px 300px 1fr;
margin: 20px;
.donut-chart-wrapper {
grid-row: 2 / span 5;
grid-column: 3;
width: fit-content;
}
.row {
display: contents;
> div {
padding: 8px 20px;
&:first-of-type {
font-weight: 600;
}
}
&:not(.chart-row):hover {
> div {
background-color: var(--iqser-alt-background);
}
}
}
.section-title {
grid-column: span 3;
padding: 20px 20px 8px;
margin-bottom: 8px;
border-bottom: 1px solid var(--iqser-separator);
}
.chart-row > div {
grid-column: span 3;
}
}
.exceeded-capacity {
color: var(--iqser-red-1);
}

View File

@ -0,0 +1,89 @@
import { Component } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { LicenseService } from '@services/license.service';
import { map } from 'rxjs/operators';
import type { DonutChartConfig, ILicenseReport } from '@red/domain';
import { ChartDataset } from 'chart.js';
import { ChartBlack, ChartBlue, ChartGreen, ChartGrey, ChartRed } from '../../utils/constants';
import { getLabelsFromLicense, getLineConfig } from '../../utils/functions';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { size } from '@iqser/common-ui';
@Component({
selector: 'red-license-retention-capacity',
templateUrl: './license-retention-capacity.component.html',
styleUrls: ['./license-retention-capacity.component.scss'],
})
export class LicenseRetentionCapacityComponent {
readonly formatSize = size;
readonly donutChartConfig$ = this.licenseService.selectedLicense$.pipe(
map(() => this.licenseService.selectedLicenseReport),
map(license => this.#getDonutChartConfig(license)),
);
readonly data$ = this.licenseService.selectedLicense$.pipe(
map(() => this.licenseService.selectedLicenseReport),
map(license => ({
datasets: this.#getDatasets(license),
labels: getLabelsFromLicense(license),
})),
);
readonly size = size;
constructor(readonly licenseService: LicenseService, private readonly _translateService: TranslateService) {}
#getDonutChartConfig(license: ILicenseReport): DonutChartConfig[] {
return [
{
value: license.activeFilesUploadedBytes,
color: ChartGreen,
label: this._translateService.instant(_('license-info-screen.retention-capacity-usage.active-documents')),
},
{
value: license.archivedFilesUploadedBytes,
color: ChartBlue,
label: this._translateService.instant(_('license-info-screen.retention-capacity-usage.archived-documents')),
},
{
value: license.trashFilesUploadedBytes,
color: ChartRed,
label: this._translateService.instant(_('license-info-screen.retention-capacity-usage.trash-documents')),
},
{
value: Math.max(this.licenseService.retentionCapacityBytes - license.totalFilesUploadedBytes, 0),
color: ChartGrey,
label: this._translateService.instant(_('license-info-screen.retention-capacity-usage.unused')),
},
];
}
#getDatasets(license: ILicenseReport): ChartDataset[] {
const monthlyData = license.monthlyData;
return [
{
data: monthlyData.flatMap(d => d.activeFilesUploadedBytes),
label: this._translateService.instant('license-info-screen.retention-capacity-usage.active-documents'),
...getLineConfig(ChartGreen, false, 'origin'),
stack: 'storage',
},
{
data: monthlyData.flatMap(d => d.archivedFilesUploadedBytes),
label: this._translateService.instant('license-info-screen.retention-capacity-usage.archived-documents'),
...getLineConfig(ChartBlue, false, '-1'),
stack: 'storage',
},
{
data: monthlyData.flatMap(d => d.trashFilesUploadedBytes),
label: this._translateService.instant('license-info-screen.retention-capacity-usage.trash-documents'),
...getLineConfig(ChartRed, false, '-1'),
stack: 'storage',
},
{
data: monthlyData.flatMap(d => d.activeFilesUploadedBytes + d.archivedFilesUploadedBytes + d.trashFilesUploadedBytes),
label: this._translateService.instant('license-info-screen.retention-capacity-usage.used-capacity'),
...getLineConfig(ChartBlack, true, false),
borderWidth: 2,
},
];
}
}

View File

@ -1,6 +1,6 @@
<div *ngIf="licenses$ | async as licenses" class="iqser-input-group w-400">
<mat-form-field>
<mat-select (valueChange)="licenseChanged($event)" *ngIf="value" [(ngModel)]="value" [iqserHelpMode]="'license_information'">
<mat-select (valueChange)="licenseChanged($event)" *ngIf="value" [(ngModel)]="value" [attr.help-mode-key]="'license_information'">
<mat-select-trigger>
<ng-container *ngTemplateOutlet="licenseInfo; context: { license: value }"></ng-container>
</mat-select-trigger>
@ -14,9 +14,9 @@
<ng-template #licenseInfo let-license="license">
<div class="space-between flex-align-items-center">
<span>{{ license.name }}</span>
<div>{{ license.name }}</div>
<div class="mr-10 flex-align-items-center">
<div [class.green]="license.id === licenseService.activeLicenseId" class="dot mr-4"></div>
<div [class.green]="license.id === licenseService.activeLicense.id" class="dot mr-4"></div>
<span class="small-label">{{ getStatus(license.id) | translate | uppercase }}</span>
</div>
</div>

View File

@ -29,7 +29,7 @@ export class LicenseSelectComponent {
constructor(readonly licenseService: LicenseService, private readonly _permissionsService: IqserPermissionsService) {}
getStatus(id) {
return id === this.licenseService.activeLicenseId ? translations.active : translations.inactive;
return id === this.licenseService.activeLicense.id ? translations.active : translations.inactive;
}
async licenseChanged(license: ILicense) {

View File

@ -1,18 +0,0 @@
<combo-chart-component
*ngIf="lineChartSeries$ | async as lineChartSeries"
[animations]="true"
[colorSchemeLine]="lineChartScheme"
[legendTitle]="'license-info-screen.chart.legend' | translate"
[legend]="true"
[lineChart]="lineChartSeries"
[results]="barChart"
[scheme]="comboBarScheme"
[showGridLines]="true"
[showRightYAxisLabel]="true"
[showYAxisLabel]="true"
[view]="[1000, 300]"
[xAxis]="true"
[yAxisLabelRight]="'license-info-screen.chart.total-pages' | translate"
[yAxisLabel]="'license-info-screen.chart.pages-per-month' | translate"
[yAxis]="true"
></combo-chart-component>

View File

@ -1,142 +0,0 @@
import { Component } from '@angular/core';
import { ComboBarScheme, LICENSE_STORAGE_KEY, LineChartScheme } from '../utils/constants';
import dayjs from 'dayjs';
import { IDateRange, ILicense, ILicenseReport } from '@red/domain';
import { LicenseService } from '@services/license.service';
import { switchMap, tap } from 'rxjs/operators';
import { List, LoadingService } from '@iqser/common-ui';
import { isCurrentMonth, toDate, verboseDate } from '../utils/functions';
import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { Series } from '@swimlane/ngx-charts';
@Component({
selector: 'redaction-license-chart',
templateUrl: './license-chart.component.html',
})
export class LicenseChartComponent {
readonly lineChartScheme = LineChartScheme;
readonly comboBarScheme = ComboBarScheme;
readonly lineChartSeries$ = this.#licenseChartSeries$;
barChart = [];
constructor(
private readonly _translateService: TranslateService,
private readonly _licenseService: LicenseService,
private readonly _loadingService: LoadingService,
) {}
get #licenseChartSeries$(): Observable<Series[]> {
return this._licenseService.selectedLicense$.pipe(
tap(() => this._loadingService.start()),
switchMap(license => this.#getLicenseData(license)),
tap(() => this._loadingService.stop()),
);
}
async #getLicenseData(license: ILicense): Promise<Series[]> {
const startDate = dayjs(license.validFrom);
const endDate = dayjs(license.validUntil);
const startDay: number = startDate.date();
const startMonth: number = startDate.month();
const startYear: number = startDate.year();
const dateRanges = [];
for (let dt = startDate; dt <= endDate; dt = dt.add(1, 'month')) {
const end = dt.add(1, 'month');
dateRanges.push({ startMonth: dt.month(), startYear: dt.year(), endMonth: end.month(), endYear: end.year() });
}
if (dateRanges.length > 0) {
dateRanges[0].startDay = startDay;
}
const reports = await this.#getReports(dateRanges, license.id);
return this.#mapRangesToReports(startMonth, startYear, dateRanges, reports);
}
#mapRangesToReports(month: number, year: number, dateRanges: List<IDateRange>, reports: List<ILicenseReport>): Series[] {
return [
{
name: this._translateService.instant('license-info-screen.chart.total-pages'),
series: this.#totalLicensedPagesSeries(dateRanges),
},
{
name: this._translateService.instant('license-info-screen.chart.cumulative'),
series: this.#setBar(month, year, reports),
},
];
}
#setBar(month: number, year: number, reports: List<ILicenseReport>) {
let cumulativePages = 0;
const cumulativePagesSeries = [];
this.barChart = [];
const monthNames = dayjs.monthsShort();
for (const report of reports) {
cumulativePages += report.numberOfAnalyzedPages;
const name = `${monthNames[month]} ${year}`;
this.barChart.push({
name,
value: report.numberOfAnalyzedPages,
});
cumulativePagesSeries.push({
name,
value: cumulativePages,
});
month++;
if (month === 12) {
month = 0;
year++;
}
}
if (cumulativePages !== this._licenseService.currentLicenseInfo.numberOfAnalyzedPages) {
this._licenseService.wipeStoredReportsAndReloadSelectedLicenseData();
}
return cumulativePagesSeries;
}
#getReports(dateRanges: List<IDateRange>, id: string) {
const reports = dateRanges.map(range => {
const startMonth = range.startMonth + 1;
const endMonth = range.endMonth + 1;
const key = `${id}-${startMonth}.${range.startYear}-${endMonth}.${range.endYear}`;
const existingReport = this._licenseService.storedReports[key];
if (existingReport) {
return existingReport;
}
const startDate = toDate(startMonth, range.startYear, range.startDay);
const endDate = toDate(endMonth, range.endYear);
const requestedReport = this._licenseService.getReport({ startDate, endDate });
return requestedReport.then(report => this.#storeReportIfNotCurrentMonth(range, report, key));
});
return Promise.all(reports);
}
#storeReportIfNotCurrentMonth(dateRange: IDateRange, report: ILicenseReport, key: string) {
if (!isCurrentMonth(dateRange.startMonth + 1, dateRange.startYear)) {
this._licenseService.storedReports[key] = report;
localStorage.setItem(LICENSE_STORAGE_KEY, JSON.stringify(this._licenseService.storedReports));
}
return report;
}
#totalLicensedPagesSeries(dateRanges: List<IDateRange>) {
return dateRanges.map(dateRange => ({
name: verboseDate(dateRange),
value: this._licenseService.totalLicensedNumberOfPages,
}));
}
}

View File

@ -1,100 +1,77 @@
<section class="settings">
<div>
<iqser-page-header
(closeAction)="routerHistoryService.navigateToLastDossiersScreen()"
[buttonConfigs]="buttonConfigs"
[pageLabel]="'license-information' | translate"
[showCloseButton]="currentUser.isUser && permissionsService.has$(roles.dossiers.read) | async"
></iqser-page-header>
<iqser-page-header
(closeAction)="routerHistoryService.navigateToLastDossiersScreen()"
[buttonConfigs]="buttonConfigs"
[pageLabel]="'license-information' | translate"
[showCloseButton]="currentUser.isUser && permissionsService.has$(roles.dossiers.read) | async"
></iqser-page-header>
<div class="content-inner">
<div class="content-container">
<div *ngIf="licenseService.licenseData$ | async" class="grid-container">
<div class="row">
<div translate="license-info-screen.backend-version"></div>
<div>{{ configService.values.BACKEND_APP_VERSION || '-' }}</div>
</div>
<div class="content-inner">
<div class="content-container">
<div *ngIf="licenseService.licenseData$ | async" class="grid-container">
<div class="row">
<div translate="license-info-screen.backend-version"></div>
<div>{{ configService.values.BACKEND_APP_VERSION || '-' }}</div>
</div>
<div class="row">
<div translate="license-info-screen.custom-app-title"></div>
<div>{{ configService.values.APP_NAME || '-' }}</div>
</div>
<div class="row">
<div translate="license-info-screen.custom-app-title"></div>
<div>{{ configService.values.APP_NAME || '-' }}</div>
</div>
<div class="row">
<div translate="license-info-screen.copyright-claim-title"></div>
<div>
{{ 'license-info-screen.copyright-claim-text' | translate : { currentYear: currentYear } }}
</div>
</div>
<div class="row">
<div translate="license-info-screen.copyright-claim-title"></div>
<div>
{{ 'license-info-screen.copyright-claim-text' | translate : { currentYear: currentYear } }}
</div>
</div>
<div class="row">
<div translate="license-info-screen.end-user-license-title"></div>
<div translate="license-info-screen.end-user-license-text"></div>
</div>
<div class="row">
<div translate="license-info-screen.end-user-license-title"></div>
<div translate="license-info-screen.end-user-license-text"></div>
</div>
<div class="section-title all-caps-label" translate="license-info-screen.licensing-details"></div>
<div class="section-title all-caps-label" translate="license-info-screen.licensing-details.section-title"></div>
<div class="row">
<div class="flex-align-items-center" translate="license-info-screen.license-title"></div>
<div>
<redaction-license-select></redaction-license-select>
</div>
</div>
<div class="row">
<div class="flex-align-items-center" translate="license-info-screen.licensing-details.license-title"></div>
<div>
<redaction-license-select></redaction-license-select>
</div>
</div>
<ng-container *ngIf="licenseService.selectedLicense$ | async as selectedLicense">
<div class="row">
<div translate="license-info-screen.licensed-to"></div>
<div>{{ selectedLicense.licensedTo || '-' }}</div>
</div>
<div class="row">
<div translate="license-info-screen.licensing-period"></div>
<div>
{{ (selectedLicense.validFrom | date : 'dd-MM-yyyy') || '-' }} /
{{ (selectedLicense.validUntil | date : 'dd-MM-yyyy') || '-' }}
</div>
</div>
</ng-container>
<div class="row">
<div>{{ 'license-info-screen.licensed-page-count' | translate }}</div>
<div>{{ licenseService.totalLicensedNumberOfPages }}</div>
</div>
<div class="section-title all-caps-label" translate="license-info-screen.usage-details"></div>
<div class="row">
<div translate="license-info-screen.current-analyzed"></div>
<div>
{{ licenseService.analyzedPagesInCurrentLicensingPeriod }}
({{ analysisPercentageOfLicense$ | async | number : '1.0-2' }}%)
</div>
</div>
<div class="row">
<div translate="license-info-screen.ocr-analyzed-pages"></div>
<div>{{ licenseService.currentLicenseInfo.numberOfOcrPages }}</div>
</div>
<div *ngIf="!!licenseService.unlicensedPages" class="row">
<div translate="license-info-screen.unlicensed-analyzed"></div>
<div>{{ licenseService.unlicensedPages }}</div>
</div>
<div class="row">
<div [innerHTML]="'license-info-screen.total-analyzed' | translate"></div>
<div>{{ licenseService.allLicensesInfo.numberOfAnalyzedPages }}</div>
</div>
<div class="row">
<div [innerHTML]="'license-info-screen.total-ocr-analyzed' | translate"></div>
<div>{{ licenseService.allLicensesInfo.numberOfOcrPages }}</div>
</div>
<ng-container *ngIf="licenseService.selectedLicense$ | async as selectedLicense">
<div class="row">
<div translate="license-info-screen.licensing-details.licensed-to"></div>
<div>{{ selectedLicense.licensedTo || '-' }}</div>
</div>
<redaction-license-chart></redaction-license-chart>
<div class="row">
<div translate="license-info-screen.licensing-details.licensing-period"></div>
<div>
{{ (selectedLicense.validFrom | date : 'dd-MM-yyyy') || '-' }} /
{{ (selectedLicense.validUntil | date : 'dd-MM-yyyy') || '-' }}
</div>
</div>
</ng-container>
<div *ngIf="licenseService.isPageBased" class="row">
<div>{{ 'license-info-screen.licensing-details.licensed-page-count' | translate }}</div>
<div>{{ licenseService.totalLicensedNumberOfPages }}</div>
</div>
<div *ngIf="licenseService.isCapacityBased" class="row">
<div>{{ 'license-info-screen.licensing-details.licensed-analysis-capacity' | translate }}</div>
<div>{{ licenseService.analysisCapacityBytes | size }}</div>
</div>
<div *ngIf="licenseService.isCapacityBased" class="row">
<div>{{ 'license-info-screen.licensing-details.licensed-retention-capacity' | translate }}</div>
<div>{{ licenseService.retentionCapacityBytes | size }}</div>
</div>
</div>
<red-license-page-usage *ngIf="licenseService.isPageBased"></red-license-page-usage>
<red-license-analysis-capacity-usage *ngIf="licenseService.isCapacityBased"></red-license-analysis-capacity-usage>
<red-license-retention-capacity *ngIf="licenseService.isCapacityBased"></red-license-retention-capacity>
</div>
</section>
</div>

View File

@ -4,15 +4,10 @@
overflow: auto;
@include common-mixins.scroll-bar;
display: flex;
flex-direction: column;
align-items: center;
.grid-container {
width: calc(100% - 40px);
display: grid;
grid-template-columns: 1fr 2fr;
margin: 20px 20px 50px 20px;
grid-template-columns: 250px 300px 1fr;
margin: 20px;
.row {
display: contents;
@ -23,6 +18,10 @@
&:first-of-type {
font-weight: 600;
}
&:nth-child(2) {
grid-column: span 2;
}
}
&:hover {
@ -33,14 +32,14 @@
}
.section-title {
grid-column: span 2;
grid-column: span 3;
padding: 20px 20px 8px;
margin-bottom: 8px;
border-bottom: 1px solid var(--iqser-separator);
}
}
redaction-license-chart {
margin-bottom: 50px;
redaction-chart {
grid-column: span 3;
}
}
}

View File

@ -1,20 +1,12 @@
import { Component } from '@angular/core';
import { ConfigService } from '@services/config.service';
import { TranslateService } from '@ngx-translate/core';
import {
ButtonConfig,
getCurrentUser,
IconButtonTypes,
IqserPermissionsService,
LoadingService,
OverlappingElements,
} from '@iqser/common-ui';
import { ButtonConfig, IconButtonTypes, IqserPermissionsService } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { RouterHistoryService } from '@services/router-history.service';
import { LicenseService } from '@services/license.service';
import { map } from 'rxjs/operators';
import { getCurrentUser } from '@users/user.service';
import { ROLES } from '@users/roles';
import type { User } from '@red/domain';
@Component({
templateUrl: './license-screen.component.html',
@ -22,7 +14,7 @@ import type { User } from '@red/domain';
})
export class LicenseScreenComponent {
readonly roles = ROLES;
readonly currentUser = getCurrentUser<User>();
readonly currentUser = getCurrentUser();
readonly currentYear = new Date().getFullYear();
readonly buttonConfigs: readonly ButtonConfig[] = [
{
@ -31,28 +23,16 @@ export class LicenseScreenComponent {
type: IconButtonTypes.primary,
helpModeKey: 'license_information',
hide: !this.permissionsService.has(ROLES.license.readReport),
overlappingElements: [OverlappingElements.USER_MENU],
},
];
readonly analysisPercentageOfLicense$ = this.licenseService.selectedLicense$.pipe(map(() => this.getAnalysisPercentageOfLicense()));
constructor(
readonly configService: ConfigService,
readonly licenseService: LicenseService,
readonly permissionsService: IqserPermissionsService,
private readonly _loadingService: LoadingService,
readonly routerHistoryService: RouterHistoryService,
private readonly _translateService: TranslateService,
) {
_loadingService.start();
}
getAnalysisPercentageOfLicense() {
const totalLicensedNumberOfPages = this.licenseService.totalLicensedNumberOfPages;
const numberOfAnalyzedPages = this.licenseService.analyzedPagesInCurrentLicensingPeriod;
return totalLicensedNumberOfPages > 0 ? (numberOfAnalyzedPages / totalLicensedNumberOfPages) * 100 : 100;
}
) {}
sendMail(): void {
const licenseCustomer = this.licenseService.selectedLicense.licensedTo;
@ -62,7 +42,7 @@ export class LicenseScreenComponent {
const lineBreak = '%0D%0A';
const body = [
this._translateService.instant('license-info-screen.email.body.analyzed', {
pages: this.licenseService.currentLicenseInfo.numberOfAnalyzedPages,
pages: this.licenseService.selectedLicenseReport.numberOfAnalyzedPages,
}),
this._translateService.instant('license-info-screen.email.body.licensed', {
pages: this.licenseService.totalLicensedNumberOfPages,

View File

@ -1,16 +1,19 @@
import { inject, NgModule } from '@angular/core';
import { LicenseScreenComponent } from './license-screen/license-screen.component';
import { LicenseSelectComponent } from './license-select/license-select.component';
import { LicenseChartComponent } from './license-chart/license-chart.component';
import { LicenseSelectComponent } from './components/license-select/license-select.component';
import { RouterModule, Routes } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { MatSelectModule } from '@angular/material/select';
import { IqserHelpModeModule, IqserListingModule } from '@iqser/common-ui';
import { NgxChartsModule } from '@swimlane/ngx-charts';
import { ComboChartComponent, ComboSeriesVerticalComponent, YAxisComponent } from './combo-chart';
import { IqserHelpModeModule, IqserListingModule, SizePipe } from '@iqser/common-ui';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { LicenseService } from '@services/license.service';
import { ChartComponent } from './components/chart/chart.component';
import { NgChartsModule } from 'ng2-charts';
import { LicenseRetentionCapacityComponent } from './components/license-retention-capacity-usage/license-retention-capacity.component';
import { LicensePageUsageComponent } from './components/license-page-usage/license-page-usage.component';
import { LicenseAnalysisCapacityUsageComponent } from './components/license-analysis-capacity-usage/license-analysis-capacity-usage.component';
import { DonutChartComponent } from '@shared/components/donut-chart/donut-chart.component';
const routes: Routes = [
{
@ -26,10 +29,10 @@ const routes: Routes = [
declarations: [
LicenseScreenComponent,
LicenseSelectComponent,
LicenseChartComponent,
ComboChartComponent,
ComboSeriesVerticalComponent,
YAxisComponent,
ChartComponent,
LicenseRetentionCapacityComponent,
LicensePageUsageComponent,
LicenseAnalysisCapacityUsageComponent,
],
imports: [
RouterModule.forChild(routes),
@ -37,9 +40,11 @@ const routes: Routes = [
TranslateModule,
MatSelectModule,
FormsModule,
NgxChartsModule,
IqserListingModule,
IqserHelpModeModule,
SizePipe,
DonutChartComponent,
NgChartsModule,
],
})
export class LicenseModule {}

View File

@ -1,17 +1,12 @@
import { Color, ScaleType } from '@swimlane/ngx-charts';
export const ChartRed = '#dd4d50';
export const ChartGreen = '#5ce594';
export const ChartBlue = '#0389ec';
export const ChartGrey = '#ccced3'; // grey-5
export const ChartBlack = '#283241'; // grey-1
export const ComboBarScheme: Color = {
name: 'Combo bar scheme',
selectable: true,
group: ScaleType.Ordinal,
domain: ['#0389ec'],
};
export const LineChartScheme: Color = {
name: 'Line chart scheme',
selectable: true,
group: ScaleType.Ordinal,
domain: ['#dd4d50', '#5ce594', '#0389ec'],
};
export const LICENSE_STORAGE_KEY = 'redaction-license-reports';
export enum LicenseFeatures {
PROCESSING_PAGES = 'processingPages',
PDFTRON = 'pdftron',
ANALYSIS_CAPACITY_BYTES = 'analysisCapacityBytes',
RETENTION_CAPACITY_BYTES = 'retentionCapacityBytes',
}

View File

@ -1,18 +1,46 @@
import dayjs from 'dayjs';
import { IDateRange } from '@red/domain';
export function toDate(month: number, year: number, day: number = 1) {
return dayjs(`${day}-${month}-${year}`, 'D-M-YYYY').toDate();
}
export function isCurrentMonth(month: number, year: number) {
const now = dayjs();
const currentMonth = now.month() + 1;
const currentYear = now.year();
return month === currentMonth && year === currentYear;
}
import dayjs, { Dayjs } from 'dayjs';
import { FillTarget } from 'chart.js';
import { hexToRgba } from '@utils/functions';
import { ILicenseReport } from '@red/domain';
import { ComplexFillTarget } from 'chart.js/dist/types';
const monthNames = dayjs.monthsShort();
export const verboseDate = (range: IDateRange) => `${monthNames[range.startMonth]} ${range.startYear}`;
export const verboseDate = (date: Dayjs) => `${monthNames[date.month()]} ${date.year()}`;
export const getLineConfig: (
color: string,
displayLine: boolean,
target: FillTarget,
) => {
type: 'line';
borderColor: string;
backgroundColor: string;
pointBackgroundColor: string;
fill: ComplexFillTarget;
} = (color, displayLine, target) => ({
type: 'line',
borderColor: hexToRgba(color, displayLine ? 1 : 0),
backgroundColor: hexToRgba(color, 1),
pointBackgroundColor: hexToRgba(color, 1),
fill: {
target: target ?? '-1',
above: hexToRgba(color, 0.3),
below: hexToRgba(color, 0.3),
},
pointStyle: false,
});
export const getLabelsFromLicense = (license: ILicenseReport) => {
let startMonth = dayjs(license.startDate).startOf('month');
const endMonth = dayjs(license.endDate).endOf('month');
const result = [];
while (startMonth.isBefore(endMonth)) {
result.push(verboseDate(startMonth));
startMonth = startMonth.add(1, 'month');
}
return result;
};

View File

@ -7,6 +7,7 @@ import { TemplateStatsComponent } from './components/template-stats/template-sta
import { BreadcrumbTypes } from '@red/domain';
import { TranslateModule } from '@ngx-translate/core';
import { IqserButtonsModule, IqserHelpModeModule, IqserPermissionsModule } from '@iqser/common-ui';
import { DonutChartComponent } from '@shared/components/donut-chart/donut-chart.component';
const routes = [
{
@ -28,6 +29,7 @@ const routes = [
IqserButtonsModule,
IqserHelpModeModule,
IqserPermissionsModule,
DonutChartComponent,
],
})
export class DashboardModule {}

View File

@ -13,6 +13,7 @@ import {
IqserScrollbarModule,
IqserSharedModule,
IqserUsersModule,
StatusBarComponent,
} from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
import { DossierOverviewScreenComponent } from './screen/dossier-overview-screen.component';
@ -26,6 +27,7 @@ import { WorkflowItemComponent } from './components/workflow-item/workflow-item.
import { DossierOverviewScreenHeaderComponent } from './components/screen-header/dossier-overview-screen-header.component';
import { ViewModeSelectionComponent } from './components/view-mode-selection/view-mode-selection.component';
import { FileAttributeComponent } from './components/file-attribute/file-attribute.component';
import { DonutChartComponent } from '@shared/components/donut-chart/donut-chart.component';
const routes: Routes = [
{
@ -67,6 +69,8 @@ const routes: Routes = [
IqserScrollbarModule,
IqserPermissionsModule,
IqserInputsModule,
StatusBarComponent,
DonutChartComponent,
],
})
export class DossierOverviewModule {}

View File

@ -11,6 +11,7 @@ import {
IqserSharedModule,
IqserUsersModule,
LogPipe,
StatusBarComponent,
} from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
import { DossiersListingScreenComponent } from './screen/dossiers-listing-screen.component';
@ -24,6 +25,7 @@ import { DossierWorkloadColumnComponent } from './components/dossier-workload-co
import { DossierDocumentsStatusComponent } from './components/dossier-documents-status/dossier-documents-status.component';
import { DossierFilesGuard } from '@guards/dossier-files-guard';
import { ACTIVE_DOSSIERS_SERVICE } from '../../tokens';
import { DonutChartComponent } from '@shared/components/donut-chart/donut-chart.component';
const routes: Routes = [
{
@ -59,6 +61,8 @@ const routes: Routes = [
IqserSharedModule,
IqserPermissionsModule,
LogPipe,
StatusBarComponent,
DonutChartComponent,
],
})
export class DossiersListingModule {}

View File

@ -13,6 +13,7 @@ import {
IqserSharedModule,
IqserUploadFileModule,
IqserUsersModule,
StatusBarComponent,
} from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
import { RouterModule, Routes } from '@angular/router';
@ -125,6 +126,7 @@ const components = [
IqserFiltersModule,
IqserScrollbarModule,
IqserPermissionsModule,
StatusBarComponent,
],
providers: [FilePreviewDialogService, ManualRedactionService, DocumentUnloadedGuard, SuggestionsService],
})

View File

@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SearchScreenComponent } from './search-screen/search-screen.component';
import { RouterModule } from '@angular/router';
import { IqserListingModule, IqserSharedModule, IqserUsersModule } from '@iqser/common-ui';
import { IqserListingModule, IqserSharedModule, IqserUsersModule, StatusBarComponent } from '@iqser/common-ui';
import { SharedModule } from '@shared/shared.module';
import { TranslateModule } from '@ngx-translate/core';
import { SearchItemTemplateComponent } from './search-item-template/search-item-template.component';
@ -19,6 +19,7 @@ const routes = [{ path: '', component: SearchScreenComponent }];
TranslateModule,
IqserListingModule,
IqserSharedModule,
StatusBarComponent,
],
})
export class SearchModule {}

View File

@ -21,6 +21,7 @@ import {
IqserScrollbarModule,
IqserSharedModule,
IqserUsersModule,
StatusBarComponent,
} from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
import { DossiersListingActionsComponent } from './components/dossiers-listing-actions/dossiers-listing-actions.component';
@ -55,6 +56,7 @@ const services = [FileAssignService];
IqserInputsModule,
IqserScrollbarModule,
IqserPermissionsModule,
StatusBarComponent,
],
})
export class SharedDossiersModule {}

View File

@ -18,8 +18,13 @@
</svg>
<div [style]="'height: ' + size + 'px; width: ' + size + 'px; padding: ' + (strokeWidth + 5) + 'px;'" class="text-container">
<div class="heading-xl">{{ displayedDataTotal }}</div>
<div class="heading-xl">{{ getFormattedValue(displayedDataTotal) }}</div>
<div *ngIf="subtitles.length === 1" class="mt-5">{{ subtitles[0] }}</div>
<div *ngIf="subtitleTemplate as t" class="mt-5">
<ng-container *ngTemplateOutlet="t"></ng-container>
</div>
<mat-select
(selectionChange)="subtitleChanged.emit(subtitles.indexOf($event.value))"
*ngIf="subtitles.length > 1"
@ -30,7 +35,7 @@
</mat-select>
</div>
<div [iqserHelpMode]="helpModeKey" class="breakdown-container">
<div [attr.help-mode-key]="helpModeKey" class="breakdown-container">
<div
(click)="val.key && selectValue(val.key)"
*ngFor="let val of config"
@ -48,8 +53,7 @@
}
]"
[small]="true"
>
</iqser-status-bar>
></iqser-status-bar>
</div>
</div>
</div>

View File

@ -1,16 +1,20 @@
import { Component, EventEmitter, Input, OnChanges, OnInit, Optional, Output } from '@angular/core';
import { Component, EventEmitter, Input, OnChanges, OnInit, Optional, Output, TemplateRef } from '@angular/core';
import { DonutChartConfig } from '@red/domain';
import { FilterService, get, INestedFilter, shareLast } from '@iqser/common-ui';
import { FilterService, get, INestedFilter, IqserHelpModeModule, shareLast, StatusBarComponent } from '@iqser/common-ui';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { AsyncPipe, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
import { MatSelectModule } from '@angular/material/select';
@Component({
selector: 'redaction-donut-chart',
templateUrl: './donut-chart.component.html',
styleUrls: ['./donut-chart.component.scss'],
standalone: true,
imports: [NgForOf, NgIf, MatSelectModule, IqserHelpModeModule, StatusBarComponent, AsyncPipe, NgTemplateOutlet],
})
export class DonutChartComponent implements OnChanges, OnInit {
@Input() subtitles: string[];
@Input() subtitles: string[] = [];
@Input() config: DonutChartConfig[] = [];
@Input() radius = 85;
@Input() strokeWidth = 20;
@ -19,6 +23,8 @@ export class DonutChartComponent implements OnChanges, OnInit {
@Input() counterText: string;
@Input() filterKey;
@Input() helpModeKey;
@Input() valueFormatter?: (value: number) => string;
@Input() subtitleTemplate?: TemplateRef<any>;
@Output() readonly subtitleChanged = new EventEmitter<number>();
@ -28,10 +34,6 @@ export class DonutChartComponent implements OnChanges, OnInit {
size = 0;
filters$: Observable<INestedFilter[]>;
constructor(@Optional() readonly filterService: FilterService) {
// TODO: move this component to a separate module, split into smaller components, improve filters
}
get circumference(): number {
return 2 * Math.PI * this.radius;
}
@ -44,12 +46,17 @@ export class DonutChartComponent implements OnChanges, OnInit {
return this.totalType === 'sum' ? this.dataTotal : this.config.length;
}
constructor(@Optional() readonly filterService: FilterService) {
// TODO: move this component to a separate module, split into smaller components, improve filters
}
ngOnInit() {
this.filters$ =
this.filterService?.getFilterModels$(this.filterKey).pipe(
map(filters => filters ?? []),
shareLast(),
) ?? of([]);
const filterModels$ = this.filterService?.getFilterModels$(this.filterKey).pipe(
map(filters => filters ?? []),
shareLast(),
);
this.filters$ = filterModels$ ?? of([]);
}
ngOnChanges(): void {
@ -66,6 +73,10 @@ export class DonutChartComponent implements OnChanges, OnInit {
);
}
getFormattedValue(value: number): string {
return this.valueFormatter ? this.valueFormatter(value) : value.toString();
}
calculateChartData() {
let angleOffset = -90;
this.chartData = this.config.map(dataVal => {
@ -96,8 +107,8 @@ export class DonutChartComponent implements OnChanges, OnInit {
return this.totalType === 'simpleLabel'
? `${label}`
: this.totalType === 'sum'
? `${value} ${label}`
: `${label} (${value} ${this.counterText})`;
? `${this.getFormattedValue(value)} ${label}`
: `${label} (${this.getFormattedValue(value)} ${this.counterText})`;
}
selectValue(key: string): void {

View File

@ -7,7 +7,6 @@ import { MatConfigModule } from '../mat-config/mat-config.module';
import { IconsModule } from '../icons/icons.module';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AnnotationIconComponent } from './components/annotation-icon/annotation-icon.component';
import { DonutChartComponent } from './components/donut-chart/donut-chart.component';
import {
IqserButtonsModule,
IqserHelpModeModule,
@ -50,7 +49,6 @@ const buttons = [FileDownloadBtnComponent];
const components = [
PaginationComponent,
AnnotationIconComponent,
DonutChartComponent,
SelectComponent,
DictionaryManagerComponent,
AssignUserDropdownComponent,

View File

@ -1,18 +1,12 @@
import { Injectable } from '@angular/core';
import { GenericService, QueryParam } from '@iqser/common-ui';
import { ILicense, ILicenseReport, ILicenseReportRequest, ILicenses } from '@red/domain';
import { inject, Injectable } from '@angular/core';
import { GenericService, Toaster } from '@iqser/common-ui';
import { ILicense, ILicenseFeature, ILicenseReport, ILicenseReportRequest, ILicenses } from '@red/domain';
import { BehaviorSubject, firstValueFrom, Observable, of } from 'rxjs';
import { catchError, filter, tap } from 'rxjs/operators';
import { LICENSE_STORAGE_KEY } from '../modules/admin/screens/license/utils/constants';
import { catchError, filter } from 'rxjs/operators';
import dayjs from 'dayjs';
import { NGXLogger } from 'ngx-logger';
import { UI } from '@pdftron/webviewer';
import off = UI.Hotkeys.off;
export function getStoredReports() {
const rawStoredReports = localStorage.getItem(LICENSE_STORAGE_KEY);
return JSON.parse(rawStoredReports ?? '{}') as Record<string, ILicenseReport>;
}
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { LicenseFeatures } from '../modules/admin/screens/license/utils/constants';
const defaultOnError: ILicenses = {
activeLicense: 'err',
@ -23,16 +17,16 @@ const defaultOnError: ILicenses = {
product: 'Error',
licensedTo: 'Error',
licensedToEmail: 'Error',
validFrom: '01-01-2022',
validUntil: '01-01-2023',
validFrom: '01-01-2023',
validUntil: '01-01-2024',
features: [
{
name: 'processingPages',
name: LicenseFeatures.PROCESSING_PAGES,
type: 'NUMBER',
value: '2000000',
},
{
name: 'pdftron',
name: LicenseFeatures.PDFTRON,
type: 'STRING',
value:
'S25lY29uIEFHKGVuLmtuZWNvbi5zd2lzcyk6T0VNOkREQS1SOjpCKzpBTVMoMj' +
@ -47,27 +41,15 @@ const defaultOnError: ILicenses = {
providedIn: 'root',
})
export class LicenseService extends GenericService<ILicenseReport> {
storedReports = getStoredReports();
readonly licenseData$: Observable<ILicenses>;
readonly selectedLicense$: Observable<ILicense>;
activeLicenseId: string;
totalLicensedNumberOfPages = 0;
currentLicenseInfo: ILicenseReport = {};
allLicensesInfo: ILicenseReport = {};
unlicensedPages = 0;
analyzedPagesInCurrentLicensingPeriod = 0;
selectedLicenseReport: ILicenseReport = {};
allLicensesReport: ILicenseReport = {};
protected readonly _defaultModelPath = 'report';
readonly #licenseData$ = new BehaviorSubject<ILicenses | undefined>(undefined);
readonly #selectedLicense$ = new BehaviorSubject<ILicense | undefined>(undefined);
constructor(private readonly _logger: NGXLogger) {
super();
this.selectedLicense$ = this.#selectedLicense$.pipe(filter(license => !!license));
this.licenseData$ = this.#licenseData$.pipe(
filter(licenses => !!licenses),
tap(data => (this.activeLicenseId = data.activeLicense)),
);
}
readonly #logger = inject(NGXLogger);
readonly #toaster = inject(Toaster);
get selectedLicense() {
return this.#selectedLicense$.value;
@ -83,64 +65,88 @@ export class LicenseService extends GenericService<ILicenseReport> {
}
get activeLicenseKey(): string {
return this.activeLicense.features.find(f => f.name === 'pdftron').value;
const activeLicense = this.activeLicense;
if (!activeLicense) {
return '';
}
return activeLicense.features.find(f => f.name === LicenseFeatures.PDFTRON).value as string;
}
wipeStoredReportsAndReloadSelectedLicenseData() {
this._logger.info('[LICENSE] Wiping stored reports and reloading license data');
this.storedReports = {};
this.setSelectedLicense(this.selectedLicense);
get totalLicensedNumberOfPages(): number {
const processingPagesFeature = this.getFeature(LicenseFeatures.PROCESSING_PAGES);
return Number(processingPagesFeature?.value ?? '-1');
}
get analyzedPagesPercentageForSelectedLicensePercentage(): number {
return this.totalLicensedNumberOfPages > 0
? (this.selectedLicenseReport.numberOfAnalyzedPages / this.totalLicensedNumberOfPages) * 100
: -1;
}
get analysisCapacityBytes(): number {
const capacityFeature = this.getFeature(LicenseFeatures.ANALYSIS_CAPACITY_BYTES);
return (Number(capacityFeature?.value ?? '-1') / 10) * 10;
}
get retentionCapacityBytes(): number {
const capacityFeature = this.getFeature(LicenseFeatures.RETENTION_CAPACITY_BYTES);
return Number(capacityFeature?.value ?? '-1');
}
get analysisCapacityBytesForSelectedLicensePercentage(): number {
return this.analysisCapacityBytes > 0 ? (this.selectedLicenseReport.analysedFilesBytes / this.analysisCapacityBytes) * 100 : -1;
}
get retentionCapacityBytesForSelectedLicensePercentage(): number {
return this.retentionCapacityBytes > 0
? (this.selectedLicenseReport.totalFilesUploadedBytes / this.retentionCapacityBytes) * 100
: -1;
}
get unlicensedPages(): number {
return Math.max(0, this.selectedLicenseReport.numberOfAnalyzedPages - this.totalLicensedNumberOfPages);
}
get isPageBased(): boolean {
return this.totalLicensedNumberOfPages >= 0;
}
get isCapacityBased(): boolean {
return this.analysisCapacityBytes >= 0 && this.retentionCapacityBytes >= 0;
}
constructor() {
super();
this.selectedLicense$ = this.#selectedLicense$.pipe(filter(license => !!license));
this.licenseData$ = this.#licenseData$.pipe(filter(licenses => !!licenses));
}
getFeature(name: string): ILicenseFeature | undefined {
return this.selectedLicense.features?.find(f => f.name === name);
}
async loadLicenseData(license: ILicense = this.selectedLicense) {
this.totalLicensedNumberOfPages = this.getTotalLicensedNumberOfPages(license);
const startDate = dayjs(license.validFrom);
const endDate = dayjs(license.validUntil);
const currentLicenseConfig = {
this.selectedLicenseReport = await this.getReport({
startDate: startDate.toDate(),
endDate: endDate.toDate(),
};
});
const allLicensesConfig = {
this.allLicensesReport = await this.getReport({
startDate: '2020-01-01T00:00:00.000Z',
endDate: '2023-12-31T00:00:00.000Z',
};
const configs = [currentLicenseConfig, allLicensesConfig];
const reports = configs.map(config => this.getReport(config));
[this.currentLicenseInfo, this.allLicensesInfo] = await Promise.all(reports);
if (this.currentLicenseInfo.numberOfAnalyzedPages > this.totalLicensedNumberOfPages) {
this.unlicensedPages = this.currentLicenseInfo.numberOfAnalyzedPages - this.totalLicensedNumberOfPages;
} else {
this.unlicensedPages = 0;
}
this.analyzedPagesInCurrentLicensingPeriod = this.currentLicenseInfo.numberOfAnalyzedPages;
}
getTotalLicensedNumberOfPages(license: ILicense) {
const processingPagesFeature = license.features?.find(f => f.name === 'processingPages');
return Number(processingPagesFeature?.value ?? '0');
});
}
setDefaultSelectedLicense() {
this.setSelectedLicense(this.activeLicense);
}
getReport(body: ILicenseReportRequest, limit?: number, offset?: number) {
const queryParams: QueryParam[] = [];
if (limit) {
queryParams.push({ key: 'limit', value: limit });
}
if (offset) {
queryParams.push({ key: 'offset', value: offset });
}
return firstValueFrom(this._post(body, `${this._defaultModelPath}/license`, queryParams));
getReport(body: ILicenseReportRequest) {
return firstValueFrom(this._post(body, `${this._defaultModelPath}/license`));
}
async loadLicenses() {
@ -148,6 +154,11 @@ export class LicenseService extends GenericService<ILicenseReport> {
const licenses = await firstValueFrom(licenses$);
this.#licenseData$.next(licenses);
this.setDefaultSelectedLicense();
const activeLicense = this.activeLicense;
if (!activeLicense) {
this.#logger.error('[LICENSE] No active license found!');
this.#toaster.warning(_('no-active-license'));
}
}
setSelectedLicense($event: ILicense) {

View File

@ -14,6 +14,11 @@ export function hexToRgb(hex: string) {
: null;
}
export function hexToRgba(hex: string, alpha: number) {
const rgb = hexToRgb(hex);
return rgb ? `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})` : null;
}
export function getFirstRelevantTextPart(text: string, direction: 'FORWARD' | 'BACKWARD') {
let spaceCount = 0;
let accumulator = '';

View File

@ -1,7 +1,7 @@
{
"ADMIN_CONTACT_NAME": null,
"ADMIN_CONTACT_URL": null,
"API_URL": "https://dev-05.iqser.cloud/redaction-gateway-v1",
"API_URL": "https://frontend1.iqser.cloud/redaction-gateway-v1",
"APP_NAME": "RedactManager",
"AUTO_READ_TIME": 3,
"BACKEND_APP_VERSION": "4.4.40",
@ -11,7 +11,7 @@
"MAX_RETRIES_ON_SERVER_ERROR": 3,
"OAUTH_CLIENT_ID": "redaction",
"OAUTH_IDP_HINT": null,
"OAUTH_URL": "https://dev-05.iqser.cloud/auth/realms/redaction",
"OAUTH_URL": "https://frontend1.iqser.cloud/auth/realms/redaction",
"RECENT_PERIOD_IN_HOURS": 24,
"SELECTION_MODE": "structural",
"MANUAL_BASE_URL": "https://docs.redactmanager.com/preview",

View File

@ -1633,16 +1633,18 @@
"table-header": "{length} {length, plural, one{justification} other{justifications}}"
},
"license-info-screen": {
"backend-version": "Backend Application Version",
"chart": {
"cumulative": "Cumulative Pages",
"legend": "Legend",
"pages-per-month": "Pages per Month",
"total-pages": "Total Pages"
"analysis-capacity-usage": {
"analyzed-cumulative": "Cumulative Analyzed Data Volume",
"analyzed-per-month": "Analyzed Data Volume per Month",
"licensed": "Licensed Capacity",
"section-title": "Analysis Capacity Details",
"total-analyzed-data": "Total Analyzed Data",
"used-in-period": "Analysis Capacity Used in Licensing Period",
"used-in-total": "Total Analysis Capacity Used"
},
"copyright-claim-text": "Copyright © 2020 - {currentYear} knecon AG (powered by IQSER)",
"backend-version": "Backend Application Version",
"copyright-claim-text": "Copyright © 2020 - {currentYear} knecon",
"copyright-claim-title": "Copyright Claim",
"current-analyzed": "Analyzed Pages in Licensing Period",
"custom-app-title": "Custom Application Title",
"email-report": "Email Report",
"email": {
@ -1652,22 +1654,42 @@
},
"title": "License Report {licenseCustomer}"
},
"end-user-license-text": "The use of this product is subject to the terms of the Redaction End User Agreement, unless otherwise specified therein.",
"end-user-license-text": "The use of this product is subject to the terms of the DocuMine End User License Agreement, unless otherwise specified therein.",
"end-user-license-title": "End User License Agreement",
"license-title": "License Title",
"licensed-page-count": "Licensed Pages",
"licensed-to": "Licensed to",
"licensing-details": "Licensing Details",
"licensing-period": "Licensing Period",
"ocr-analyzed-pages": "OCR Processed Pages in Licensing Period",
"licensing-details": {
"license-title": "License Title",
"licensed-analysis-capacity": "Licensed Analysis Capacity",
"licensed-page-count": "Licensed Pages",
"licensed-retention-capacity": "Licensed Retention Capacity",
"licensed-to": "Licensed to",
"section-title": "Licensing Details",
"licensing-period": "Licensing Period"
},
"page-usage": {
"cumulative-pages": "Cumulative Pages",
"current-analyzed-pages": "Analyzed Pages in Licensing Period",
"ocr-analyzed-pages": "OCR Processed Pages in Licensing Period",
"pages-per-month": "Pages per Month",
"section-title": "Page Usage Details",
"total-analyzed": "Total Analyzed Pages",
"total-ocr-analyzed": "Total OCR Processed Pages",
"total-pages": "Total Pages",
"unlicensed-analyzed": "Unlicensed Analyzed Pages"
},
"retention-capacity-usage": {
"active-documents": "Active Documents",
"archived-documents": "Archived Documents",
"exceeded-capacity": "Exceeded Capacity",
"section-title": "Retention Capacity Details",
"storage-capacity": "Capacity",
"trash-documents": "Documents in Trash",
"unused": "Unused Retention Capacity",
"used-capacity": "Retention Capacity Used"
},
"status": {
"active": "Active",
"inactive": "Inactive"
},
"total-analyzed": "Total Analyzed Pages",
"total-ocr-analyzed": "Total OCR Processed Pages",
"unlicensed-analyzed": "Unlicensed Analyzed Pages",
"usage-details": "Usage Details"
}
},
"license-information": "License Information",
"load-all-annotations-success": "All annotations were loaded and are now visible in the document thumbnails",

View File

@ -1633,16 +1633,18 @@
"table-header": "{length} {length, plural, one{justification} other{justifications}}"
},
"license-info-screen": {
"backend-version": "Backend Application Version",
"chart": {
"cumulative": "Cumulative Pages",
"legend": "Legend",
"pages-per-month": "Pages per Month",
"total-pages": "Total Pages"
"analysis-capacity-usage": {
"analyzed-cumulative": "Cumulative Analyzed Data Volume",
"analyzed-per-month": "Analyzed Data Volume per Month",
"licensed": "Licensed Capacity",
"section-title": "Analysis Capacity Details",
"total-analyzed-data": "Total Analyzed Data",
"used-in-period": "Analysis Capacity Used in Licensing Period",
"used-in-total": "Total Analysis Capacity Used"
},
"copyright-claim-text": "Copyright © 2020 - {currentYear} knecon AG (powered by IQSER)",
"backend-version": "Backend Application Version",
"copyright-claim-text": "Copyright © 2020 - {currentYear} knecon",
"copyright-claim-title": "Copyright Claim",
"current-analyzed": "Analyzed Pages in Licensing Period",
"custom-app-title": "Custom Application Title",
"email-report": "Email Report",
"email": {
@ -1652,22 +1654,42 @@
},
"title": "License Report {licenseCustomer}"
},
"end-user-license-text": "The use of this product is subject to the terms of the Component End User Agreement, unless otherwise specified therein.",
"end-user-license-text": "The use of this product is subject to the terms of the DocuMine End User License Agreement, unless otherwise specified therein.",
"end-user-license-title": "End User License Agreement",
"license-title": "License Title",
"licensed-page-count": "Licensed pages",
"licensed-to": "Licensed to",
"licensing-details": "Licensing Details",
"licensing-period": "Licensing Period",
"ocr-analyzed-pages": "OCR Processed Pages in Licensing Period",
"licensing-details": {
"license-title": "License Title",
"licensed-analysis-capacity": "Licensed Analysis Capacity",
"licensed-page-count": "Licensed Pages",
"licensed-retention-capacity": "Licensed Retention Capacity",
"licensed-to": "Licensed to",
"section-title": "Licensing Details",
"licensing-period": "Licensing Period"
},
"page-usage": {
"cumulative-pages": "Cumulative Pages",
"current-analyzed-pages": "Analyzed Pages in Licensing Period",
"ocr-analyzed-pages": "OCR Processed Pages in Licensing Period",
"pages-per-month": "Pages per Month",
"section-title": "Page Usage Details",
"total-analyzed": "Total Analyzed Pages",
"total-ocr-analyzed": "Total OCR Processed Pages",
"total-pages": "Total Pages",
"unlicensed-analyzed": "Unlicensed Analyzed Pages"
},
"retention-capacity-usage": {
"active-documents": "Active Documents",
"archived-documents": "Archived Documents",
"exceeded-capacity": "Exceeded Capacity",
"section-title": "Retention Capacity Details",
"storage-capacity": "Capacity",
"trash-documents": "Documents in Trash",
"unused": "Unused Retention Capacity",
"used-capacity": "Retention Capacity Used"
},
"status": {
"active": "Active",
"inactive": "Inactive"
},
"total-analyzed": "Total Analyzed Pages Since {date}",
"total-ocr-analyzed": "Total OCR Processed Pages Since {date}",
"unlicensed-analyzed": "Unlicensed Analyzed Pages",
"usage-details": "Usage Details"
}
},
"license-information": "License Information",
"load-all-annotations-success": "All annotations were loaded and are now visible in the document thumbnails",

@ -1 +1 @@
Subproject commit ac3ccb1d7dda79fa196269d3f0565bbcf5d9067d
Subproject commit 7312caa8d0119c5c153f02ddc22075ec9e945451

View File

@ -1,8 +1,4 @@
import { List } from '@iqser/common-ui';
export interface ILicenseReportRequest {
dossierIds?: List;
endDate?: Date | string;
requestId?: string;
startDate?: Date | string;
}

View File

@ -1,16 +1,20 @@
import { IReportData } from './report-data';
export interface ILicenseReport {
data?: IReportData[];
export interface ILicenseData {
activeFilesUploadedBytes?: number;
archivedFilesUploadedBytes?: number;
totalFilesUploadedBytes?: number;
trashFilesUploadedBytes?: number;
numberOfAnalyzedPages?: number;
numberOfOcrPages?: number;
analysedFilesBytes?: number;
startDate?: Date;
endDate?: Date;
}
export interface ILicenseReport extends ILicenseData {
limit?: number;
numberOfAnalyses?: number;
numberOfAnalyzedFiles?: number;
numberOfAnalyzedPages?: number;
numberOfDossiers?: number;
numberOfOcrFiles?: number;
numberOfOcrPages?: number;
offset?: number;
requestId?: string;
startDate?: Date;
monthlyData?: ILicenseData[];
}

View File

@ -40,7 +40,7 @@
"@ngx-translate/http-loader": "^7.0.0",
"@nrwl/angular": "15.6.3",
"@pdftron/webviewer": "10.1.1",
"@swimlane/ngx-charts": "^20.0.1",
"chart.js": "4.4.0",
"dayjs": "^1.11.5",
"file-saver": "^2.0.5",
"jwt-decode": "^3.1.2",
@ -48,6 +48,7 @@
"keycloak-js": "20.0.3",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.34.0",
"ng2-charts": "5.0.3",
"ngx-color-picker": "^13.0.0",
"ngx-logger": "^5.0.11",
"ngx-toastr": "^16.0.2",

189
yarn.lock
View File

@ -2556,6 +2556,11 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
"@kurkle/color@^0.3.0":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f"
integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==
"@leichtgewicht/ip-codec@^2.0.1":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
@ -3692,26 +3697,6 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
"@swimlane/ngx-charts@^20.0.1":
version "20.4.1"
resolved "https://registry.yarnpkg.com/@swimlane/ngx-charts/-/ngx-charts-20.4.1.tgz#42f3d63c1326cfe347d62d1f626840d6c1511276"
integrity sha512-DyTQe0fcqLDoLEZca45gkdjxP8iLH7kh4pCkr+TCFIkmgEdfQ5DpavNBOOVO0qd5J5uV/tbtSnkYWSx8JkbFpg==
dependencies:
d3-array "^3.1.1"
d3-brush "^3.0.0"
d3-color "^3.1.0"
d3-ease "^3.0.1"
d3-format "^3.1.0"
d3-hierarchy "^3.1.0"
d3-interpolate "^3.0.1"
d3-scale "^4.0.2"
d3-selection "^3.0.0"
d3-shape "^3.2.0"
d3-time-format "^3.0.0"
d3-transition "^3.0.1"
rfdc "^1.3.0"
tslib "^2.0.0"
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
@ -5317,6 +5302,13 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
chart.js@4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.0.tgz#df843fdd9ec6bd88d7f07e2b95348d221bd2698c"
integrity sha512-vQEj6d+z0dcsKLlQvbKIMYFHd3t8W/7L2vfJIbYcfyPcRx92CsHqECpueN8qVGNlKyDcr5wBrYAYKnfu/9Q1hQ==
dependencies:
"@kurkle/color" "^0.3.0"
chokidar@3.5.3, "chokidar@>=3.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.5.1, chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
@ -5873,143 +5865,6 @@ cuint@^0.2.2:
resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b"
integrity sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==
d3-array@2:
version "2.12.1"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81"
integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==
dependencies:
internmap "^1.0.0"
"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.1:
version "3.2.4"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5"
integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
dependencies:
internmap "1 - 2"
d3-brush@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c"
integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==
dependencies:
d3-dispatch "1 - 3"
d3-drag "2 - 3"
d3-interpolate "1 - 3"
d3-selection "3"
d3-transition "3"
"d3-color@1 - 3", d3-color@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
"d3-dispatch@1 - 3":
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
"d3-drag@2 - 3":
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba"
integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
dependencies:
d3-dispatch "1 - 3"
d3-selection "3"
"d3-ease@1 - 3", d3-ease@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
"d3-format@1 - 3", d3-format@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
d3-hierarchy@^3.1.0:
version "3.1.2"
resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6"
integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==
"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
dependencies:
d3-color "1 - 3"
d3-path@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
d3-scale@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
dependencies:
d3-array "2.10.0 - 3"
d3-format "1 - 3"
d3-interpolate "1.2.0 - 3"
d3-time "2.1.1 - 3"
d3-time-format "2 - 4"
d3-selection@3, d3-selection@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
d3-shape@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
dependencies:
d3-path "^3.1.0"
"d3-time-format@2 - 4":
version "4.1.0"
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
dependencies:
d3-time "1 - 3"
d3-time-format@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6"
integrity sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==
dependencies:
d3-time "1 - 2"
"d3-time@1 - 2":
version "2.1.1"
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682"
integrity sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==
dependencies:
d3-array "2"
"d3-time@1 - 3", "d3-time@2.1.1 - 3":
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
dependencies:
d3-array "2 - 3"
"d3-timer@1 - 3":
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
d3-transition@3, d3-transition@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f"
integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
dependencies:
d3-color "1 - 3"
d3-dispatch "1 - 3"
d3-ease "1 - 3"
d3-interpolate "1 - 3"
d3-timer "1 - 3"
data-urls@^3.0.1, data-urls@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143"
@ -7728,16 +7583,6 @@ internal-slot@^1.0.4:
has "^1.0.3"
side-channel "^1.0.4"
"internmap@1 - 2":
version "2.0.3"
resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
internmap@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95"
integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==
ip@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da"
@ -8962,7 +8807,7 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash-es@^4.17.21:
lodash-es@^4.17.15, lodash-es@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
@ -9420,6 +9265,14 @@ neo-async@^2.6.2:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
ng2-charts@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/ng2-charts/-/ng2-charts-5.0.3.tgz#ce48e85ba375864928ca5c2788e11cbf1023cea2"
integrity sha512-/lTY64tiCN/pJPx+oIWRWOhtCk+ZbAU9yAUDNnRJwhe+a8ajcO5yS0tVOm5k7pj3doVp9+UdBRahyt6woJ95Rw==
dependencies:
lodash-es "^4.17.15"
tslib "^2.3.0"
ngx-color-picker@^13.0.0:
version "13.0.0"
resolved "https://registry.yarnpkg.com/ngx-color-picker/-/ngx-color-picker-13.0.0.tgz#fe6d3b2def721ebc4f2a1a4ed83e552e0cfe3601"