Добавлено сохранение отчёта для дейли и загрузка

This commit is contained in:
Pavel Gnedov 2022-11-12 20:24:41 +07:00
parent ffc6757b5e
commit bfb6c29138
9 changed files with 234 additions and 34 deletions

View file

@ -3,6 +3,7 @@
"dbs": { "dbs": {
"changes": "", "changes": "",
"userMetaInfo": "", "userMetaInfo": "",
"eccmDailyReports": ""
} }
}, },
"telegramBotToken": "", "telegramBotToken": "",

27
package-lock.json generated
View file

@ -21,6 +21,7 @@
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"hbs": "^4.2.0", "hbs": "^4.2.0",
"imap-simple": "^5.1.0", "imap-simple": "^5.1.0",
"luxon": "^3.1.0",
"nano": "^10.0.0", "nano": "^10.0.0",
"node-telegram-bot-api": "^0.59.0", "node-telegram-bot-api": "^0.59.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
@ -36,6 +37,7 @@
"@types/cache-manager": "^4.0.1", "@types/cache-manager": "^4.0.1",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/jest": "27.4.0", "@types/jest": "27.4.0",
"@types/luxon": "^3.1.0",
"@types/node": "^16.0.0", "@types/node": "^16.0.0",
"@types/node-telegram-bot-api": "^0.57.1", "@types/node-telegram-bot-api": "^0.57.1",
"@types/supertest": "^2.0.11", "@types/supertest": "^2.0.11",
@ -2088,6 +2090,12 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true "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": { "node_modules/@types/mime": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
@ -7195,6 +7203,14 @@
"node": ">=12" "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": { "node_modules/macos-release": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.0.tgz", "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.0.tgz",
@ -11901,6 +11917,12 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true "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": { "@types/mime": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.13.2.tgz",
"integrity": "sha512-VJL3nIpA79TodY/ctmZEfhASgqekbT574/c4j3jn4bKXbSCnTTCH/KltZyvL2GlV+tGSMtsWyem8DCX7qKTMBA==" "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": { "macos-release": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.0.tgz", "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.0.tgz",

View file

@ -33,6 +33,7 @@
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"hbs": "^4.2.0", "hbs": "^4.2.0",
"imap-simple": "^5.1.0", "imap-simple": "^5.1.0",
"luxon": "^3.1.0",
"nano": "^10.0.0", "nano": "^10.0.0",
"node-telegram-bot-api": "^0.59.0", "node-telegram-bot-api": "^0.59.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
@ -48,6 +49,7 @@
"@types/cache-manager": "^4.0.1", "@types/cache-manager": "^4.0.1",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/jest": "27.4.0", "@types/jest": "27.4.0",
"@types/luxon": "^3.1.0",
"@types/node": "^16.0.0", "@types/node": "^16.0.0",
"@types/node-telegram-bot-api": "^0.57.1", "@types/node-telegram-bot-api": "^0.57.1",
"@types/supertest": "^2.0.11", "@types/supertest": "^2.0.11",

View file

@ -29,6 +29,7 @@ import { CurrentIssuesEccmReportController } from './reports/current-issues-eccm
import { DailyEccmReportController } from './reports/daily-eccm.report.controller'; import { DailyEccmReportController } from './reports/daily-eccm.report.controller';
import { DailyEccmReportService } from './reports/daily-eccm.report.service'; import { DailyEccmReportService } from './reports/daily-eccm.report.service';
import { ChangesService } from './changes/changes.service'; import { ChangesService } from './changes/changes.service';
import { DailyEccmReportsDatasource } from './couchdb-datasources/daily-eccm-reports.datasource';
@Module({ @Module({
imports: [ imports: [
@ -64,6 +65,7 @@ import { ChangesService } from './changes/changes.service';
CurrentIssuesEccmBotHandlerService, CurrentIssuesEccmBotHandlerService,
DailyEccmReportService, DailyEccmReportService,
ChangesService, ChangesService,
DailyEccmReportsDatasource,
], ],
}) })
export class AppModule implements OnModuleInit { export class AppModule implements OnModuleInit {
@ -88,6 +90,7 @@ export class AppModule implements OnModuleInit {
Users.getDatasource(); Users.getDatasource();
Changes.getDatasource(); Changes.getDatasource();
UserMetaInfo.getDatasource(); UserMetaInfo.getDatasource();
DailyEccmReportsDatasource.getDatasource();
this.enhancerService.addEnhancer([ this.enhancerService.addEnhancer([
this.timestampEnhancer, this.timestampEnhancer,

View file

@ -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<DailyEccmReport.Models.Report>
> {
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<DailyEccmReport.Models.Report>
> {
return await DailyEccmReportsDatasource.getDatasource();
}
}

View file

@ -13,6 +13,7 @@ export type AppConfig = {
dbs: { dbs: {
changes: string; changes: string;
userMetaInfo: string; userMetaInfo: string;
eccmDailyReports: string;
}; };
}; };
telegramBotToken: string; telegramBotToken: string;

View file

@ -1,4 +1,4 @@
import { Controller, Get, Query, Render } from '@nestjs/common'; import { Controller, Get, Param, Query, Render } from '@nestjs/common';
import { import {
DailyEccmReport, DailyEccmReport,
DailyEccmReportService, DailyEccmReportService,
@ -10,28 +10,61 @@ export class DailyEccmReportController {
@Get() @Get()
@Render('daily-eccm-report') @Render('daily-eccm-report')
async getReportDefault(
@Query('from') from: string,
@Query('to') to: string,
): Promise<DailyEccmReport.Models.Report> {
return await this.getReport(from, to);
}
@Get('/delete/:name')
async deleteReport(@Param('name') name: string): Promise<boolean> {
return await this.dailyEccmReportService.deleteReport(name);
}
@Get('/load/:name')
@Render('daily-eccm-report')
async loadReport(
@Param('name') name: string,
): Promise<DailyEccmReport.Models.Report> {
return await this.dailyEccmReportService.loadReport(name);
}
@Get('/load/:name/raw')
async loadReportRawData(
@Param('name') name: string,
): Promise<DailyEccmReport.Models.Report> {
return await this.dailyEccmReportService.loadReport(name);
}
@Get('/generate')
@Render('daily-eccm-report')
async getReport( async getReport(
@Query('from') from: string, @Query('from') from: string,
@Query('to') to: string, @Query('to') to: string,
@Query('name') name?: string,
@Query('overwrite') overwrite?: boolean,
): Promise<DailyEccmReport.Models.Report> { ): Promise<DailyEccmReport.Models.Report> {
const now = new Date().toISOString(); return await this.getReportRawData(from, to, name, overwrite);
return await this.dailyEccmReportService.generateReport({
from: from,
to: to,
reportDate: now,
});
} }
@Get('/raw') @Get('/generate/raw')
async getReportRawData( async getReportRawData(
@Query('from') from: string, @Query('from') from: string,
@Query('to') to: string, @Query('to') to: string,
@Query('name') name?: string,
@Query('overwrite') overwrite?: boolean,
): Promise<DailyEccmReport.Models.Report> { ): Promise<DailyEccmReport.Models.Report> {
const now = new Date().toISOString(); const params = this.dailyEccmReportService.generateParams(from, to, name);
return await this.dailyEccmReportService.generateReport({ const data = await this.dailyEccmReportService.generateReport(params);
from: from, if (name) {
to: to, const saveResult = await this.dailyEccmReportService.saveReport(
reportDate: now, data,
}); overwrite,
);
return saveResult ? data : null;
} else {
return data;
}
} }
} }

View file

@ -10,6 +10,10 @@ import { TimestampConverter } from '@app/event-emitter/utils/timestamp-converter
import { IssuesService } from '@app/event-emitter/issues/issues.service'; import { IssuesService } from '@app/event-emitter/issues/issues.service';
import { UsersService } from '@app/event-emitter/users/users.service'; import { UsersService } from '@app/event-emitter/users/users.service';
import { GetParentsHint } from '@app/event-emitter/utils/get-parents-hint'; 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 // eslint-disable-next-line @typescript-eslint/no-namespace
export namespace DailyEccmReport { export namespace DailyEccmReport {
@ -96,7 +100,9 @@ export namespace DailyEccmReport {
export type Params = { export type Params = {
from: string; from: string;
to: string; to: string;
reportDate: string; name: string;
project: string;
versions: string[];
}; };
export type IssueAndChanges = { export type IssueAndChanges = {
@ -140,7 +146,8 @@ export namespace DailyEccmReport {
private eccmUsers: RedmineTypes.User[], private eccmUsers: RedmineTypes.User[],
private issuesService: IssuesService, private issuesService: IssuesService,
) { ) {
const users: Models.UserReport[] = this.eccmUsers.map((user) => { const users: Models.UserReport[] = this.eccmUsers
.map((user) => {
if (!user) { if (!user) {
this.logger.error(`Not found user data`); this.logger.error(`Not found user data`);
return; return;
@ -155,13 +162,16 @@ export namespace DailyEccmReport {
activities: [], activities: [],
issuesGroupedByStatus: [], issuesGroupedByStatus: [],
} as Models.UserReport; } as Models.UserReport;
}).filter((user) => Boolean(user)); })
.filter((user) => Boolean(user));
this.report = { this.report = {
byUsers: users, byUsers: users,
params: { params: {
from: '', from: '',
to: '', to: '',
reportDate: '', name: '',
project: '',
versions: [],
}, },
}; };
} }
@ -173,7 +183,9 @@ export namespace DailyEccmReport {
setParams(params: Models.Params): void { setParams(params: Models.Params): void {
this.report.params.from = params.from; this.report.params.from = params.from;
this.report.params.to = params.to; 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<void> { async setCurrentIssues(issues: RedmineTypes.Issue[]): Promise<void> {
@ -355,6 +367,7 @@ export class DailyEccmReportService {
private changesService: ChangesService, private changesService: ChangesService,
private issuesService: IssuesService, private issuesService: IssuesService,
private usersService: UsersService, private usersService: UsersService,
private dailyEccmReportsDatasource: DailyEccmReportsDatasource,
) { ) {
this.eccmConfig = this.configService.get<EccmConfig.Config>('redmineEccm'); this.eccmConfig = this.configService.get<EccmConfig.Config>('redmineEccm');
} }
@ -389,6 +402,83 @@ export class DailyEccmReportService {
return report.get(); 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<boolean> {
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<boolean> {
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<DailyEccmReport.Report> { private async createEmptyReport(): Promise<DailyEccmReport.Report> {
const usersList: string[] = this.eccmConfig.groups.reduce((acc, group) => { const usersList: string[] = this.eccmConfig.groups.reduce((acc, group) => {
group.people.forEach((fullname) => { group.people.forEach((fullname) => {

View file

@ -10,7 +10,9 @@
<ul> <ul>
<li>От - {{this.params.from}}</li> <li>От - {{this.params.from}}</li>
<li>До - {{this.params.to}}</li> <li>До - {{this.params.to}}</li>
<li>Дата выгрузки - {{this.params.reportDate}}</li> <li>Имя отчёта - {{this.params.name}}</li>
<li>Имя проекта - {{this.params.project}}</li>
<li>Версии - {{this.params.versions}}</li>
</ul> </ul>
</details> </details>
<h1>Отчёт по работникам</h1> <h1>Отчёт по работникам</h1>