Skip to content

Commit

Permalink
Generate base Impact table csv report
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeh committed Dec 19, 2023
1 parent 63a2418 commit 6c61faf
Show file tree
Hide file tree
Showing 15 changed files with 323 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import {
GetActualVsScenarioImpactTableDto,
GROUP_BY_VALUES,
ORDER_BY,
} from 'modules/impact/dto/impact-table.dto';
import { IndicatorsService } from 'modules/indicators/indicators.service';
Expand Down Expand Up @@ -408,7 +409,7 @@ export class ActualVsScenarioImpactService {

private createActualVsScenarioImpactTableDataByIndicator(
indicator: Indicator,
groupBy: string,
groupBy: GROUP_BY_VALUES,
): ActualVsScenarioImpactTableDataByIndicator {
return {
indicatorShortName: indicator.shortName as string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import {
GetActualVsScenarioImpactTableDto,
GetScenarioVsScenarioImpactTableDto,
GROUP_BY_VALUES,
ORDER_BY,
} from 'modules/impact/dto/impact-table.dto';
import { IndicatorsService } from 'modules/indicators/indicators.service';
Expand Down Expand Up @@ -503,7 +504,7 @@ export class ScenarioVsScenarioImpactService {

private createScenarioVsScenarioImpactTableDataByIndicator(
indicator: Indicator,
groupBy: string,
groupBy: GROUP_BY_VALUES,
): ScenarioVsScenarioImpactTableDataByIndicator {
return {
indicatorShortName: indicator.shortName as string,
Expand Down
2 changes: 1 addition & 1 deletion api/src/modules/impact/dto/impact-table.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class BaseImpactTableDto {
@IsEnum(GROUP_BY_VALUES, {
message: 'Available options: ' + Object.values(GROUP_BY_VALUES).toString(),
})
groupBy!: string;
groupBy!: GROUP_BY_VALUES;

@ApiPropertyOptional({ name: 'materialIds[]' })
@IsOptional()
Expand Down
4 changes: 3 additions & 1 deletion api/src/modules/impact/dto/response-actual-scenario.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ImpactTableBaseRowsValues,
ImpactTablePurchasedTonnes,
} from 'modules/impact/dto/response-impact-table.dto';
import { GROUP_BY_VALUES } from 'modules/impact/dto/impact-table.dto';

export class ActualVsScenarioImpactTable {
@ApiProperty({
Expand Down Expand Up @@ -32,7 +33,7 @@ export class ActualVsScenarioImpactTableDataByIndicator {
@ApiProperty()
indicatorId: string;
@ApiProperty()
groupBy: string;
groupBy: GROUP_BY_VALUES;
@ApiProperty({ type: () => ActualVsScenarioImpactTableRows, isArray: true })
rows: ActualVsScenarioImpactTableRows[];
@ApiProperty({
Expand All @@ -52,6 +53,7 @@ export class ActualVsScenarioImpactTablePurchasedTonnes {
@ApiProperty()
isProjected: boolean;
}

export class ActualVsScenarioImpactTableRows {
@ApiProperty()
name: string;
Expand Down
9 changes: 8 additions & 1 deletion api/src/modules/impact/dto/response-impact-table.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ScenarioVsScenarioImpactTableRows,
ScenarioVsScenarioImpactTableRowsValues,
} from 'modules/impact/dto/response-scenario-scenario.dto';
import { GROUP_BY_VALUES } from 'modules/impact/dto/impact-table.dto';

export class ImpactTable {
@ApiProperty({ type: () => ImpactTableDataByIndicator, isArray: true })
Expand All @@ -28,6 +29,7 @@ export class ImpactTableDataAggregationInfo {
numberOfAggregatedEntities: number;
sort: string;
}

export class ImpactTableDataAggregatedValue {
year: number;
value: number;
Expand All @@ -39,7 +41,7 @@ export class ImpactTableDataByIndicator {
@ApiProperty()
indicatorId: string;
@ApiProperty()
groupBy: string;
groupBy: GROUP_BY_VALUES;
@ApiProperty({ type: () => ImpactTableRows, isArray: true })
rows: ImpactTableRows[];
@ApiProperty({ type: () => YearSumData, isArray: true })
Expand All @@ -61,6 +63,7 @@ export class ImpactTablePurchasedTonnes {
@ApiProperty()
isProjected: boolean;
}

export class ImpactTableRows {
@ApiProperty()
name: string;
Expand All @@ -74,22 +77,26 @@ export class ImpactTableRows {
})
children: ImpactTableRows[];
}

export class BaseIndicatorSumByYearData {
@ApiProperty()
year: number;
@ApiProperty()
isProjected: boolean;
}

export class YearSumData {
@ApiProperty()
value: number;
}

export class ImpactTableBaseRowsValues {
@ApiProperty()
year: number;
@ApiProperty()
isProjected: boolean;
}

export class ImpactTableRowsValues extends ImpactTableBaseRowsValues {
@ApiProperty()
value: number;
Expand Down
4 changes: 3 additions & 1 deletion api/src/modules/impact/dto/response-scenario-scenario.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ImpactTableBaseRowsValues,
ImpactTablePurchasedTonnes,
} from 'modules/impact/dto/response-impact-table.dto';
import { GROUP_BY_VALUES } from 'modules/impact/dto/impact-table.dto';

export class ScenarioVsScenarioImpactTable {
@ApiProperty({
Expand Down Expand Up @@ -32,7 +33,7 @@ export class ScenarioVsScenarioImpactTableDataByIndicator {
@ApiProperty()
indicatorId: string;
@ApiProperty()
groupBy: string;
groupBy: GROUP_BY_VALUES;
@ApiProperty({ type: () => ScenarioVsScenarioImpactTableRows, isArray: true })
rows: ScenarioVsScenarioImpactTableRows[];
@ApiProperty({
Expand All @@ -52,6 +53,7 @@ export class ScenarioVsScenarioImpactTablePurchasedTonnes {
@ApiProperty()
isProjected: boolean;
}

export class ScenarioVsScenarioImpactTableRows {
@ApiProperty()
name: string;
Expand Down
32 changes: 19 additions & 13 deletions api/src/modules/impact/impact-report.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,21 @@ import {
Controller,
Get,
Query,
Res,
UseInterceptors,
ValidationPipe,
} from '@nestjs/common';
import { ImpactService } from 'modules/impact/impact.service';
import { ActualVsScenarioImpactService } from 'modules/impact/comparison/actual-vs-scenario.service';
import { ScenarioVsScenarioImpactService } from 'modules/impact/comparison/scenario-vs-scenario.service';
import { ApiOkResponse, ApiOperation } from '@nestjs/swagger';
import {
ImpactTable,
PaginatedImpactTable,
} from './dto/response-impact-table.dto';
import { JSONAPIPaginationQueryParams } from '../../decorators/json-api-parameters.decorator';
import { SetScenarioIdsInterceptor } from './set-scenario-ids.interceptor';
import { GetImpactTableDto } from './dto/impact-table.dto';
import { ImpactReportService } from './impact.report';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { SetScenarioIdsInterceptor } from 'modules/impact/set-scenario-ids.interceptor';
import { ImpactReportService } from 'modules/impact/impact.report';
import { Response } from 'express';
import { GetImpactTableDto } from 'modules/impact/dto/impact-table.dto';

@Controller('/api/v1/impact')
@ApiTags('Impact')
export class ImpactReportController {
constructor(
private readonly impactService: ImpactService,
Expand All @@ -30,15 +28,23 @@ export class ImpactReportController {
@ApiOperation({
description: 'Get a Impact Table CSV Report',
})
@JSONAPIPaginationQueryParams()
@UseInterceptors(SetScenarioIdsInterceptor)
@Get('table/report')
async getImpactTable(
@Query(ValidationPipe) impactTableDto: GetImpactTableDto,
): Promise<string> {
const table: any = await this.impactService.getImpactTable(impactTableDto, {
@Res() res: Response,
): Promise<void> {
const { data } = await this.impactService.getImpactTable(impactTableDto, {
disablePagination: true,
});
return this.impactReports.generateImpactReport(table);
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);
}
}
97 changes: 91 additions & 6 deletions api/src/modules/impact/impact.report.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,102 @@
import { Inject, Injectable } from '@nestjs/common';
import { IReportService } from 'modules/reports/report-service.interface';
import { ICSVReportService } from 'modules/reports/report-service.interface';
import { ReportServiceToken } from 'modules/reports/reports.module';
import {
ImpactTableDataByIndicator,
ImpactTableRows,
} from 'modules/impact/dto/response-impact-table.dto';
import { ImpactTableCSVReport } from 'modules/impact/table-reports/types';
import { ParserOptions } from '@json2csv/plainjs';
import { GROUP_BY_VALUES } from 'modules/impact/dto/impact-table.dto';

@Injectable()
export class ImpactReportService {
constructor(
@Inject(ReportServiceToken) private reportService: IReportService,
@Inject(ReportServiceToken) private reportService: ICSVReportService,
) {}

async generateImpactReport(data: any): Promise<string> {
const parserOptions: { fields: ['line', 'error'] } = {
fields: ['line', 'error'],
async generateImpactReport(
data: ImpactTableDataByIndicator[],
): Promise<string> {
const parsedData: ImpactTableCSVReport[] = this.parseToCSVShape(data);
const columnOrder: Pick<ParserOptions, 'fields'> = {
fields: this.getColumnOrder(parsedData[0]),
};
return this.reportService.generateReport(data, {});

return this.reportService.generateCSVReport(parsedData, columnOrder);
}

private parseToCSVShape(
data: ImpactTableDataByIndicator[],
): ImpactTableCSVReport[] {
const results: ImpactTableCSVReport[] = [];

data.forEach((indicator: ImpactTableDataByIndicator) => {
const unit: string = indicator.metadata.unit;
indicator.rows.forEach((row: ImpactTableRows) => {
this.processNode(
row,
indicator.indicatorShortName,
indicator.groupBy,
unit,
results,
);
});
});

return results;
}

private processNode(
node: ImpactTableRows,
indicatorName:
| Pick<ImpactTableDataByIndicator, 'indicatorShortName'>
| string,
groupBy: GROUP_BY_VALUES,
unit: string,
accumulator: ImpactTableCSVReport[],
): void {
const groupName: string = `Group by ${groupBy}`;
const targets: ImpactTableRows[] =
node.children && node.children.length > 0 ? node.children : [node];

targets.forEach((target: any) => {
const resultObject: any = {
[`Indicator`]: `${indicatorName} (${unit})`,
[groupName]: target.name,
};

if (target.values && target.values.length > 0) {
target.values.forEach((value: any) => {
let yearKey: string = value.year.toString();
if (value.isProjected) {
yearKey = yearKey.concat(' (projected)');
}
resultObject[yearKey] = value.value;
});

accumulator.push(resultObject);
}

if (target.children && target.children.length > 0) {
this.processNode(target, indicatorName, groupBy, unit, accumulator);
}
});
}

private getColumnOrder(dataSample: ImpactTableCSVReport): string[] {
const indicatorKey: string[] = [];
const groupByKey: string[] = [];
const yearKeys: string[] = [];
Object.keys(dataSample).forEach((key: string) => {
if (key.includes('Indicator')) {
groupByKey.push(key);
} else if (key.includes('Group by')) {
groupByKey.push(key);
} else {
yearKeys.push(key);
}
});
return [...indicatorKey, ...groupByKey, ...yearKeys];
}
}
3 changes: 2 additions & 1 deletion api/src/modules/impact/impact.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import {
GetImpactTableDto,
GetRankedImpactTableDto,
GROUP_BY_VALUES,
ORDER_BY,
} from 'modules/impact/dto/impact-table.dto';
import { IndicatorsService } from 'modules/indicators/indicators.service';
Expand Down Expand Up @@ -485,7 +486,7 @@ export class ImpactService {

private createImpactTableDataByIndicator(
indicator: Indicator,
groupBy: string,
groupBy: GROUP_BY_VALUES,
): ImpactTableDataByIndicator {
return {
indicatorShortName: indicator.shortName as string,
Expand Down
14 changes: 14 additions & 0 deletions api/src/modules/impact/table-reports/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
type YearData = {
[key: string]: string | number;
};

type indicatorField = {
Indicator: string;
};
type DynamicGroupByField = {
[key: `Group by ${string}`]: string;
};

export type ImpactTableCSVReport = DynamicGroupByField &
YearData &
indicatorField;
23 changes: 18 additions & 5 deletions api/src/modules/reports/csv-report.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Injectable } from '@nestjs/common';
import { IReportService } from 'modules/reports/report-service.interface';
import {
Injectable,
Logger,
ServiceUnavailableException,
} from '@nestjs/common';
import { ICSVReportService } from 'modules/reports/report-service.interface';
import { ParserOptions } from '@json2csv/plainjs';

// import { AsyncParser } from '@json2csv/node';
Expand All @@ -9,12 +13,21 @@ import { ParserOptions } from '@json2csv/plainjs';
const { AsyncParser } = require('@json2csv/node');

@Injectable()
export class CSVReportService implements IReportService {
export class CSVReportService implements ICSVReportService {
logger: Logger = new Logger(CSVReportService.name);

private getParser(parserOptions: ParserOptions): typeof AsyncParser {
return new AsyncParser(parserOptions);
}

generateReport(data: any, options: ParserOptions): Promise<string> {
return this.getParser(options).parse(data).promise();
async generateCSVReport(data: any, options: ParserOptions): Promise<string> {
try {
return this.getParser(options).parse(data).promise();
} catch (e) {
this.logger.error(`Error generating CSV from data: `, e);
throw new ServiceUnavailableException(
`Could not generate CSV Report, contact Administrator`,
);
}
}
}
6 changes: 4 additions & 2 deletions api/src/modules/reports/report-service.interface.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export interface IReportService {
generateReport(data: any, options: object): Promise<string>;
import { ParserOptions } from '@json2csv/plainjs';

export interface ICSVReportService {
generateCSVReport(data: any, options: ParserOptions): Promise<string>;
}
Loading

0 comments on commit 6c61faf

Please sign in to comment.