Генерация отчёта для дейли
This commit is contained in:
parent
ebd630b25a
commit
7d94350ae0
19 changed files with 909 additions and 37 deletions
|
|
@ -1,5 +1,11 @@
|
|||
{
|
||||
"currentVersions": [],
|
||||
"projectName": "",
|
||||
"currentIssuesStatuses": []
|
||||
"currentIssuesStatuses": [],
|
||||
"groups": [
|
||||
{
|
||||
"name": "",
|
||||
"people": [""]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -65,7 +65,8 @@ export class RedmineEventsGateway {
|
|||
res = await this.redmineDataLoader.loadIssues(issueNumbers);
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
`Error load issues: ${e.message} for issues: ${issueNumbers}`,
|
||||
`Error load issues: ${e.message} ` +
|
||||
`for issues: ${JSON.stringify(issueNumbers)}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { RedmineTypes } from '../models/redmine-types';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { CacheTTL, Injectable, Logger } from '@nestjs/common';
|
||||
import { Issues } from '../couchdb-datasources/issues';
|
||||
import { RedmineEventsGateway } from '../events/redmine-events.gateway';
|
||||
import { RedmineIssuesCacheWriterService } from '../issue-cache-writer/redmine-issues-cache-writer.service';
|
||||
import { RedmineDataLoader } from '../redmine-data-loader/redmine-data-loader';
|
||||
import { MemoryCache } from '../utils/memory-cache';
|
||||
import nano from 'nano';
|
||||
import { UNLIMITED } from '../consts/consts';
|
||||
import { GetParentsHint } from '../utils/get-parents-hint';
|
||||
|
||||
export const ISSUE_MEMORY_CACHE_LIFETIME = 30 * 1000;
|
||||
const ISSUE_MEMORY_CACHE_AUTOCLEAN_INTERVAL = 1000 * 60 * 5;
|
||||
|
|
@ -32,6 +34,49 @@ export class IssuesService {
|
|||
return res.docs;
|
||||
}
|
||||
|
||||
@CacheTTL(60)
|
||||
async getIssues(ids: number[]): Promise<RedmineTypes.Issue[]> {
|
||||
const issueDb = await this.issues.getDatasource();
|
||||
try {
|
||||
const docs = await issueDb.find({
|
||||
selector: {
|
||||
id: {
|
||||
$in: ids,
|
||||
},
|
||||
},
|
||||
limit: UNLIMITED,
|
||||
});
|
||||
return docs.docs;
|
||||
} catch (ex) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@CacheTTL(120)
|
||||
async getParents(
|
||||
issueId: number,
|
||||
count?: number,
|
||||
): Promise<RedmineTypes.Issue[]> {
|
||||
let index = 0;
|
||||
const res: RedmineTypes.Issue[] = [];
|
||||
let currentIssue = await this.getIssue(issueId);
|
||||
res.unshift(currentIssue);
|
||||
let parentIssueId = currentIssue.parent?.id || null;
|
||||
while (
|
||||
parentIssueId !== null &&
|
||||
currentIssue.id >= 0 &&
|
||||
(typeof count === 'undefined' || count === null || index < count)
|
||||
) {
|
||||
currentIssue = await this.getIssue(parentIssueId);
|
||||
res.unshift(currentIssue);
|
||||
parentIssueId = currentIssue.parent?.id || null;
|
||||
index++;
|
||||
}
|
||||
const parentsHint = GetParentsHint(res);
|
||||
this.logger.debug(`Parents for issue #${issueId} - ${parentsHint}`);
|
||||
return res;
|
||||
}
|
||||
|
||||
async getIssue(
|
||||
issueId: number,
|
||||
force = false,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,15 @@ export module RedmineTypes {
|
|||
details?: JournalDetail[];
|
||||
};
|
||||
|
||||
export type ChildIssue = {
|
||||
id: number;
|
||||
tracker: IdAndName;
|
||||
subject: string;
|
||||
children?: Children;
|
||||
};
|
||||
|
||||
export type Children = ChildIssue[];
|
||||
|
||||
export type Issue = {
|
||||
id: number;
|
||||
project: IdAndName;
|
||||
|
|
@ -48,6 +57,8 @@ export module RedmineTypes {
|
|||
closed_on?: string;
|
||||
relations?: Record<string, any>[];
|
||||
journals?: Journal[];
|
||||
children?: Children;
|
||||
parent?: { id: number };
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-namespace-keyword, @typescript-eslint/no-namespace
|
||||
|
|
|
|||
|
|
@ -24,7 +24,14 @@ export class RedmineDataLoader {
|
|||
|
||||
async loadIssue(issueNumber: number): Promise<RedmineTypes.Issue | null> {
|
||||
const url = this.getIssueUrl(issueNumber);
|
||||
const resp = await axios.get(url);
|
||||
let resp;
|
||||
try {
|
||||
resp = await axios.get(url);
|
||||
} catch (ex) {
|
||||
const errorMsg = ex.message || 'Unknown error';
|
||||
this.logger.error(`${errorMsg} for url = ${url}`);
|
||||
throw ex;
|
||||
}
|
||||
if (!resp || !resp.data || !resp.data.issue) {
|
||||
this.logger.error(
|
||||
`Failed to load issue from redmine, issueNumber = ${issueNumber}`,
|
||||
|
|
@ -34,7 +41,15 @@ export class RedmineDataLoader {
|
|||
this.logger.debug(
|
||||
`Loaded issue, issueNumber = ${issueNumber}, subject = ${resp.data.issue.subject}`,
|
||||
);
|
||||
return await this.enhancerService.enhanceIssue(resp.data.issue);
|
||||
let enhancedIssue;
|
||||
try {
|
||||
enhancedIssue = await this.enhancerService.enhanceIssue(resp.data.issue);
|
||||
} catch (ex) {
|
||||
const errorMsg = ex.message || 'Unknown error';
|
||||
this.logger.error(`${errorMsg} at enhance issue #${issueNumber}`);
|
||||
throw ex;
|
||||
}
|
||||
return enhancedIssue;
|
||||
}
|
||||
|
||||
async loadUsers(users: number[]): Promise<(RedmineTypes.User | null)[]> {
|
||||
|
|
@ -43,8 +58,19 @@ export class RedmineDataLoader {
|
|||
}
|
||||
|
||||
async loadUser(userNumber: number): Promise<RedmineTypes.User | null> {
|
||||
if (userNumber <= 0) {
|
||||
this.logger.warn(`Invalid userNumber = ${userNumber}`);
|
||||
return null;
|
||||
}
|
||||
const url = this.getUserUrl(userNumber);
|
||||
const resp = await axios.get(url);
|
||||
let resp;
|
||||
try {
|
||||
resp = await axios.get(url);
|
||||
} catch (ex) {
|
||||
const errorMsg = ex.message || 'Unknown error';
|
||||
this.logger.error(`${errorMsg} at load user by url ${url}`);
|
||||
return null;
|
||||
}
|
||||
if (!resp || !resp.data?.user) {
|
||||
this.logger.error(
|
||||
`Failed to load user from redmine, userNumber = ${userNumber}`,
|
||||
|
|
|
|||
|
|
@ -72,6 +72,15 @@ export class UsersService {
|
|||
return RedmineTypes.CreateUser(userFromDb);
|
||||
}
|
||||
|
||||
async findUserByFullname(
|
||||
fullname: string,
|
||||
): Promise<RedmineTypes.User | null> {
|
||||
const parts = fullname.split(' ').map((item) => item.trim());
|
||||
const lastname = parts.splice(parts.length - 1, 1).join(' ');
|
||||
const firstname = parts.join(' ');
|
||||
return await this.findUserByName(firstname, lastname);
|
||||
}
|
||||
|
||||
private async getUserFromRedmine(
|
||||
userId: number,
|
||||
): Promise<RedmineTypes.User | null> {
|
||||
|
|
|
|||
13
libs/event-emitter/src/utils/get-parents-hint.ts
Normal file
13
libs/event-emitter/src/utils/get-parents-hint.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { RedmineTypes } from '../models/redmine-types';
|
||||
|
||||
export function GetParentsHint(issues: RedmineTypes.Issue[]): string {
|
||||
const parentsHint = issues
|
||||
.map((issue) => {
|
||||
if (issue.id < 0) {
|
||||
return '...';
|
||||
}
|
||||
return `${issue.tracker.name} #${issue.id} (${issue.subject})`;
|
||||
})
|
||||
.join(' > ');
|
||||
return parentsHint;
|
||||
}
|
||||
49
package-lock.json
generated
49
package-lock.json
generated
|
|
@ -19,6 +19,7 @@
|
|||
"axios": "^0.27.2",
|
||||
"cache-manager": "^4.1.0",
|
||||
"handlebars": "^4.7.7",
|
||||
"hbs": "^4.2.0",
|
||||
"imap-simple": "^5.1.0",
|
||||
"nano": "^10.0.0",
|
||||
"node-telegram-bot-api": "^0.59.0",
|
||||
|
|
@ -4864,6 +4865,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/foreachasync": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz",
|
||||
"integrity": "sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw=="
|
||||
},
|
||||
"node_modules/forever-agent": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||
|
|
@ -5343,6 +5349,19 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hbs": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/hbs/-/hbs-4.2.0.tgz",
|
||||
"integrity": "sha512-dQwHnrfWlTk5PvG9+a45GYpg0VpX47ryKF8dULVd6DtwOE6TEcYQXQ5QM6nyOx/h7v3bvEQbdn19EDAcfUAgZg==",
|
||||
"dependencies": {
|
||||
"handlebars": "4.7.7",
|
||||
"walk": "2.3.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/hexoid": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
|
||||
|
|
@ -9899,6 +9918,14 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/walk": {
|
||||
"version": "2.3.15",
|
||||
"resolved": "https://registry.npmjs.org/walk/-/walk-2.3.15.tgz",
|
||||
"integrity": "sha512-4eRTBZljBfIISK1Vnt69Gvr2w/wc3U6Vtrw7qiN5iqYJPH7LElcYh/iU4XWhdCy2dZqv1ToMyYlybDylfG/5Vg==",
|
||||
"dependencies": {
|
||||
"foreachasync": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/walker": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
||||
|
|
@ -14010,6 +14037,11 @@
|
|||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
|
||||
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
|
||||
},
|
||||
"foreachasync": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz",
|
||||
"integrity": "sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw=="
|
||||
},
|
||||
"forever-agent": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||
|
|
@ -14346,6 +14378,15 @@
|
|||
"has-symbols": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"hbs": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/hbs/-/hbs-4.2.0.tgz",
|
||||
"integrity": "sha512-dQwHnrfWlTk5PvG9+a45GYpg0VpX47ryKF8dULVd6DtwOE6TEcYQXQ5QM6nyOx/h7v3bvEQbdn19EDAcfUAgZg==",
|
||||
"requires": {
|
||||
"handlebars": "4.7.7",
|
||||
"walk": "2.3.15"
|
||||
}
|
||||
},
|
||||
"hexoid": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
|
||||
|
|
@ -17740,6 +17781,14 @@
|
|||
"xml-name-validator": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"walk": {
|
||||
"version": "2.3.15",
|
||||
"resolved": "https://registry.npmjs.org/walk/-/walk-2.3.15.tgz",
|
||||
"integrity": "sha512-4eRTBZljBfIISK1Vnt69Gvr2w/wc3U6Vtrw7qiN5iqYJPH7LElcYh/iU4XWhdCy2dZqv1ToMyYlybDylfG/5Vg==",
|
||||
"requires": {
|
||||
"foreachasync": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"walker": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
"axios": "^0.27.2",
|
||||
"cache-manager": "^4.1.0",
|
||||
"handlebars": "^4.7.7",
|
||||
"hbs": "^4.2.0",
|
||||
"imap-simple": "^5.1.0",
|
||||
"nano": "^10.0.0",
|
||||
"node-telegram-bot-api": "^0.59.0",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { EnhancerService } from '@app/event-emitter/issue-enhancers/enhancer.ser
|
|||
import { TimestampEnhancer } from '@app/event-emitter/issue-enhancers/timestamps-enhancer';
|
||||
import { MainController } from '@app/event-emitter/main/main.controller';
|
||||
import { CacheModule, Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { switchMap, tap } from 'rxjs';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
|
@ -25,6 +25,10 @@ import { PersonalNotificationAdapterService } from './notifications/adapters/per
|
|||
import { StatusChangeAdapterService } from './notifications/adapters/status-change.adapter.service';
|
||||
import { CurrentIssuesEccmReportService } from './reports/current-issues-eccm.report.service';
|
||||
import { CurrentIssuesBotHandlerService } from './telegram-bot/handlers/current-issues.bot-handler.service';
|
||||
import { CurrentIssuesEccmReportController } from './reports/current-issues-eccm.report.controller';
|
||||
import { DailyEccmReportController } from './reports/daily-eccm.report.controller';
|
||||
import { DailyEccmReportService } from './reports/daily-eccm.report.service';
|
||||
import { ChangesService } from './changes/changes.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -36,7 +40,12 @@ import { CurrentIssuesBotHandlerService } from './telegram-bot/handlers/current-
|
|||
isGlobal: true,
|
||||
}),
|
||||
],
|
||||
controllers: [AppController, MainController],
|
||||
controllers: [
|
||||
AppController,
|
||||
MainController,
|
||||
CurrentIssuesEccmReportController,
|
||||
DailyEccmReportController,
|
||||
],
|
||||
providers: [
|
||||
AppService,
|
||||
CustomFieldsEnhancer,
|
||||
|
|
@ -53,6 +62,8 @@ import { CurrentIssuesBotHandlerService } from './telegram-bot/handlers/current-
|
|||
StatusChangeAdapterService,
|
||||
CurrentIssuesEccmReportService,
|
||||
CurrentIssuesBotHandlerService,
|
||||
DailyEccmReportService,
|
||||
ChangesService,
|
||||
],
|
||||
})
|
||||
export class AppModule implements OnModuleInit {
|
||||
|
|
|
|||
15
src/changes/changes.service.ts
Normal file
15
src/changes/changes.service.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Changes } from 'src/couchdb-datasources/changes';
|
||||
import nano from 'nano';
|
||||
import { Change } from 'src/models/change.model';
|
||||
|
||||
@Injectable()
|
||||
export class ChangesService {
|
||||
constructor(private changes: Changes) {}
|
||||
|
||||
async find(query: nano.MangoQuery): Promise<Change[]> {
|
||||
const changesDb = await this.changes.getDatasource();
|
||||
const res = await changesDb.find(query);
|
||||
return res.docs;
|
||||
}
|
||||
}
|
||||
22
src/main.ts
22
src/main.ts
|
|
@ -1,12 +1,30 @@
|
|||
import { NestFactory } from '@nestjs/core';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { join } from 'path';
|
||||
import { AppModule } from './app.module';
|
||||
import * as hbs from 'hbs';
|
||||
import configuration from './configs/app';
|
||||
|
||||
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = "0";
|
||||
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
||||
logger: ['debug', 'error', 'warn', 'log', 'verbose'],
|
||||
});
|
||||
|
||||
app.setBaseViewsDir(join(__dirname, '..', 'views'));
|
||||
app.setViewEngine('hbs');
|
||||
|
||||
// TODO: Продумать как правильно иницировать partial-ы в handlebars
|
||||
// Возможно подойдёт решение описанное тут - https://www.makeuseof.com/handlebars-nestjs-templating/
|
||||
// Низкоуровневое решение по исходному коду hbs:
|
||||
const redminePublicUrl =
|
||||
configuration().redmineIssueEventEmitterConfig.redmineUrlPublic;
|
||||
hbs.registerPartial(
|
||||
'redmineIssueAHref',
|
||||
`<a href="${redminePublicUrl}/issues/{{issue.id}}">{{issue.tracker.name}} #{{issue.id}}</a>`,
|
||||
);
|
||||
|
||||
await app.listen(process.env['PORT'] || 3000);
|
||||
}
|
||||
bootstrap();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
export namespace EccmConfig {
|
||||
export type UserGroup = {
|
||||
name: string;
|
||||
people: string[];
|
||||
};
|
||||
|
||||
export type Config = {
|
||||
currentVersions: string[];
|
||||
projectName: string;
|
||||
currentIssuesStatuses: string[];
|
||||
groups: UserGroup[];
|
||||
};
|
||||
}
|
||||
|
|
|
|||
60
src/reports/current-issues-eccm.report.controller.ts
Normal file
60
src/reports/current-issues-eccm.report.controller.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { Controller, Get, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { EccmConfig } from 'src/models/eccm-config.model';
|
||||
import {
|
||||
CurrentIssuesEccmReport,
|
||||
CurrentIssuesEccmReportService,
|
||||
} from './current-issues-eccm.report.service';
|
||||
|
||||
@Controller('current-issues-eccm')
|
||||
export class CurrentIssuesEccmReportController {
|
||||
private logger = new Logger(CurrentIssuesEccmReportController.name);
|
||||
private eccmConfig: EccmConfig.Config;
|
||||
|
||||
/* eslint-disable */
|
||||
private reportTemplate = [
|
||||
// context - this.report: UserReport[]
|
||||
'{{#each this}}',
|
||||
'<div>',
|
||||
// context - UserReport
|
||||
'<p>{{user.firstname}} {{user.lastname}}:</p>',
|
||||
'<ul>',
|
||||
'{{#each issuesGroupedByStatus}}',
|
||||
'<li>',
|
||||
// context - IssuesAndStatus
|
||||
'{{status.name}}:',
|
||||
'<ul>',
|
||||
'{{#each issues}}',
|
||||
'<li>',
|
||||
// context - RedmineTypes.Issue
|
||||
`{{>redmineIssueAHref issue=.}}: {{subject}} (прио - {{priority.name}}, версия - {{fixed_version.name}})`,
|
||||
'</li>',
|
||||
'{{/each}}',
|
||||
'</ul>',
|
||||
'</li>',
|
||||
'{{/each}}',
|
||||
'</ul>',
|
||||
'</div>',
|
||||
'{{/each}}',
|
||||
].join('\n');
|
||||
/* eslint-enable */
|
||||
|
||||
constructor(
|
||||
private currentIssuesEccmReportService: CurrentIssuesEccmReportService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.eccmConfig = this.configService.get<EccmConfig.Config>('redmineEccm');
|
||||
this.logger.debug(`Eccm config - ${JSON.stringify(this.eccmConfig)}`);
|
||||
}
|
||||
|
||||
@Get()
|
||||
async getReport(): Promise<string> {
|
||||
return await this.currentIssuesEccmReportService.getReport({
|
||||
project: this.eccmConfig.projectName,
|
||||
versions: this.eccmConfig.currentVersions,
|
||||
statuses: this.eccmConfig.currentIssuesStatuses,
|
||||
template: this.reportTemplate,
|
||||
fields: CurrentIssuesEccmReport.Defaults.currentIssuesFields,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ import { RedmineTypes } from '@app/event-emitter/models/redmine-types';
|
|||
import nano from 'nano';
|
||||
import Handlebars from 'handlebars';
|
||||
import { RedminePublicUrlConverter } from 'src/converters/redmine-public-url.converter';
|
||||
import { EccmConfig } from 'src/models/eccm-config.model';
|
||||
import { IssuesService } from '@app/event-emitter/issues/issues.service';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace CurrentIssuesEccmReport {
|
||||
|
|
@ -15,6 +17,8 @@ export namespace CurrentIssuesEccmReport {
|
|||
versions?: string[];
|
||||
userIds?: number[];
|
||||
project?: string;
|
||||
template?: string;
|
||||
fields?: string[];
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
|
|
@ -29,6 +33,22 @@ export namespace CurrentIssuesEccmReport {
|
|||
];
|
||||
|
||||
export const projectName = 'ECCM';
|
||||
|
||||
export const currentIssuesFields = [
|
||||
'id',
|
||||
'tracker.name',
|
||||
'status.id',
|
||||
'status.name',
|
||||
'priority.id',
|
||||
'priority.name',
|
||||
'fixed_version.name',
|
||||
'subject',
|
||||
'updated_on',
|
||||
'updated_on_timestamp',
|
||||
'current_user.id',
|
||||
'current_user.firstname',
|
||||
'current_user.lastname',
|
||||
];
|
||||
}
|
||||
|
||||
export type User = {
|
||||
|
|
@ -55,7 +75,6 @@ export namespace CurrentIssuesEccmReport {
|
|||
export class UsersReport {
|
||||
private report: UserReport[] = [];
|
||||
|
||||
private reportFormatter: HandlebarsTemplateDelegate<UserReport[]>;
|
||||
private reportTemplate = [
|
||||
// context - this.report: UserReport[]
|
||||
'{{#each this}}',
|
||||
|
|
@ -78,7 +97,6 @@ export namespace CurrentIssuesEccmReport {
|
|||
private redminePublicUrlConverter: RedminePublicUrlConverter,
|
||||
private redminePublicUrl: string,
|
||||
) {
|
||||
this.reportFormatter = Handlebars.compile(this.reportTemplate);
|
||||
Handlebars.registerPartial(
|
||||
'redmineIssueAHref',
|
||||
`<a href="${this.redminePublicUrl}/issues/{{issue.id}}">{{issue.tracker.name}} #{{issue.id}}</a>`,
|
||||
|
|
@ -128,23 +146,25 @@ export namespace CurrentIssuesEccmReport {
|
|||
return issuesAndStatus;
|
||||
}
|
||||
|
||||
getAllUsersReportByTemplate(): string {
|
||||
return this.reportFormatter(this.report);
|
||||
getAllUsersReportByTemplate(template?: string): string {
|
||||
const reportFormatter = Handlebars.compile(template || this.reportTemplate);
|
||||
return reportFormatter(this.report);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CurrentIssuesEccmReportService {
|
||||
private eccmVersions: string[];
|
||||
private eccmConfig: EccmConfig.Config;
|
||||
private logger = new Logger(CurrentIssuesEccmReportService.name);
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private issuesDatasource: Issues,
|
||||
private redminePublicUrlConverter: RedminePublicUrlConverter,
|
||||
private issuesService: IssuesService,
|
||||
) {
|
||||
this.eccmVersions = this.configService.get<string[]>('redmineEccmVersions');
|
||||
this.eccmConfig = this.configService.get<EccmConfig.Config>('redmineEccm');
|
||||
}
|
||||
|
||||
async getData(
|
||||
|
|
@ -158,29 +178,17 @@ export class CurrentIssuesEccmReportService {
|
|||
$in: CurrentIssuesEccmReport.Defaults.statuses,
|
||||
},
|
||||
'fixed_version.name': {
|
||||
$in: this.eccmVersions,
|
||||
$in: this.eccmConfig.currentVersions,
|
||||
},
|
||||
'project.name':
|
||||
options?.project || CurrentIssuesEccmReport.Defaults.projectName,
|
||||
},
|
||||
fields: [
|
||||
'id',
|
||||
'tracker.name',
|
||||
'status.id',
|
||||
'status.name',
|
||||
'priority.id',
|
||||
'priority.name',
|
||||
'fixed_version.name',
|
||||
'subject',
|
||||
'updated_on',
|
||||
'updated_on_timestamp',
|
||||
'current_user.id',
|
||||
'current_user.firstname',
|
||||
'current_user.lastname',
|
||||
],
|
||||
limit: UNLIMITED,
|
||||
};
|
||||
|
||||
if (options && options.fields) {
|
||||
query.selector.fields = options.fields;
|
||||
}
|
||||
if (options && options.statuses) {
|
||||
query.selector['status.name'] = {
|
||||
$in: options.statuses,
|
||||
|
|
@ -203,9 +211,9 @@ export class CurrentIssuesEccmReportService {
|
|||
|
||||
this.logger.debug(`Query for get report data: ${JSON.stringify(query)}`);
|
||||
|
||||
const rawData = await datasource.find(query);
|
||||
this.logger.debug(`Raw data for report: ${JSON.stringify(rawData)}`);
|
||||
const data = rawData.docs as RedmineTypes.Issue[];
|
||||
const data = await this.issuesService.find(query);
|
||||
this.logger.debug(`Found issues for report: ${JSON.stringify(data.length)}`);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
|
@ -218,6 +226,6 @@ export class CurrentIssuesEccmReportService {
|
|||
this.configService.get<string>('redmineUrlPublic'),
|
||||
);
|
||||
data.forEach((item) => report.push(item));
|
||||
return report.getAllUsersReportByTemplate();
|
||||
return report.getAllUsersReportByTemplate(options?.template);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
31
src/reports/daily-eccm.report.controller.ts
Normal file
31
src/reports/daily-eccm.report.controller.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { Controller, Get, Logger, Render } from '@nestjs/common';
|
||||
import {
|
||||
DailyEccmReport,
|
||||
DailyEccmReportService,
|
||||
} from './daily-eccm.report.service';
|
||||
|
||||
@Controller('daily-eccm')
|
||||
export class DailyEccmReportController {
|
||||
constructor(private dailyEccmReportService: DailyEccmReportService) {}
|
||||
|
||||
// TODO: Заменить хардкоды на параметры
|
||||
|
||||
@Get()
|
||||
@Render('daily-eccm-report')
|
||||
async getReport(): Promise<DailyEccmReport.Models.Report> {
|
||||
return await this.dailyEccmReportService.generateReport({
|
||||
from: '2022-09-01T00:00:00+07:00',
|
||||
to: '2022-11-04T00:00:00+07:00',
|
||||
reportDate: '',
|
||||
});
|
||||
}
|
||||
|
||||
@Get('/raw')
|
||||
async getReportRawData(): Promise<DailyEccmReport.Models.Report> {
|
||||
return await this.dailyEccmReportService.generateReport({
|
||||
from: '2022-09-01T00:00:00+07:00',
|
||||
to: '2022-11-04T00:00:00+07:00',
|
||||
reportDate: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
511
src/reports/daily-eccm.report.service.ts
Normal file
511
src/reports/daily-eccm.report.service.ts
Normal file
|
|
@ -0,0 +1,511 @@
|
|||
import { RedmineTypes } from '@app/event-emitter/models/redmine-types';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ChangesService } from 'src/changes/changes.service';
|
||||
import { EccmConfig } from 'src/models/eccm-config.model';
|
||||
import { CurrentIssuesEccmReportService } from './current-issues-eccm.report.service';
|
||||
import nano from 'nano';
|
||||
import { UNLIMITED } from '@app/event-emitter/consts/consts';
|
||||
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';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace DailyEccmReport {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace Models {
|
||||
export type User = {
|
||||
id: number;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
};
|
||||
|
||||
export type Status = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type IssuesAndStatus = {
|
||||
status: Status;
|
||||
issues: IssueWithParents[];
|
||||
};
|
||||
|
||||
export type UserReport = {
|
||||
user: User;
|
||||
dailyMessage?: string;
|
||||
teamleadMessage?: string;
|
||||
issuesGroupedByStatus: IssuesAndStatus[];
|
||||
activities: IssueAndChanges[];
|
||||
};
|
||||
|
||||
export type NewIssues = {
|
||||
newIssues: IssueWithParents[];
|
||||
newIssuesCount: number;
|
||||
};
|
||||
|
||||
export type IssuesByStatuses = {
|
||||
status: string;
|
||||
issues: IssueWithParents[];
|
||||
issuesCount: number;
|
||||
};
|
||||
|
||||
export type MetricsByVersion = {
|
||||
version: string;
|
||||
newIssues: NewIssues;
|
||||
issuesByStatuses: IssuesByStatuses[];
|
||||
};
|
||||
|
||||
export type MetricsByParents = {
|
||||
parentIssueId: number;
|
||||
parentIssue: RedmineTypes.Issue;
|
||||
issuesByStatuses: IssuesByStatuses[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Метрики сгруппированные по версиям
|
||||
*/
|
||||
export type Metrics = {
|
||||
byVersions: MetricsByVersion[];
|
||||
byParents: MetricsByParents[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Все данные для отчёта
|
||||
*/
|
||||
export type Report = {
|
||||
byUsers: UserReport[];
|
||||
metrics?: Metrics;
|
||||
/** Переданные параметры для создания отчёта */
|
||||
params: Params;
|
||||
};
|
||||
|
||||
/**
|
||||
* Данные по задаче + для hint-а подсказка о родительских задачах
|
||||
*
|
||||
* Полезно будет для восстановления контекста
|
||||
*/
|
||||
export type IssueWithParents = {
|
||||
issue: RedmineTypes.Issue;
|
||||
parents: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Параметры отчёта
|
||||
*/
|
||||
export type Params = {
|
||||
from: string;
|
||||
to: string;
|
||||
reportDate: string;
|
||||
};
|
||||
|
||||
export type IssueAndChanges = {
|
||||
issue: RedmineTypes.Issue;
|
||||
changes: Change[];
|
||||
};
|
||||
|
||||
export type Change = {
|
||||
initiator?: RedmineTypes.PublicUser;
|
||||
dev?: RedmineTypes.PublicUser;
|
||||
cr?: RedmineTypes.PublicUser;
|
||||
qa?: RedmineTypes.PublicUser;
|
||||
current_user?: RedmineTypes.PublicUser;
|
||||
author?: RedmineTypes.PublicUser;
|
||||
issue_id: number;
|
||||
issue_url: string;
|
||||
issue_tracker: string;
|
||||
issue_subject: string;
|
||||
journal_note?: string;
|
||||
old_status?: {
|
||||
id?: number;
|
||||
name?: string;
|
||||
} | null;
|
||||
new_status?: {
|
||||
id?: number;
|
||||
name?: string;
|
||||
};
|
||||
created_on: string;
|
||||
created_on_timestamp: number | null;
|
||||
change_message: string;
|
||||
recipient: RedmineTypes.PublicUser;
|
||||
};
|
||||
}
|
||||
|
||||
export class Report {
|
||||
private report: Models.Report;
|
||||
private logger = new Logger(Report.name);
|
||||
|
||||
constructor(
|
||||
private eccmUsers: RedmineTypes.User[],
|
||||
private issuesService: IssuesService,
|
||||
) {
|
||||
const users: Models.UserReport[] = this.eccmUsers.map((user) => {
|
||||
const u: Models.User = {
|
||||
id: user.id,
|
||||
lastname: user.lastname,
|
||||
firstname: user.firstname,
|
||||
};
|
||||
return {
|
||||
user: u,
|
||||
activities: [],
|
||||
issuesGroupedByStatus: [],
|
||||
} as Models.UserReport;
|
||||
});
|
||||
this.report = {
|
||||
byUsers: users,
|
||||
params: {
|
||||
from: '',
|
||||
to: '',
|
||||
reportDate: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get(): Models.Report {
|
||||
return this.report;
|
||||
}
|
||||
|
||||
setParams(params: Models.Params): void {
|
||||
this.report.params.from = params.from;
|
||||
this.report.params.to = params.to;
|
||||
this.report.params.reportDate = params.reportDate;
|
||||
}
|
||||
|
||||
async setCurrentIssues(issues: RedmineTypes.Issue[]): Promise<void> {
|
||||
for (let i = 0; i < issues.length; i++) {
|
||||
const issue = issues[i];
|
||||
await this.appendCurrentIssue(issue);
|
||||
}
|
||||
this.logger.debug(`Issues for report loaded successed`);
|
||||
}
|
||||
|
||||
async setActivities(activities: Models.Change[]): Promise<void> {
|
||||
for (let i = 0; i < activities.length; i++) {
|
||||
const activity = activities[i];
|
||||
await this.appendActivity(activity);
|
||||
}
|
||||
this.logger.debug(`Activities for report loaded successed`);
|
||||
}
|
||||
|
||||
async appendCurrentIssue(
|
||||
issue: RedmineTypes.Issue & Record<string, any>,
|
||||
): Promise<boolean> {
|
||||
const currentUser: RedmineTypes.PublicUser = issue.current_user;
|
||||
if (!currentUser) {
|
||||
return false;
|
||||
}
|
||||
const byUser = this.report.byUsers.find(
|
||||
(item) => item.user.id == currentUser.id,
|
||||
);
|
||||
if (!byUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let byStatus: Models.IssuesAndStatus = byUser.issuesGroupedByStatus.find(
|
||||
(item) => item.status.name == issue.status.name,
|
||||
);
|
||||
if (!byStatus) {
|
||||
byStatus = {
|
||||
status: { ...issue.status },
|
||||
issues: [],
|
||||
};
|
||||
byUser.issuesGroupedByStatus.push(byStatus);
|
||||
}
|
||||
|
||||
const existsIssue = byStatus.issues.find(
|
||||
(item) => item.issue.id == issue.id,
|
||||
);
|
||||
if (existsIssue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parents = await this.issuesService.getParents(issue.id);
|
||||
// parents.pop();
|
||||
const parentsHint = GetParentsHint(parents);
|
||||
byStatus.issues.push({
|
||||
issue: issue,
|
||||
parents: parentsHint,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async appendActivity(activity: Models.Change): Promise<boolean> {
|
||||
const recipient = activity.recipient;
|
||||
if (!recipient) {
|
||||
this.logger.debug(
|
||||
`Recipient is empty for activity ` +
|
||||
`created_on ${activity.created_on} ` +
|
||||
`for issue #${activity.issue_id} (${activity.issue_subject})`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const recipientId = recipient.id;
|
||||
|
||||
const byUser = this.report.byUsers.find(
|
||||
(byUser) => byUser.user.id == recipientId,
|
||||
);
|
||||
if (!byUser) {
|
||||
this.logger.log(`Not interested user ${recipient.name}`);
|
||||
return false;
|
||||
} else {
|
||||
this.logger.debug(
|
||||
`Found user section in report data ` +
|
||||
`user=${JSON.stringify(byUser.user)}`,
|
||||
);
|
||||
}
|
||||
|
||||
let issueAndChanges: Models.IssueAndChanges = byUser.activities.find(
|
||||
(issueAndChanges) => {
|
||||
return issueAndChanges.issue.id == activity.issue_id;
|
||||
},
|
||||
);
|
||||
if (!issueAndChanges) {
|
||||
const issue = await this.issuesService.getIssue(activity.issue_id);
|
||||
if (!issue) {
|
||||
this.logger.error(`Not found issue data for #${activity.issue_id}`);
|
||||
return false;
|
||||
}
|
||||
issueAndChanges = {
|
||||
issue: issue,
|
||||
changes: [],
|
||||
};
|
||||
this.logger.debug(
|
||||
`Created issue section in report data ` +
|
||||
`user=${JSON.stringify(byUser.user)} ` +
|
||||
`issue=${JSON.stringify({
|
||||
id: issueAndChanges.issue.id,
|
||||
subject: issueAndChanges.issue.subject,
|
||||
})}`,
|
||||
);
|
||||
byUser.activities.push(issueAndChanges);
|
||||
} else {
|
||||
this.logger.debug(
|
||||
`Found issue section in report data ` +
|
||||
`user=${JSON.stringify(byUser.user)} ` +
|
||||
`issue=${JSON.stringify({
|
||||
id: issueAndChanges.issue.id,
|
||||
subject: issueAndChanges.issue.subject,
|
||||
})}`,
|
||||
);
|
||||
}
|
||||
|
||||
issueAndChanges.changes.push(activity);
|
||||
this.logger.debug(
|
||||
`Activity pushed into report ` +
|
||||
`user=${JSON.stringify(byUser.user)} ` +
|
||||
`issue=${JSON.stringify({
|
||||
id: issueAndChanges.issue.id,
|
||||
subject: issueAndChanges.issue.subject,
|
||||
})} ` +
|
||||
`activity=${JSON.stringify({
|
||||
created_on: activity.created_on,
|
||||
change_message: activity.change_message,
|
||||
})}`,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
sortUsers(): void {
|
||||
this.report.byUsers.sort((a, b) => {
|
||||
const nameA = `${a.user.firstname} ${a.user.lastname}`;
|
||||
const nameB = `${b.user.firstname} ${b.user.lastname}`;
|
||||
if (nameA > nameB) {
|
||||
return 1;
|
||||
}
|
||||
if (nameA < nameB) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
sortActivities(): void {
|
||||
for (let i = 0; i < this.report.byUsers.length; i++) {
|
||||
const byUser = this.report.byUsers[i];
|
||||
for (let j = 0; j < byUser.activities.length; j++) {
|
||||
const activity = byUser.activities[j];
|
||||
activity.changes.sort((a, b) => {
|
||||
return a.created_on_timestamp - b.created_on_timestamp;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DailyEccmReportService {
|
||||
private logger = new Logger(DailyEccmReportService.name);
|
||||
private eccmConfig: EccmConfig.Config;
|
||||
private report: DailyEccmReport.Report;
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private currentIssuesEccmReportService: CurrentIssuesEccmReportService,
|
||||
private changesService: ChangesService,
|
||||
private issuesService: IssuesService,
|
||||
private usersService: UsersService,
|
||||
) {
|
||||
this.eccmConfig = this.configService.get<EccmConfig.Config>('redmineEccm');
|
||||
}
|
||||
|
||||
async getReport(): Promise<DailyEccmReport.Report> {
|
||||
if (!this.report) {
|
||||
await this.createEmptyReport();
|
||||
}
|
||||
return this.report;
|
||||
}
|
||||
|
||||
async generateReport(
|
||||
params: DailyEccmReport.Models.Params,
|
||||
): Promise<DailyEccmReport.Models.Report> {
|
||||
await this.createEmptyReport();
|
||||
const report = this.report;
|
||||
report.setParams(params);
|
||||
|
||||
const currentIssues = await this.getCurrentIssues();
|
||||
this.logger.log(`Current issues count - ${currentIssues.length}`);
|
||||
await report.setCurrentIssues(currentIssues);
|
||||
|
||||
const activities = await this.getActivities(params);
|
||||
this.logger.log(`Activities count - ${activities.length}`);
|
||||
await report.setActivities(activities);
|
||||
|
||||
report.sortUsers();
|
||||
report.sortActivities();
|
||||
|
||||
this.logger.log(`Daily eccm report created successed`);
|
||||
|
||||
return report.get();
|
||||
}
|
||||
|
||||
private async createEmptyReport(): Promise<DailyEccmReport.Report> {
|
||||
const usersList: string[] = this.eccmConfig.groups.reduce((acc, group) => {
|
||||
group.people.forEach((fullname) => {
|
||||
acc.push(fullname);
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
this.logger.debug(
|
||||
`Create new daily report for users - ${JSON.stringify(usersList)}`,
|
||||
);
|
||||
const usersPromises = usersList.map((fullname) => {
|
||||
return this.usersService.findUserByFullname(fullname);
|
||||
});
|
||||
const users = await Promise.all(usersPromises);
|
||||
this.report = new DailyEccmReport.Report(users, this.issuesService);
|
||||
return this.report;
|
||||
}
|
||||
|
||||
private async getCurrentIssues(): Promise<RedmineTypes.Issue[]> {
|
||||
return await this.currentIssuesEccmReportService.getData({
|
||||
project: this.eccmConfig.projectName,
|
||||
statuses: this.eccmConfig.currentIssuesStatuses,
|
||||
versions: this.eccmConfig.currentVersions,
|
||||
});
|
||||
}
|
||||
|
||||
private async getActivities(
|
||||
params: DailyEccmReport.Models.Params,
|
||||
): Promise<DailyEccmReport.Models.Change[]> {
|
||||
const fromTimestamp = TimestampConverter.toTimestamp(params.from);
|
||||
const toTimestamp = TimestampConverter.toTimestamp(params.to);
|
||||
const query: nano.MangoQuery = {
|
||||
selector: {
|
||||
$and: [
|
||||
{
|
||||
created_on_timestamp: {
|
||||
$gte: fromTimestamp,
|
||||
},
|
||||
},
|
||||
{
|
||||
created_on_timestamp: {
|
||||
$lt: toTimestamp,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
limit: UNLIMITED,
|
||||
};
|
||||
this.logger.debug(
|
||||
`Query for get activities data - ${JSON.stringify(query)}`,
|
||||
);
|
||||
const changes = await this.changesService.find(query);
|
||||
|
||||
this.logger.log(
|
||||
`Loaded changes for daily report - count = ${changes.length}`,
|
||||
);
|
||||
|
||||
const uniqIssueIds: number[] = [];
|
||||
changes.forEach((change) => {
|
||||
const issueId = change.issue_id;
|
||||
if (!uniqIssueIds.includes(issueId)) {
|
||||
uniqIssueIds.push(issueId);
|
||||
}
|
||||
});
|
||||
|
||||
this.logger.debug(`Uniq issues - ${JSON.stringify(uniqIssueIds)}`);
|
||||
|
||||
const issues = await this.issuesService.getIssues(uniqIssueIds);
|
||||
this.logger.debug(`Loaded data for uniq issues - count = ${issues.length}`);
|
||||
const issuesMap: Record<number, RedmineTypes.Issue> = {};
|
||||
issues.forEach((issue) => {
|
||||
const version = issue.fixed_version.name;
|
||||
if (this.eccmConfig.currentVersions.includes(version)) {
|
||||
issuesMap[issue.id] = issue;
|
||||
}
|
||||
});
|
||||
|
||||
const reportChanges: DailyEccmReport.Models.Change[] = changes.reduce(
|
||||
(acc: DailyEccmReport.Models.Change[], change) => {
|
||||
change.messages.forEach((message) => {
|
||||
if (!message || !message.recipient || !message.change_message) {
|
||||
return;
|
||||
}
|
||||
const c: DailyEccmReport.Models.Change = {
|
||||
initiator: change.initiator,
|
||||
dev: change.dev,
|
||||
cr: change.cr,
|
||||
qa: change.qa,
|
||||
current_user: change.current_user,
|
||||
author: change.author,
|
||||
issue_id: change.issue_id,
|
||||
issue_url: change.issue_url,
|
||||
issue_tracker: change.issue_tracker,
|
||||
issue_subject: change.issue_subject,
|
||||
journal_note: change.journal_note,
|
||||
old_status: change.old_status && {
|
||||
id: change.old_status.id,
|
||||
name: change.old_status.name,
|
||||
},
|
||||
new_status: change.new_status && {
|
||||
id: change.new_status.id,
|
||||
name: change.new_status.name,
|
||||
},
|
||||
created_on: change.created_on,
|
||||
created_on_timestamp: change.created_on_timestamp,
|
||||
recipient: {
|
||||
firstname: message.recipient.firstname,
|
||||
lastname: message.recipient.lastname,
|
||||
id: message.recipient.id,
|
||||
login: message.recipient.login,
|
||||
name: message.recipient.name,
|
||||
},
|
||||
change_message: message.change_message,
|
||||
};
|
||||
acc.push(c);
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
[] as DailyEccmReport.Models.Change[],
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Prepared activities for report - count = ${reportChanges.length}`,
|
||||
);
|
||||
|
||||
return reportChanges;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||
import { ConfigService } from '@nestjs/config';
|
||||
import TelegramBot from 'node-telegram-bot-api';
|
||||
import { EccmConfig } from 'src/models/eccm-config.model';
|
||||
import { CurrentIssuesEccmReportService } from 'src/reports/current-issues-eccm.report.service';
|
||||
import { CurrentIssuesEccmReport, CurrentIssuesEccmReportService } from 'src/reports/current-issues-eccm.report.service';
|
||||
import { UserMetaInfoService } from 'src/user-meta-info/user-meta-info.service';
|
||||
import { TelegramBotService } from '../telegram-bot.service';
|
||||
|
||||
|
|
@ -36,6 +36,7 @@ export class CurrentIssuesBotHandlerService {
|
|||
project: this.eccmConfig.projectName,
|
||||
versions: this.eccmConfig.currentVersions,
|
||||
statuses: this.eccmConfig.currentIssuesStatuses,
|
||||
fields: CurrentIssuesEccmReport.Defaults.currentIssuesFields,
|
||||
});
|
||||
this.logger.debug(`Current issues eccm report: ${report}`);
|
||||
bot.sendMessage(msg.chat.id, report || 'empty report', {
|
||||
|
|
|
|||
50
views/daily-eccm-report.hbs
Normal file
50
views/daily-eccm-report.hbs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Daily Eccm Report</title>
|
||||
</head>
|
||||
<body>
|
||||
<details>
|
||||
<summary>Параметры отчёта</summary>
|
||||
<ul>
|
||||
<li>От - {{this.params.from}}</li>
|
||||
<li>До - {{this.params.to}}</li>
|
||||
<li>Дата выгрузки - {{this.params.reportDate}}</li>
|
||||
</ul>
|
||||
</details>
|
||||
<h1>Отчёт по работникам</h1>
|
||||
{{#each this.byUsers}}
|
||||
|
||||
<h2>{{this.user.firstname}} {{this.user.lastname}}</h2>
|
||||
|
||||
<h3>Текущие задачи</h3>
|
||||
<ul>
|
||||
{{#each this.issuesGroupedByStatus}}
|
||||
<li>
|
||||
{{this.status.name}}:
|
||||
<ul>
|
||||
{{#each this.issues}}
|
||||
<li title="{{this.parents}}">{{>redmineIssueAHref issue=this.issue}} (приоритет {{this.issue.priority.name}}; версия {{this.issue.fixed_version.name}}) - {{this.issue.subject}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
||||
<h3>Активности за период</h3>
|
||||
<ul>
|
||||
{{#each this.activities}}
|
||||
<li>
|
||||
{{>redmineIssueAHref issue=this.issue}} (приоритет {{this.issue.priority.name}}; версия {{this.issue.fixed_version.name}}) - {{this.issue.subject}}
|
||||
<ul>
|
||||
{{#each this.changes}}
|
||||
<li>{{this.created_on}}: {{this.change_message}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/each}}
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in a new issue