Добавлена доска задач сгруппированная по работникам
This commit is contained in:
parent
a51b8dae7c
commit
4509a729cb
4 changed files with 149 additions and 1 deletions
|
|
@ -23,6 +23,7 @@ import { RootIssueSubTreesWidgetService } from './project-dashboard/widgets/root
|
||||||
import { DynamicLoader } from './configs/dynamic-loader';
|
import { DynamicLoader } from './configs/dynamic-loader';
|
||||||
import { RedminePublicUrlConverter } from './converters/redmine-public-url.converter';
|
import { RedminePublicUrlConverter } from './converters/redmine-public-url.converter';
|
||||||
import { IssueUrlEnhancer } from './issue-enhancers/issue-url-enhancer';
|
import { IssueUrlEnhancer } from './issue-enhancers/issue-url-enhancer';
|
||||||
|
import { ListIssuesByUsersWidgetService } from './project-dashboard/widgets/list-issues-by-users.widget.service';
|
||||||
|
|
||||||
@Module({})
|
@Module({})
|
||||||
export class EventEmitterModule implements OnModuleInit {
|
export class EventEmitterModule implements OnModuleInit {
|
||||||
|
|
@ -50,6 +51,7 @@ export class EventEmitterModule implements OnModuleInit {
|
||||||
DynamicLoader,
|
DynamicLoader,
|
||||||
RedminePublicUrlConverter,
|
RedminePublicUrlConverter,
|
||||||
IssueUrlEnhancer,
|
IssueUrlEnhancer,
|
||||||
|
ListIssuesByUsersWidgetService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
EventEmitterService,
|
EventEmitterService,
|
||||||
|
|
@ -69,6 +71,7 @@ export class EventEmitterModule implements OnModuleInit {
|
||||||
DynamicLoader,
|
DynamicLoader,
|
||||||
RedminePublicUrlConverter,
|
RedminePublicUrlConverter,
|
||||||
IssueUrlEnhancer,
|
IssueUrlEnhancer,
|
||||||
|
ListIssuesByUsersWidgetService,
|
||||||
],
|
],
|
||||||
controllers: [MainController, UsersController, IssuesController],
|
controllers: [MainController, UsersController, IssuesController],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-namespace */
|
||||||
|
import { IssuesService } from '@app/event-emitter/issues/issues.service';
|
||||||
|
import { RedmineTypes } from '@app/event-emitter/models/redmine-types';
|
||||||
|
import { FlatIssuesStore } from '@app/event-emitter/utils/flat-issues-store';
|
||||||
|
import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store';
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import nano from 'nano';
|
||||||
|
import { WidgetInterface } from '../widget-interface';
|
||||||
|
|
||||||
|
export namespace ListIssuesByUsersWidgetNs {
|
||||||
|
export namespace Models {
|
||||||
|
export type Params = {
|
||||||
|
fromRootIssueId?: number;
|
||||||
|
fromQuery?: nano.MangoQuery;
|
||||||
|
userKey: string;
|
||||||
|
statuses: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FindResult = {
|
||||||
|
result?: any;
|
||||||
|
error?: FindErrors;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum FindErrors {
|
||||||
|
NOT_FOUND = 'NOT_FOUND',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Params = ListIssuesByUsersWidgetNs.Models.Params;
|
||||||
|
type ExtendedIssue = RedmineTypes.Issue & Record<string, any>;
|
||||||
|
type FindResult = ListIssuesByUsersWidgetNs.Models.FindResult;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ListIssuesByUsersWidgetService
|
||||||
|
implements WidgetInterface<Params, any, any>
|
||||||
|
{
|
||||||
|
private logger = new Logger(ListIssuesByUsersWidgetService.name);
|
||||||
|
|
||||||
|
constructor(private issuesService: IssuesService) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isMyConfig(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async render(widgetParams: Params): Promise<any> {
|
||||||
|
// if (widgetParams)
|
||||||
|
let store: FlatIssuesStore;
|
||||||
|
if (widgetParams.fromRootIssueId) {
|
||||||
|
store = await this.getListFromRoot(widgetParams.fromRootIssueId);
|
||||||
|
} else if (widgetParams.fromQuery) {
|
||||||
|
store = await this.getListByQuery(widgetParams.fromQuery);
|
||||||
|
} else {
|
||||||
|
const errMsg = `Wrong widgetParams value`;
|
||||||
|
this.logger.error(`Wrong widgetParams value`);
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
const grouped = store.groupByStatusWithExtra((issue) => {
|
||||||
|
const res = this.getUserValueByKey(issue, widgetParams.userKey);
|
||||||
|
return res.result || 'Unknown';
|
||||||
|
}, widgetParams.statuses);
|
||||||
|
const res = [] as any[];
|
||||||
|
for (const user in grouped) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(grouped, user)) {
|
||||||
|
const data = grouped[user];
|
||||||
|
res.push({
|
||||||
|
data: data,
|
||||||
|
metainfo: this.createMetaInfo(user),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getListFromRoot(issueId: number): Promise<FlatIssuesStore> {
|
||||||
|
const treeStore = new TreeIssuesStore();
|
||||||
|
const rootIssue = await this.issuesService.getIssue(issueId);
|
||||||
|
treeStore.setRootIssue(rootIssue);
|
||||||
|
await treeStore.fillData(this.issuesLoader.bind(this));
|
||||||
|
return treeStore.getFlatStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getListByQuery(
|
||||||
|
query: nano.MangoQuery,
|
||||||
|
): Promise<FlatIssuesStore> {
|
||||||
|
const rawData = await this.issuesService.find(query);
|
||||||
|
const store = new FlatIssuesStore();
|
||||||
|
for (let i = 0; i < rawData.length; i++) {
|
||||||
|
const issue = rawData[i];
|
||||||
|
store.push(issue);
|
||||||
|
}
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async issuesLoader(
|
||||||
|
ids: number[],
|
||||||
|
): Promise<Record<number, RedmineTypes.Issue | null>> {
|
||||||
|
const issues = await this.issuesService.getIssues(ids);
|
||||||
|
const res = {} as Record<number, RedmineTypes.Issue | null>;
|
||||||
|
for (let i = 0; i < issues.length; i++) {
|
||||||
|
const issue = issues[i];
|
||||||
|
res[issue.id] = issue;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUserValueByKey(issue: ExtendedIssue, key: string): FindResult {
|
||||||
|
const keys = key.split('.');
|
||||||
|
let res: any = issue;
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
const k = keys[i];
|
||||||
|
if (!res.hasOwnProperty(k)) {
|
||||||
|
return { error: ListIssuesByUsersWidgetNs.Models.FindErrors.NOT_FOUND };
|
||||||
|
}
|
||||||
|
res = res[k];
|
||||||
|
}
|
||||||
|
return { result: res };
|
||||||
|
}
|
||||||
|
|
||||||
|
private createMetaInfo(user: string): Record<string, any> {
|
||||||
|
return {
|
||||||
|
title: user,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { DynamicLoader } from '@app/event-emitter/configs/dynamic-loader';
|
import { DynamicLoader } from '@app/event-emitter/configs/dynamic-loader';
|
||||||
|
import { ListIssuesByUsersWidgetService } from '@app/event-emitter/project-dashboard/widgets/list-issues-by-users.widget.service';
|
||||||
import { RootIssueSubTreesWidgetService } from '@app/event-emitter/project-dashboard/widgets/root-issue-subtrees.widget.service';
|
import { RootIssueSubTreesWidgetService } from '@app/event-emitter/project-dashboard/widgets/root-issue-subtrees.widget.service';
|
||||||
import { Controller, Get, Param, Render } from '@nestjs/common';
|
import { Controller, Get, Param, Render } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
@ -12,6 +13,7 @@ export class SimpleKanbanBoardController {
|
||||||
private rootIssueSubTreesWidgetService: RootIssueSubTreesWidgetService,
|
private rootIssueSubTreesWidgetService: RootIssueSubTreesWidgetService,
|
||||||
private dynamicLoader: DynamicLoader,
|
private dynamicLoader: DynamicLoader,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
|
private listIssuesByUsersWidgetService: ListIssuesByUsersWidgetService,
|
||||||
) {
|
) {
|
||||||
this.path = this.configService.get<string>('simpleKanbanBoard.path');
|
this.path = this.configService.get<string>('simpleKanbanBoard.path');
|
||||||
}
|
}
|
||||||
|
|
@ -31,4 +33,20 @@ export class SimpleKanbanBoardController {
|
||||||
async get(@Param('name') name: string): Promise<any> {
|
async get(@Param('name') name: string): Promise<any> {
|
||||||
return await this.getRawData(name);
|
return await this.getRawData(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/by-users/:name/raw')
|
||||||
|
async getByUsersRawData(@Param('name') name: string): Promise<any> {
|
||||||
|
const cfg = this.dynamicLoader.load(name, {
|
||||||
|
path: this.path,
|
||||||
|
ext: 'jsonc',
|
||||||
|
parser: parse,
|
||||||
|
});
|
||||||
|
return await this.listIssuesByUsersWidgetService.render(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/by-users/:name')
|
||||||
|
@Render('simple-kanban-board')
|
||||||
|
async getByUsers(@Param('name') name: string): Promise<any> {
|
||||||
|
return await this.getByUsersRawData(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@
|
||||||
<div class="kanban-header">{{this.status}}</div>
|
<div class="kanban-header">{{this.status}}</div>
|
||||||
{{#each this.issues}}
|
{{#each this.issues}}
|
||||||
<div class="kanban-card">
|
<div class="kanban-card">
|
||||||
<div class="kanban-card-title">{{this.tracker.name}} #{{this.id}} - {{this.subject}}</div>
|
<div class="kanban-card-title"><a href="{{{this.url.url}}}">{{this.tracker.name}} #{{this.id}} - {{this.subject}}</a></div>
|
||||||
<div>Исп.: {{this.current_user.name}}</div>
|
<div>Исп.: {{this.current_user.name}}</div>
|
||||||
<div>Прогресс: {{this.done_ration}}</div>
|
<div>Прогресс: {{this.done_ration}}</div>
|
||||||
<div>Трудозатраты: {{this.total_spent_hours}} / {{this.total_estimated_hours}}</div>
|
<div>Трудозатраты: {{this.total_spent_hours}} / {{this.total_estimated_hours}}</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue