Compare commits
2 commits
159e2ea17a
...
f3a8cded97
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3a8cded97 | ||
|
|
d5d604eb40 |
6 changed files with 508 additions and 20 deletions
|
|
@ -9,20 +9,25 @@ export class CouchDb {
|
|||
private static logger = new Logger(CouchDb.name);
|
||||
private static couchdb: nano.ServerScope | null = null;
|
||||
private static initialized = false;
|
||||
private static url: string | null = null;
|
||||
|
||||
static getCouchDb(): nano.ServerScope | null {
|
||||
if (CouchDb.initialized) {
|
||||
return CouchDb.couchdb;
|
||||
}
|
||||
CouchDb.initialized = true;
|
||||
const url = config.couchDb?.url;
|
||||
if (!url) {
|
||||
this.url = config.couchDb?.url;
|
||||
if (!this.url) {
|
||||
return null;
|
||||
} else {
|
||||
const n = nano(url);
|
||||
CouchDb.logger.log(`CouchDb connected by url ${url} ...`);
|
||||
const n = nano(this.url);
|
||||
CouchDb.logger.log(`CouchDb connected by url ${this.url} ...`);
|
||||
CouchDb.couchdb = n;
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
||||
static getCouchDbUrl(): string | null {
|
||||
return this.url;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ export module RedmineTypes {
|
|||
done_ratio: number;
|
||||
spent_hours: number;
|
||||
total_spent_hours: number;
|
||||
estimated_hours: number;
|
||||
total_estimated_hours: number;
|
||||
custom_fields: CustomField[];
|
||||
created_on: string;
|
||||
updated_on?: string;
|
||||
|
|
@ -89,6 +91,8 @@ export module RedmineTypes {
|
|||
done_ratio: num,
|
||||
spent_hours: num,
|
||||
total_spent_hours: num,
|
||||
estimated_hours: num,
|
||||
total_estimated_hours: num,
|
||||
custom_fields: [],
|
||||
created_on: date,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { CouchDb } from '@app/event-emitter/couchdb-datasources/couchdb';
|
||||
import nano from 'nano';
|
||||
import { Report } from 'src/reports/daily-eccm-v2-report-task-runner.service';
|
||||
import axios from 'axios';
|
||||
|
||||
const DB_NAME = 'eccm_daily_reports_v2';
|
||||
const DATETIME_INDEX_ASC = 'datetime-json-index';
|
||||
|
||||
type Report = any; // TODO fix this type
|
||||
|
||||
@Injectable()
|
||||
export class DailyEccmReportsV2Datasource {
|
||||
|
|
@ -15,15 +20,61 @@ export class DailyEccmReportsV2Datasource {
|
|||
}
|
||||
DailyEccmReportsV2Datasource.initilized = true;
|
||||
const n = CouchDb.getCouchDb();
|
||||
const dbName = 'eccm_daily_reports_v2';
|
||||
const dbName = DB_NAME;
|
||||
const dbs = await n.db.list();
|
||||
if (!dbs.includes(dbName)) {
|
||||
await n.db.create(dbName);
|
||||
}
|
||||
DailyEccmReportsV2Datasource.db = await n.db.use(dbName);
|
||||
await this.checkAndCreateIndex();
|
||||
DailyEccmReportsV2Datasource.logger.log(
|
||||
`Connected to daily reports db - ${dbName}`,
|
||||
);
|
||||
return DailyEccmReportsV2Datasource.db;
|
||||
}
|
||||
|
||||
static async checkAndCreateIndex(): Promise<void> {
|
||||
const couchDbUrl = CouchDb.getCouchDbUrl();
|
||||
const indexes = await DailyEccmReportsV2Datasource.getIndexes(couchDbUrl);
|
||||
const index = indexes.find(
|
||||
(index: any) => index.name === DATETIME_INDEX_ASC,
|
||||
);
|
||||
if (!index) {
|
||||
await DailyEccmReportsV2Datasource.createIndex(couchDbUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private static async getIndexes(couchDbUrl: string): Promise<any[]> {
|
||||
const response = await axios.get(
|
||||
`${couchDbUrl}/${DB_NAME}/_index?skip=0&limit=999999`,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
this.logger.debug(`Indexes: ${JSON.stringify(response.data.indexes)}`);
|
||||
return response.data.indexes;
|
||||
}
|
||||
|
||||
private static async createIndex(couchDbUrl: string): Promise<boolean> {
|
||||
this.logger.debug(`Creating index ${DATETIME_INDEX_ASC}`);
|
||||
const body = {
|
||||
index: { fields: [{ datetime: 'asc' }] },
|
||||
name: DATETIME_INDEX_ASC,
|
||||
type: 'json',
|
||||
};
|
||||
const response = await axios.post(`${couchDbUrl}/${DB_NAME}/_index`, body, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
this.logger.debug(
|
||||
`Index ${DATETIME_INDEX_ASC} created ` +
|
||||
`with response: ${JSON.stringify(response.data)}`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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 { RedmineTypes } from '@app/event-emitter/models/redmine-types';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
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;
|
||||
};
|
||||
import { Params } from './daily-eccm-v2-report-task-handler';
|
||||
|
||||
export type Job = {
|
||||
id: string;
|
||||
params: Params;
|
||||
};
|
||||
|
||||
export type JobHandlerParams = {
|
||||
job: Job;
|
||||
dashboard: Dashboard;
|
||||
widget: Widget;
|
||||
};
|
||||
|
||||
export const WIDGET_TYPE = '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(
|
||||
dashboard: Dashboard,
|
||||
widget: Widget,
|
||||
|
|
@ -180,6 +500,7 @@ export class DailyEccmV2ReportTaskRunnerService {
|
|||
updated_on: issue.updated_on,
|
||||
closed_on: issue.closed_on,
|
||||
status: issue.status,
|
||||
fixed_version: issue.fixed_version,
|
||||
priority: issue.priority,
|
||||
author: issue.author,
|
||||
assigned_to: issue.assigned_to,
|
||||
|
|
@ -192,9 +513,33 @@ export class DailyEccmV2ReportTaskRunnerService {
|
|||
due_date: issue.due_date,
|
||||
done_ratio: issue.done_ratio,
|
||||
estimated_hours: issue.estimated_hours,
|
||||
total_estimated_hours: issue.total_estimated_hours,
|
||||
spent_hours: issue.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 = {
|
||||
_id: id,
|
||||
id: id,
|
||||
|
|
@ -203,6 +548,8 @@ export class DailyEccmV2ReportTaskRunnerService {
|
|||
datetime,
|
||||
datetimeFormatted,
|
||||
reportIssues,
|
||||
issuesMetrics,
|
||||
latest: true,
|
||||
};
|
||||
await datasource.insert(item);
|
||||
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