Добавлена доска задач сгруппированная по работникам

This commit is contained in:
Pavel Gnedov 2023-02-09 19:04:32 +07:00
parent a51b8dae7c
commit 4509a729cb
4 changed files with 149 additions and 1 deletions

View file

@ -23,6 +23,7 @@ import { RootIssueSubTreesWidgetService } from './project-dashboard/widgets/root
import { DynamicLoader } from './configs/dynamic-loader';
import { RedminePublicUrlConverter } from './converters/redmine-public-url.converter';
import { IssueUrlEnhancer } from './issue-enhancers/issue-url-enhancer';
import { ListIssuesByUsersWidgetService } from './project-dashboard/widgets/list-issues-by-users.widget.service';
@Module({})
export class EventEmitterModule implements OnModuleInit {
@ -50,6 +51,7 @@ export class EventEmitterModule implements OnModuleInit {
DynamicLoader,
RedminePublicUrlConverter,
IssueUrlEnhancer,
ListIssuesByUsersWidgetService,
],
exports: [
EventEmitterService,
@ -69,6 +71,7 @@ export class EventEmitterModule implements OnModuleInit {
DynamicLoader,
RedminePublicUrlConverter,
IssueUrlEnhancer,
ListIssuesByUsersWidgetService,
],
controllers: [MainController, UsersController, IssuesController],
};

View file

@ -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,
};
}
}

View file

@ -1,4 +1,5 @@
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 { Controller, Get, Param, Render } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@ -12,6 +13,7 @@ export class SimpleKanbanBoardController {
private rootIssueSubTreesWidgetService: RootIssueSubTreesWidgetService,
private dynamicLoader: DynamicLoader,
private configService: ConfigService,
private listIssuesByUsersWidgetService: ListIssuesByUsersWidgetService,
) {
this.path = this.configService.get<string>('simpleKanbanBoard.path');
}
@ -31,4 +33,20 @@ export class SimpleKanbanBoardController {
async get(@Param('name') name: string): Promise<any> {
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);
}
}

View file

@ -52,7 +52,7 @@
<div class="kanban-header">{{this.status}}</div>
{{#each this.issues}}
<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.done_ration}}</div>
<div>Трудозатраты: {{this.total_spent_hours}} / {{this.total_estimated_hours}}</div>