Добавлена функция расчёта метрик
This commit is contained in:
parent
d5d604eb40
commit
f3a8cded97
4 changed files with 446 additions and 14 deletions
|
|
@ -51,6 +51,8 @@ export module RedmineTypes {
|
||||||
done_ratio: number;
|
done_ratio: number;
|
||||||
spent_hours: number;
|
spent_hours: number;
|
||||||
total_spent_hours: number;
|
total_spent_hours: number;
|
||||||
|
estimated_hours: number;
|
||||||
|
total_estimated_hours: number;
|
||||||
custom_fields: CustomField[];
|
custom_fields: CustomField[];
|
||||||
created_on: string;
|
created_on: string;
|
||||||
updated_on?: string;
|
updated_on?: string;
|
||||||
|
|
@ -89,6 +91,8 @@ export module RedmineTypes {
|
||||||
done_ratio: num,
|
done_ratio: num,
|
||||||
spent_hours: num,
|
spent_hours: num,
|
||||||
total_spent_hours: num,
|
total_spent_hours: num,
|
||||||
|
estimated_hours: num,
|
||||||
|
total_estimated_hours: num,
|
||||||
custom_fields: [],
|
custom_fields: [],
|
||||||
created_on: date,
|
created_on: date,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
77
src/reports/daily-eccm-v2-report-task-handler.ts
Normal file
77
src/reports/daily-eccm-v2-report-task-handler.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
import { IssuesService } from '@app/event-emitter/issues/issues.service';
|
||||||
|
|
||||||
|
export type Params = {
|
||||||
|
query: any; // TODO: add type
|
||||||
|
schedule: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Report = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
datetime: number;
|
||||||
|
datetimeFormatted: string;
|
||||||
|
jobInfo: JobInfo;
|
||||||
|
params: Params; // query?, schedule?, etc TODO
|
||||||
|
data: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JobInfo = {
|
||||||
|
jobId: string;
|
||||||
|
dashboardId: string;
|
||||||
|
widgetId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class DailyEccmV2ReportTaskHandlerService {
|
||||||
|
private logger = new Logger(DailyEccmV2ReportTaskHandlerService.name);
|
||||||
|
|
||||||
|
private issuesService: IssuesService | null = null;
|
||||||
|
|
||||||
|
constructor(public params: Params, public jobInfo: JobInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIssuesService(issuesService: IssuesService): void {
|
||||||
|
this.issuesService = issuesService;
|
||||||
|
}
|
||||||
|
|
||||||
|
getIssuesService(): IssuesService | null {
|
||||||
|
if (!this.issuesService) {
|
||||||
|
this.logger.warn(
|
||||||
|
'DailyEccmV2ReportTaskHandlerService is not initialized, issuesService is null',
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.issuesService;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createReport(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const report = await this.prepareReportData();
|
||||||
|
await this.saveNewReport(report);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async prepareReportData(): Promise<Report> {
|
||||||
|
const issuesService = this.getIssuesService();
|
||||||
|
if (!issuesService) {
|
||||||
|
throw new Error(
|
||||||
|
'Cannot create report without issuesService, DailyEccmV2ReportTaskHandlerService is not initialized',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const issues = await this.issuesService.mergedTreesAndFind(
|
||||||
|
this.params.query,
|
||||||
|
);
|
||||||
|
return {} as Report;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveNewReport(report: Report): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePreviousReport(report: Report): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,26 +8,19 @@ import { DailyEccmReportsV2Datasource } from 'src/couchdb-datasources/daily-eccm
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import { RedmineTypes } from '@app/event-emitter/models/redmine-types';
|
import { RedmineTypes } from '@app/event-emitter/models/redmine-types';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { Params } from './daily-eccm-v2-report-task-handler';
|
||||||
export type Params = {
|
|
||||||
query: any; // TODO: add type
|
|
||||||
schedule: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Report = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
datetime: number;
|
|
||||||
datetimeFormatted: string;
|
|
||||||
params: Params; // query?, schedule?, etc TODO
|
|
||||||
data: any;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Job = {
|
export type Job = {
|
||||||
id: string;
|
id: string;
|
||||||
params: Params;
|
params: Params;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type JobHandlerParams = {
|
||||||
|
job: Job;
|
||||||
|
dashboard: Dashboard;
|
||||||
|
widget: Widget;
|
||||||
|
};
|
||||||
|
|
||||||
export const WIDGET_TYPE = 'daily_eccm_v2';
|
export const WIDGET_TYPE = 'daily_eccm_v2';
|
||||||
|
|
||||||
export const JOB_PREFIX = 'daily_eccm_v2';
|
export const JOB_PREFIX = 'daily_eccm_v2';
|
||||||
|
|
@ -160,6 +153,333 @@ export class DailyEccmV2ReportTaskRunnerService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async analyzeIssueMetrics(
|
||||||
|
dashboard: Dashboard,
|
||||||
|
widget: Widget,
|
||||||
|
issues: RedmineTypes.Issue[],
|
||||||
|
previousIssues?: RedmineTypes.Issue[],
|
||||||
|
previousMetrics?: Record<string, any>,
|
||||||
|
): Promise<Record<string, any>> {
|
||||||
|
this.logger.log(
|
||||||
|
`Analyzing issues metrics for dashboard ${dashboard.id}, widget ${widget.id}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ШАГ 1. Подсчет количества задач по статусам
|
||||||
|
this.logger.debug(`Step 1. Calculating issues by status`);
|
||||||
|
const issuesByStatusCount = issues.reduce((acc, issue) => {
|
||||||
|
const status = issue.status?.name;
|
||||||
|
if (status) {
|
||||||
|
if (!acc[status]) {
|
||||||
|
acc[status] = {
|
||||||
|
count: 0,
|
||||||
|
issueIds: [],
|
||||||
|
byVersion: {} as Record<
|
||||||
|
string,
|
||||||
|
{ count: number; issueIds: number[] }
|
||||||
|
>,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
acc[status].count = acc[status].count + 1;
|
||||||
|
acc[status].issueIds.push(issue.id);
|
||||||
|
const version = issue.fixed_version?.name;
|
||||||
|
if (version) {
|
||||||
|
if (!acc[status].byVersion[version]) {
|
||||||
|
acc[status].byVersion[version] = { count: 0, issueIds: [] };
|
||||||
|
acc[status].byVersion[version].count++;
|
||||||
|
acc[status].byVersion[version].issueIds.push(issue.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
this.logger.debug(
|
||||||
|
'Step 1. Calculating issues by status - done',
|
||||||
|
JSON.stringify(issuesByStatusCount),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ШАГ 2. Подсчет количества задач по версиям
|
||||||
|
this.logger.debug('Step 2. Calculating issues by versions');
|
||||||
|
const issuesByVersionsCount = issues.reduce((acc, issue) => {
|
||||||
|
const version = issue.fixed_version?.name;
|
||||||
|
if (version) {
|
||||||
|
if (!acc[version]) {
|
||||||
|
acc[version] = {
|
||||||
|
count: 0,
|
||||||
|
issueIds: [],
|
||||||
|
byStatus: {} as Record<
|
||||||
|
string,
|
||||||
|
{ count: number; issueIds: number[] }
|
||||||
|
>,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
acc[version].count = acc[version].count + 1;
|
||||||
|
acc[version].issueIds.push(issue.id);
|
||||||
|
const status = issue.status?.name;
|
||||||
|
if (status) {
|
||||||
|
if (!acc[version].byStatus[status]) {
|
||||||
|
acc[version].byStatus[status] = { count: 0, issueIds: [] };
|
||||||
|
}
|
||||||
|
acc[version].byStatus[status].count += 1;
|
||||||
|
acc[version].byStatus[status].issueIds.push(issue.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
this.logger.debug(
|
||||||
|
'Step 2. Calculating issues by versions - done',
|
||||||
|
JSON.stringify(issuesByVersionsCount),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ШАГ 3. Подсчет количества внутренних изменений
|
||||||
|
this.logger.debug('Step 3. Calculating internal changes');
|
||||||
|
const changesInterval =
|
||||||
|
(widget?.dataLoaderParams['changesInterval'] as number) ??
|
||||||
|
24 * 60 * 60 * 1000;
|
||||||
|
const changesCount = {
|
||||||
|
totalChanges: 0,
|
||||||
|
totalComments: 0,
|
||||||
|
byIssue: {} as Record<number, { changes: number; comments: number }>,
|
||||||
|
byStatus: {} as Record<string, { changes: number; comments: number }>,
|
||||||
|
byVersion: {} as Record<string, { changes: number; comments: number }>,
|
||||||
|
byUserName: {} as Record<string, { changes: number; comments: number }>,
|
||||||
|
};
|
||||||
|
const now = DateTime.now().toMillis();
|
||||||
|
if (
|
||||||
|
typeof changesInterval === 'number' &&
|
||||||
|
changesInterval > 0 &&
|
||||||
|
issues?.length > 0
|
||||||
|
) {
|
||||||
|
issues.forEach((issue) => {
|
||||||
|
const status = issue.status?.name ?? 'no_status';
|
||||||
|
const version = issue.fixed_version?.name ?? 'no_version';
|
||||||
|
const changes = issue.journals?.reduce(
|
||||||
|
(acc, journal) => {
|
||||||
|
const createdOnTimestamp = DateTime.fromISO(
|
||||||
|
journal.created_on,
|
||||||
|
).toMillis();
|
||||||
|
const currentUser = journal?.user?.name ?? 'unknown';
|
||||||
|
if (
|
||||||
|
createdOnTimestamp > now - changesInterval &&
|
||||||
|
createdOnTimestamp <= now
|
||||||
|
) {
|
||||||
|
if (!changesCount.byStatus[status]) {
|
||||||
|
changesCount.byStatus[status] = { changes: 0, comments: 0 };
|
||||||
|
}
|
||||||
|
if (!changesCount.byVersion[version]) {
|
||||||
|
changesCount.byVersion[version] = { changes: 0, comments: 0 };
|
||||||
|
}
|
||||||
|
if (!changesCount.byUserName[currentUser]) {
|
||||||
|
changesCount.byUserName[currentUser] = {
|
||||||
|
changes: 0,
|
||||||
|
comments: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
acc.changes += 1;
|
||||||
|
changesCount.totalChanges += 1;
|
||||||
|
changesCount.byStatus[status].changes += 1;
|
||||||
|
changesCount.byVersion[version].changes += 1;
|
||||||
|
changesCount.byUserName[currentUser].changes += 1;
|
||||||
|
if (
|
||||||
|
typeof journal.notes === 'string' &&
|
||||||
|
journal.notes.length > 0
|
||||||
|
) {
|
||||||
|
acc.comments += 1;
|
||||||
|
changesCount.totalComments += 1;
|
||||||
|
changesCount.byStatus[status].comments += 1;
|
||||||
|
changesCount.byVersion[version].comments += 1;
|
||||||
|
changesCount.byUserName[currentUser].comments += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
changes: 0,
|
||||||
|
comments: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
changesCount.byIssue[issue.id] = changes;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.logger.debug(
|
||||||
|
'Step 3. Calculating internal changes - done',
|
||||||
|
JSON.stringify(changesCount),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ШАГ 4: Количество задач с оценками трудозатрат
|
||||||
|
this.logger.debug('Step 4. Counting issues with estimates and spent hours');
|
||||||
|
const issuesWithEstimatesAndSpenthoursCount = {
|
||||||
|
withEstimates: { count: 0, issueIds: [] },
|
||||||
|
withoutEstimates: { count: 0, issueIds: [] },
|
||||||
|
withSpentHoursOverEstimates: { count: 0, issueIds: [] },
|
||||||
|
byVersion: {} as Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
withEstimates: { count: number; issueIds: number[] };
|
||||||
|
withoutEstimates: { count: number; issueIds: number[] };
|
||||||
|
withSpentHoursOverEstimates: { count: number; issueIds: number[] };
|
||||||
|
}
|
||||||
|
>,
|
||||||
|
byStatus: {} as Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
withEstimates: { count: number; issueIds: number[] };
|
||||||
|
withoutEstimates: { count: number; issueIds: number[] };
|
||||||
|
withSpentHoursOverEstimates: { count: number; issueIds: number[] };
|
||||||
|
}
|
||||||
|
>,
|
||||||
|
};
|
||||||
|
issues.forEach((issue) => {
|
||||||
|
const version = issue.fixed_version?.name;
|
||||||
|
if (
|
||||||
|
version &&
|
||||||
|
!issuesWithEstimatesAndSpenthoursCount.byVersion[version]
|
||||||
|
) {
|
||||||
|
issuesWithEstimatesAndSpenthoursCount.byVersion[version] = {
|
||||||
|
withEstimates: { count: 0, issueIds: [] },
|
||||||
|
withoutEstimates: { count: 0, issueIds: [] },
|
||||||
|
withSpentHoursOverEstimates: { count: 0, issueIds: [] },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = issue.status?.name;
|
||||||
|
if (status && !issuesWithEstimatesAndSpenthoursCount.byStatus[status]) {
|
||||||
|
issuesWithEstimatesAndSpenthoursCount.byStatus[status] = {
|
||||||
|
withEstimates: { count: 0, issueIds: [] },
|
||||||
|
withoutEstimates: { count: 0, issueIds: [] },
|
||||||
|
withSpentHoursOverEstimates: { count: 0, issueIds: [] },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof issue.estimated_hours === 'number' &&
|
||||||
|
issue.estimated_hours > 0
|
||||||
|
) {
|
||||||
|
issuesWithEstimatesAndSpenthoursCount.withEstimates.count += 1;
|
||||||
|
issuesWithEstimatesAndSpenthoursCount.withEstimates.issueIds.push(
|
||||||
|
issue.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
issuesWithEstimatesAndSpenthoursCount.byStatus[status].withEstimates.count += 1;
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
issuesWithEstimatesAndSpenthoursCount.byStatus[status].withEstimates.issueIds.push(issue.id);
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
issuesWithEstimatesAndSpenthoursCount.byVersion[version].withEstimates.count += 1;
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
issuesWithEstimatesAndSpenthoursCount.byVersion[version].withEstimates.issueIds.push(issue.id);
|
||||||
|
} else {
|
||||||
|
issuesWithEstimatesAndSpenthoursCount.withoutEstimates.count += 1;
|
||||||
|
issuesWithEstimatesAndSpenthoursCount.withoutEstimates.issueIds.push(
|
||||||
|
issue.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
issuesWithEstimatesAndSpenthoursCount.byStatus[status].withoutEstimates.count += 1;
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
issuesWithEstimatesAndSpenthoursCount.byStatus[status].withoutEstimates.issueIds.push(issue.id);
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
issuesWithEstimatesAndSpenthoursCount.byVersion[version].withoutEstimates.count += 1;
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
issuesWithEstimatesAndSpenthoursCount.byVersion[version].withoutEstimates.issueIds.push(issue.id);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof issue.spent_hours === 'number' &&
|
||||||
|
typeof issue.estimated_hours === 'number' &&
|
||||||
|
issue.spent_hours > issue.estimated_hours
|
||||||
|
) {
|
||||||
|
issuesWithEstimatesAndSpenthoursCount.withSpentHoursOverEstimates.count += 1;
|
||||||
|
issuesWithEstimatesAndSpenthoursCount.withSpentHoursOverEstimates.issueIds.push(
|
||||||
|
issue.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
issuesWithEstimatesAndSpenthoursCount.byStatus[status].withSpentHoursOverEstimates.count += 1;
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
issuesWithEstimatesAndSpenthoursCount.byStatus[status].withSpentHoursOverEstimates.issueIds.push(issue.id);
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
issuesWithEstimatesAndSpenthoursCount.byVersion[version].withSpentHoursOverEstimates.count += 1;
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
issuesWithEstimatesAndSpenthoursCount.byVersion[version].withSpentHoursOverEstimates.issueIds.push(issue.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.logger.debug(
|
||||||
|
'Step 4. Counting issues with estimates and spent hours - done',
|
||||||
|
JSON.stringify(issuesWithEstimatesAndSpenthoursCount),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ШАГ 5: Счётчики сравнения с предыдущим отчётом
|
||||||
|
this.logger.debug('Step 5: Calculating differences with previous report');
|
||||||
|
const differencesCount = {
|
||||||
|
newIssues: { count: 0, issueIds: [] },
|
||||||
|
lostIssues: { count: 0, issueIds: [] },
|
||||||
|
reopenedIssues: { count: 0, issueIds: [] },
|
||||||
|
closedIssues: { count: 0, issueIds: [] },
|
||||||
|
};
|
||||||
|
issues.forEach((issue) => {
|
||||||
|
const issueIntoPreviousReport = previousIssues.find(
|
||||||
|
(prevIssue) => prevIssue.id === issue.id,
|
||||||
|
);
|
||||||
|
if (!issueIntoPreviousReport) {
|
||||||
|
differencesCount.newIssues.count += 1;
|
||||||
|
differencesCount.newIssues.issueIds.push(issue.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
previousIssues.forEach((prevIssue) => {
|
||||||
|
const issueIntoCurrentReport = issues.find(
|
||||||
|
(currIssue) => currIssue.id === prevIssue.id,
|
||||||
|
);
|
||||||
|
if (!issueIntoCurrentReport) {
|
||||||
|
differencesCount.lostIssues.count += 1;
|
||||||
|
differencesCount.lostIssues.issueIds.push(prevIssue.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
issues.forEach((issue) => {
|
||||||
|
const issueStatus: any = issue.status;
|
||||||
|
if (issueStatus.is_closed) {
|
||||||
|
const prevReportClosedIssue = previousIssues.find((prevIssue) => {
|
||||||
|
return (
|
||||||
|
prevIssue.id === issue.id &&
|
||||||
|
prevIssue.status &&
|
||||||
|
(prevIssue.status as any).is_closed
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (!prevReportClosedIssue) {
|
||||||
|
differencesCount.closedIssues.count += 1;
|
||||||
|
differencesCount.closedIssues.issueIds.push(issue.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const prevReportOpenIssue = previousIssues.find((prevIssue) => {
|
||||||
|
return (
|
||||||
|
prevIssue.id === issue.id &&
|
||||||
|
prevIssue.status &&
|
||||||
|
!(prevIssue.status as any)?.is_closed
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (!prevReportOpenIssue) {
|
||||||
|
differencesCount.reopenedIssues.count += 1;
|
||||||
|
differencesCount.reopenedIssues.issueIds.push(issue.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.logger.debug(
|
||||||
|
'Step 5: Calculating differences with previous report - done',
|
||||||
|
JSON.stringify(differencesCount),
|
||||||
|
);
|
||||||
|
|
||||||
|
const metrics = {
|
||||||
|
issuesByStatusCount: issuesByStatusCount,
|
||||||
|
issuesByVersionsCount: issuesByVersionsCount,
|
||||||
|
changesCount: changesCount,
|
||||||
|
issuesWithEstimatesAndSpenthoursCount:
|
||||||
|
issuesWithEstimatesAndSpenthoursCount,
|
||||||
|
differencesCount: differencesCount,
|
||||||
|
};
|
||||||
|
this.logger.log(
|
||||||
|
`Metrics generated for dashboard ${dashboard.id}, widget ${widget.id}`,
|
||||||
|
);
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
private async saveReport(
|
private async saveReport(
|
||||||
dashboard: Dashboard,
|
dashboard: Dashboard,
|
||||||
widget: Widget,
|
widget: Widget,
|
||||||
|
|
@ -180,6 +500,7 @@ export class DailyEccmV2ReportTaskRunnerService {
|
||||||
updated_on: issue.updated_on,
|
updated_on: issue.updated_on,
|
||||||
closed_on: issue.closed_on,
|
closed_on: issue.closed_on,
|
||||||
status: issue.status,
|
status: issue.status,
|
||||||
|
fixed_version: issue.fixed_version,
|
||||||
priority: issue.priority,
|
priority: issue.priority,
|
||||||
author: issue.author,
|
author: issue.author,
|
||||||
assigned_to: issue.assigned_to,
|
assigned_to: issue.assigned_to,
|
||||||
|
|
@ -192,9 +513,33 @@ export class DailyEccmV2ReportTaskRunnerService {
|
||||||
due_date: issue.due_date,
|
due_date: issue.due_date,
|
||||||
done_ratio: issue.done_ratio,
|
done_ratio: issue.done_ratio,
|
||||||
estimated_hours: issue.estimated_hours,
|
estimated_hours: issue.estimated_hours,
|
||||||
|
total_estimated_hours: issue.total_estimated_hours,
|
||||||
|
spent_hours: issue.spent_hours,
|
||||||
total_spent_hours: issue.total_spent_hours,
|
total_spent_hours: issue.total_spent_hours,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
const prevDataResponse = await datasource.find({
|
||||||
|
selector: {
|
||||||
|
dashboardId: dashboardId,
|
||||||
|
widgetId: widget.id,
|
||||||
|
latest: true,
|
||||||
|
},
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
const prevData = prevDataResponse.docs[0];
|
||||||
|
const prevIssues = prevData ? prevData.reportIssues : [];
|
||||||
|
const prevMetrics = prevData ? prevData.issuesMetrics : {};
|
||||||
|
if (prevData) {
|
||||||
|
prevData.latest = false;
|
||||||
|
await datasource.insert(prevData);
|
||||||
|
}
|
||||||
|
const issuesMetrics = await this.analyzeIssueMetrics(
|
||||||
|
dashboard,
|
||||||
|
widget,
|
||||||
|
issues,
|
||||||
|
prevIssues,
|
||||||
|
prevMetrics,
|
||||||
|
);
|
||||||
const item: any = {
|
const item: any = {
|
||||||
_id: id,
|
_id: id,
|
||||||
id: id,
|
id: id,
|
||||||
|
|
@ -203,6 +548,8 @@ export class DailyEccmV2ReportTaskRunnerService {
|
||||||
datetime,
|
datetime,
|
||||||
datetimeFormatted,
|
datetimeFormatted,
|
||||||
reportIssues,
|
reportIssues,
|
||||||
|
issuesMetrics,
|
||||||
|
latest: true,
|
||||||
};
|
};
|
||||||
await datasource.insert(item);
|
await datasource.insert(item);
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
|
|
|
||||||
4
src/reports/daily-eccm-v2-report.service.ts
Normal file
4
src/reports/daily-eccm-v2-report.service.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DailyEccmV2ReportService {}
|
||||||
Loading…
Reference in a new issue