diff --git a/configs/main-config.jsonc.dist b/configs/main-config.jsonc.dist index a42a743..9e26c4d 100644 --- a/configs/main-config.jsonc.dist +++ b/configs/main-config.jsonc.dist @@ -3,6 +3,7 @@ "dbs": { "changes": "", "userMetaInfo": "", + "eccmDailyReports": "" } }, "telegramBotToken": "", diff --git a/package-lock.json b/package-lock.json index bc367dd..6e0d453 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "handlebars": "^4.7.7", "hbs": "^4.2.0", "imap-simple": "^5.1.0", + "luxon": "^3.1.0", "nano": "^10.0.0", "node-telegram-bot-api": "^0.59.0", "reflect-metadata": "^0.1.13", @@ -36,6 +37,7 @@ "@types/cache-manager": "^4.0.1", "@types/express": "^4.17.13", "@types/jest": "27.4.0", + "@types/luxon": "^3.1.0", "@types/node": "^16.0.0", "@types/node-telegram-bot-api": "^0.57.1", "@types/supertest": "^2.0.11", @@ -2088,6 +2090,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/luxon": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.1.0.tgz", + "integrity": "sha512-gCd/HcCgjqSxfMrgtqxCgYk/22NBQfypwFUG7ZAyG/4pqs51WLTcUzVp1hqTbieDYeHS3WoVEh2Yv/2l+7B0Vg==", + "dev": true + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -7195,6 +7203,14 @@ "node": ">=12" } }, + "node_modules/luxon": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.1.0.tgz", + "integrity": "sha512-7w6hmKC0/aoWnEsmPCu5Br54BmbmUp5GfcqBxQngRcXJ+q5fdfjEzn7dxmJh2YdDhgW8PccYtlWKSv4tQkrTQg==", + "engines": { + "node": ">=12" + } + }, "node_modules/macos-release": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.0.tgz", @@ -11901,6 +11917,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "@types/luxon": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.1.0.tgz", + "integrity": "sha512-gCd/HcCgjqSxfMrgtqxCgYk/22NBQfypwFUG7ZAyG/4pqs51WLTcUzVp1hqTbieDYeHS3WoVEh2Yv/2l+7B0Vg==", + "dev": true + }, "@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -15760,6 +15782,11 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.13.2.tgz", "integrity": "sha512-VJL3nIpA79TodY/ctmZEfhASgqekbT574/c4j3jn4bKXbSCnTTCH/KltZyvL2GlV+tGSMtsWyem8DCX7qKTMBA==" }, + "luxon": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.1.0.tgz", + "integrity": "sha512-7w6hmKC0/aoWnEsmPCu5Br54BmbmUp5GfcqBxQngRcXJ+q5fdfjEzn7dxmJh2YdDhgW8PccYtlWKSv4tQkrTQg==" + }, "macos-release": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.0.tgz", diff --git a/package.json b/package.json index 4bfddfc..54839ed 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "handlebars": "^4.7.7", "hbs": "^4.2.0", "imap-simple": "^5.1.0", + "luxon": "^3.1.0", "nano": "^10.0.0", "node-telegram-bot-api": "^0.59.0", "reflect-metadata": "^0.1.13", @@ -48,6 +49,7 @@ "@types/cache-manager": "^4.0.1", "@types/express": "^4.17.13", "@types/jest": "27.4.0", + "@types/luxon": "^3.1.0", "@types/node": "^16.0.0", "@types/node-telegram-bot-api": "^0.57.1", "@types/supertest": "^2.0.11", diff --git a/src/app.module.ts b/src/app.module.ts index 46f35c8..0b6f99b 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -29,6 +29,7 @@ import { CurrentIssuesEccmReportController } from './reports/current-issues-eccm import { DailyEccmReportController } from './reports/daily-eccm.report.controller'; import { DailyEccmReportService } from './reports/daily-eccm.report.service'; import { ChangesService } from './changes/changes.service'; +import { DailyEccmReportsDatasource } from './couchdb-datasources/daily-eccm-reports.datasource'; @Module({ imports: [ @@ -64,6 +65,7 @@ import { ChangesService } from './changes/changes.service'; CurrentIssuesEccmBotHandlerService, DailyEccmReportService, ChangesService, + DailyEccmReportsDatasource, ], }) export class AppModule implements OnModuleInit { @@ -88,6 +90,7 @@ export class AppModule implements OnModuleInit { Users.getDatasource(); Changes.getDatasource(); UserMetaInfo.getDatasource(); + DailyEccmReportsDatasource.getDatasource(); this.enhancerService.addEnhancer([ this.timestampEnhancer, diff --git a/src/couchdb-datasources/daily-eccm-reports.datasource.ts b/src/couchdb-datasources/daily-eccm-reports.datasource.ts new file mode 100644 index 0000000..8ff3f22 --- /dev/null +++ b/src/couchdb-datasources/daily-eccm-reports.datasource.ts @@ -0,0 +1,41 @@ +import { CouchDb } from '@app/event-emitter/couchdb-datasources/couchdb'; +import { Injectable, Logger } from '@nestjs/common'; +import nano from 'nano'; +import { DailyEccmReport } from 'src/reports/daily-eccm.report.service'; +import configuration from '../configs/app'; + +const config = configuration(); + +@Injectable() +export class DailyEccmReportsDatasource { + private static logger = new Logger(DailyEccmReportsDatasource.name); + private static db = null; + private static initilized = false; + + static async getDatasource(): Promise< + nano.DocumentScope + > { + if (DailyEccmReportsDatasource.initilized) { + return DailyEccmReportsDatasource.db; + } + DailyEccmReportsDatasource.initilized = true; + const n = CouchDb.getCouchDb(); + const dbName = config.couchDb.dbs.eccmDailyReports; + const dbs = await n.db.list(); + if (!dbs.includes(dbName)) { + await n.db.create(dbName); + } + DailyEccmReportsDatasource.db = await n.db.use(dbName); + DailyEccmReportsDatasource.initilized = true; + DailyEccmReportsDatasource.logger.log( + `Connected to eccm-daily-reports db - ${dbName}`, + ); + return DailyEccmReportsDatasource.db; + } + + async getDatasource(): Promise< + nano.DocumentScope + > { + return await DailyEccmReportsDatasource.getDatasource(); + } +} diff --git a/src/models/app-config.model.ts b/src/models/app-config.model.ts index 2f04e56..b1352af 100644 --- a/src/models/app-config.model.ts +++ b/src/models/app-config.model.ts @@ -13,6 +13,7 @@ export type AppConfig = { dbs: { changes: string; userMetaInfo: string; + eccmDailyReports: string; }; }; telegramBotToken: string; diff --git a/src/reports/daily-eccm.report.controller.ts b/src/reports/daily-eccm.report.controller.ts index 8721885..3c9f971 100644 --- a/src/reports/daily-eccm.report.controller.ts +++ b/src/reports/daily-eccm.report.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Query, Render } from '@nestjs/common'; +import { Controller, Get, Param, Query, Render } from '@nestjs/common'; import { DailyEccmReport, DailyEccmReportService, @@ -10,28 +10,61 @@ export class DailyEccmReportController { @Get() @Render('daily-eccm-report') + async getReportDefault( + @Query('from') from: string, + @Query('to') to: string, + ): Promise { + return await this.getReport(from, to); + } + + @Get('/delete/:name') + async deleteReport(@Param('name') name: string): Promise { + return await this.dailyEccmReportService.deleteReport(name); + } + + @Get('/load/:name') + @Render('daily-eccm-report') + async loadReport( + @Param('name') name: string, + ): Promise { + return await this.dailyEccmReportService.loadReport(name); + } + + @Get('/load/:name/raw') + async loadReportRawData( + @Param('name') name: string, + ): Promise { + return await this.dailyEccmReportService.loadReport(name); + } + + @Get('/generate') + @Render('daily-eccm-report') async getReport( @Query('from') from: string, @Query('to') to: string, + @Query('name') name?: string, + @Query('overwrite') overwrite?: boolean, ): Promise { - const now = new Date().toISOString(); - return await this.dailyEccmReportService.generateReport({ - from: from, - to: to, - reportDate: now, - }); + return await this.getReportRawData(from, to, name, overwrite); } - @Get('/raw') + @Get('/generate/raw') async getReportRawData( @Query('from') from: string, @Query('to') to: string, + @Query('name') name?: string, + @Query('overwrite') overwrite?: boolean, ): Promise { - const now = new Date().toISOString(); - return await this.dailyEccmReportService.generateReport({ - from: from, - to: to, - reportDate: now, - }); + const params = this.dailyEccmReportService.generateParams(from, to, name); + const data = await this.dailyEccmReportService.generateReport(params); + if (name) { + const saveResult = await this.dailyEccmReportService.saveReport( + data, + overwrite, + ); + return saveResult ? data : null; + } else { + return data; + } } } diff --git a/src/reports/daily-eccm.report.service.ts b/src/reports/daily-eccm.report.service.ts index f249cd8..45010e6 100644 --- a/src/reports/daily-eccm.report.service.ts +++ b/src/reports/daily-eccm.report.service.ts @@ -10,6 +10,10 @@ import { TimestampConverter } from '@app/event-emitter/utils/timestamp-converter import { IssuesService } from '@app/event-emitter/issues/issues.service'; import { UsersService } from '@app/event-emitter/users/users.service'; import { GetParentsHint } from '@app/event-emitter/utils/get-parents-hint'; +import { DailyEccmReportsDatasource } from 'src/couchdb-datasources/daily-eccm-reports.datasource'; +import { Timestamped } from '@app/event-emitter/models/timestamped'; +import { TimestampNowFill } from '@app/event-emitter/utils/timestamp-now-fill'; +import { DateTime } from 'luxon'; // eslint-disable-next-line @typescript-eslint/no-namespace export namespace DailyEccmReport { @@ -96,7 +100,9 @@ export namespace DailyEccmReport { export type Params = { from: string; to: string; - reportDate: string; + name: string; + project: string; + versions: string[]; }; export type IssueAndChanges = { @@ -140,28 +146,32 @@ export namespace DailyEccmReport { private eccmUsers: RedmineTypes.User[], private issuesService: IssuesService, ) { - const users: Models.UserReport[] = this.eccmUsers.map((user) => { - if (!user) { - this.logger.error(`Not found user data`); - return; - } - const u: Models.User = { - id: user.id, - lastname: user.lastname, - firstname: user.firstname, - }; - return { - user: u, - activities: [], - issuesGroupedByStatus: [], - } as Models.UserReport; - }).filter((user) => Boolean(user)); + const users: Models.UserReport[] = this.eccmUsers + .map((user) => { + if (!user) { + this.logger.error(`Not found user data`); + return; + } + const u: Models.User = { + id: user.id, + lastname: user.lastname, + firstname: user.firstname, + }; + return { + user: u, + activities: [], + issuesGroupedByStatus: [], + } as Models.UserReport; + }) + .filter((user) => Boolean(user)); this.report = { byUsers: users, params: { from: '', to: '', - reportDate: '', + name: '', + project: '', + versions: [], }, }; } @@ -173,7 +183,9 @@ export namespace DailyEccmReport { setParams(params: Models.Params): void { this.report.params.from = params.from; this.report.params.to = params.to; - this.report.params.reportDate = params.reportDate; + this.report.params.name = params.name; + this.report.params.versions = [...params.versions]; + this.report.params.project = params.project; } async setCurrentIssues(issues: RedmineTypes.Issue[]): Promise { @@ -355,6 +367,7 @@ export class DailyEccmReportService { private changesService: ChangesService, private issuesService: IssuesService, private usersService: UsersService, + private dailyEccmReportsDatasource: DailyEccmReportsDatasource, ) { this.eccmConfig = this.configService.get('redmineEccm'); } @@ -389,6 +402,83 @@ export class DailyEccmReportService { return report.get(); } + generateParams( + from: string, + to: string, + name?: string, + ): DailyEccmReport.Models.Params { + const fromDate = DateTime.fromISO(from); + if (!fromDate.isValid) throw new Error('from is invalid date'); + const toDate = DateTime.fromISO(to); + if (!toDate.isValid) throw new Error('to is invalid date'); + let nameValue: string | null = name || null; + if (!nameValue) { + nameValue = DateTime.now().toFormat('yyyy-MM-dd'); + } + return { + from: fromDate.toISO(), + to: toDate.toISO(), + name: nameValue, + project: this.eccmConfig.projectName, + versions: [...this.eccmConfig.currentVersions], + }; + } + + async saveReport( + report: DailyEccmReport.Models.Report, + overwrite?: boolean, + ): Promise { + const name = report.params.name; + const existsReport = await this.loadReport(name); + if (existsReport && !overwrite) { + return false; + } + + type ReportType = nano.DocumentGetResponse & + DailyEccmReport.Models.Report & + Timestamped; + + const newReport: ReportType = TimestampNowFill({ + ...report, + _id: name, + _rev: existsReport?._rev, + }); + + const datasource = await this.dailyEccmReportsDatasource.getDatasource(); + await datasource.insert(newReport); + + return true; + } + + async loadReport( + name: string, + ): Promise< + | (DailyEccmReport.Models.Report & nano.DocumentGetResponse & Timestamped) + | null + > { + const datasource = await this.dailyEccmReportsDatasource.getDatasource(); + let resp: any = null; + try { + resp = await datasource.get(name); + } catch (ex) { + this.logger.warn( + `Cannot load report ${name} with error message ${ex.message}`, + ); + } + return resp; + } + + async deleteReport(name: string): Promise { + const datasource = await this.dailyEccmReportsDatasource.getDatasource(); + const report = await this.loadReport(name); + let res = false; + try { + await datasource.destroy(report._id, report._rev); + res = true; + } catch (ex) {} + return res; + } + private async createEmptyReport(): Promise { const usersList: string[] = this.eccmConfig.groups.reduce((acc, group) => { group.people.forEach((fullname) => { diff --git a/views/daily-eccm-report.hbs b/views/daily-eccm-report.hbs index 691ee45..02c752f 100644 --- a/views/daily-eccm-report.hbs +++ b/views/daily-eccm-report.hbs @@ -10,7 +10,9 @@
  • От - {{this.params.from}}
  • До - {{this.params.to}}
  • -
  • Дата выгрузки - {{this.params.reportDate}}
  • +
  • Имя отчёта - {{this.params.name}}
  • +
  • Имя проекта - {{this.params.project}}
  • +
  • Версии - {{this.params.versions}}

Отчёт по работникам