522 lines
15 KiB
TypeScript
522 lines
15 KiB
TypeScript
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;
|
||
parents: string;
|
||
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) => {
|
||
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: '',
|
||
},
|
||
};
|
||
}
|
||
|
||
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;
|
||
}
|
||
const parents = await this.issuesService.getParents(activity.issue_id);
|
||
const parentsHint = GetParentsHint(parents);
|
||
issueAndChanges = {
|
||
issue: issue,
|
||
parents: parentsHint,
|
||
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) => {
|
||
if (!issue || !issue.fixed_version) {
|
||
return;
|
||
}
|
||
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;
|
||
}
|
||
}
|