diff --git a/src/modules/reports/report-bookmark/report-bookmark.module.ts b/src/modules/reports/report-bookmark/report-bookmark.module.ts index 4a3a2a8..ed44501 100644 --- a/src/modules/reports/report-bookmark/report-bookmark.module.ts +++ b/src/modules/reports/report-bookmark/report-bookmark.module.ts @@ -1,9 +1,15 @@ import { Module } from '@nestjs/common'; import { ReportBookmarkController } from './report-bookmark.controller'; import { ReportBookmarkService } from './report-bookmark.service'; +import { ConfigModule } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; +import { ReportBookmarkModel } from '../shared/models/report-bookmark.model'; @Module({ - imports: [], + imports: [ + TypeOrmModule.forFeature([ReportBookmarkModel], CONNECTION_NAME.DEFAULT), + ], controllers: [ReportBookmarkController], providers: [ReportBookmarkService], }) diff --git a/src/modules/reports/report-bookmark/report-bookmark.service.ts b/src/modules/reports/report-bookmark/report-bookmark.service.ts index 4f76e2b..7cbe05f 100644 --- a/src/modules/reports/report-bookmark/report-bookmark.service.ts +++ b/src/modules/reports/report-bookmark/report-bookmark.service.ts @@ -5,15 +5,45 @@ import { GetLabelReportBookmarkDto, GetReportBookmarkDto, } from '../shared/dto/report-bookmark.get.dto'; +import { Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import { ReportBookmarkModel } from '../shared/models/report-bookmark.model'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; @Injectable() export class ReportBookmarkService extends BaseReportService { + constructor( + @InjectRepository(ReportBookmarkModel, CONNECTION_NAME.DEFAULT) + private repo: Repository, + ) { + super(); + } + async create(body: CreateReportBookmarkDto) { return 'you hit API for create report bookmark'; } async getAll(query: GetReportBookmarkDto) { - return 'you hit API for get all report bookmark'; + const modelName = ReportBookmarkModel.name; + + const requestor_id = this.userProvider.user.id; + const unique_names = query.unique_names; + const group_names = query.group_names; + + const qb = this.repo + .createQueryBuilder(modelName) + .where((query) => { + if (unique_names) { + query.andWhere(`unique_name IN (:...unique_names)`, { unique_names }); + } + if (group_names) { + query.andWhere(`group_name =IN (:...group_names)`, { group_names }); + } + query.andWhere(`requestor_id = :requestor_id`, { requestor_id }); + }) + .orderBy(`${modelName}.created_at`, 'DESC'); + + return await qb.getMany(); } async getAllLabelHistory(query: GetLabelReportBookmarkDto) { diff --git a/src/modules/reports/report-export/report-export.module.ts b/src/modules/reports/report-export/report-export.module.ts index bbd3269..0506d59 100644 --- a/src/modules/reports/report-export/report-export.module.ts +++ b/src/modules/reports/report-export/report-export.module.ts @@ -1,9 +1,18 @@ import { Module } from '@nestjs/common'; import { ReportExportController } from './report-export.controller'; import { ReportExportService } from './report-export.service'; +import { ConfigModule } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ExportReportHistoryModel } from '../shared/models/export-report-history.model'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; @Module({ - imports: [], + imports: [ + TypeOrmModule.forFeature( + [ExportReportHistoryModel], + CONNECTION_NAME.DEFAULT, + ), + ], controllers: [ReportExportController], providers: [ReportExportService], }) diff --git a/src/modules/reports/report-export/report-export.service.ts b/src/modules/reports/report-export/report-export.service.ts index c5b6322..1586329 100644 --- a/src/modules/reports/report-export/report-export.service.ts +++ b/src/modules/reports/report-export/report-export.service.ts @@ -6,9 +6,23 @@ import { GetReportExportFileNameDto, GetReportExportProcessingDto, } from '../shared/dto/report-export.get.dto'; +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { ExportReportHistoryModel } from '../shared/models/export-report-history.model'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; +import { DataSource, Repository } from 'typeorm'; @Injectable() export class ReportExportService extends BaseReportService { + constructor( + @InjectDataSource(CONNECTION_NAME.DEFAULT) + private dataSource: DataSource, + + @InjectRepository(ExportReportHistoryModel, CONNECTION_NAME.DEFAULT) + private exportHistoryRepo: Repository, + ) { + super(); + } + async create(body: CreateReportExportDto) { return 'you hit API for create report export'; } diff --git a/src/modules/reports/report/report.controller.ts b/src/modules/reports/report/report.controller.ts index 5ccbb55..315474b 100644 --- a/src/modules/reports/report/report.controller.ts +++ b/src/modules/reports/report/report.controller.ts @@ -21,4 +21,9 @@ export class ReportController { async getReportData(@Body() body: GetReportDataDto) { return await this.service.getReportData(body); } + + @Post('meta') + async getReportMeta(@Body() body: GetReportDataDto) { + return await this.service.getReportMeta(body); + } } diff --git a/src/modules/reports/report/report.module.ts b/src/modules/reports/report/report.module.ts index b624fe0..805a2d1 100644 --- a/src/modules/reports/report/report.module.ts +++ b/src/modules/reports/report/report.module.ts @@ -1,9 +1,17 @@ import { Module } from '@nestjs/common'; import { ReportController } from './report.controller'; import { ReportService } from './report.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ExportReportHistoryModel } from '../shared/models/export-report-history.model'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; @Module({ - imports: [], + imports: [ + TypeOrmModule.forFeature( + [ExportReportHistoryModel], + CONNECTION_NAME.DEFAULT, + ), + ], controllers: [ReportController], providers: [ReportService], }) diff --git a/src/modules/reports/report/report.service.ts b/src/modules/reports/report/report.service.ts index 390737d..9bcb8fe 100644 --- a/src/modules/reports/report/report.service.ts +++ b/src/modules/reports/report/report.service.ts @@ -1,15 +1,150 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { BaseReportService } from '../shared/services/base-report.service'; import { GetReportConfigDto } from '../shared/dto/report-config.get.dto'; import { GetReportDataDto } from '../shared/dto/report-data.get.dto'; +import { ReportConfigs } from '../shared/configs'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; +import { DataSource } from 'typeorm'; +import { ReportConfigEntity } from '../shared/entities/report-config.entity'; +import { ReportQueryBuilder } from '../shared/helpers'; +import { DATA_FORMAT } from '../shared/constant'; +import { roundingCurrency } from '../shared/helpers/rounding-currency'; @Injectable() export class ReportService extends BaseReportService { + private readonly logger = new Logger(ReportService.name); + + constructor( + @InjectDataSource(CONNECTION_NAME.DEFAULT) + private dataSource: DataSource, + ) { + super(); + } + async getReportConfig(query: GetReportConfigDto) { - return 'you hit API for get report config'; + const { unique_names = [], group_names = [] } = query; + + let configs = ReportConfigs; + if (group_names.length > 0) { + configs = configs.filter((item) => group_names.includes(item.group_name)); + } + + if (unique_names.length > 0) { + configs = configs.filter((item) => + unique_names.includes(item.unique_name), + ); + } + + return configs; + } + + getReportConfigByUniqueName(unique_name): ReportConfigEntity { + return ReportConfigs.find((item) => item.unique_name === unique_name); } async getReportData(body: GetReportDataDto) { - return 'you hit API for get report data'; + try { + const queryModel = body.query_model; + const reportConfig = this.getReportConfigByUniqueName(body.unique_name); + const builder = new ReportQueryBuilder(reportConfig, queryModel); + const SQL = builder.getSql(); + const queryResult = await this.dataSource.query(SQL); + + const realData = []; + const configColumns = reportConfig.column_configs; + + for (const item of queryResult) { + const realItem = {}; + for (const itemKey of Object.keys(item)) { + if (itemKey === 'count_child_group') { + const realValue = item[itemKey] ?? 0; + Object.assign(realItem, { [`${itemKey}`]: Number(realValue) }); + } else { + const confCol = configColumns.find((c) => c.column === itemKey); + const isNumber = confCol.format === DATA_FORMAT.NUMBER; + const isCurrency = confCol.format === DATA_FORMAT.CURRENCY; + const isMinusCurrency = + confCol.format === DATA_FORMAT.MINUS_CURRENCY; + const isBoolean = confCol.format === DATA_FORMAT.BOOLEAN; + const isPercentage = confCol.format === DATA_FORMAT.PERCENTAGE; + const isSetInitNull = isNumber || isCurrency || isMinusCurrency; + const isTextUpperCase = + confCol.format === DATA_FORMAT.TEXT_UPPERCASE; + const isTextLowerCase = + confCol.format === DATA_FORMAT.TEXT_LOWERCASE; + + if (isSetInitNull) { + const realValue = item[itemKey] ?? 0; + if (isCurrency) { + Object.assign(realItem, { + [`${itemKey}`]: roundingCurrency(realValue), + }); + } else if (isMinusCurrency) { + Object.assign(realItem, { + [`${itemKey}`]: roundingCurrency(realValue) * -1, + }); + } else { + Object.assign(realItem, { [`${itemKey}`]: realValue }); + } + } else if (isPercentage) { + const realValue = item[itemKey] + ? `${item[itemKey]}%` + : item[itemKey]; + Object.assign(realItem, { [`${itemKey}`]: realValue }); + } else if (isBoolean) { + let realValue = ''; + if (item[itemKey] === true || item[itemKey] === 1) + realValue = 'Yes'; + else if (item[itemKey] === false || item[itemKey] === 0) + realValue = 'No'; + Object.assign(realItem, { [`${itemKey}`]: realValue }); + } else if (isTextUpperCase) { + Object.assign(realItem, { + [`${itemKey}`]: item[itemKey]?.toUpperCase(), + }); + } else if (isTextLowerCase) { + Object.assign(realItem, { + [`${itemKey}`]: item[itemKey]?.toLowerCase(), + }); + } else { + Object.assign(realItem, { [`${itemKey}`]: item[itemKey] }); + } + } + } + realData.push(realItem); + } + + return realData; + } catch (error) { + this.logger.error(error); + throw error; + } + } + + async getReportMeta(body: GetReportDataDto) { + try { + const queryModel = body.query_model; + const reportConfig = this.getReportConfigByUniqueName(body.unique_name); + const builder = new ReportQueryBuilder(reportConfig, queryModel); + const SQL_COUNT = builder.getSqlCount(); + const queryResult = await this.dataSource.query(SQL_COUNT); + + const totalRow = parseInt(queryResult[0].count); + const startRow = queryModel.startRow; + const endRow = queryModel.endRow; + const pageSize = endRow - startRow; + + const meta = { + total_row: totalRow, + limit: pageSize, + offset: startRow, + }; + + return meta; + } catch (error) { + this.logger.error(error); + throw error; + } } } diff --git a/src/modules/reports/shared/configs/general-report/configs/sample.report.ts b/src/modules/reports/shared/configs/general-report/configs/sample.report.ts new file mode 100644 index 0000000..a832ba5 --- /dev/null +++ b/src/modules/reports/shared/configs/general-report/configs/sample.report.ts @@ -0,0 +1,40 @@ +import { DATA_FORMAT, DATA_TYPE, REPORT_GROUP } from '../../../constant'; +import { ReportConfigEntity } from '../../../entities/report-config.entity'; + +export default { + group_name: REPORT_GROUP.general_report, + unique_name: `${REPORT_GROUP.general_report}__sample`, + label: 'Sample General Report ', + table_schema: 'season_types main', + main_table_alias: 'main', + defaultOrderBy: [], + lowLevelOrderBy: [], + filter_period_config: { + hidden: true, + }, + + column_configs: [ + { + column: 'main__created_at', + query: 'main.created_at', + label: 'Created Date', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.DATE_EPOCH, + }, + { + column: 'main__updated_at', + query: 'main.updated_at', + label: 'Updated Date', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.DATE_EPOCH, + }, + { + column: 'main__name', + query: 'main.name', + label: 'Name', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + ], + filter_configs: [], +}; diff --git a/src/modules/reports/shared/configs/general-report/index.ts b/src/modules/reports/shared/configs/general-report/index.ts new file mode 100644 index 0000000..1bba05d --- /dev/null +++ b/src/modules/reports/shared/configs/general-report/index.ts @@ -0,0 +1,5 @@ +import { ReportConfigEntity } from '../../entities/report-config.entity'; + +import SampleReport from './configs/sample.report'; + +export const GeneralReportConfig: ReportConfigEntity[] = [SampleReport]; diff --git a/src/modules/reports/shared/configs/index.ts b/src/modules/reports/shared/configs/index.ts index e69de29..603161a 100644 --- a/src/modules/reports/shared/configs/index.ts +++ b/src/modules/reports/shared/configs/index.ts @@ -0,0 +1,8 @@ +import { ReportConfigEntity } from '../entities/report-config.entity'; +import { GeneralReportConfig } from './general-report'; +import { TenantReportConfig } from './tenant-report'; + +export const ReportConfigs: ReportConfigEntity[] = [ + ...GeneralReportConfig, + ...TenantReportConfig, +]; diff --git a/src/modules/reports/shared/configs/tenant-report/configs/sample.report.ts b/src/modules/reports/shared/configs/tenant-report/configs/sample.report.ts new file mode 100644 index 0000000..62f2871 --- /dev/null +++ b/src/modules/reports/shared/configs/tenant-report/configs/sample.report.ts @@ -0,0 +1,39 @@ +import { DATA_FORMAT, DATA_TYPE, REPORT_GROUP } from '../../../constant'; +import { ReportConfigEntity } from '../../../entities/report-config.entity'; + +export default { + group_name: REPORT_GROUP.tenant_report, + unique_name: `${REPORT_GROUP.tenant_report}__sample`, + label: 'Sample Tenant Report ', + table_schema: 'season_types main', + main_table_alias: 'main', + defaultOrderBy: [], + lowLevelOrderBy: [], + filter_period_config: { + hidden: true, + }, + column_configs: [ + { + column: 'main__created_at', + query: 'main.created_at', + label: 'Created Date', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.DATE_EPOCH, + }, + { + column: 'main__updated_at', + query: 'main.updated_at', + label: 'Updated Date', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.DATE_EPOCH, + }, + { + column: 'main__name', + query: 'main.name', + label: 'Name', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + ], + filter_configs: [], +}; diff --git a/src/modules/reports/shared/configs/tenant-report/index.ts b/src/modules/reports/shared/configs/tenant-report/index.ts new file mode 100644 index 0000000..dfe07b1 --- /dev/null +++ b/src/modules/reports/shared/configs/tenant-report/index.ts @@ -0,0 +1,4 @@ +import { ReportConfigEntity } from '../../entities/report-config.entity'; +import SampleReport from './configs/sample.report'; + +export const TenantReportConfig: ReportConfigEntity[] = [SampleReport]; diff --git a/src/modules/reports/shared/constant/report-group.constant.ts b/src/modules/reports/shared/constant/report-group.constant.ts index 80476fa..3fc0b60 100644 --- a/src/modules/reports/shared/constant/report-group.constant.ts +++ b/src/modules/reports/shared/constant/report-group.constant.ts @@ -1,5 +1,6 @@ export enum REPORT_GROUP { // PATTERN => MODULE__MENU__SUB_MENU // EXAMPLE: - contact__reports = 'contact__reports', + general_report = 'general_report', + tenant_report = 'tenant_report', } diff --git a/src/modules/reports/shared/dto/report-data.get.dto.ts b/src/modules/reports/shared/dto/report-data.get.dto.ts index a66b84b..b33da70 100644 --- a/src/modules/reports/shared/dto/report-data.get.dto.ts +++ b/src/modules/reports/shared/dto/report-data.get.dto.ts @@ -1,13 +1,22 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsObject, IsString, ValidateIf } from 'class-validator'; import { QueryModelEntity } from '../entities/query-model.entity'; +import { REPORT_GROUP } from '../constant'; export class GetReportDataDto { - @ApiProperty({ name: 'group_name', required: true }) + @ApiProperty({ + name: 'group_name', + required: true, + default: REPORT_GROUP.general_report, + }) @IsString() group_name: string; - @ApiProperty({ name: 'unique_name', required: true }) + @ApiProperty({ + name: 'unique_name', + required: true, + default: `${REPORT_GROUP.general_report}__sample`, + }) @IsString() unique_name: string; @@ -15,6 +24,17 @@ export class GetReportDataDto { name: 'query_model', type: Object, required: true, + default: { + startRow: 0, + endRow: 100, + rowGroupCols: [], + valueCols: [], + pivotCols: [], + pivotMode: true, + groupKeys: [], + filterModel: {}, + sortModel: [], + }, }) @IsObject() @ValidateIf((body) => body.query_model) diff --git a/src/modules/reports/shared/entities/report-config.entity.ts b/src/modules/reports/shared/entities/report-config.entity.ts new file mode 100644 index 0000000..bca0f5c --- /dev/null +++ b/src/modules/reports/shared/entities/report-config.entity.ts @@ -0,0 +1,59 @@ +import { DATA_FORMAT, DATA_TYPE, FILTER_TYPE } from '../constant'; + +export interface ReportColumnConfigEntity { + column: string; + query: string; + label: string; + type: DATA_TYPE; + format: DATA_FORMAT; + date_format?: string; +} + +export interface FilterConfigEntity { + filter_column: string; + filter_type: FILTER_TYPE; + + filed_label: string; + field_type: string; + hide_field?: boolean; + + select_data_source_url?: string; + select_custom_options?: string[]; + select_value_key?: string; + select_label_key?: string; +} + +export interface FilterPeriodConfigEntity { + key: string; + type: FILTER_TYPE; + note?: string; + hidden?: boolean; +} + +export interface ReportConfigEntity { + group_name: string; + unique_name: string; + label: string; + + table_schema: string; + main_table_alias?: string; + customVirtualTableSchema?( + filterModel: any, + findQueryConfig: (column: string) => string, + createFilterSql: (key: string, item: any) => string, + ): string; + whereCondition?(filterModel: any): string[]; + whereDefaultConditions?: { + column: string; + filter_type: FILTER_TYPE; + values: string[]; + }[]; + defaultOrderBy?: string[]; + lowLevelOrderBy?: string[]; + + column_configs: ReportColumnConfigEntity[]; + filter_configs?: FilterConfigEntity[]; + filter_period_config?: FilterPeriodConfigEntity; + ignore_filter_keys?: string[]; + customQueryColumn?(column: string): string; +} diff --git a/src/modules/reports/shared/helpers/index.ts b/src/modules/reports/shared/helpers/index.ts new file mode 100644 index 0000000..0fe5354 --- /dev/null +++ b/src/modules/reports/shared/helpers/index.ts @@ -0,0 +1 @@ +export * from './query-builder'; diff --git a/src/modules/reports/shared/helpers/query-builder.ts b/src/modules/reports/shared/helpers/query-builder.ts new file mode 100644 index 0000000..825b76f --- /dev/null +++ b/src/modules/reports/shared/helpers/query-builder.ts @@ -0,0 +1,520 @@ +import { FILTER_TYPE } from '../constant'; +import { QueryModelEntity } from '../entities/query-model.entity'; +import { + ReportColumnConfigEntity, + ReportConfigEntity, +} from '../entities/report-config.entity'; + +export class ReportQueryBuilder { + public reportConfig: ReportConfigEntity; + public queryModel: QueryModelEntity; + + constructor(reportConfig: ReportConfigEntity, queryModel: QueryModelEntity) { + this.reportConfig = reportConfig; + this.queryModel = queryModel; + } + + getBaseConfig() { + const tableSchema = this.reportConfig.table_schema; + const mainTableAlias = this.reportConfig.main_table_alias ?? 'main'; + + const selectSql = this.createSelectSql(); + const selectSqlExport = this.createSelectSqlExport(); + + const whereSql = this.createWhereSql(); + const limitSql = this.createLimitSql(); + + const orderBySql = this.createOrderBySql(); + const orderBySqlExport = this.createOrderBySqlExport(); + + const groupBy = this.createGroupBySql(); + const groupByExport = this.createGroupBySqlExport(); + + return { + tableSchema, + mainTableAlias, + selectSql, + selectSqlExport, + whereSql, + limitSql, + orderBySql, + orderBySqlExport, + ...groupBy, + ...groupByExport, + }; + } + + getSql(): string { + const { + selectSql, + tableSchema, + whereSql, + groupByQuery, + orderBySql, + limitSql, + } = this.getBaseConfig(); + return `SELECT ${selectSql} FROM ${tableSchema} ${whereSql} ${groupByQuery} ${orderBySql} ${limitSql}` as string; + } + + getSqlCount(): string { + const { groupByColumn, mainTableAlias, tableSchema, whereSql } = + this.getBaseConfig(); + + return `SELECT COUNT(${ + groupByColumn ? `DISTINCT ${groupByColumn}` : `${mainTableAlias}.id` + }) ${ + groupByColumn + ? `+ COUNT(DISTINCT CASE WHEN ${groupByColumn} IS NULL THEN 1 END)` + : '' + } AS count FROM ${tableSchema} ${whereSql}` as string; + } + + getSqlExport(): string { + const { + selectSqlExport, + tableSchema, + whereSql, + groupByQueryExport, + orderBySqlExport, + } = this.getBaseConfig(); + return `SELECT ${selectSqlExport} FROM ${tableSchema} ${whereSql} ${groupByQueryExport} ${orderBySqlExport}` as string; + } + + getSqlCountExport(): string { + const { groupByColumnExport, mainTableAlias, tableSchema, whereSql } = + this.getBaseConfig(); + return `SELECT COUNT(${ + groupByColumnExport + ? `DISTINCT ${groupByColumnExport}` + : `${mainTableAlias}.id` + }) ${ + groupByColumnExport + ? `+ COUNT(DISTINCT CASE WHEN ${groupByColumnExport} IS NULL THEN 1 END)` + : '' + } AS count FROM ${tableSchema} ${whereSql}` as string; + } + + isDoingGrouping(rowGroupCols, groupKeys) { + // we are not doing grouping if at the lowest level. we are at the lowest level + // if we are grouping by more columns than we have keys for (that means the user + // has not expanded a lowest level group, OR we are not grouping at all). + return rowGroupCols.length > groupKeys.length; + } + + interpolate(str, o) { + return str.replace(/{([^{}]*)}/g, function (a, b) { + const r = o[b]; + return typeof r === 'string' || typeof r === 'number' ? r : a; + }); + } + + createLimitSql() { + const startRow = this.queryModel.startRow; + const endRow = this.queryModel.endRow; + const pageSize = endRow - startRow; + return ' LIMIT ' + (pageSize + 1) + ' OFFSET ' + startRow; + } + + getRowCount(results) { + if (results === null || results === undefined || results.length === 0) { + return null; + } + const currentLastRow = this.queryModel.startRow + results.length; + return currentLastRow <= this.queryModel.endRow ? currentLastRow : -1; + } + + cutResultsToPageSize(results) { + const pageSize = this.queryModel.endRow - this.queryModel.startRow; + if (results && results.length > pageSize) { + return results.splice(0, pageSize); + } else { + return results; + } + } + + findQueryConfig(column: string) { + const configColumns = this.reportConfig.column_configs ?? []; + + const queryColumn: ReportColumnConfigEntity = configColumns.find( + (el) => el.column === column, + ); + const customQueryColumn = this.reportConfig?.customQueryColumn + ? this.reportConfig.customQueryColumn(column) + : undefined; + + if (customQueryColumn) return customQueryColumn; + else if (queryColumn) return queryColumn.query; + + return column.replace('__', '.'); + } + + // GENERATE SELECT QUERY ============================================ + createSelectSql(): string { + const configColumns = this.reportConfig.column_configs; + const mainTableAlias = this.reportConfig.main_table_alias ?? 'main'; + + const rowGroupCols = this.queryModel.rowGroupCols; + const valueCols = this.queryModel.valueCols; + const groupKeys = this.queryModel.groupKeys; + + if (this.isDoingGrouping(rowGroupCols, groupKeys)) { + const colsToSelect = []; + const rowGroupCol = rowGroupCols[groupKeys.length]; + const rowGroupColChildCount = rowGroupCols[groupKeys.length + 1]; + + colsToSelect.push( + this.findQueryConfig(rowGroupCol.field) + ` AS ${rowGroupCol.field}`, + ); + + if (rowGroupColChildCount) { + colsToSelect.push( + `COUNT(DISTINCT ${this.findQueryConfig( + rowGroupColChildCount.field, + )}) AS count_child_group`, + ); + } else { + colsToSelect.push(`COUNT(${mainTableAlias}.id) AS count_child_group`); + } + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const thisSelf = this; + valueCols.forEach(function (valueCol) { + colsToSelect.push( + `${valueCol.aggFunc} (${thisSelf.findQueryConfig( + valueCol.field, + )}) AS ${valueCol.field}`, + ); + }); + return colsToSelect.join(', '); + } + + const columns = configColumns.map( + (item) => `${item.query} AS ${item.column}`, + ); + + return columns.join(', '); + } + + createSelectSqlExport(): string { + const configColumns = this.reportConfig.column_configs; + + const rowGroupCols = this.queryModel.rowGroupCols; + const valueCols = this.queryModel.valueCols; + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const thisSelf = this; + + if (rowGroupCols.length > 0) { + const colsToSelect = []; + rowGroupCols.forEach(function (rowGroupCol) { + colsToSelect.push( + thisSelf.findQueryConfig(rowGroupCol.field) + + ` AS ${rowGroupCol.field}`, + ); + }); + + valueCols.forEach(function (valueCol) { + colsToSelect.push( + `${valueCol.aggFunc} (${thisSelf.findQueryConfig( + valueCol.field, + )}) AS ${valueCol.field}`, + ); + }); + + return colsToSelect.join(', '); + } + + const columns = configColumns.map((item) => `${item.query} ${item.column}`); + + return columns.join(', '); + } + // ================================================================== + + // GENERATE WHERE QUERY ============================================= + createFilterSql(column: string, item: { type: FILTER_TYPE; filter: any }) { + switch (item.type) { + // TEXT + case FILTER_TYPE.TEXT_EQUAL: + return `${column} = '${item.filter}'`; + + case FILTER_TYPE.TEXT_NOT_EQUAL: + return `${column} != '${item.filter}'`; + + case FILTER_TYPE.TEXT_CONTAINS: + return `${column} ILIKE '%${item.filter}%'`; + + case FILTER_TYPE.TEXT_NOT_CONTAINS: + return `${column} NOT ILIKE '%${item.filter}%'`; + + case FILTER_TYPE.TEXT_START_WITH: + return `${column} ILIKE '${item.filter}%'`; + + case FILTER_TYPE.TEXT_END_WITH: + return `${column} ILIKE '%${item.filter}'`; + + case FILTER_TYPE.TEXT_IN_MEMBER: + return item.filter?.length > 0 + ? `${column} IN(${item.filter.map((i) => `'${i}'`).join(', ')})` + : null; + + case FILTER_TYPE.TEXT_MULTIPLE_CONTAINS: + return item.filter?.length > 0 + ? `${column} ILIKE ANY(ARRAY[${item.filter + .map((i) => `'%${i}%'`) + .join(', ')}])` + : null; + + case FILTER_TYPE.TEXT_MULTIPLE_REGEXP_CONTAINS: + return item.filter?.length > 0 + ? `${column} REGEXP '${item.filter.join('|')}'` + : null; + + case FILTER_TYPE.DATE_IN_RANGE_EPOCH: + return `(${column} >= ${item.filter[0]} AND ${column} <= ${item.filter[1]})`; + + case FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP: + return `(${column} BETWEEN '${item.filter[0]}' AND '${item.filter[1]}')`; + + case FILTER_TYPE.TEXT_IN_RANGE: + return `(${column} >= '${item.filter[0]}' AND ${column} <= '${item.filter[1]}')`; + + // NUMBER + case FILTER_TYPE.NUMBER_EQUAL: + return `${column} = ${item.filter}`; + + case FILTER_TYPE.NUMBER_NOT_EQUAL: + return `${column} != ${item.filter}`; + + case FILTER_TYPE.NUMBER_GREATER_THAN: + return `${column} > ${item.filter}`; + + case FILTER_TYPE.NUMBER_GREATER_THAN_OR_EQUAL: + return `${column} >= ${item.filter}`; + + case FILTER_TYPE.NUMBER_LESS_THAN: + return `${column} < ${item.filter}`; + + case FILTER_TYPE.NUMBER_LESS_THAN_OR_EQUAL: + return `${column} <= ${item.filter}`; + + case FILTER_TYPE.NUMBER_IN_RANGE: + return `(${column} >= ${item.filter[0]} AND ${column} <= ${item.filter[1]})`; + + default: + console.log('UNKNOWN FILTER TYPE:', item.type); + return 'true'; + } + } + + createWhereSql() { + const configFilters = this.reportConfig.filter_configs ?? []; + const ignoreFilterKeys = this.reportConfig.ignore_filter_keys ?? []; + const ignoreFilter = configFilters + .filter((el) => el.hide_field) + .map((el) => el.filter_column); + + const ignoreFilterKey = [...ignoreFilter, ...ignoreFilterKeys]; + + const whereCondition = this.reportConfig?.whereCondition + ? this.reportConfig.whereCondition(this.queryModel.filterModel) + : []; + + const rowGroupCols = this.queryModel.rowGroupCols; + const groupKeys = this.queryModel.groupKeys; + const filterModel = this.queryModel.filterModel; + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const thisSelf = this; + + const whereParts = []; + if (groupKeys.length > 0) { + groupKeys.forEach(function (key, index) { + const colName = rowGroupCols[index].field; + // whereParts.push(colName + ' = "' + key + '"'); + whereParts.push(`${thisSelf.findQueryConfig(colName)} = '${key}'`); + }); + } + + if (filterModel) { + const keySet = Object.keys(filterModel); + keySet.forEach(function (key) { + if (!ignoreFilterKey.includes(key)) { + const item = filterModel[key]; + const newKey = thisSelf.findQueryConfig(key); + whereParts.push(thisSelf.createFilterSql(newKey, item)); + } + }); + } + + // set default where conditions + const defaultConditions = this.reportConfig.whereDefaultConditions; + const defaultWhereOptions = []; + if (defaultConditions) { + defaultConditions.forEach((condition) => { + defaultWhereOptions.push( + this.createFilterSql(condition.column, { + type: condition.filter_type, + filter: condition.values, + }), + ); + }); + } + + const tableWhereConditions = [...whereCondition, ...whereParts]; + const defaultWhereConditions = defaultWhereOptions; + + if (tableWhereConditions.length > 0) { + return `WHERE (${ + defaultWhereConditions.length + ? defaultWhereConditions?.filter(Boolean).join(' AND ') + ' AND ' + : ' ' + } ${tableWhereConditions.filter(Boolean).join(' AND ')})`; + } else if (defaultWhereConditions.length) { + return `WHERE (${defaultWhereConditions.filter(Boolean).join(' AND ')})`; + } else { + return ''; + } + } + // ================================================================== + + // GENERATE ORDER QUERY ============================================= + createOrderBySql() { + const mainTableAlias = this.reportConfig.main_table_alias ?? 'main'; + const defaultOrderBy = this.reportConfig.defaultOrderBy ?? []; + const lowLevelOrderBy = this.reportConfig.lowLevelOrderBy ?? []; + + const rowGroupCols = this.queryModel.rowGroupCols; + const groupKeys = this.queryModel.groupKeys; + const sortModel = this.queryModel.sortModel; + + const grouping = this.isDoingGrouping(rowGroupCols, groupKeys); + + const sortParts = []; + if (sortModel) { + const groupColIds = rowGroupCols + .map((groupCol) => groupCol.id) + .slice(0, groupKeys.length + 1); + + sortModel.forEach(function (item) { + if (grouping && groupColIds.indexOf(item.colId) < 0) { + // ignore + } else { + sortParts.push(item.colId + ' ' + item.sort); + } + }); + } + + const defaultOrder = defaultOrderBy[0] + ? defaultOrderBy + : [`${mainTableAlias}.created_at DESC`]; + + const lowLevelOrder = lowLevelOrderBy[0] + ? lowLevelOrderBy + : [`${mainTableAlias}.id DESC`]; + + if (sortParts.length > 0) { + if (rowGroupCols?.length > 0) { + if (groupKeys.length > 0) { + const sortBy = sortParts[groupKeys.length]; + if (sortBy) return ' ORDER BY ' + sortBy; + // return ''; + } + + return ' ORDER BY ' + sortParts.join(', '); + } + return ( + ' ORDER BY ' + sortParts.join(', ') + `, ${lowLevelOrder.join(', ')}` + ); + } else { + if (rowGroupCols?.length > 0) { + const sortGroupData = rowGroupCols[groupKeys.length]; + if (sortGroupData) return ' ORDER BY ' + `${sortGroupData['id']} DESC`; + // return ''; + } + + return ( + ` ORDER BY ` + defaultOrder.join(', ') + `, ${lowLevelOrder.join(', ')}` + ); + } + } + + createOrderBySqlExport() { + const mainTableAlias = this.reportConfig.main_table_alias ?? 'main'; + const defaultOrderBy = this.reportConfig.defaultOrderBy ?? []; + const lowLevelOrderBy = this.reportConfig.lowLevelOrderBy ?? []; + + const rowGroupCols = this.queryModel.rowGroupCols; + const sortModel = this.queryModel.sortModel; + + const defaultOrder = defaultOrderBy[0] + ? defaultOrderBy + : [`${mainTableAlias}.created_at DESC`]; + + const lowLevelOrder = lowLevelOrderBy[0] + ? lowLevelOrderBy + : [`${mainTableAlias}.id DESC`]; + + if (sortModel.length > 0) { + const sortParts = sortModel.map((i) => `${i.colId} ${i.sort}`); + return ' ORDER BY ' + `${sortParts.join(', ')}`; + } else { + if (rowGroupCols?.length > 0) { + const sortParts = rowGroupCols.map((i) => `${i.id} DESC`); + return ' ORDER BY ' + `${sortParts.join(', ')}`; + } + + return ( + ` ORDER BY ` + defaultOrder.join(', ') + `, ${lowLevelOrder.join(', ')}` + ); + } + } + // ================================================================== + + // GENERATE GROUP BY QUERY ========================================== + createGroupBySql() { + const rowGroupCols = this.queryModel.rowGroupCols; + const groupKeys = this.queryModel.groupKeys; + + if (this.isDoingGrouping(rowGroupCols, groupKeys)) { + const colsToGroupBy = []; + + const rowGroupCol = rowGroupCols[groupKeys.length]; + colsToGroupBy.push(this.findQueryConfig(rowGroupCol.field)); + + return { + groupByQuery: ' GROUP BY ' + colsToGroupBy.join(', '), + groupByColumn: colsToGroupBy.join(', '), + }; + } else { + return { + groupByQuery: '', + groupByColumn: null, + }; + } + } + + createGroupBySqlExport() { + const rowGroupCols = this.queryModel.rowGroupCols; + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const thisSelf = this; + + if (rowGroupCols.length > 0) { + const colsToGroupBy = []; + rowGroupCols.forEach(function (rowGroupCol) { + colsToGroupBy.push(thisSelf.findQueryConfig(rowGroupCol.field)); + }); + + return { + groupByQueryExport: ' GROUP BY ' + colsToGroupBy.join(', '), + groupByColumnExport: colsToGroupBy.join(', '), + }; + } else { + return { + groupByQueryExport: '', + groupByColumnExport: null, + }; + } + } + // ================================================================== +} diff --git a/src/modules/reports/shared/helpers/rounding-currency.ts b/src/modules/reports/shared/helpers/rounding-currency.ts new file mode 100644 index 0000000..323239b --- /dev/null +++ b/src/modules/reports/shared/helpers/rounding-currency.ts @@ -0,0 +1,4 @@ +export function roundingCurrency(value) { + if (!value) return value; + return Number(value).toFixed(2); +}