diff --git a/angular.json b/angular.json index c05451e7d..2913f8855 100644 --- a/angular.json +++ b/angular.json @@ -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, diff --git a/apps/red-ui/src/app/modules/admin/admin.module.ts b/apps/red-ui/src/app/modules/admin/admin.module.ts index 92aa673d4..79238e28e 100644 --- a/apps/red-ui/src/app/modules/admin/admin.module.ts +++ b/apps/red-ui/src/app/modules/admin/admin.module.ts @@ -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 {} diff --git a/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-chart.component.html b/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-chart.component.html deleted file mode 100644 index b1f569bc3..000000000 --- a/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-chart.component.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-chart.component.scss b/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-chart.component.scss deleted file mode 100644 index c2ef9aba1..000000000 --- a/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-chart.component.scss +++ /dev/null @@ -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; - } - } -} diff --git a/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-chart.component.ts b/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-chart.component.ts deleted file mode 100644 index 58db4f019..000000000 --- a/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-chart.component.ts +++ /dev/null @@ -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 }); - } -} diff --git a/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-series-vertical.component.ts b/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-series-vertical.component.ts deleted file mode 100644 index b8d9fe052..000000000 --- a/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-series-vertical.component.ts +++ /dev/null @@ -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: ` - - `, - 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 = ` - ${tooltipLabel} - - Y1 - ${value.toLocaleString()} • Y2 - ${lineValue.toLocaleString()}% - - `; - - 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; - } -} diff --git a/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/index.ts b/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/index.ts deleted file mode 100644 index 07ef678f5..000000000 --- a/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './combo-chart.component'; -export * from './combo-series-vertical.component'; -export * from './y-axis.component'; diff --git a/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/y-axis.component.ts b/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/y-axis.component.ts deleted file mode 100644 index 91690fea4..000000000 --- a/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/y-axis.component.ts +++ /dev/null @@ -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: ` - - - - - `, - 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); - } - } -} diff --git a/apps/red-ui/src/app/modules/admin/screens/license/components/chart/chart.component.html b/apps/red-ui/src/app/modules/admin/screens/license/components/chart/chart.component.html new file mode 100644 index 000000000..c879845d0 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/license/components/chart/chart.component.html @@ -0,0 +1,3 @@ + + {{ chart }} + diff --git a/apps/red-ui/src/app/modules/admin/screens/license/components/chart/chart.component.scss b/apps/red-ui/src/app/modules/admin/screens/license/components/chart/chart.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/apps/red-ui/src/app/modules/admin/screens/license/components/chart/chart.component.ts b/apps/red-ui/src/app/modules/admin/screens/license/components/chart/chart.component.ts new file mode 100644 index 000000000..ff1dc90f1 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/license/components/chart/chart.component.ts @@ -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, + }); + } +} diff --git a/apps/red-ui/src/app/modules/admin/screens/license/components/license-analysis-capacity-usage/license-analysis-capacity-usage.component.html b/apps/red-ui/src/app/modules/admin/screens/license/components/license-analysis-capacity-usage/license-analysis-capacity-usage.component.html new file mode 100644 index 000000000..eb78b00ed --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/license/components/license-analysis-capacity-usage/license-analysis-capacity-usage.component.html @@ -0,0 +1,35 @@ +
+
+ +
+
+
+ {{ licenseService.selectedLicenseReport.analysedFilesBytes | size }} + + ({{ licenseService.analysisCapacityBytesForSelectedLicensePercentage | number : '1.0-2' }}%) + +
+
+ +
+
+
+ {{ licenseService.allLicensesReport.analysedFilesBytes | size }} +
+
+ +
+
+ +
+
+
diff --git a/apps/red-ui/src/app/modules/admin/screens/license/components/license-analysis-capacity-usage/license-analysis-capacity-usage.component.scss b/apps/red-ui/src/app/modules/admin/screens/license/components/license-analysis-capacity-usage/license-analysis-capacity-usage.component.scss new file mode 100644 index 000000000..e91dc2856 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/license/components/license-analysis-capacity-usage/license-analysis-capacity-usage.component.scss @@ -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); + } + } +} diff --git a/apps/red-ui/src/app/modules/admin/screens/license/components/license-analysis-capacity-usage/license-analysis-capacity-usage.component.ts b/apps/red-ui/src/app/modules/admin/screens/license/components/license-analysis-capacity-usage/license-analysis-capacity-usage.component.ts new file mode 100644 index 000000000..d50c7fdd1 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/license/components/license-analysis-capacity-usage/license-analysis-capacity-usage.component.ts @@ -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; + } +} diff --git a/apps/red-ui/src/app/modules/admin/screens/license/components/license-page-usage/license-page-usage.component.html b/apps/red-ui/src/app/modules/admin/screens/license/components/license-page-usage/license-page-usage.component.html new file mode 100644 index 000000000..bd83c0bb1 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/license/components/license-page-usage/license-page-usage.component.html @@ -0,0 +1,47 @@ +
+
+ +
+
+
+ {{ licenseService.selectedLicenseReport.numberOfAnalyzedPages }} + + ({{ licenseService.analyzedPagesPercentageForSelectedLicensePercentage | number : '1.0-2' }}%) + +
+
+ +
+
+
{{ licenseService.selectedLicenseReport.numberOfOcrPages }}
+
+ +
+
+
{{ licenseService.unlicensedPages }}
+
+ +
+
+
{{ licenseService.allLicensesReport.numberOfAnalyzedPages }}
+
+ +
+
+
{{ licenseService.allLicensesReport.numberOfOcrPages }}
+
+ +
+
+ +
+
+
diff --git a/apps/red-ui/src/app/modules/admin/screens/license/components/license-page-usage/license-page-usage.component.scss b/apps/red-ui/src/app/modules/admin/screens/license/components/license-page-usage/license-page-usage.component.scss new file mode 100644 index 000000000..e91dc2856 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/license/components/license-page-usage/license-page-usage.component.scss @@ -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); + } + } +} diff --git a/apps/red-ui/src/app/modules/admin/screens/license/components/license-page-usage/license-page-usage.component.ts b/apps/red-ui/src/app/modules/admin/screens/license/components/license-page-usage/license-page-usage.component.ts new file mode 100644 index 000000000..b76d30d21 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/license/components/license-page-usage/license-page-usage.component.ts @@ -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), + }, + ]; + } +} diff --git a/apps/red-ui/src/app/modules/admin/screens/license/components/license-retention-capacity-usage/license-retention-capacity.component.html b/apps/red-ui/src/app/modules/admin/screens/license/components/license-retention-capacity-usage/license-retention-capacity.component.html new file mode 100644 index 000000000..4d92e71d8 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/license/components/license-retention-capacity-usage/license-retention-capacity.component.html @@ -0,0 +1,65 @@ +
+
+ +
+
+
{{ licenseService.selectedLicenseReport.activeFilesUploadedBytes | size }}
+
+ +
+
+
{{ licenseService.selectedLicenseReport.archivedFilesUploadedBytes | size }}
+
+
+
+
{{ licenseService.selectedLicenseReport.trashFilesUploadedBytes | size }}
+
+ +
+
+
+ {{ licenseService.selectedLicenseReport.totalFilesUploadedBytes | size }} + + ({{ licenseService.retentionCapacityBytesForSelectedLicensePercentage | number : '1.0-2' }}%) + +
+
+ +
+ +
+ +
+
+ +
+
+
+ + + + {{ 'license-info-screen.retention-capacity-usage.storage-capacity' | translate }} + + + + + {{ 'license-info-screen.retention-capacity-usage.exceeded-capacity' | translate }} + + + diff --git a/apps/red-ui/src/app/modules/admin/screens/license/components/license-retention-capacity-usage/license-retention-capacity.component.scss b/apps/red-ui/src/app/modules/admin/screens/license/components/license-retention-capacity-usage/license-retention-capacity.component.scss new file mode 100644 index 000000000..3863510aa --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/license/components/license-retention-capacity-usage/license-retention-capacity.component.scss @@ -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); +} diff --git a/apps/red-ui/src/app/modules/admin/screens/license/components/license-retention-capacity-usage/license-retention-capacity.component.ts b/apps/red-ui/src/app/modules/admin/screens/license/components/license-retention-capacity-usage/license-retention-capacity.component.ts new file mode 100644 index 000000000..f7227cbe2 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/license/components/license-retention-capacity-usage/license-retention-capacity.component.ts @@ -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, + }, + ]; + } +} diff --git a/apps/red-ui/src/app/modules/admin/screens/license/license-select/license-select.component.html b/apps/red-ui/src/app/modules/admin/screens/license/components/license-select/license-select.component.html similarity index 86% rename from apps/red-ui/src/app/modules/admin/screens/license/license-select/license-select.component.html rename to apps/red-ui/src/app/modules/admin/screens/license/components/license-select/license-select.component.html index 06bd9b457..9649c841b 100644 --- a/apps/red-ui/src/app/modules/admin/screens/license/license-select/license-select.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/license/components/license-select/license-select.component.html @@ -1,6 +1,6 @@
- + @@ -14,9 +14,9 @@
- {{ license.name }} +
{{ license.name }}
-
+
{{ getStatus(license.id) | translate | uppercase }}
diff --git a/apps/red-ui/src/app/modules/admin/screens/license/license-select/license-select.component.scss b/apps/red-ui/src/app/modules/admin/screens/license/components/license-select/license-select.component.scss similarity index 100% rename from apps/red-ui/src/app/modules/admin/screens/license/license-select/license-select.component.scss rename to apps/red-ui/src/app/modules/admin/screens/license/components/license-select/license-select.component.scss diff --git a/apps/red-ui/src/app/modules/admin/screens/license/license-select/license-select.component.ts b/apps/red-ui/src/app/modules/admin/screens/license/components/license-select/license-select.component.ts similarity index 92% rename from apps/red-ui/src/app/modules/admin/screens/license/license-select/license-select.component.ts rename to apps/red-ui/src/app/modules/admin/screens/license/components/license-select/license-select.component.ts index ab46c24cf..30f1a4cb6 100644 --- a/apps/red-ui/src/app/modules/admin/screens/license/license-select/license-select.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/license/components/license-select/license-select.component.ts @@ -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) { diff --git a/apps/red-ui/src/app/modules/admin/screens/license/license-chart/license-chart.component.html b/apps/red-ui/src/app/modules/admin/screens/license/license-chart/license-chart.component.html deleted file mode 100644 index 8da1b9488..000000000 --- a/apps/red-ui/src/app/modules/admin/screens/license/license-chart/license-chart.component.html +++ /dev/null @@ -1,18 +0,0 @@ - diff --git a/apps/red-ui/src/app/modules/admin/screens/license/license-chart/license-chart.component.ts b/apps/red-ui/src/app/modules/admin/screens/license/license-chart/license-chart.component.ts deleted file mode 100644 index 99b78884f..000000000 --- a/apps/red-ui/src/app/modules/admin/screens/license/license-chart/license-chart.component.ts +++ /dev/null @@ -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 { - return this._licenseService.selectedLicense$.pipe( - tap(() => this._loadingService.start()), - switchMap(license => this.#getLicenseData(license)), - tap(() => this._loadingService.stop()), - ); - } - - async #getLicenseData(license: ILicense): Promise { - 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, reports: List): 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) { - 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, 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) { - return dateRanges.map(dateRange => ({ - name: verboseDate(dateRange), - value: this._licenseService.totalLicensedNumberOfPages, - })); - } -} diff --git a/apps/red-ui/src/app/modules/admin/screens/license/license-screen/license-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/license/license-screen/license-screen.component.html index eba7eed13..66d316184 100644 --- a/apps/red-ui/src/app/modules/admin/screens/license/license-screen/license-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/license/license-screen/license-screen.component.html @@ -1,100 +1,77 @@ -
-
- + -
-
-
-
-
-
{{ configService.values.BACKEND_APP_VERSION || '-' }}
-
+
+
+
+
+
+
{{ configService.values.BACKEND_APP_VERSION || '-' }}
+
-
-
-
{{ configService.values.APP_NAME || '-' }}
-
+
+
+
{{ configService.values.APP_NAME || '-' }}
+
-
-
-
- {{ 'license-info-screen.copyright-claim-text' | translate : { currentYear: currentYear } }} -
-
+
+
+
+ {{ 'license-info-screen.copyright-claim-text' | translate : { currentYear: currentYear } }} +
+
-
-
-
-
+
+
+
+
-
+
-
-
-
- -
-
+
+
+
+ +
+
- -
-
-
{{ selectedLicense.licensedTo || '-' }}
-
- -
-
-
- {{ (selectedLicense.validFrom | date : 'dd-MM-yyyy') || '-' }} / - {{ (selectedLicense.validUntil | date : 'dd-MM-yyyy') || '-' }} -
-
-
- -
-
{{ 'license-info-screen.licensed-page-count' | translate }}
-
{{ licenseService.totalLicensedNumberOfPages }}
-
- -
- -
-
-
- {{ licenseService.analyzedPagesInCurrentLicensingPeriod }} - ({{ analysisPercentageOfLicense$ | async | number : '1.0-2' }}%) -
-
- -
-
-
{{ licenseService.currentLicenseInfo.numberOfOcrPages }}
-
- -
-
-
{{ licenseService.unlicensedPages }}
-
- -
-
- -
{{ licenseService.allLicensesInfo.numberOfAnalyzedPages }}
-
- -
-
-
{{ licenseService.allLicensesInfo.numberOfOcrPages }}
-
+ +
+
+
{{ selectedLicense.licensedTo || '-' }}
- +
+
+
+ {{ (selectedLicense.validFrom | date : 'dd-MM-yyyy') || '-' }} / + {{ (selectedLicense.validUntil | date : 'dd-MM-yyyy') || '-' }} +
+
+
+ +
+
{{ 'license-info-screen.licensing-details.licensed-page-count' | translate }}
+
{{ licenseService.totalLicensedNumberOfPages }}
+
+ +
+
{{ 'license-info-screen.licensing-details.licensed-analysis-capacity' | translate }}
+
{{ licenseService.analysisCapacityBytes | size }}
+
+ +
+
{{ 'license-info-screen.licensing-details.licensed-retention-capacity' | translate }}
+
{{ licenseService.retentionCapacityBytes | size }}
+ + + +
-
+
diff --git a/apps/red-ui/src/app/modules/admin/screens/license/license-screen/license-screen.component.scss b/apps/red-ui/src/app/modules/admin/screens/license/license-screen/license-screen.component.scss index 9912f1c10..e4d10b52a 100644 --- a/apps/red-ui/src/app/modules/admin/screens/license/license-screen/license-screen.component.scss +++ b/apps/red-ui/src/app/modules/admin/screens/license/license-screen/license-screen.component.scss @@ -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; + } } } diff --git a/apps/red-ui/src/app/modules/admin/screens/license/license-screen/license-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/license/license-screen/license-screen.component.ts index 26382e550..f927ff722 100644 --- a/apps/red-ui/src/app/modules/admin/screens/license/license-screen/license-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/license/license-screen/license-screen.component.ts @@ -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(); + 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, diff --git a/apps/red-ui/src/app/modules/admin/screens/license/license.module.ts b/apps/red-ui/src/app/modules/admin/screens/license/license.module.ts index d578901b2..26d5ea2c9 100644 --- a/apps/red-ui/src/app/modules/admin/screens/license/license.module.ts +++ b/apps/red-ui/src/app/modules/admin/screens/license/license.module.ts @@ -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 {} diff --git a/apps/red-ui/src/app/modules/admin/screens/license/utils/constants.ts b/apps/red-ui/src/app/modules/admin/screens/license/utils/constants.ts index a139c9732..89f97c45c 100644 --- a/apps/red-ui/src/app/modules/admin/screens/license/utils/constants.ts +++ b/apps/red-ui/src/app/modules/admin/screens/license/utils/constants.ts @@ -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', +} diff --git a/apps/red-ui/src/app/modules/admin/screens/license/utils/functions.ts b/apps/red-ui/src/app/modules/admin/screens/license/utils/functions.ts index 0dd196237..107bcbf6b 100644 --- a/apps/red-ui/src/app/modules/admin/screens/license/utils/functions.ts +++ b/apps/red-ui/src/app/modules/admin/screens/license/utils/functions.ts @@ -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; +}; diff --git a/apps/red-ui/src/app/modules/dashboard/dashboard.module.ts b/apps/red-ui/src/app/modules/dashboard/dashboard.module.ts index 6197c84c7..d6502bbf6 100644 --- a/apps/red-ui/src/app/modules/dashboard/dashboard.module.ts +++ b/apps/red-ui/src/app/modules/dashboard/dashboard.module.ts @@ -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 {} diff --git a/apps/red-ui/src/app/modules/dossier-overview/dossier-overview.module.ts b/apps/red-ui/src/app/modules/dossier-overview/dossier-overview.module.ts index 779e81777..0b86be5dc 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/dossier-overview.module.ts +++ b/apps/red-ui/src/app/modules/dossier-overview/dossier-overview.module.ts @@ -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 {} diff --git a/apps/red-ui/src/app/modules/dossiers-listing/dossiers-listing.module.ts b/apps/red-ui/src/app/modules/dossiers-listing/dossiers-listing.module.ts index 7bfb2c734..312f727d8 100644 --- a/apps/red-ui/src/app/modules/dossiers-listing/dossiers-listing.module.ts +++ b/apps/red-ui/src/app/modules/dossiers-listing/dossiers-listing.module.ts @@ -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 {} diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview.module.ts b/apps/red-ui/src/app/modules/file-preview/file-preview.module.ts index 6ec62c0bc..968f58841 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview.module.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview.module.ts @@ -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], }) diff --git a/apps/red-ui/src/app/modules/search/search.module.ts b/apps/red-ui/src/app/modules/search/search.module.ts index 7a3f551fa..c7056eeab 100644 --- a/apps/red-ui/src/app/modules/search/search.module.ts +++ b/apps/red-ui/src/app/modules/search/search.module.ts @@ -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 {} diff --git a/apps/red-ui/src/app/modules/shared-dossiers/shared-dossiers.module.ts b/apps/red-ui/src/app/modules/shared-dossiers/shared-dossiers.module.ts index 0659a7b1b..3b7a219d7 100644 --- a/apps/red-ui/src/app/modules/shared-dossiers/shared-dossiers.module.ts +++ b/apps/red-ui/src/app/modules/shared-dossiers/shared-dossiers.module.ts @@ -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 {} diff --git a/apps/red-ui/src/app/modules/shared/components/donut-chart/donut-chart.component.html b/apps/red-ui/src/app/modules/shared/components/donut-chart/donut-chart.component.html index ade5095d8..b744b9f11 100644 --- a/apps/red-ui/src/app/modules/shared/components/donut-chart/donut-chart.component.html +++ b/apps/red-ui/src/app/modules/shared/components/donut-chart/donut-chart.component.html @@ -18,8 +18,13 @@
-
{{ displayedDataTotal }}
+
{{ getFormattedValue(displayedDataTotal) }}
{{ subtitles[0] }}
+ +
+ +
+
-
+
- + >
diff --git a/apps/red-ui/src/app/modules/shared/components/donut-chart/donut-chart.component.ts b/apps/red-ui/src/app/modules/shared/components/donut-chart/donut-chart.component.ts index 97fe867bf..b10ad0d96 100644 --- a/apps/red-ui/src/app/modules/shared/components/donut-chart/donut-chart.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/donut-chart/donut-chart.component.ts @@ -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; @Output() readonly subtitleChanged = new EventEmitter(); @@ -28,10 +34,6 @@ export class DonutChartComponent implements OnChanges, OnInit { size = 0; filters$: Observable; - 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 { diff --git a/apps/red-ui/src/app/modules/shared/shared.module.ts b/apps/red-ui/src/app/modules/shared/shared.module.ts index 4589f81f2..8fff30d13 100644 --- a/apps/red-ui/src/app/modules/shared/shared.module.ts +++ b/apps/red-ui/src/app/modules/shared/shared.module.ts @@ -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, diff --git a/apps/red-ui/src/app/services/license.service.ts b/apps/red-ui/src/app/services/license.service.ts index c3d3801e3..9ceb27830 100644 --- a/apps/red-ui/src/app/services/license.service.ts +++ b/apps/red-ui/src/app/services/license.service.ts @@ -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; -} +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 { - storedReports = getStoredReports(); readonly licenseData$: Observable; readonly selectedLicense$: Observable; - activeLicenseId: string; - totalLicensedNumberOfPages = 0; - currentLicenseInfo: ILicenseReport = {}; - allLicensesInfo: ILicenseReport = {}; - unlicensedPages = 0; - analyzedPagesInCurrentLicensingPeriod = 0; + selectedLicenseReport: ILicenseReport = {}; + allLicensesReport: ILicenseReport = {}; protected readonly _defaultModelPath = 'report'; readonly #licenseData$ = new BehaviorSubject(undefined); readonly #selectedLicense$ = new BehaviorSubject(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 { } 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 { 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) { diff --git a/apps/red-ui/src/app/utils/functions.ts b/apps/red-ui/src/app/utils/functions.ts index 02e0dffa1..ed720c96f 100644 --- a/apps/red-ui/src/app/utils/functions.ts +++ b/apps/red-ui/src/app/utils/functions.ts @@ -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 = ''; diff --git a/apps/red-ui/src/assets/config/config.json b/apps/red-ui/src/assets/config/config.json index 477027562..c7d5a8669 100644 --- a/apps/red-ui/src/assets/config/config.json +++ b/apps/red-ui/src/assets/config/config.json @@ -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", diff --git a/apps/red-ui/src/assets/i18n/redact/en.json b/apps/red-ui/src/assets/i18n/redact/en.json index 5de2962aa..067e10f2a 100644 --- a/apps/red-ui/src/assets/i18n/redact/en.json +++ b/apps/red-ui/src/assets/i18n/redact/en.json @@ -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", diff --git a/apps/red-ui/src/assets/i18n/scm/en.json b/apps/red-ui/src/assets/i18n/scm/en.json index 59f92efd4..3785b37a9 100644 --- a/apps/red-ui/src/assets/i18n/scm/en.json +++ b/apps/red-ui/src/assets/i18n/scm/en.json @@ -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", diff --git a/libs/common-ui b/libs/common-ui index ac3ccb1d7..7312caa8d 160000 --- a/libs/common-ui +++ b/libs/common-ui @@ -1 +1 @@ -Subproject commit ac3ccb1d7dda79fa196269d3f0565bbcf5d9067d +Subproject commit 7312caa8d0119c5c153f02ddc22075ec9e945451 diff --git a/libs/red-domain/src/lib/reports/license-report.request.ts b/libs/red-domain/src/lib/reports/license-report.request.ts index 796fb114a..99330d5bd 100644 --- a/libs/red-domain/src/lib/reports/license-report.request.ts +++ b/libs/red-domain/src/lib/reports/license-report.request.ts @@ -1,8 +1,4 @@ -import { List } from '@iqser/common-ui'; - export interface ILicenseReportRequest { - dossierIds?: List; endDate?: Date | string; - requestId?: string; startDate?: Date | string; } diff --git a/libs/red-domain/src/lib/reports/license-report.ts b/libs/red-domain/src/lib/reports/license-report.ts index d0095cdca..7986bdd1f 100644 --- a/libs/red-domain/src/lib/reports/license-report.ts +++ b/libs/red-domain/src/lib/reports/license-report.ts @@ -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[]; } diff --git a/package.json b/package.json index 2db755493..02e0f702e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index d26b3e60f..2389e46d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"