From 4d60b66ac032ac74e8183a44c5bce15142f3db58 Mon Sep 17 00:00:00 2001 From: alexeh Date: Wed, 20 Dec 2023 10:27:59 +0300 Subject: [PATCH] Generate comparison reports --- .../impact/impact-report.controller.ts | 69 ++++++++++++++++++- .../modules/impact/reports/impact.report.ts | 66 +++++++++++++----- .../new-coefficients-intervention.response.ts | 2 + 3 files changed, 120 insertions(+), 17 deletions(-) diff --git a/api/src/modules/impact/impact-report.controller.ts b/api/src/modules/impact/impact-report.controller.ts index 2b4630829..ccbb2a067 100644 --- a/api/src/modules/impact/impact-report.controller.ts +++ b/api/src/modules/impact/impact-report.controller.ts @@ -13,7 +13,12 @@ import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { SetScenarioIdsInterceptor } from 'modules/impact/set-scenario-ids.interceptor'; import { ImpactReportService } from 'modules/impact/reports/impact.report'; import { Response } from 'express'; -import { GetImpactTableDto } from 'modules/impact/dto/impact-table.dto'; +import { + GetActualVsScenarioImpactTableDto, + GetImpactTableDto, + GetScenarioVsScenarioImpactTableDto, +} from 'modules/impact/dto/impact-table.dto'; +import { CheckUserOwnsScenario } from 'modules/authorization/formodule/scenario-ownership.interceptor'; @Controller('/api/v1/impact') @ApiTags('Impact') @@ -47,4 +52,66 @@ export class ImpactReportController { ); res.send(report); } + + @ApiOperation({ + description: + 'Get a Actual Vs Scenario Impact Table CSV Report for a given scenario', + }) + @CheckUserOwnsScenario({ + bypassIfScenarioIsPublic: true, + isComparisonMode: true, + }) + @UseInterceptors(SetScenarioIdsInterceptor) + @Get('compare/scenario/vs/actual/report') + async getActualVsScenarioImpactTable( + @Query(ValidationPipe) + actualVsScenarioImpactTableDto: GetActualVsScenarioImpactTableDto, + @Res() res: Response, + ): Promise { + const { data } = + await this.actualVsScenarioImpactService.getActualVsScenarioImpactTable( + actualVsScenarioImpactTableDto, + { disablePagination: true }, + ); + const report: string = await this.impactReports.generateImpactReport( + data.impactTable, + ); + res.setHeader('Content-Type', 'text/csv'); + res.setHeader( + 'Content-Disposition', + 'attachment; filename=impact_report.csv', + ); + res.send(report); + } + + @ApiOperation({ + description: + 'Get a Scenario Vs Scenario Impact Table CSV Report for 2 Scenarios', + }) + @CheckUserOwnsScenario({ + bypassIfScenarioIsPublic: true, + isComparisonMode: true, + }) + @UseInterceptors(SetScenarioIdsInterceptor) + @Get('compare/scenario/vs/scenario/report') + async getTwoScenariosImpactTable( + @Query(ValidationPipe) + scenarioVsScenarioImpactTableDto: GetScenarioVsScenarioImpactTableDto, + @Res() res: Response, + ): Promise { + const { data } = + await this.scenarioVsScenarioService.getScenarioVsScenarioImpactTable( + scenarioVsScenarioImpactTableDto, + { disablePagination: true }, + ); + const report: string = await this.impactReports.generateImpactReport( + data.impactTable, + ); + res.setHeader('Content-Type', 'text/csv'); + res.setHeader( + 'Content-Disposition', + 'attachment; filename=impact_report.csv', + ); + res.send(report); + } } diff --git a/api/src/modules/impact/reports/impact.report.ts b/api/src/modules/impact/reports/impact.report.ts index 932b6ff99..04b880fd3 100644 --- a/api/src/modules/impact/reports/impact.report.ts +++ b/api/src/modules/impact/reports/impact.report.ts @@ -2,13 +2,25 @@ import { Inject, Injectable } from '@nestjs/common'; import { ICSVReportService } from 'modules/reports/report-service.interface'; import { ReportServiceToken } from 'modules/reports/reports.module'; import { + AnyImpactTableRows, + AnyImpactTableRowsValues, ImpactTableDataByIndicator, - ImpactTableRows, - ImpactTableRowsValues, } from 'modules/impact/dto/response-impact-table.dto'; import { ImpactTableCSVReport } from 'modules/impact/reports/types'; import { ParserOptions } from '@json2csv/plainjs'; import { GROUP_BY_VALUES } from 'modules/impact/dto/impact-table.dto'; +import { ScenarioVsScenarioImpactTableDataByIndicator } from 'modules/impact/dto/response-scenario-scenario.dto'; +import { ActualVsScenarioImpactTableDataByIndicator } from 'modules/impact/dto/response-actual-scenario.dto'; + +const IndicatorColumnKey: string = 'Indicator'; +const GroupByColumnKey: string = 'Group by'; +const AbsoluteDifferenceColumnKey: string = 'Absolute Difference'; +const PercentageDifferenceColumnKey: string = 'Percentage Difference'; + +type ImpactTableToCSVInput = + | ImpactTableDataByIndicator + | ScenarioVsScenarioImpactTableDataByIndicator + | ActualVsScenarioImpactTableDataByIndicator; @Injectable() export class ImpactReportService { @@ -16,9 +28,7 @@ export class ImpactReportService { @Inject(ReportServiceToken) private reportService: ICSVReportService, ) {} - async generateImpactReport( - data: ImpactTableDataByIndicator[], - ): Promise { + async generateImpactReport(data: ImpactTableToCSVInput[]): Promise { const parsedData: ImpactTableCSVReport[] = this.parseToCSVShape(data); const columnOrder: Pick = { fields: this.getColumnOrder(parsedData[0]), @@ -28,13 +38,13 @@ export class ImpactReportService { } private parseToCSVShape( - data: ImpactTableDataByIndicator[], + data: ImpactTableToCSVInput[], ): ImpactTableCSVReport[] { const results: ImpactTableCSVReport[] = []; - data.forEach((indicator: ImpactTableDataByIndicator) => { + data.forEach((indicator: ImpactTableToCSVInput) => { const unit: string = indicator.metadata.unit; - indicator.rows.forEach((row: ImpactTableRows) => { + indicator.rows.forEach((row: AnyImpactTableRows) => { this.processNode( row, indicator.indicatorShortName, @@ -49,7 +59,7 @@ export class ImpactReportService { } private processNode( - node: ImpactTableRows, + node: AnyImpactTableRows, indicatorName: | Pick | string, @@ -57,22 +67,41 @@ export class ImpactReportService { unit: string, accumulator: ImpactTableCSVReport[], ): void { - const groupName: string = `Group by ${groupBy}`; + const groupName: string = `${GroupByColumnKey} ${groupBy}`; const resultObject: ImpactTableCSVReport = { [`Indicator`]: `${indicatorName} (${unit})`, [groupName]: node.name, }; - node.values.forEach((nodeValue: ImpactTableRowsValues) => { + node.values.forEach((nodeValue: AnyImpactTableRowsValues) => { let yearKey: string = nodeValue.year.toString(); if (nodeValue.isProjected) { yearKey = yearKey.concat(' (projected)'); } - resultObject[yearKey] = nodeValue.value; + if ('baseScenarioValue' in nodeValue) { + resultObject[`${yearKey} (base Scenario)`] = + nodeValue.baseScenarioValue; + resultObject[`${yearKey} (compared Scenario)`] = + nodeValue.comparedScenarioValue; + } else if ('comparedScenarioValue' in nodeValue) { + resultObject[yearKey] = nodeValue.value; + resultObject[`${yearKey} (compared Scenario)`] = + nodeValue.comparedScenarioValue; + } else { + resultObject[yearKey] = nodeValue.value; + } + if ('absoluteDifference' in nodeValue) { + resultObject[AbsoluteDifferenceColumnKey] = + nodeValue.absoluteDifference; + } + if ('percentageDifference' in nodeValue) { + resultObject[PercentageDifferenceColumnKey] = + nodeValue.percentageDifference; + } }); accumulator.push(resultObject); if (node.children && node.children.length) { - node.children.forEach((children: ImpactTableRows) => { + node.children.forEach((children: AnyImpactTableRows) => { this.processNode(children, indicatorName, groupBy, unit, accumulator); }); } @@ -82,15 +111,20 @@ export class ImpactReportService { const indicatorKey: string[] = []; const groupByKey: string[] = []; const yearKeys: string[] = []; + const differenceKeys: string[] = []; Object.keys(dataSample).forEach((key: string) => { - if (key.includes('Indicator')) { + if (key.includes(IndicatorColumnKey)) { groupByKey.push(key); - } else if (key.includes('Group by')) { + } else if (key.includes(GroupByColumnKey)) { groupByKey.push(key); + } else if (key.includes(AbsoluteDifferenceColumnKey)) { + differenceKeys.push(key); + } else if (key.includes(PercentageDifferenceColumnKey)) { + differenceKeys.push(key); } else { yearKeys.push(key); } }); - return [...indicatorKey, ...groupByKey, ...yearKeys]; + return [...indicatorKey, ...groupByKey, ...yearKeys, ...differenceKeys]; } } diff --git a/api/test/e2e/impact/mocks/actual-vs-scenario-responses/new-coefficients-intervention.response.ts b/api/test/e2e/impact/mocks/actual-vs-scenario-responses/new-coefficients-intervention.response.ts index a366dd16a..f707a49c5 100644 --- a/api/test/e2e/impact/mocks/actual-vs-scenario-responses/new-coefficients-intervention.response.ts +++ b/api/test/e2e/impact/mocks/actual-vs-scenario-responses/new-coefficients-intervention.response.ts @@ -1,3 +1,5 @@ +// Response DTO example for a new coefficients type of scenario intervention + export const newCoefficientsScenarioInterventionTable = { impactTable: [ {