TEST-1234 test(test): test

This commit is contained in:
bakhirev 2023-11-13 16:55:35 +03:00
parent 2964b8b5e1
commit 127d978b31
67 changed files with 2295 additions and 592 deletions

17
build/asset-manifest.json Normal file
View file

@ -0,0 +1,17 @@
{
"files": {
"main.css": "./static/css/main.bd89a1c5.css",
"main.js": "./static/js/main.b114e843.js",
"static/media/car.png": "./static/media/car.b8dd8738e37fe866285f.png",
"index.html": "./index.html",
"static/media/warning.svg": "./static/media/warning.e39a87773603f3ab157f.svg",
"static/media/info.svg": "./static/media/info.954631f6b19e3fe9c495.svg",
"static/media/alert.svg": "./static/media/alert.41e2b99c481139c13074.svg",
"main.bd89a1c5.css.map": "./static/css/main.bd89a1c5.css.map",
"main.b114e843.js.map": "./static/js/main.b114e843.js.map"
},
"entrypoints": [
"static/css/main.bd89a1c5.css",
"static/js/main.b114e843.js"
]
}

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="#84858D" xmlns="http://www.w3.org/2000/svg">
<path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"></path>
</svg>

After

Width:  |  Height:  |  Size: 189 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="#84858D" xmlns="http://www.w3.org/2000/svg">
<path d="M20 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h15c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 2v3H5V5h15zm-5 14h-5v-9h5v9zM5 10h3v9H5v-9zm12 9v-9h3v9h-3z"></path>
</svg>

After

Width:  |  Height:  |  Size: 266 B

1
build/index.html Normal file
View file

@ -0,0 +1 @@
<!doctype html><html lang="ru"><head><meta name="viewport" content="width=device-width,height=device-height,initial-scale=1,user-scalable=no,maximum-scale=1"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><meta http-equiv="Cache-Control" content="no-cache"><meta http-equiv="cleartype" content="on"><meta name="HandheldFriendly" content="True"><meta name="format-detection" content="telephone=no"><meta name="format-detection" content="address=no"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><script type="text/javascript">var report=[]</script><script src="/log.txt"></script><script src="./log.txt"></script><script src="../log.txt"></script><script src="./log-0.txt"></script><script src="./log-1.txt"></script><script src="./log-2.txt"></script><script src="./log-3.txt"></script><script src="./log-4.txt"></script><script src="./log-5.txt"></script><script src="./log-6.txt"></script><script src="./report/log-0.txt"></script><script src="./report/log-1.txt"></script><script src="./report/log-2.txt"></script><script src="./report/log-3.txt"></script><script src="./report/log-4.txt"></script><script src="./report/log-5.txt"></script><script src="./report/log-6.txt"></script><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./logo192.png"/><link rel="manifest" href="./manifest.json"/><title>Git статистика</title><meta name="description" content="Простой и быстрый отчёт по истории коммитов в git."><meta name="keywords" content="git, статистика, аудит, история, log, мониторинг, контроль сотрудников"><meta name="author" content="Bakhirev Aleksei"><meta name="copyright" content="(c) Bakhirev Aleksei"><meta http-equiv="Reply-to" content="alexey-bakhirev@yandex.ru"><meta name="application-name" content="GIT Статистика"><meta name="msapplication-tooltip" content="Простой и быстрый отчёт по истории коммитов в git."><meta property="og:title" content="GIT Статистика"><meta property="og:description" content="Простой и быстрый отчёт по истории коммитов в git."><meta property="og:image" content="http://assayo.jp/assets/seo/custom_icon_256.png"><meta property="og:site_name" content="Assayo"><meta property="og:url" content="http://assayo.jp/"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="GIT Статистика"><meta name="twitter:description" content="Простой и быстрый отчёт по истории коммитов в git."><meta name="twitter:creator" content="Bakhirev Aleksei"><meta name="twitter:image:src" content="http://assayo.jp/assets/seo/custom_icon_256.png"><meta name="twitter:domain" content="assayo.jp"><meta name="twitter:site" content="assayo.jp"><meta itemprop="name" content="GIT Статистика"><meta itemprop="description" content="Простой и быстрый отчёт по истории коммитов в git."><meta itemprop="image" content="http://assayo.jp/assets/seo/custom_icon_256.png"><script defer="defer" src="./static/js/main.b114e843.js"></script><link href="./static/css/main.bd89a1c5.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,74 @@
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @remix-run/router v1.3.1
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/**
* React Router DOM v6.8.0
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/**
* React Router v6.8.0
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" fill="#E29893"><path d="M2 42 24 4l22 38Zm5.2-3h33.6L24 10Zm17-2.85q.65 0 1.075-.425.425-.425.425-1.075 0-.65-.425-1.075-.425-.425-1.075-.425-.65 0-1.075.425Q22.7 34 22.7 34.65q0 .65.425 1.075.425.425 1.075.425Zm-1.5-5.55h3V19.4h-3Zm1.3-6.1Z"/></svg>

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" fill="#0386D4"><path d="M22.65 34h3V22h-3ZM24 18.3q.7 0 1.175-.45.475-.45.475-1.15t-.475-1.2Q24.7 15 24 15q-.7 0-1.175.5-.475.5-.475 1.2t.475 1.15q.475.45 1.175.45ZM24 44q-4.1 0-7.75-1.575-3.65-1.575-6.375-4.3-2.725-2.725-4.3-6.375Q4 28.1 4 23.95q0-4.1 1.575-7.75 1.575-3.65 4.3-6.35 2.725-2.7 6.375-4.275Q19.9 4 24.05 4q4.1 0 7.75 1.575 3.65 1.575 6.35 4.275 2.7 2.7 4.275 6.35Q44 19.85 44 24q0 4.1-1.575 7.75-1.575 3.65-4.275 6.375t-6.35 4.3Q28.15 44 24 44Zm.05-3q7.05 0 12-4.975T41 23.95q0-7.05-4.95-12T24 7q-7.05 0-12.025 4.95Q7 16.9 7 24q0 7.05 4.975 12.025Q16.95 41 24.05 41ZM24 24Z"/></svg>

After

Width:  |  Height:  |  Size: 660 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" fill="#FB9A1F"><path d="M31.3 21.35q1.15 0 1.925-.8.775-.8.775-1.9 0-1.15-.775-1.925-.775-.775-1.925-.775-1.1 0-1.9.775-.8.775-.8 1.925 0 1.1.8 1.9.8.8 1.9.8Zm-14.6 0q1.15 0 1.925-.8.775-.8.775-1.9 0-1.15-.775-1.925-.775-.775-1.925-.775-1.1 0-1.9.775-.8.775-.8 1.925 0 1.1.8 1.9.8.8 1.9.8Zm7.3 5.8q-3.35 0-6.075 1.875T13.9 34h2.65q1.1-2.1 3.1-3.25t4.4-1.15q2.35 0 4.325 1.175T31.5 34h2.6q-1.25-3.15-4-5T24 27.15ZM24 44q-4.15 0-7.8-1.575-3.65-1.575-6.35-4.275-2.7-2.7-4.275-6.35Q4 28.15 4 24t1.575-7.8Q7.15 12.55 9.85 9.85q2.7-2.7 6.35-4.275Q19.85 4 24 4t7.8 1.575q3.65 1.575 6.35 4.275 2.7 2.7 4.275 6.35Q44 19.85 44 24t-1.575 7.8q-1.575 3.65-4.275 6.35-2.7 2.7-6.35 4.275Q28.15 44 24 44Zm0-20Zm0 17q7.1 0 12.05-4.95Q41 31.1 41 24q0-7.1-4.95-12.05Q31.1 7 24 7q-7.1 0-12.05 4.95Q7 16.9 7 24q0 7.1 4.95 12.05Q16.9 41 24 41Z"/></svg>

After

Width:  |  Height:  |  Size: 893 B

View file

@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" fill="#84858D" xmlns="http://www.w3.org/2000/svg">
<path d="M19 5v2h-4V5h4M9 5v6H5V5h4m10 8v6h-4v-6h4M9 17v2H5v-2h4M21 3h-8v6h8V3zM11 3H3v10h8V3zm10 8h-8v10h8V11zm-10 4H3v6h8v-6z"></path>
</svg>

After

Width:  |  Height:  |  Size: 225 B

View file

@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" fill="#84858D" xmlns="http://www.w3.org/2000/svg">
<path d="M20 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h15c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 2v3H5V5h15zm-5 14h-5v-9h5v9zM5 10h3v9H5v-9zm12 9v-9h3v9h-3z"></path>
</svg>

After

Width:  |  Height:  |  Size: 245 B

View file

@ -3,6 +3,7 @@ import { HashRouter } from 'react-router-dom';
import { render } from 'react-dom'; import { render } from 'react-dom';
import ru from 'ts/config/translations/ru'; import ru from 'ts/config/translations/ru';
import en from 'ts/config/translations/en';
import Authorization from 'ts/pages/Authorization'; import Authorization from 'ts/pages/Authorization';
import userSettings from 'ts/store/UserSettings'; import userSettings from 'ts/store/UserSettings';
import Notifications from 'ts/components/Notifications'; import Notifications from 'ts/components/Notifications';
@ -19,7 +20,7 @@ if (module.hot) {
} }
// @ts-ignore // @ts-ignore
console.dir(ru + ''); console.dir(ru + en + '');
function getParametersFromString(text: string) { function getParametersFromString(text: string) {
return Object.fromEntries((text || '') return Object.fromEntries((text || '')

View file

@ -1,22 +1,26 @@
import React from 'react'; import React from 'react';
import ALL_ACHIEVEMENTS from 'ts/helpers/achievement/constants/list'; import ALL_ACHIEVEMENTS from 'ts/helpers/achievement/constants/list';
import localization from 'ts/helpers/Localization';
import style from '../styles/index.module.scss'; import style from '../styles/index.module.scss';
interface IAchievementProps { interface IAchievementProps {
type: string; code: string;
} }
function Achievement({ type }: IAchievementProps) { function Achievement({ code }: IAchievementProps) {
if (!ALL_ACHIEVEMENTS[type]) return null; if (!ALL_ACHIEVEMENTS[code]) return null;
const [title, description, statusIndex] = ALL_ACHIEVEMENTS[type]; const title = localization.get(`achievements.${code}.title`);
const description = localization.get(`achievements.${code}.description`);
const statusIndex = ALL_ACHIEVEMENTS[code];
const className = [ const className = [
style.achievement_good, style.achievement_good,
style.achievement_middle, style.achievement_middle,
style.achievement_bad, style.achievement_bad,
][statusIndex]; ][statusIndex - 1];
return ( return (
<div className={style.achievement}> <div className={style.achievement}>
@ -24,7 +28,7 @@ function Achievement({ type }: IAchievementProps) {
<div className={`${style.achievement_icon} ${className || ''}`}> <div className={`${style.achievement_icon} ${className || ''}`}>
<img <img
className={style.achievement_icon_svg} className={style.achievement_icon_svg}
src={`./assets/achievements/${type}.svg`} src={`./assets/achievements/${code}.svg`}
/> />
</div> </div>
</div> </div>

View file

@ -8,10 +8,10 @@ interface IAchievementsProps {
} }
function Achievements({ list }: IAchievementsProps) { function Achievements({ list }: IAchievementsProps) {
const items = list?.map((type: string) => ( const items = list?.map((code: string) => (
<Achievement <Achievement
key={type} key={code}
type={type} code={code}
/> />
)); ));

View file

@ -0,0 +1,67 @@
import React from 'react';
import { IColumn } from 'ts/components/Table/interfaces/Column';
import Line from './Line';
import Title from './Title';
import style from '../styles/index.module.scss';
interface ICardProps {
item: any;
lines: IColumn[];
className?: string;
}
function Card({
item,
lines,
className,
}: ICardProps) {
const parts = lines.map((line: IColumn, columnIndex: number) => {
const value = line.properties
? item[line.properties]
: item;
const formattedValue = line.formatter
? line.formatter(value)
: value;
if (typeof line.template === 'function') {
return line.template(formattedValue, item);
}
const content = `${line.prefixes ?? ''}${formattedValue ?? ''}${line.suffixes ?? ''}`;
if (!columnIndex) {
return (
<Title
key={`${line.title}_${columnIndex}`}
item={item}
column={line}
value={content}
/>
);
}
return (
<Line
key={`${line.title}_${columnIndex}`}
item={item}
column={line}
value={content}
/>
);
});
return (
<div className={`${style.card} ${className}`}>
{parts}
</div>
);
}
Card.defaultProps = {
className: '',
};
export default Card;

View file

@ -0,0 +1,44 @@
import React from 'react';
import { IColumn } from 'ts/components/Table/interfaces/Column';
import localization from 'ts/helpers/Localization';
import style from '../styles/index.module.scss';
interface ILineProps {
column: IColumn,
item: any,
value?: string | number | boolean | null;
className?: string | Function,
}
function Line({
column,
item,
value,
className,
}: ILineProps): JSX.Element {
const columnClassName = typeof column.className === 'function'
? column.className('body', item)
: column.className;
return (
<div
key={column.title}
className={`${style.card_line} ${className || ''} ${columnClassName || ''}`}
>
<div className={style.card_line_title}>
{localization.get(column.title)}
</div>
<div className={style.card_line_value}>
{value}
</div>
</div>
);
}
Line.defaultPeops = {
className: '',
};
export default Line;

View file

@ -0,0 +1,38 @@
import React from 'react';
import { IColumn } from 'ts/components/Table/interfaces/Column';
import style from '../styles/index.module.scss';
interface ILineProps {
column: IColumn,
item: any,
className?: string | Function,
value?: string | number | boolean | null;
}
function LineTitle({
column,
item,
className,
value,
}: ILineProps): JSX.Element {
const columnClassName = typeof column.className === 'function'
? column.className('body', item)
: column.className;
return (
<div
key={column.title}
className={`${style.card_title} ${className || ''} ${columnClassName || ''}`}
>
{value}
</div>
);
}
LineTitle.defaultPeops = {
className: '',
};
export default LineTitle;

View file

@ -0,0 +1,41 @@
import { IColumn, ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
function getCardConfigs(
dirtyColumns: IColumn[] = [],
): IColumn[] {
const groups = dirtyColumns.reduce((
acc: any,
column: IColumn,
index: number,
) => {
const nextColumn = dirtyColumns[index + 1];
if (column.template === ColumnTypesEnum.SHORT_NUMBER
&& typeof nextColumn?.template === 'function') {
acc.text.push({
...column,
title: nextColumn?.title,
});
return acc;
}
if (typeof column.template === 'function') {
if (index > 0 && dirtyColumns[index - 1].template === ColumnTypesEnum.SHORT_NUMBER) {
acc.shortChart.push(column);
} else {
acc.longChart.push(column);
}
} else {
acc.text.push(column);
}
return acc;
}, { text: [], shortChart: [], longChart: [] });
return [
...groups.text,
...groups.longChart,
// ...groups.shortChart,
] as IColumn[];
}
export default getCardConfigs;

View file

@ -0,0 +1,47 @@
import React from 'react';
import { IColumn } from 'ts/components/Table/interfaces/Column';
import getDefaultProps from 'ts/components/Table/helpers/getDefaultProps';
import Card from './components/Card';
import getCardConfigs from './helpers/getCardConfigs';
import style from './styles/index.module.scss';
interface ICardsProps {
items: any[];
className?: string;
children: React.ReactNode | React.ReactNode[];
}
function Cards({
items = [],
className,
children,
}: ICardsProps): React.ReactElement | null {
if (!items || !items.length) return null;
const configs = getDefaultProps(children) as IColumn[];
const lines = getCardConfigs(configs) as IColumn[];
const cards = items?.map((item: any, index: number) => (
<Card
key={index}
item={item}
lines={lines}
className={className}
/>
));
return (
<div className={style.card_wrapper}>
{cards}
</div>
);
}
Cards.defaultProps = {
items: [],
className: undefined,
};
export default Cards;

View file

@ -0,0 +1,84 @@
@import '../../../../styles/variables';
.card {
display: inline-block;
width: 100%;
padding: var(--space-s);
margin: 0 0 var(--space-xxl) 0;
border-radius: var(--border-radius-s);
border: 1px solid var(--color-border);
vertical-align: top;
box-sizing: border-box;
box-shadow: 4px 4px 4px #CCCCCC;
&_wrapper {
margin-top: var(--space-xxl);
column-count: 4;
column-gap: var(--space-xxl);
}
&_title {
font-size: var(--font-m);
font-weight: bold;
display: block;
margin: 0 0 var(--space-l);
line-height: 1.3;
white-space: normal;
text-overflow: ellipsis;
text-align: center;
}
&_line {
display: block;
white-space: nowrap;
padding: var(--space-xxs);
box-sizing: border-box;
border: none;
&_title,
&_value {
font-size: var(--font-s);
position: relative;
display: inline-block;
line-height: 1.3;
white-space: normal;
text-overflow: ellipsis;
vertical-align: top;
box-sizing: border-box;
text-align: left;
}
&_title {
width: 60%;
max-width: 200px;
}
&_value {
width: 40%;
text-align: right;
}
}
}
.card_line + .card_line {
border-top: 1px solid var(--color-border);
}
@media (max-width: 1350px) {
.card_wrapper {
column-count: 3;
}
}
@media (max-width: 1100px) {
.card_wrapper {
column-count: 2;
}
}
@media (max-width: 700px) {
.card_wrapper {
column-count: 1;
}
}

View file

@ -0,0 +1,16 @@
@import '../../../styles/variables';
.data_view {
&_icon {
position: absolute;
top: -48px;
right: 24px;
display: inline-block;
width: var(--space-xxl);
height: var(--space-xxl);
padding: 0;
cursor: pointer;
}
}

View file

@ -0,0 +1,84 @@
import React, { useState } from 'react';
import ISort from 'ts/interfaces/Sort';
import Table from 'ts/components/Table';
import Cards from 'ts/components/Cards';
import style from './index.module.scss';
interface IDataViewProps {
rows: any[];
type?: string;
sort?: ISort[];
className?: string,
disabledRow?: (row: any) => boolean;
updateSort?: Function,
children: React.ReactNode | React.ReactNode[];
}
function DataView({
rows = [],
sort = [],
type,
className,
disabledRow,
updateSort,
children,
}: IDataViewProps): React.ReactElement | null {
const [localType, setType] = useState<string>(type || 'table');
if (!rows || !rows.length) return null;
const icon = {
table: './assets/icons/Cards.svg',
cards: './assets/icons/Table.svg',
}[localType];
const title = {
table: 'Отобразить карточками',
cards: 'Отобразить таблицой',
}[localType];
return (
<>
<img
title={title}
src={icon}
className={style.data_view_icon}
onClick={() => {
setType(localType === 'table' ? 'cards' : 'table');
}}
/>
{localType === 'table' && (
<Table
rows={rows}
sort={sort}
disabledRow={disabledRow}
updateSort={updateSort}
>
{children}
</Table>
)}
{localType === 'cards' && (
<Cards
items={rows}
className={className}
>
{children}
</Cards>
)}
</>
);
}
DataView.defaultProps = {
rows: [],
sort: [],
type: 'table',
updateSort: () => {
},
};
export default DataView;

View file

@ -1,14 +1,14 @@
function evalCsvFile(text: string, onChange: Function) { // function evalCsvFile(text: string, onChange: Function) {
const byTaskId = {}; // const byTaskId = {};
text.split('\n').forEach(row => { // text.split('\n').forEach(row => {
const [taskId, type, scopeOrTitle, title] = row.split('|'); // const [taskId, type, scopeOrTitle, title] = row.split('|');
const scope = title ? scopeOrTitle : ''; // const scope = title ? scopeOrTitle : '';
byTaskId[taskId] = { type, scope }; // byTaskId[taskId] = { type, scope };
}); // });
onChange('meta', { byTaskId }); // onChange('meta', { byTaskId });
} // }
function evalJsFile(text: string, onChange: Function) { export function getStringsForParser(text: string) {
// @ts-ignore // @ts-ignore
let temp = window.report; // @ts-ignore let temp = window.report; // @ts-ignore
window.report = []; window.report = [];
@ -27,48 +27,37 @@ function evalJsFile(text: string, onChange: Function) {
} }
// @ts-ignore // @ts-ignore
onChange('dump', window.report); return window.report;
} }
export function getOnDrop(setLoading: Function, onChange: Function) { export async function getStringFromFileList(files: any) {
return function dropFile(event: DragEvent) { const text: string[] = await Promise.all(
event.preventDefault(); files.map((file: any) => file.text()),
event.stopPropagation(); );
const dropItems = [...(event?.dataTransfer?.items || [])] return text
.map((file: any) => file.kind === 'file' ? file?.getAsFile() : null)
.filter(file => file);
setLoading(false);
if (!dropItems.length) return;
if (dropItems[0].type === 'application/json') {
Promise.all(
dropItems.map((file: any) => file.text()),
).then((text: string[]) => {
const telegrammMessages = text
.map(file => JSON.parse(file)?.messages)
.flat(1);
// @ts-ignore
onChange('telegramm', telegrammMessages);
});
return;
}
Promise.all(
dropItems.map((file: any) => file.text()),
).then((text: string[]) => {
const sortedText = text
.filter(file => file) .filter(file => file)
.map((item: string) => ({ key: item.substring(13, 32), text: item })) .map((item: string) => ({ key: item.substring(13, 32), text: item }))
.sort((a: any, b: any) => (a.key || '').localeCompare(b.key || '')) .sort((a: any, b: any) => (a.key || '').localeCompare(b.key || ''))
.map(item => item.text) .map(item => item.text)
.join('\n'); .join('\n');
}
evalJsFile(sortedText, onChange); export function getOnDrop(setLoading: Function, onChange: Function) {
return; // file.type return async function dropFile(event: DragEvent) {
if (text[0] === 'text/csv') evalCsvFile(text[0], onChange); event.preventDefault();
}); event.stopPropagation();
const files = [...(event?.dataTransfer?.items || [])]
.map((file: any) => file.kind === 'file' ? file?.getAsFile() : null)
.filter(file => file);
setLoading(false);
if (!files.length) return;
const text = await getStringFromFileList(files);
const report = getStringsForParser(text);
onChange('dump', report);
}; };
} }

View file

@ -1,8 +1,33 @@
import React from 'react'; import React from 'react';
import Description from 'ts/components/Description'; import Description from 'ts/components/Description';
import localization from 'ts/helpers/Localization';
import RECOMMENDATION_TYPES from 'ts/helpers/Recommendations/contstants';
import style from '../styles/card.module.scss'; import style from '../styles/card.module.scss';
function getClassName(recommendation?: any) {
const type = recommendation?.type;
return {
[RECOMMENDATION_TYPES.INFO]: style.card_info,
[RECOMMENDATION_TYPES.FACT]: style.card_fact,
[RECOMMENDATION_TYPES.WARNING]: style.card_warning,
[RECOMMENDATION_TYPES.ALERT]: style.card_error,
}[type || RECOMMENDATION_TYPES.INFO] ?? style.card_fact;
}
function getDescriptionText(recommendation?: any) {
const descriptionArgs = recommendation?.arguments?.description;
const { description } = recommendation;
const list = Array.isArray(description)
? description
: [description];
return list.map((textId: string) => (
localization.get(textId, descriptionArgs)
)).join('\n');
}
interface IRecommendationsProps { interface IRecommendationsProps {
recommendation: any; recommendation: any;
} }
@ -12,8 +37,7 @@ function Card({
}: IRecommendationsProps) { }: IRecommendationsProps) {
if (!recommendation) return null; if (!recommendation) return null;
const [title, description, type] = recommendation; const { title } = recommendation;
let formattedTitle = title || ''; let formattedTitle = title || '';
if (Array.isArray(title)) { if (Array.isArray(title)) {
formattedTitle = title.length > 1 formattedTitle = title.length > 1
@ -21,14 +45,9 @@ function Card({
: title[0]; : title[0];
} }
const className = { const className = getClassName(recommendation);
info: style.card_info, const titleArgs = recommendation?.arguments?.title;
fact: style.card_fact, const parts = getDescriptionText(recommendation).split('\n');
warning: style.card_warning,
error: style.card_error,
}[type || 'info'] ?? style.card_fact;
const parts = (description || '').split('\n');
const previewText = parts.shift(); const previewText = parts.shift();
const mainText = parts.join('\n'); const mainText = parts.join('\n');
@ -37,7 +56,7 @@ function Card({
<div className={style.card_wrapper}> <div className={style.card_wrapper}>
<h5 className={style.card_title}> <h5 className={style.card_title}>
<span className={style.card_icon}></span> <span className={style.card_icon}></span>
{formattedTitle} {localization.get(formattedTitle, titleArgs)}
</h5> </h5>
<Description <Description
style={{ color: '#12131B' }} style={{ color: '#12131B' }}

View file

@ -1,9 +1,10 @@
import React from 'react'; import React from 'react';
import { IColumn } from '../interfaces/Column'; import { IColumn } from '../interfaces/Column';
import style from '../styles/index.module.scss';
import DefaultCell from './cells/CellDefault'; import DefaultCell from './cells/CellDefault';
import style from '../styles/index.module.scss';
interface IBodyProps { interface IBodyProps {
rows: any[]; rows: any[];
columns: IColumn[]; columns: IColumn[];
@ -22,9 +23,11 @@ function Body({
const value = column.properties const value = column.properties
? row[column.properties] ? row[column.properties]
: row; : row;
const formattedValue = column.formatter const formattedValue = column.formatter
? column.formatter(value) ? column.formatter(value)
: value; : value;
const content: any = typeof column.template === 'function' const content: any = typeof column.template === 'function'
? column.template(formattedValue, row) ? column.template(formattedValue, row)
: `${column.prefixes ?? ''}${formattedValue ?? ''}${column.suffixes ?? ''}`; : `${column.prefixes ?? ''}${formattedValue ?? ''}${column.suffixes ?? ''}`;

View file

@ -19,6 +19,7 @@ function DefaultCell({
const columnClassName = typeof column.className === 'function' const columnClassName = typeof column.className === 'function'
? column.className('body', row) ? column.className('body', row)
: column.className; : column.className;
const onClick = column.onClick const onClick = column.onClick
? (() => { if (column.onClick) column.onClick(row); }) ? (() => { if (column.onClick) column.onClick(row); })
: undefined; : undefined;

View file

@ -0,0 +1,747 @@
import localization from 'ts/helpers/Localization';
localization.parse('en', `
§ uiKit.console: Copy
§ uiKit.dataLoader.page: Page
§ uiKit.dataLoader.size: Отображается по
§ uiKit.dataLoader.from: from
§ uiKit.dataLoader.all: Show all
§ uiKit.hoursChart.work: стандартное рабочее время (будни, с 07:00 до 20:00)
§ uiKit.hoursChart.weekend: выходные дни или время до/после рабочего дня
§ uiKit.hoursChart.days: суммарное количество коммитов за все время в конкретный день и час
§ uiKit.page.remove: Remove
§ uiKit.races.go: Поехали
§ uiKit.nothingFound.common.title: Нет или недостаточно данных для отображения
§ uiKit.nothingFound.common.description: Система обработает больше данных, если коммиты будут подписаны в формате [Git commit message convention|https://www.conventionalcommits.org/en/v1.0.0/]. Шаблон:
§ uiKit.nothingFound.common.console: Task_number type(фича): message
§ uiKit.nothingFound.common.example: Example:
§ uiKit.nothingFound.staff.title: Нет данных для этого сотрудника
§ uiKit.nothingFound.staff.description1:
Он вносил правки не каждый рабочий день и получил статус «Помошник».
Работой сотрудников с таким статусом по данному проекту можно пренебречь, т.к. его влад на общем фоне незначителен.
§ uiKit.nothingFound.staff.description2:
Поэтому система не рассчитывает для него ряд показателей.
Если это ошибка и данного сотрудника нужно рассчитать как обычного, перейдите в раздел «Настройки» и измените его тип.
§ common.filters: Filters
§ common.notifications.save: Изменения сохранены
§ common.notifications.setting: Настройки сохранены
§ sidebar.switch.team: Team
§ sidebar.switch.person: Employee
§ sidebar.buttons.settings: Settings
§ sidebar.buttons.print: Print
§ sidebar.filters.all: all time
§ sidebar.filters.year: year
§ sidebar.filters.halfYear: half year
§ sidebar.filters.month: month
§ sidebar.filters.week: week
§ sidebar.team.total: Common info
§ sidebar.team.scope: Features
§ sidebar.team.author: Employees
§ sidebar.team.type: Task types
§ sidebar.team.pr: Pull requests
§ sidebar.team.day: By day
§ sidebar.team.week: By week
§ sidebar.team.month: By month
§ sidebar.team.tree: Files
§ sidebar.team.hours: Расписание
§ sidebar.team.commits: All commits
§ sidebar.team.changes: All changes
§ sidebar.team.words: Popular words
§ sidebar.team.top: Викторина
§ sidebar.team.settings: Settings
§ sidebar.person.total: Common info
§ sidebar.person.money: Work cost
§ sidebar.person.speed: Speed
§ sidebar.person.day: By day
§ sidebar.person.week: By week
§ sidebar.person.month: By month
§ sidebar.person.hours: Расписание
§ sidebar.person.commits: All commits
§ sidebar.person.changes: All changes
§ sidebar.person.words: Popular words
§ sidebar.person.settings: Settings
§ page.welcome.step1: Run this command in your project folder
§ page.welcome.step2: Move the file log.txt to this page
§ page.welcome.description1: Git создаст файл log.txt. Он содержит данные для построения отчёта. Или git shortlog -s -n -e если отчёт вам не нужен. Создайте файл
§ page.welcome.description2: [.mailmap|https://git-scm.com/docs/gitmailmap] в корне проекта, чтобы обьединить статистику по сотрудникам.
§ page.welcome.description: Git создаст файл log.txt. Он содержит данные для построения отчёта. Или git shortlog -s -n -e если отчёт вам не нужен. Создайте файл [.mailmap|https://git-scm.com/docs/gitmailmap] в корне проекта, чтобы обьединить статистику по сотрудникам.
§ page.welcome.warning1: Сервис *НЕ ХРАНИТ* и *НЕ ПЕРЕДАЁТ* ваши данные. Все расчёты выполняются локально в вашем браузере прямо на вашей машине.
§ page.welcome.warning2: Сервис *НЕ СОБИРАЕТ СТАТИСТИКУ* по проектам. Вы можете отключить интернет, проверить трафик и даже собрать локальный билд из [исходников|https://github.com/bakhirev/assayo].
§ page.common.words.title: Statistic by words
§ page.common.words.description: самое популярное слово. Встречается $1 раза.
§ page.common.commits.title: Commits number by days
§ page.common.commits.description: ($1) самый продуктивный день по числу коммитов.
§ page.common.commits.title2: $1 сделано коммитов: $2
§ page.common.filter.allUsers: Не имеет значения
§ page.settings.document.title: Display settings
§ page.settings.document.name: Page title
§ page.settings.document.language: Language
§ page.settings.links.title: Link prefixes
§ page.settings.links.task: For task number
§ page.settings.links.pr: For Pull Requests
§ page.settings.user.title: Employees settings
§ page.settings.user.notFound: Индивидуальных настроек нет. Данные по всем сотрудникам вычисляются по общим параметрам.
§ page.settings.user.subTitle: Дополнение к трудовому договору $1
§ page.settings.user.from: Дата начала действия
§ page.settings.mailmap: .mailmap settings
§ page.settings.common.title: Общие данные по зарплате
§ page.settings.common.type.title: Work type
§ page.settings.common.type.full: Full-time
§ page.settings.common.type.part: Проектная работа
§ page.settings.common.salary: Зарплата в месяц
§ page.settings.common.currency: Currency
§ page.settings.common.workDaysInYear: Количество рабочих дней в году
§ page.settings.common.vacationDaysInYear: Количество дней отпуска в год
§ page.settings.common.workDaysInWeek: Рабочие дни
§ page.settings.form.save: Save
§ page.settings.form.cancel: Cancel
§ page.settings.form.remove: Remove
§ page.settings.form.addEmployee: Add employee
§ page.settings.form.addContract: Добавить трудовой договор
§ page.print.title: What are we printing?
§ page.print.page: This page
§ page.print.type: This section
§ page.print.all: All statistics
§ page.print.cancel: Cancel
§ page.team.author.title: Статистика по сотрудникам
§ page.team.author.description1: *Часть статитики* (скорость работы, затраченные деньги и т.п.) *по сотрудникам с типом «Помошник» не считается*, т.к. это эпизодическая роль в проекте. Предпологаем, что они не влияют на проект, а их правками можно пренебречь на фоне общего объема работы.
§ page.team.author.description2: *Сортировка по умолчанию* это сортировка по количеству задач и группам (текущие, уволенные, помогающие сотрудники).
§ page.team.author.types: Types
§ page.team.author.commits: Commits
§ page.team.author.commitsSmall: commits
§ page.team.author.tasks: Tasks
§ page.team.author.tasksSmall: tasks
§ page.team.author.workedLosses: Days with and without commits
§ page.team.author.worked: work
§ page.team.author.losses: days without commits
§ page.team.author.days: days
§ page.team.author.daysForTask: Дней на задачу
§ page.team.author.scopes: Features
§ page.team.author.moneyAll: Получил
§ page.team.author.moneyWorked: Отработал
§ page.team.author.moneyLosses: Переплата
§ page.team.hours.title: Распределение коммитов в течении каждого дня недели
§ page.team.month.title: Календарь работы по проекту
§ page.team.scope.title: Statistic by features
§ page.team.scope.scope: Feature
§ page.team.scope.days: Раб. дней
§ page.team.scope.authorsDays: Человеко-дней
§ page.team.scope.tasks: Tasks
§ page.team.scope.commits: Commits
§ page.team.scope.commitsSmall: commits
§ page.team.scope.types: Types
§ page.team.scope.authors: Персональный вклад
§ page.team.scope.cost: Cost
§ page.team.type.title: Статистика по типам задач
§ page.team.type.description: *Персональный вклад* считается по количеству коммитов, а не объему измененных строк или файлов. Поэтому следует так же смотреть раздел «Анализ файлов», чтобы оценить масштаб изменений.
§ page.team.type.type: Task types
§ page.team.type.tasks: Tasks
§ page.team.type.tasksSmall: tasks
§ page.team.type.days: Days
§ page.team.type.daysSmall: days
§ page.team.type.authorsDays: Человеко-дней
§ page.team.type.commits: Commits
§ page.team.type.authors: Персональный вклад
§ page.team.total.titleA: Scope of work
§ page.team.total.titleB: Cost
§ page.team.total.daysWorked.title: человеко-дней
§ page.team.total.daysWorked.description: Учтены только дни, в которые делались коммиты
§ page.team.total.commits.title: commits
§ page.team.total.commits.description: Удалённые ветки не считаются
§ page.team.total.daysLosses.title: days without commits
§ page.team.total.daysLosses.description: Все дни минус: праздники, выходные, отпуск, дни с коммитами
§ page.team.total.employment.title: работает / уволилось
§ page.team.total.employment.description: Если сотрудник в течении месяца не сделал ни одного коммита, он считается уволенным
§ page.team.total.moneyAll.title: общая
§ page.team.total.moneyAll.description: Суммарные затраты на зп
§ page.team.total.moneyWorked.title: фактическая
§ page.team.total.moneyWorked.description: Фактически отработанные дни умноженные на среднюю зп
§ page.team.total.moneyLosses.title: possible overpayment
§ page.team.total.moneyLosses.description: Оплаченные рабочие дни, когда коммитов не было
§ page.team.total.weekendPayment.title: work on weekend
§ page.team.total.weekendPayment.description: Суммарная переплата за работу в выходные дни
§ page.team.total.workSpeed.title: tasks in day
§ page.team.total.workSpeed.description: Средняя скорость работы команды при текущем составе сотрудников
§ page.team.total.moneySpeed.title: в месяц
§ page.team.total.moneySpeed.description: Прогнозируемая сумма выплаты на зп при текущем составе сотрудников без учета налогов и сопутствующих затрат
§ page.team.total.description1: *Человеко-дни* это работа одного сотрудника в течение одного рабочего дня. Например, за один календарный день, команда из трех сотрудников выдает объем работы в три человеко-дня.
§ page.team.total.description2: *Днями прогулов* считаются только рабочие дни, когда коммиты могли бы быть сделаны. Выходные, государственные праздники и отпуска в расчёте не участвуют.
§ page.team.total.description3: Карточка *работает и уволилось* показывает фактический состав сотрудников, которые постоянно участвуют в работе. Кроме этого, есть «помощники» это сотрудники, как правило другой специализации, которые могут иногда делать коммиты в проект.
§ page.team.total.description4: *Переплатой* считаются только рабочие дни, когда коммиты могли бы быть сделаны. Выходные, государственные праздники и отпуска в расчёте не участвуют. Именно поэтому переплата + фактическая стоимость != общей. В общей стоимости заложена оплата выходных, государственных праздников и отпусков.
§ page.team.total.description5: *Работой на выходных* считается по коэфициенту х2 от оплаты обычного дня. Выше отображена именно переплата (х1), т.к. сам факт переработки в данном контексте не интересен. Мы не смотрим скорость сжигания бюджета. Мы смотрим переплату при увеличении скорости работы.
§ page.team.tree.title: Дерево проекта с учётом выбранных фильтров
§ page.team.tree.filters.author: Employee
§ page.team.tree.filters.commits: Commits number
§ page.team.tree.filters.help: Минимальное количество коммитов, которое сделал сотрудник в файле
§ page.team.tree.filters.all: All employees
§ page.team.tree.add: Who added it
§ page.team.tree.change: Who changed it
§ page.team.tree.remove: Who removed it
§ page.team.tree.line: lines
§ page.team.tree.lineAdd: added
§ page.team.tree.lineRemove: changed
§ page.team.week.date: Date
§ page.team.week.numberTasks: Количество задач
§ page.team.week.people: Количество человек
§ page.team.week.line: Изменение строк
§ page.team.week.days: Days with and without commits
§ page.team.week.lossesDetails: Кто не коммитил
§ page.team.week.add: added
§ page.team.week.change: changed
§ page.team.week.remove: removed
§ page.team.week.hasCommits: были коммиты
§ page.team.week.hasNotCommits: небыло коммитов
§ page.team.week.days: days
§ page.team.week.tasks: tasks
§ page.team.pr.task: Task
§ page.team.pr.tasks: tasks
§ page.team.pr.firstCommitTime: First commit
§ page.team.pr.lastCommitTime: Last
§ page.team.pr.workDays: Дней разработки
§ page.team.pr.delayDays: Дней ожидания влития
§ page.team.pr.commits: Commits
§ page.team.pr.date: Дата влития
§ page.team.pr.mergeAuthor: Влил
§ page.team.pr.author: Employee
§ page.team.pr.middleTimeRelease: Среднее время поставки (дни)
§ page.team.pr.work: разработка
§ page.team.pr.delay: waiting
§ page.team.pr.days: days
§ page.team.pr.oneTaskDays: Время потраченное на одну задачу
§ page.team.pr.description1: *Время разработки* это разница времени от первого до последнего коммита по задаче. Не важно были перерывы в несколько дней между коммитами или нет. Сам факт какого-либо коммита увеличивает время.
§ page.team.pr.description2: *Время ожидания* это время между последним коммитом и влитием кода. Оно показывает фактический простой в ожидании чего-либо.
§ page.team.pr.description3: *Зачем отображать время разработки* без разбивки на кодинг и код-ревью? Затем, чтобы показать бизнесу фактическое время поставки кода. Ожидание тестирования, замечания на ревью, проблемы DevOps и прочие несовершенства процесса, как раз уже заложены в этот срок.
§ page.team.pr.statByAuthors: Statistics by employee
§ page.team.pr.longDelay: Длительное ожидание влития
§ page.person.print.photo.title: Photo
§ page.person.print.photo.description: место для фотографии
§ page.person.total.title: Основные характеристики
§ page.person.total.daysWorked.title: days of work
§ page.person.total.daysWorked.description: Учтены только дни, в которые делались коммиты
§ page.person.total.tasks.title: tasks
§ page.person.total.tasks.description: Если коммиты правильно подписаны
§ page.person.character.title: Персонаж
§ page.person.achievement.title: Achievements
§ page.person.achievement.positive: Positive
§ page.person.achievement.normal: Neutral
§ page.person.achievement.negative: Negative
§ page.person.achievement.description: Чем больше сотрудник набрал отрицательных достижений, тем больше вероятность, что ситуация нестандартная. Возможно, стоит изменить режим его работы, задачи или отчётность. Следует поговорить с ним и узнать, какие проблемы мешают его работе.
§ page.person.gets.title: Взятые геты:
§ page.person.gets.description: «Взять гет» в данном случае означает первым оставить коммит к&nbsp;задаче с&nbsp;&laquo;красивым&raquo; номером.
§ page.person.business.days.title: дней работы
§ page.person.business.days.description: Учтены только дни, в которые делались коммиты
§ page.person.business.tasks.title: tasks
§ page.person.business.tasks.description: Если коммиты правильно подписаны
§ page.person.business.losses.title: days without commits
§ page.person.business.losses.description: Все дни минус: праздники, выходные, отпуск, дни с коммитами
§ page.person.business.commits.title: commits
§ page.person.business.commits.description: Удалённые ветки не считаются
§ page.person.business.time.description: Время от первого, до последнего коммита (в том числе, нерабочие дни)
§ page.person.business.time.title: Дней на проекте:
§ page.person.business.time.dismissed: (dismissed)
§ page.person.business.time.staff: (not in the team)
§ page.person.business.achievements: Achievements
§ page.person.changes.title: Achievements
§ page.person.changes.description:
При некоторых видах форматирования git отмечает строки как «удалённые» и «добавленные»,
хотя на самом деле они были «изменёны». Поэтому, если вы провели большой рефакторинг,
git может показать малое количество изменений в статистике, а фактический результат
будет отмечен, как скачок «удаленных» и «добавленных» строк.
§ page.person.changes.description: Список коммитов и количество изменений в них за этот день:
§ page.person.commits.title: Commits list:
§ page.person.money.title.total: For all the time
§ page.person.money.title.middle: Middle cost
§ page.person.money.moneyAll.title: received
§ page.person.money.moneyAll.description: Предполагаемая сумма зп с проекта (см. настройки)
§ page.person.money.moneyWorked.title: отработал
§ page.person.money.moneyWorked.description: Фактически отработанные дни умноженные на среднюю зп
§ page.person.money.moneyLosses.title: possible overpayment
§ page.person.money.moneyLosses.description: Дни без коммитов умноженные на среднюю зп
§ page.person.money.tasks.title: task
§ page.person.money.tasks.description: Количество закрытых задач к стоимости дня
§ page.person.money.commits.title: commit
§ page.person.money.commits.description: Количество коммитов к стоимости рабочего дня
§ page.person.speed.task: One task on average is
§ page.person.speed.max: Максимальная скорость в день
§ page.person.speed.days.title: days
§ page.person.speed.days.description: Имеются ввиду рабочие дни, если коммиты правильно подписаны
§ page.person.speed.commits.title: commits
§ page.person.speed.commits.description: Отрезаны 10% максимальных и минимальных значений
§ page.person.speed.line.title: code lines
§ page.person.speed.line.description: Отрезаны 10% максимальных и минимальных значений
§ page.person.speed.tasks.title: tasks
§ page.person.speed.tasks.description: Задача может быть не доделана, но работа по ней должна быть
§ page.person.speed.maxCommits.title: commits
§ page.person.speed.maxCommits.description: Задача может быть не доделана, но работа по ней должна быть
§ page.person.hours.title: Распределение коммитов в течении каждого дня недели
§ page.person.week.date: Date
§ page.person.week.tasks: Number of tasks
§ page.person.week.workDays: Days with commits
§ page.person.week.taskInDay: Tasks per day
§ page.person.week.days: days
§ page.person.week.workDay: weekdays
§ page.person.week.weekends: weekends
§ recommendations.title
Рекомендации и факты
§ recommendations.scope.parallelism.not.title
Нет паралельных работ
§ recommendations.scope.parallelism.not.description
любую фичу в один момент времени делает один человек.
# Метод расчёта:
- человеко-дни делятся на фактические дни для каждой фичи;
- находим среднее арифметическое;
- если результат меньше 1.3 считаем, что паралельных работ в рамках большинства фичей обычно нет;
# Почему это плохо:
- повышается bus factor;
- сотрудники медленее развиваются;
- трудно качественно проверить работу сотрудника;
# Почему это хорошо:
- появляюся эксперты, которые очень глубоко погружены в предметную область и могут предложить более качественные решения;
- скорее всего не бывает merge конфликтов;
- проект может очень быстро паралельно развиваться в разные стороны;
§ recommendations.scope.parallelism.has.title
Часть работ паралельно
§ recommendations.scope.parallelism.has.description
Иногда фичу делают одновременно несколько человек.
# Метод расчёта:
- человеко-дни делятся на фактические дни для каждой фичи;
- находим среднее арифметическое;
- если результат от 1.3 до 2.0 считаем, что часть работ в рамках разных фичей иногда делалается паралельно;
§ recommendations.scope.parallelism.every.title
Паралельные работы
§ recommendations.scope.parallelism.every.description
любую фичу в один момент времени делают несколько человек
# Метод расчёта:
- человеко-дни делятся на фактические дни для каждой фичи;
- находим среднее арифметическое;
- если результат больше двух считаем, что большая часть работ в рамках разных фичей обычно делалается паралельно;
§ recommendations.scope.money
в такую сумму можно оценить работу по данному проекту.
# Метод расчёта:
- человеко-дни затраченные на разработку умножаются на индивидуальную зарплату разработчиков;
Изменить зарплату каждого разработчика, для более точной суммы, можно в разделе «Настройки»
# Это много или мало?
Для ответа на этот вопрос, нужно ответить на следующие:
- Можно ли за эти деньги было купить готовое решение?
- Можно ли за эти деньги сделать более хороший продукт?
Если ответ на оба вопроса «да», то возможно, разработка с нуля не стоила потраченных на неё денег.
§ recommendations.scope.bus.everyHasOne.title
Bus factor = 1
§ recommendations.scope.bus.everyHasOne.description
В большинство фич погружен один человек.
Надо переключать людей.
# Почему это плохо:
- если сотрудники будут увольнятся, будет трудно продолжить их работу;
- невозможно контролировать качество его кода;
# Как делается выборка:
- более 80% коммитов в фичу делает один человек;
- проект имеет более 60% таких фичей;
§ recommendations.scope.bus.oneMaintainer
в фичи погружен один человек.
# Почему это плохо:
- если он уволится, будет трудно продолжить разработку;
- снижается качество code-review;
- трудно запаралелить разработку при необходимости;
# Как делается выборка:
- более 80% коммитов в фичу сделал один человек;
§ recommendations.scope.types.process.title
Плохие процессы
§ recommendations.scope.types.process.description
Большинство фич содержат один тип задач.
§ recommendations.scope.types.one
фичи содержат один тип задач.
§ recommendations.scope.types.common
Возможно, разработчики неправильно подписывают коммиты или менеджер заводит один и тот же тип задач.
# Почему это важно:
- невозможно передать поддержку другой команде;
- невозможно выпустить "коробочную" версию;
- сильная зависимость от конкретных разработчиков;
- большое количество ошибок и низкое качество кода;
- вероятное замедление разработки в будущем;
# В чём ошибка менеджера:
- взгляд на продукт, только с позиции «работающей демки»;
# Что должно быть:
- тесты;
- ошибки (выявленные по результатам тестов);
- рефакторинг (т.к. архитектура может измениться);
- документация;
- правки стиля (как результат опроса фокус-группы);
§ recommendations.scope.plan.title
Постройте долгосрочный план
§ recommendations.scope.plan.description
с учетом архитектуры.
При том опираться этот план должен сразу на самые трудные задачи.
# Почему отсутствие плана плохо:
- сотрудники делают минимально работающую версию, не закладывая точки расширения. После этого пишется не масштабируемый код, который тормозит следующие фичи;
# В чём ошибка менеджера:
- он не показал, как продукт будет развиваться далее и в каких точках будет рост;
# Как должно быть:
- составлятся глобальный план развития продукта;
- составлятся глобальный план развития архитектуры (с разработчиками и DBA);
- на уровне схем сразу проговариваются моменты, которые могут сильно измениться;
§ recommendations.scope.cost.title
Оцените инвестиции в фичу
§ recommendations.scope.cost.description
с количеством потенциальной прибыли.
Фичи которые дорого стоят в разработке, но приносят мало прибыли, возможно, стоит отложить или вообще отменить. Это сделает проект более комерчески успешным.
§ recommendations.author.lotOfLazy
пишет слишком мало кода.
# Может уволить?
- он тимлид, архитектор, аналитик?
- это его основной проект?
- есть какие-то зависимости от него?
# Почему нет смысла исправлять
Суммарные затраты на разработчика уже больше чем прибыль от его работы.
Если мы считаем, что обьективных помех его работе не было, то человек либо не хочет работать вообще, либо работает на двух проектах одновременно.
Увольнение и замена новым сотрудником выглядит оправданным с точки зрения общей статистики.
§ recommendations.author.manyLazy
пишет мало кода. Нужно взять на контроль.
# Как делается выборка:
- на тестовых выборках хороший программист пишет код больше 80% времени;
- в данном случае показатель от 60% до 80%;
# Как контролировать:
- дробить задачи на 1..2 дня;
- каждый день спрашивать статус;
- убедиться, что задачи хорошо расписаны и готовы к началу разработки;
- устроить парное программирование, чтобы проверить фактическую скорость;
§ recommendations.author.oneTypeMans
получает слишком однообразные задачи по типу. Может выгореть.
# Почему это важно:
- если сотрудник выгорит, его скорость работы снизится;
- замедляется профессиональный рост;
- повышается вероятность увольнения;
# Как делается выборка:
- для каждого коммита определятся тип задачи;
- если больше 70% задач одного типа, значит человек делает одно и тоже;
§ recommendations.author.projectType.openSource.title
Открытый проект
§ recommendations.author.projectType.openSource.description
пять дней в неделю тут не работают.
Проект может быть и закрытым, просто такой темп работы обычно у открытых библиотек на GitHub.
# Метод оценки:
- берется статистика по всем активным разработчикам;
- подсчитывается среднее число дней работы и без коммитов;
- у open-source библиотек рабочих дней обычно максимум 15..20%;
# Последствия
Для проектов, где работа не постоянна, нет смысла во многих показателях. Поэтому показатели без коммитов, скорости и т.п. будут скрыты.
Как правило, оценку таких проектов делают перед началом разработки своей закрытой версии. Самые интересные показатели в этом случае вероятная стоимость и суммарное время на разработку.
§ recommendations.author.projectType.easy.title
Слабая загрузка
§ recommendations.author.projectType.easy.description
слишком много дней без коммитов. Нужно понять почему команда не пишет код.
# Метод оценки:
- берется статистика по всем активным разработчикам;
- подсчитывается среднее число дней работы и без коммитов;
- загрузка считается слабой, если процент без коммитов от 5% до 20%;
# Возможные причины:
- фактически нет задач;
- задачи есть, но хорошо ложатся на текущую архитектуру;
- разработчиков отвлекают совещаниями;
- команда не работает;
# Варианты решения:
- обсудить проблему с командой;
- уменьшить гранулярность задач, чтобы за день можно было успеть сделать одну или две задачи;
- ввести ежедневные совещания, чтобы проверять движение задач по статусу;
- устроить сеансы парного программирования, чтобы убедиться, что разработчик может работать быстрее;
§ recommendations.author.manager.title
Обозначьте дедлайны
§ recommendations.author.manager.description
У любой задачи должен быть чёткий дедлайн.
Это позволит не затягивать её выполнение на несколько дней или недель.
# Какие показатели стоит проверить:
- количество дней на одну задачу, которое тратит работник;
- количество дней ожидания влития PR (страница статистики по PR);
§ recommendations.author.shorTalk.title
Проводите ежедневные совещания
§ recommendations.author.shorTalk.description
они помогают быть в курсе проекта.
Не растягивайте их отвлекаясь на постороние темы.
# На какие вопросы должен ответить сотрудник:
- что было сделано;
- что будет сделано;
- есть ли какие-либо проблемы;
# Следует обрывать монолог, если:
- начинают подробно описывать мелкие детали, которые не важны;
- уводят диалог в сторону, от первоначального плана;
# Почему это важно:
Часто сотрудник, который ничего не делает, старается уйти от ответа. Для этого он рассказывает кучу ненужных подробностей свой работы. Это позволяет усыпить внимание участников и растянуть время ответа. Создается ощущение что он чем-то занят, хотя по факту работы не было.
§ recommendations.author.ipr.title
Составьте план обучения
§ recommendations.author.ipr.description
на каждого сотрудника.
*Индивидуальный план обучения* это список целей и задач, которые помогают человеку развиваться в определенной области.
# Как составить план:
- составить матрицу компетенций;
- определить по каким компетенциям меньше всего знаний и опыта;
- узнать какие из этих компетенций интересны сотруднику;
- придумать 3..5 целей в рамках каждой такой компетенции на пол-года или год;
- каждый месяц пытаться сделать что-либо для достижения одной цели;
- каждый месяц напоминать об общем плане достижения этих целей;
# Нужен ли план руководителю?
Да, руководитель так же должен составить план на себя. Если нет вышестоящего руководителя, то он должен проверять сам себя.
# Почему это важно:
- сотрудники становятся более лояльны к компании;
- за теже деньги вы получаете более квалифицированные кадры;
§ recommendations.author.oneToOne.title
Проводите 1-1 каждый месяц
§ recommendations.author.oneToOne.description
это поможет выявить проблемы на ранней стадии.
*One-to-one* это регулярные личные встречи руководителя с подчиненным. На таких встречах обычно обсуждают всё, что важно для сотрудника, что его волнует, и то, чем он может поделиться с руководителем только наедине.
# Почему это важно:
- легко выяснить, кто из сотрудников перегружен, а у кого есть свободное время;
- можно предотвратить выгорание сотрудника;
- можно получить быструю обратную связь о процессах, которые вы можете не замечать;
- формируется доверительное отношение, сотрудники становятся более лояльны к компании;
- повышается мотивация и вовлеченность сотрудников;
§ recommendations.author.club.title
Ходите в бар
§ recommendations.author.club.description
один раз в месяц или два.
Это поможет выстроить неформальную коммуникацию в коллективе и сплотить команду, даже если общение будет сжатым.
# Почему это важно:
- можно получить быструю обратную связь о процессах, которые вы можете не замечать;
- формируется доверительное отношение, сотрудники становятся более лояльны к компании;
- повышается вовлеченность сотрудников;
§ recommendations.hour.onlyWork.title
Выходных тут нет
§ recommendations.hour.onlyWork.description
Вероятно, стоит уволить менеджера проекта.
§ recommendations.hour.weekends.title
Работа на выходных
§ recommendations.hour.weekends.description
Вероятно, стоит проверить менеджера проекта.
§ recommendations.hour.easy.title
Бывают проблемы
§ recommendations.hour.easy.description
Вероятно, бывают завалы и приходится работать на выходных.
§ recommendations.week.lazyDays.down.title
Стало меньше прогулов
§ recommendations.week.lazyDays.down.description
за последние три недели этот показатель упал
§ recommendations.week.lazyDays.up.title
Стало больше прогулов
§ recommendations.week.lazyDays.up.description
нет задач или нужен более жесткий контроль
§ recommendations.week.notWork.title
Стабильно не дорабатывает
§ recommendations.week.notWork.description
т.к. каждую неделю пишет код не 100% времени
§ recommendations.week.upWork.title
Стабильно перерабатывает
§ recommendations.week.upWork.description
т.к. каждую неделю пишет код в выходные дни
§ recommendations.week.task.up.title
Растёт производительность
§ recommendations.week.task.up.description
или задачи стали слишком мелкие. Нужно проверить. Если гранулярность та же - закрепить результат.
§ recommendations.week.task.down.title
Падает производительность
§ recommendations.week.task.down.description
или задачи хуже разбивают. Нужно проверить. Если гранулярность та же - взять на контроль.
# Метод оценки:
- количество задач в день, над которыми работают, на протяжении последних трех недель стабильно падает.
# Возможные ошибки:
- задачи могли быть сложнее, чем казались;
- задачи могли иметь большой объём работы (нужно проверить количество изменений, падают они или нет за этот же период)
§ recommendations.type.everyHasOne.title
Не подписывают тип задачи
§ recommendations.type.everyHasOne.description
большинство типов задач делает один человек.
§ recommendations.type.oneMaintainer.title
Узкая специализация
§ recommendations.type.oneMaintainer.description
большинство задач одного типа делают одни и те же люди.
# Типы задач:
§ recommendations.type.common
# Возможно, это не так
Нужно убедиться, что остальные сотрудники верно подписывают коммиты.
Шаги, которые помогут это сделать:
- настроить пре-коммит проверку для commit message;
- объяснить команде, что нужно указывать тип;
- проверить в новых ветках, что сотрудники следуют правилу;
# Если это действительно так
Вы настроили проверки и убедились что один и тот же сотрудник, делает задачи одного и того же типа.
Почему это плохо:
- его увольнение остановит целую пачку процессов;
- уменьшается компетенция остальных членов команды;
- трудно верхнеуровнево понять его правки;
Как это исправить:
- распределять разные типы задач равномерно;
- менять область работы (тесты, документация, ошибки) между сотрудниками через спринт;
§ recommendations.type.fewTypes.title
Это локальный продукт
§ recommendations.type.fewTypes.description
для конкретного заказчика или проблемы.
# Какие признаки есть у «глобального» продукта:
- локализация;
- документация;
- большой обьем тестов;
- визуальная кастомизация;
- рефакторинг узких мест;
- и т.п.
# Почему этот продукт выглядит как «локальный»:
- у каждого «глобального» признака будет перевес по своему типу задач;
- чем больше «глобальных» признаков, тем больше вероятность «глобального» продукта;
В данном случае мы видим небольшое число типов, а следовательно, скорее всего есть недоработки, мешающие легко масштабировать продукт на мировой рынок и продавать его в других странах.
# Возможно, это не так
По типам файлов мы можем предположить тип программы (сайт, серверное приложение, DevOps скрипты и т.д.). Для frontend приложения наша гипотеза будет более верной, чем для DevOps-скриптов, которые могут быть лишь микро-модулем инициализации.
§ recommendations.type.diff.title
Разбейте лидирующий тип на подтипы
§ recommendations.type.diff.description
для детализации ошибок.
Как правило, тип задач с меткой «исправление ошибок» является лидирующим. Это делает статистику слабо-детализированной.
*Если у вас произошла такая ситуация*, вы можете разбить этот тип на подтипы (например, по месту обнаружения).
Рассмотрим несколько вариантов подтипов:
- fix_dev (ошибка выявленная в процессе разработки);
- fix_test (ошибка выявленная в процессе тестирования);
- fix (ошибка выявленная в проде);
§ recommendations.type.buddy.title
Копите мелкие задачи
§ recommendations.type.buddy.description
для новых сотрудников.
# Если задача:
- не важная;
- не большая;
- не требует сильного погружения в контекст;
- больше про рефакторинг, чем про новый код;
# Положите её в backlog с меткой «для новичков».
Когда придёт новый сотрудник, вы сможете моментально достать ему пачку небольших и разнообразных по типу задач, для ознакомления с проектом.
Также, если у вас будет застой в работе, вы сможете доставать по одной такой мелкой задаче из backlog-а.
`);
export default {};

View file

@ -1,6 +1,98 @@
import localization from 'ts/helpers/Localization'; import localization from 'ts/helpers/Localization';
localization.parse('ru', ` localization.parse('ru', `
§ achievements.commitsAfter1500.title: Сова
§ achievements.commitsAfter1500.description: 70% коммитов после 15:00
§ achievements.commitsBefore1500.title: Ранняя пташка
§ achievements.commitsBefore1500.description: 70% коммитов до обеда
§ achievements.workEveryTime.title: Раб божий
§ achievements.workEveryTime.description: есть коммит на каждый час суток
§ achievements.workNotWork.title: Стрельба холостыми
§ achievements.workNotWork.description: коммиты есть, а закрытых задач нет
§ achievements.userNotWork.title: Залётный
§ achievements.userNotWork.description: это не его основной проект
§ achievements.userIsDied.title: Мёртвая душа
§ achievements.userIsDied.description: работал, но уволился
§ achievements.lessTasks.title: Зашел и вышел
§ achievements.lessTasks.description: меньше всего закрытых задач
§ achievements.moreTasks.title: Батя грит малаца
§ achievements.moreTasks.description: больше всего закрытых задач
§ achievements.everyMessageLong.title: Мастер красноречия
§ achievements.everyMessageLong.description: стабильно самые длинные подписи коммитов
§ achievements.everyMessageShort.title: Болтун находка для шпиона
§ achievements.everyMessageShort.description: стабильно, самые короткие подписи коммитов
§ achievements.shortestName.title: Размер не главное
§ achievements.shortestName.description: самое короткое имя
§ achievements.longestName.title: Азим Азиз Иль Ам Кадир Имран II
§ achievements.longestName.description: самое длинное имя
§ achievements.moreCommits.title: Мастер бекапов
§ achievements.moreCommits.description: больше всего коммитов
§ achievements.lessCommits.title: Редко но метко
§ achievements.lessCommits.description: меньше всего коммитов
§ achievements.oneCommitOneTask.title: Точно в цель
§ achievements.oneCommitOneTask.description: в среднем один коммит на задачу
§ achievements.moreLazyDays.title: Мысленно я с вами
§ achievements.moreLazyDays.description: больше всего дней без коммитов
§ achievements.lessLazyDays.title: Папа Карло
§ achievements.lessLazyDays.description: меньше всего дней без коммитов
§ achievements.zeroLazyDays.title: Ни единого разрыва
§ achievements.zeroLazyDays.description: ни одного дня без коммитов
§ achievements.moreWorkDays.title: Ценный работник
§ achievements.moreWorkDays.description: больше всего рабочих дней
§ achievements.moreScopes.title: Стартапер
§ achievements.moreScopes.description: сделал больше всего фичей
§ achievements.lessScopes.title: Щегол
§ achievements.lessScopes.description: сделал меньше всего фичей
§ achievements.moreDaysForTask.title: Улитка на склоне
§ achievements.moreDaysForTask.description: работа по задачам идёт медленнее чем у остальных
§ achievements.more2DaysForTask.title: Cо слоу
§ achievements.more2DaysForTask.description: больше двух дней на задачу
§ achievements.moreDaysInProject.title: Старожил
§ achievements.moreDaysInProject.description: больше всего дней на проекте
§ achievements.lessDaysInProject.title: А это кто?
§ achievements.lessDaysInProject.description: меньше всего дней на проекте
§ achievements.more90DaysInProject.title: Добро пожаловать
§ achievements.more90DaysInProject.description: не уволили на испытательном
§ achievements.lessDaysForTask.title: Скорострел
§ achievements.lessDaysForTask.description: одна задача занимает меньше дня
§ achievements.adam.title: Адам
§ achievements.adam.description: первый стабильный сотрудник на проекте
§ achievements.more666DaysInProject.title: Чёрт
§ achievements.more666DaysInProject.description: отработал 666 дней на проекте
§ achievements.more777DaysInProject.title: Азино 3 топора
§ achievements.more777DaysInProject.description: отработал 777 дней на проекте
§ achievements.moreRefactoring.title: Выпускающий редактор
§ achievements.moreRefactoring.description: сделал больше всех меток «рефакторинг»
§ achievements.longestMessage.title: А разговоров то было...
§ achievements.longestMessage.description: самая длинная подпись коммита за все время
§ achievements.moreTasksInDay.title: Спиди-гонщик
§ achievements.moreTasksInDay.description: рекорд по количеству закрытых задач в день
§ achievements.hasCommitFrom0to7.title: Ночной дозор
§ achievements.hasCommitFrom0to7.description: есть коммит на каждый час ночи
§ achievements.noCommitOnDay.title: Технический перерыв
§ achievements.noCommitOnDay.description: есть определенный час и день в рабочее время в который никогда не комитит
§ achievements.hasCommitEveryTime.title: Умер на работе
§ achievements.hasCommitEveryTime.description: есть коммит на час каждого дня (включая выходные)
§ achievements.commitsAfter1800.title: Делу время
§ achievements.commitsAfter1800.description: нет ни одного коммита после 18:00
§ achievements.more1488DaysInProject.title: им. Максима Марцинкевича
§ achievements.more1488DaysInProject.description: отработал 1488 дней на проекте
§ achievements.taskNumber300.title: Знаком с трактористом
§ achievements.taskNumber300.description: первый взял в работу задачу с номером 300
§ achievements.moreFix.title: Bug hunter
§ achievements.moreFix.description: больше всего закрытых багов
§ achievements.lessWorkDays.title: Дальше без меня
§ achievements.lessWorkDays.description: меньше всего рабочих дней
§ achievements.moreCreateCode.title: Созидатель
§ achievements.moreCreateCode.description: склонен больше остальных добавлять код
§ achievements.moreRemoveCode.title: Разрушитель
§ achievements.moreRemoveCode.description: склонен больше остальных удалять код
§ achievements.moreChangeCode.title: Реформатор
§ achievements.moreChangeCode.description: склонен больше остальных изменять код
§ achievements.moreStyle.title: Полиция моды
§ achievements.moreStyle.description: склонен больше остальных изменять CSS
§ achievements.moreOnHoliday.title: Нет жизни
§ achievements.moreOnHoliday.description: относительно много коммитов в нерабочее время
§ uiKit.console: Копировать § uiKit.console: Копировать
§ uiKit.dataLoader.page: Страница § uiKit.dataLoader.page: Страница
§ uiKit.dataLoader.size: Отображается по § uiKit.dataLoader.size: Отображается по
@ -63,7 +155,8 @@ localization.parse('ru', `
§ sidebar.person.words: Популярные слова § sidebar.person.words: Популярные слова
§ sidebar.person.settings: Настройки § sidebar.person.settings: Настройки
§ page.welcome.step1: Выполните команду в корне вашего проекта § page.welcome.step1: Выполните команду в корне вашего проекта
§ page.welcome.step2: Перетащите файл log.txt на эту страницу § page.welcome.step3: Перетащите
§ page.welcome.step4: файл log.txt на эту страницу
§ page.welcome.description1: Git создаст файл log.txt. Он содержит данные для построения отчёта. Или git shortlog -s -n -e если отчёт вам не нужен. Создайте файл § page.welcome.description1: Git создаст файл log.txt. Он содержит данные для построения отчёта. Или git shortlog -s -n -e если отчёт вам не нужен. Создайте файл
§ page.welcome.description2: [.mailmap|https://git-scm.com/docs/gitmailmap] в корне проекта, чтобы обьединить статистику по сотрудникам. § page.welcome.description2: [.mailmap|https://git-scm.com/docs/gitmailmap] в корне проекта, чтобы обьединить статистику по сотрудникам.
§ page.welcome.description: Git создаст файл log.txt. Он содержит данные для построения отчёта. Или git shortlog -s -n -e если отчёт вам не нужен. Создайте файл [.mailmap|https://git-scm.com/docs/gitmailmap] в корне проекта, чтобы обьединить статистику по сотрудникам. § page.welcome.description: Git создаст файл log.txt. Он содержит данные для построения отчёта. Или git shortlog -s -n -e если отчёт вам не нужен. Создайте файл [.mailmap|https://git-scm.com/docs/gitmailmap] в корне проекта, чтобы обьединить статистику по сотрудникам.
@ -100,11 +193,14 @@ localization.parse('ru', `
§ page.settings.form.remove: Удалить § page.settings.form.remove: Удалить
§ page.settings.form.addEmployee: Добавить сотрудника § page.settings.form.addEmployee: Добавить сотрудника
§ page.settings.form.addContract: Добавить трудовой договор § page.settings.form.addContract: Добавить трудовой договор
§ page.print.title: Что распечатываем? § page.print.modal.title: Что распечатываем?
§ page.print.page: Текущую страницу § page.print.modal.page: Текущую страницу
§ page.print.type: Текущий раздел § page.print.modal.type: Текущий раздел
§ page.print.all: Всю статистику § page.print.modal.all: Всю статистику
§ page.print.cancel: Отмена § page.print.modal.cancel: Отмена
§ page.print.tableOfContents: Оглавление
§ page.print.title: Отчёт по git-репозиторию «$1»
§ page.print.description: Данные для отчёта были получены из истории коммитов.
§ page.team.author.title: Статистика по сотрудникам § page.team.author.title: Статистика по сотрудникам
§ page.team.author.description1: *Часть статитики* (скорость работы, затраченные деньги и т.п.) *по сотрудникам с типом «Помошник» не считается*, т.к. это эпизодическая роль в проекте. Предпологаем, что они не влияют на проект, а их правками можно пренебречь на фоне общего объема работы. § page.team.author.description1: *Часть статитики* (скорость работы, затраченные деньги и т.п.) *по сотрудникам с типом «Помошник» не считается*, т.к. это эпизодическая роль в проекте. Предпологаем, что они не влияют на проект, а их правками можно пренебречь на фоне общего объема работы.
§ page.team.author.description2: *Сортировка по умолчанию* это сортировка по количеству задач и группам (текущие, уволенные, помогающие сотрудники). § page.team.author.description2: *Сортировка по умолчанию* это сортировка по количеству задач и группам (текущие, уволенные, помогающие сотрудники).
@ -229,7 +325,7 @@ localization.parse('ru', `
§ page.person.achievement.negative: Негативные § page.person.achievement.negative: Негативные
§ page.person.achievement.description: Чем больше сотрудник набрал отрицательных достижений, тем больше вероятность, что ситуация нестандартная. Возможно, стоит изменить режим его работы, задачи или отчётность. Следует поговорить с ним и узнать, какие проблемы мешают его работе. § page.person.achievement.description: Чем больше сотрудник набрал отрицательных достижений, тем больше вероятность, что ситуация нестандартная. Возможно, стоит изменить режим его работы, задачи или отчётность. Следует поговорить с ним и узнать, какие проблемы мешают его работе.
§ page.person.gets.title: Взятые геты: § page.person.gets.title: Взятые геты:
§ page.person.gets.description: &laquo;Взять гет&raquo; в данном случае означает первым оставить коммит к&nbsp;задаче с&nbsp;&laquo;красивым&raquo; номером. § page.person.gets.description: «Взять гет» в данном случае означает первым оставить коммит к&nbsp;задаче с&nbsp;&laquo;красивым&raquo; номером.
§ page.person.business.days.title: дней работы § page.person.business.days.title: дней работы
§ page.person.business.days.description: Учтены только дни, в которые делались коммиты § page.person.business.days.description: Учтены только дни, в которые делались коммиты
§ page.person.business.tasks.title: задач § page.person.business.tasks.title: задач
@ -287,6 +383,53 @@ git может показать малое количество изменени
§ recommendations.title § recommendations.title
Рекомендации и факты Рекомендации и факты
§ recommendations.timestamp.firstCommit.description
сделал первый коммит
День недели: $1
§ recommendations.timestamp.lastCommit.description
сделал последний коммит
День недели: $1
§ recommendations.timestamp.common.title: $1 дней
§ recommendations.timestamp.allDays.description: от первого до последнего коммита (включая выходные и праздники).
§ recommendations.timestamp.lossesDays.description: без коммитов, даже с учётом выходных, отпуска и государственных праздников.
§ recommendations.timestamp.weekendDays.description
работы на выходных
# Почему это плохо:
- заказчик платит двойную цену за работу в выходной день;
- сотрудники быстрее выгорают;
§ recommendations.timestamp.regularWeekendWord.title: Регулярные переработки
§ recommendations.timestamp.sometimeWeekendWord.title: Бывают переработки
§ recommendations.timestamp.weekendWord.description
Вероятно, стоит сменить менеджера проекта, аналитика и архитектора.
# Почему это плохо:
- заказчик платит двойную цену за работу в выходной день;
- качество продуката, как правило, получается низкое;
- часть сотрудников увольняется;
- из-за спешки появляются новые ошибки;
# Скорее всего:
- неверно оценили сроки в самом начале;
- тех. задание отсутствует;
- слабая аналитика;
- слабая архитектура (архитектора не нанимали, а команда состоит из мидл разработчиков);
- сначала начали писать код, потом проектировать;
- нет нормальных процессов, чтобы понять ошибки;
§ recommendations.timestamp.neverWeekendWord.title: Обычно без переработок
§ recommendations.timestamp.neverWeekendWord.description
Но иногда бывают.
# Почему это плохо:
- заказчик платит двойную цену за работу в выходной день;
- сотрудники быстрее выгорают;
§ recommendations.scope.parallelism.not.title § recommendations.scope.parallelism.not.title
Нет паралельных работ Нет паралельных работ
@ -467,6 +610,40 @@ Bus factor = 1
- для каждого коммита определятся тип задачи; - для каждого коммита определятся тип задачи;
- если больше 70% задач одного типа, значит человек делает одно и тоже; - если больше 70% задач одного типа, значит человек делает одно и тоже;
§ recommendations.author.workToday.title: Работает $1
§ recommendations.author.workToday.description
над проектом в данный момент.
# Состав:
- $1;
# Почему именно они:
- рабочих дней более 50%;
- работали в течении последних 30 дней;
§ recommendations.author.dismissed.title: Уволилось $1
§ recommendations.author.dismissed.description
или работало короткий промежуток времени.
# Состав:
- $1;
# Почему именно они:
- работали в нормальном ритме (видимо, это их основной репозиторий);
- за последний месяц не было ни одного коммита;
- отпуск обычно 14 дней (их отсутствие не похоже на отпуск);
§ recommendations.author.staff.title: Помогают $1
§ recommendations.author.staff.description
Люди другой специализации, которые что-либо коммитили.
# Состав:
- $1;
# Почему именно они:
- это не open-source проект;
- рабочих дней менее 15% от общего числа;
- изменяют примерно одни и те же файлы;
§ recommendations.author.projectType.openSource.title § recommendations.author.projectType.openSource.title
Открытый проект Открытый проект
@ -593,53 +770,24 @@ Bus factor = 1
- формируется доверительное отношение, сотрудники становятся более лояльны к компании; - формируется доверительное отношение, сотрудники становятся более лояльны к компании;
- повышается вовлеченность сотрудников; - повышается вовлеченность сотрудников;
§ recommendations.hour.onlyWork.title § recommendations.hour.onlyWork.title: Выходных тут нет
Выходных тут нет § recommendations.hour.onlyWork.description: Вероятно, стоит уволить менеджера проекта.
§ recommendations.hour.weekends.title: Работа на выходных
§ recommendations.hour.onlyWork.description § recommendations.hour.weekends.description: Вероятно, стоит проверить менеджера проекта.
Вероятно, стоит уволить менеджера проекта. § recommendations.hour.easy.title: Бывают проблемы
§ recommendations.hour.easy.description: Вероятно, бывают завалы и приходится работать на выходных.
§ recommendations.hour.weekends.title § recommendations.week.lazyDays.down.title: Стало меньше прогулов
Работа на выходных § recommendations.week.lazyDays.down.description: за последние три недели этот показатель упал
§ recommendations.week.lazyDays.up.title: Стало больше прогулов
§ recommendations.hour.weekends.description § recommendations.week.lazyDays.up.description: нет задач или нужен более жесткий контроль
Вероятно, стоит проверить менеджера проекта. § recommendations.week.notWork.title: Стабильно не дорабатывает
§ recommendations.week.notWork.description: т.к. каждую неделю пишет код не 100% времени
§ recommendations.week.upWork.title: Стабильно перерабатывает
§ recommendations.hour.easy.title § recommendations.week.upWork.description: т.к. каждую неделю пишет код в выходные дни
Бывают проблемы § recommendations.week.task.up.title: Растёт производительность
§ recommendations.week.task.up.description: или задачи стали слишком мелкие. Нужно проверить. Если гранулярность та же - закрепить результат.
§ recommendations.hour.easy.description § recommendations.week.task.lazyMaintainer.description: стабильный лидер по прогулам. Уволить?
Вероятно, бывают завалы и приходится работать на выходных. § recommendations.week.task.down.title: Падает производительность
§ recommendations.week.lazyDays.down.title
Стало меньше прогулов
§ recommendations.week.lazyDays.down.description
за последние три недели этот показатель упал
§ recommendations.week.lazyDays.up.title
Стало больше прогулов
§ recommendations.week.lazyDays.up.description
нет задач или нужен более жесткий контроль
§ recommendations.week.notWork.title
Стабильно не дорабатывает
§ recommendations.week.notWork.description
т.к. каждую неделю пишет код не 100% времени
§ recommendations.week.upWork.title
Стабильно перерабатывает
§ recommendations.week.upWork.description
т.к. каждую неделю пишет код в выходные дни
§ recommendations.week.task.up.title
Растёт производительность
§ recommendations.week.task.up.description
или задачи стали слишком мелкие. Нужно проверить. Если гранулярность та же - закрепить результат.
§ recommendations.week.task.down.title
Падает производительность
§ recommendations.week.task.down.description § recommendations.week.task.down.description
или задачи хуже разбивают. Нужно проверить. Если гранулярность та же - взять на контроль. или задачи хуже разбивают. Нужно проверить. Если гранулярность та же - взять на контроль.
@ -650,18 +798,12 @@ Bus factor = 1
- задачи могли быть сложнее, чем казались; - задачи могли быть сложнее, чем казались;
- задачи могли иметь большой объём работы (нужно проверить количество изменений, падают они или нет за этот же период) - задачи могли иметь большой объём работы (нужно проверить количество изменений, падают они или нет за этот же период)
§ recommendations.type.everyHasOne.title: Не подписывают тип задачи
§ recommendations.type.everyHasOne.title § recommendations.type.everyHasOne.description: большинство типов задач делает один человек.
Не подписывают тип задачи § recommendations.type.oneMaintainer.title: Узкая специализация
§ recommendations.type.everyHasOne.description
большинство типов задач делает один человек.
§ recommendations.type.oneMaintainer.title
Узкая специализация
§ recommendations.type.oneMaintainer.description § recommendations.type.oneMaintainer.description
большинство задач одного типа делают одни и те же люди. большинство задач одного типа делают одни и те же люди.
# Типы задач: # Типы задач:
§ recommendations.type.common § recommendations.type.common

View file

@ -32,6 +32,7 @@ export default class DataGripByExtension {
extension: file.extension, extension: file.extension,
authors: {}, authors: {},
files: { [file.firstName]: 1 }, files: { [file.firstName]: 1 },
count: 1,
more: {}, more: {},
total: { total: {
added: 0, added: 0,
@ -45,6 +46,7 @@ export default class DataGripByExtension {
byExtension[file.extension].files[file.firstName] = numberNames byExtension[file.extension].files[file.firstName] = numberNames
? (numberNames + 1) ? (numberNames + 1)
: 1; : 1;
byExtension[file.extension].count += 1;
} }
for (let author in file.authors) { for (let author in file.authors) {
@ -69,7 +71,7 @@ export default class DataGripByExtension {
this.#addMorePercent(byExtension); this.#addMorePercent(byExtension);
this.statistic = Object.entries(byExtension) this.statistic = Object.entries(byExtension)
.sort((a: any, b: any) => b[1].total.total - a[1].total.total) .sort((a: any, b: any) => b[1].count - a[1].count)
.map((item: any) => item[1]); .map((item: any) => item[1]);
this.statisticByName = byExtension; this.statisticByName = byExtension;
} }

View file

@ -6,18 +6,19 @@ class Localization {
insertArguments(message: string, args?: any) { insertArguments(message: string, args?: any) {
if (!args) return message; if (!args) return message;
const list = Array.isArray(args) ? args : [args]; const list = Array.isArray(args) ? args : [args];
console.log(list);
list.forEach((text: any, index: number) => { list.forEach((text: any, index: number) => {
message = message.replace(`$${index}`, text || '_'); message = message.replace(`$${index + 1}`, text || '_');
}); });
return message; return message;
} }
get(key = '', args?: any) { get(key = '', ...args: any) {
const dictionary = this.translations[this.language]; const dictionary = this.translations[this.language];
if (!dictionary) return key || ''; if (!dictionary) return key || '';
let message = dictionary[key]; let message = dictionary[key];
if (message) return message; if (message) return this.insertArguments(message, args);
const keys = key.split('.'); const keys = key.split('.');
message = dictionary; message = dictionary;
@ -81,5 +82,7 @@ class Localization {
} }
const localization = new Localization(); const localization = new Localization();
// @ts-ignore
window.localization = localization;
export default localization; export default localization;

View file

@ -1,4 +1,5 @@
import { getDateByTimestamp } from 'ts/helpers/formatter'; import { getDateByTimestamp } from 'ts/helpers/formatter';
import RECOMMENDATION_TYPES from '../contstants';
export default class RecommendationsPersonByTimestamp { export default class RecommendationsPersonByTimestamp {
getTotalInfo(dataGrip: any) { getTotalInfo(dataGrip: any) {
@ -6,27 +7,71 @@ export default class RecommendationsPersonByTimestamp {
const byTimestamp = dataGrip.timestamp.statisticByAuthor[name]; const byTimestamp = dataGrip.timestamp.statisticByAuthor[name];
const byAuthor = dataGrip.author.statisticByName[name]; const byAuthor = dataGrip.author.statisticByName[name];
const workInWeek = byTimestamp.workByDay[5] + byTimestamp.workByDay[6]; const workInWeek = byTimestamp.workByDay[5] + byTimestamp.workByDay[6];
acc[name] = [ acc[name] = [];
workInWeek ? [`${workInWeek} дней`, 'работы на выходных', 'error'] : null,
byAuthor.daysLosses ? [`${byAuthor.daysLosses} дней`, 'без коммитов, даже с учётом выходных, отпуска и государственных праздников.', 'warning'] : null, if (workInWeek) {
[`${byAuthor.daysAll} дней`, 'от первого до последнего коммита (включая выходные и праздники)', 'fact'], acc[name].push({
this.getFirstDay(byTimestamp), title: 'recommendations.timestamp.common.title',
this.getLastDay(byTimestamp), description: 'recommendations.timestamp.weekendDays.description',
].filter(item => item); type: RECOMMENDATION_TYPES.ALERT,
arguments: {
title: [workInWeek],
},
});
}
if (byAuthor.daysLosses) {
acc[name].push({
title: 'recommendations.timestamp.common.title',
description: 'recommendations.timestamp.lossesDays.description',
type: RECOMMENDATION_TYPES.WARNING,
arguments: {
title: [byAuthor.daysLosses],
},
});
}
acc[name].push({
title: 'recommendations.timestamp.common.title',
description: 'recommendations.timestamp.allDays.description',
type: RECOMMENDATION_TYPES.FACT,
arguments: {
title: [byAuthor.daysAll],
},
});
acc[name].push(this.getFirstDay(byTimestamp));
acc[name].push(this.getLastDay(byTimestamp));
return acc; return acc;
}, {}); }, {});
} }
getFirstDay(byTimestamp: any) { getFirstDay(byTimestamp: any) {
const commit = byTimestamp.allCommitsByTimestamp[0]; const commit = byTimestamp.allCommitsByTimestamp[0];
const [ date, day ] = getDateByTimestamp(commit.timestamp); const [date, day] = getDateByTimestamp(commit.timestamp);
return [date, `сделал первый коммит\n\ень недели: ${day}`, 'fact']; return {
title: date,
description: 'recommendations.timestamp.firstCommit.description',
type: RECOMMENDATION_TYPES.FACT,
arguments: {
description: [day],
},
};
} }
getLastDay(byTimestamp: any) { getLastDay(byTimestamp: any) {
const commit = byTimestamp.allCommitsByTimestamp[(byTimestamp.allCommitsByTimestamp.length - 1)]; const commit = byTimestamp.allCommitsByTimestamp[(byTimestamp.allCommitsByTimestamp.length - 1)];
const [ date, day ] = getDateByTimestamp(commit.timestamp); const [date, day] = getDateByTimestamp(commit.timestamp);
return [date, `сделал последний коммит\n\ень недели: ${day}`, 'fact']; return {
title: date,
description: 'recommendations.timestamp.lastCommit.description',
type: RECOMMENDATION_TYPES.FACT,
arguments: {
description: [day],
},
};
} }
} }

View file

@ -1,4 +1,4 @@
import localization from 'ts/helpers/Localization'; import RECOMMENDATION_TYPES from '../contstants';
export default class RecommendationsPersonByWeek { export default class RecommendationsPersonByWeek {
getTotalInfo(dataGrip: any) { getTotalInfo(dataGrip: any) {
@ -16,51 +16,61 @@ export default class RecommendationsPersonByWeek {
getLazyDays(lastWeeks: any[], name: string) { getLazyDays(lastWeeks: any[], name: string) {
const lazyDays = lastWeeks.map(statistic => statistic.lazyDays[name]); const lazyDays = lastWeeks.map(statistic => statistic.lazyDays[name]);
if (lazyDays[0] < lazyDays[1] && lazyDays[1] < lazyDays[2]) return [
localization.get('recommendations.week.lazyDays.down.title'), if (lazyDays[0] < lazyDays[1] && lazyDays[1] < lazyDays[2]) return {
localization.get('recommendations.week.lazyDays.down.description'), title: 'recommendations.week.lazyDays.down.title',
'fact', description: 'recommendations.week.lazyDays.down.description',
]; type: RECOMMENDATION_TYPES.FACT,
if (lazyDays[0] > lazyDays[1] && lazyDays[1] > lazyDays[2]) return [ };
localization.get('recommendations.week.lazyDays.up.title'),
localization.get('recommendations.week.lazyDays.up.description'), if (lazyDays[0] > lazyDays[1] && lazyDays[1] > lazyDays[2]) return {
'error', title: 'recommendations.week.lazyDays.up.title',
]; description: 'recommendations.week.lazyDays.up.description',
type: RECOMMENDATION_TYPES.ALERT,
};
return null; return null;
} }
getNotWork(lastWeeks: any[], name: string) { getNotWork(lastWeeks: any[], name: string) {
const lazyDays = lastWeeks.map(statistic => statistic.lazyDays[name]); const lazyDays = lastWeeks.map(statistic => statistic.lazyDays[name]);
if (lazyDays[0] && lazyDays[1] && lazyDays[2]) return [
localization.get('recommendations.week.notWork.title'), if (lazyDays[0] && lazyDays[1] && lazyDays[2]) return {
localization.get('recommendations.week.notWork.description'), title: 'recommendations.week.notWork.title',
'error', description: 'recommendations.week.notWork.description',
]; type: RECOMMENDATION_TYPES.ALERT,
};
return null; return null;
} }
getUpWork(lastWeeks: any[], name: string) { getUpWork(lastWeeks: any[], name: string) {
const weekDays = lastWeeks.map(statistic => statistic.weekDays[name]); const weekDays = lastWeeks.map(statistic => statistic.weekDays[name]);
if (weekDays[0] && weekDays[1] && weekDays[2]) return [
localization.get('recommendations.week.upWork.title'), if (weekDays[0] && weekDays[1] && weekDays[2]) return {
localization.get('recommendations.week.upWork.description'), title: 'recommendations.week.upWork.title',
'error', description: 'recommendations.week.upWork.description',
]; type: RECOMMENDATION_TYPES.ALERT,
};
return null; return null;
} }
getTasks(lastWeeks: any[], name: string) { // TODO: спорно, это видно по количеству изменений getTasks(lastWeeks: any[], name: string) { // TODO: спорно, это видно по количеству изменений
const lazyDays = lastWeeks.map(statistic => statistic.taskInDay[name]); const lazyDays = lastWeeks.map(statistic => statistic.taskInDay[name]);
if (lazyDays[0] < lazyDays[1] && lazyDays[1] < lazyDays[2]) return [
localization.get('recommendations.week.task.up.title'), if (lazyDays[0] < lazyDays[1] && lazyDays[1] < lazyDays[2]) return {
localization.get('recommendations.week.task.up.description'), title: 'recommendations.week.task.up.title',
'fact', description: 'recommendations.week.task.up.description',
]; type: RECOMMENDATION_TYPES.FACT,
if (lazyDays[0] > lazyDays[1] && lazyDays[1] > lazyDays[2]) return [ };
localization.get('recommendations.week.task.down.title'),
localization.get('recommendations.week.task.down.description'), if (lazyDays[0] > lazyDays[1] && lazyDays[1] > lazyDays[2]) return {
'error', title: 'recommendations.week.task.down.title',
]; description: 'recommendations.week.task.down.description',
type: RECOMMENDATION_TYPES.ALERT,
};
return null; return null;
} }
} }

View file

@ -1,4 +1,4 @@
import localization from 'ts/helpers/Localization'; import RECOMMENDATION_TYPES from '../contstants';
export default class RecommendationsTeamByAuthor { export default class RecommendationsTeamByAuthor {
getTotalInfo(dataGrip: any) { getTotalInfo(dataGrip: any) {
@ -40,82 +40,97 @@ export default class RecommendationsTeamByAuthor {
return [ return [
projectType, projectType,
(lotOfLazy.length ? [lotOfLazy, localization.get('recommendations.author.lotOfLazy'), 'error'] : null),
(manyLazy.length ? [manyLazy, localization.get('recommendations.author.manyLazy'), 'warning'] : null),
(oneTypeMans.length ? [oneTypeMans, localization.get('recommendations.author.oneTypeMans'), 'warning'] : null),
(worker.length
? [`Работает ${worker.length}`, `над проектом в данный момент.
# Состав: (lotOfLazy.length ? {
- ${worker.join(';\n- ')}; title: lotOfLazy,
description: 'recommendations.author.lotOfLazy',
type: RECOMMENDATION_TYPES.ALERT,
} : null),
# Почему именно они: (manyLazy.length ? {
- рабочих дней более 50%; title: manyLazy,
- работали в течении последних 30 дней; description: 'recommendations.author.manyLazy',
`, 'fact'] : null), type: RECOMMENDATION_TYPES.WARNING,
(dismissed.length } : null),
? [`Уволилось ${dismissed.length}`, `или работало короткий промежуток времени.
# Состав: (oneTypeMans.length ? {
- ${dismissed.join(';\n- ')}; title: oneTypeMans,
description: 'recommendations.author.oneTypeMans',
type: RECOMMENDATION_TYPES.WARNING,
} : null),
# Почему именно они: (worker.length ? {
- работали в нормальном ритме (видимо, это их основной репозиторий); title: 'recommendations.author.workToday.title',
- за последний месяц не было ни одного коммита; description: 'recommendations.author.workToday.description',
- отпуск обычно 14 дней (их отсутствие не похоже на отпуск); type: RECOMMENDATION_TYPES.FACT,
`, 'fact'] : null), arguments: {
(staff.length title: worker.length,
? [`Помогают ${staff.length}`, `Люди другой специализации, которые что-либо коммитили. description: worker.join(';\n- '),
},
} : null),
# Состав: (dismissed.length ? {
- ${staff.join(';\n- ')}; title: 'recommendations.author.dismissed.title',
description: 'recommendations.author.dismissed.description',
type: RECOMMENDATION_TYPES.FACT,
arguments: {
title: dismissed.length,
description: dismissed.join(';\n- '),
},
} : null),
(staff.length ? {
title: 'recommendations.author.staff.title',
description: 'recommendations.author.staff.description',
type: RECOMMENDATION_TYPES.FACT,
arguments: {
title: staff.length,
description: staff.join(';\n- '),
},
} : null),
# Почему именно они:
- это не open-source проект;
- рабочих дней менее 15% от общего числа;
- изменяют примерно одни и те же файлы;
`, 'fact']
: null),
// ['Планирование', 'Задачи распределены довольно равномерно', 'info'], // ['Планирование', 'Задачи распределены довольно равномерно', 'info'],
[ {
localization.get('recommendations.author.manager.title'), title: 'recommendations.author.manager.title',
localization.get('recommendations.author.manager.description'), description: 'recommendations.author.manager.description',
'info', type: RECOMMENDATION_TYPES.INFO,
], },
[ {
localization.get('recommendations.author.shorTalk.title'), title: 'recommendations.author.shorTalk.title',
localization.get('recommendations.author.shorTalk.description'), description: 'recommendations.author.shorTalk.description',
'info', type: RECOMMENDATION_TYPES.INFO,
], },
[ {
localization.get('recommendations.author.ipr.title'), title: 'recommendations.author.ipr.title',
localization.get('recommendations.author.ipr.description'), description: 'recommendations.author.ipr.description',
'info', type: RECOMMENDATION_TYPES.INFO,
], },
[ {
localization.get('recommendations.author.oneToOne.title'), title: 'recommendations.author.oneToOne.title',
localization.get('recommendations.author.oneToOne.description'), description: 'recommendations.author.oneToOne.description',
'info', type: RECOMMENDATION_TYPES.INFO,
], },
[ {
localization.get('recommendations.author.club.title'), title: 'recommendations.author.club.title',
localization.get('recommendations.author.club.description'), description: 'recommendations.author.club.description',
'info', type: RECOMMENDATION_TYPES.INFO,
], },
].filter(item => item); ].filter(item => item);
} }
getProjectType(workLazyTotal: number) { getProjectType(workLazyTotal: number) {
if (workLazyTotal < 1) return [ if (workLazyTotal < 1) return {
localization.get('recommendations.author.projectType.openSource.title'), title: 'recommendations.author.projectType.openSource.title',
localization.get('recommendations.author.projectType.openSource.description'), description: 'recommendations.author.projectType.openSource.description',
'fact', type: RECOMMENDATION_TYPES.FACT,
]; };
if (workLazyTotal < 5) return [
localization.get('recommendations.author.projectType.easy.title'), if (workLazyTotal < 5) return {
localization.get('recommendations.author.projectType.easy.description'), title: 'recommendations.author.projectType.easy.title',
'error', description: 'recommendations.author.projectType.easy.description',
]; type: RECOMMENDATION_TYPES.ALERT,
};
return null; return null;
} }
} }

View file

@ -1,4 +1,4 @@
import localization from 'ts/helpers/Localization'; import RECOMMENDATION_TYPES from '../contstants';
export default class RecommendationsTeamByHour { export default class RecommendationsTeamByHour {
getTotalInfo(dataGrip: any) { getTotalInfo(dataGrip: any) {
@ -16,21 +16,24 @@ export default class RecommendationsTeamByHour {
const weekends = Math.max(...statistic.commitsByDayAndHourTotal.slice(5, 7)); const weekends = Math.max(...statistic.commitsByDayAndHourTotal.slice(5, 7));
const workAndWeekends = weekends / weekday; const workAndWeekends = weekends / weekday;
if (workAndWeekends > 0.45) return [ if (workAndWeekends > 0.45) return {
localization.get('recommendations.hour.onlyWork.title'), title: 'recommendations.hour.onlyWork.title',
localization.get('recommendations.hour.onlyWork.description'), description: 'recommendations.hour.onlyWork.description',
'error', type: RECOMMENDATION_TYPES.ALERT,
]; };
if (workAndWeekends > 0.2) return [
localization.get('recommendations.hour.weekends.title'), if (workAndWeekends > 0.2) return {
localization.get('recommendations.hour.weekends.description'), title: 'recommendations.hour.weekends.title',
'error', description: 'recommendations.hour.weekends.description',
]; type: RECOMMENDATION_TYPES.ALERT,
if (workAndWeekends > 0) return [ };
localization.get('recommendations.hour.easy.title'),
localization.get('recommendations.hour.easy.description'), if (workAndWeekends > 0) return {
'warning', title: 'recommendations.hour.easy.title',
]; description: 'recommendations.hour.easy.description',
type: RECOMMENDATION_TYPES.WARNING,
};
return null; return null;
} }
} }

View file

@ -1,5 +1,5 @@
import { getMoney } from 'ts/helpers/formatter'; import { getMoney } from 'ts/helpers/formatter';
import localization from 'ts/helpers/Localization'; import RECOMMENDATION_TYPES from '../contstants';
export default class RecommendationsTeamByScope { export default class RecommendationsTeamByScope {
getTotalInfo(dataGrip: any) { getTotalInfo(dataGrip: any) {
@ -8,17 +8,21 @@ export default class RecommendationsTeamByScope {
this.getBusFactor(dataGrip), this.getBusFactor(dataGrip),
this.getManyTypes(dataGrip), this.getManyTypes(dataGrip),
this.getParallelism(dataGrip), this.getParallelism(dataGrip),
[money, localization.get('recommendations.scope.money'), 'fact'], {
[ title: money,
localization.get('recommendations.scope.plan.title'), description: 'recommendations.scope.money',
localization.get('recommendations.scope.plan.description'), type: RECOMMENDATION_TYPES.FACT,
'info', },
], {
[ title: 'recommendations.scope.plan.title',
localization.get('recommendations.scope.cost.title'), description: 'recommendations.scope.plan.description',
localization.get('recommendations.scope.cost.description'), type: RECOMMENDATION_TYPES.INFO,
'info', },
], {
title: 'recommendations.scope.cost.title',
description: 'recommendations.scope.cost.description',
type: RECOMMENDATION_TYPES.INFO,
},
].filter(item => item); ].filter(item => item);
} }
@ -38,23 +42,23 @@ export default class RecommendationsTeamByScope {
const total = data.reduce((sum, value) => sum + value, 0); const total = data.reduce((sum, value) => sum + value, 0);
const parallelism = total / data.length; const parallelism = total / data.length;
if (parallelism < 1.3) return [ if (parallelism < 1.3) return {
localization.get('recommendations.scope.parallelism.not.title'), title: 'recommendations.scope.parallelism.not.title',
localization.get('recommendations.scope.parallelism.not.description'), description: 'recommendations.scope.parallelism.not.description',
'fact', type: RECOMMENDATION_TYPES.FACT,
]; };
if (parallelism < 2) return [ if (parallelism < 2) return {
localization.get('recommendations.scope.parallelism.has.title'), title: 'recommendations.scope.parallelism.has.title',
localization.get('recommendations.scope.parallelism.has.description'), description: 'recommendations.scope.parallelism.has.description',
'fact', type: RECOMMENDATION_TYPES.FACT,
]; };
return [ return {
localization.get('recommendations.scope.parallelism.every.title'), title: 'recommendations.scope.parallelism.every.title',
localization.get('recommendations.scope.parallelism.every.description'), description: 'recommendations.scope.parallelism.every.description',
'fact', type: RECOMMENDATION_TYPES.FACT,
]; };
} }
getBusFactor(dataGrip: any) { getBusFactor(dataGrip: any) {
@ -68,17 +72,18 @@ export default class RecommendationsTeamByScope {
if (!oneMaintainer.length) return null; if (!oneMaintainer.length) return null;
const everyHasOne = oneMaintainer.length > dataGrip.scope.statistic.length * 0.6; const everyHasOne = oneMaintainer.length > dataGrip.scope.statistic.length * 0.6;
if (everyHasOne) return [
localization.get('recommendations.scope.bus.everyHasOne.title'),
localization.get('recommendations.scope.bus.everyHasOne.description'),
'warning',
];
return [ if (everyHasOne) return {
oneMaintainer, title: 'recommendations.scope.bus.everyHasOne.title',
localization.get('recommendations.scope.bus.oneMaintainer'), description: 'recommendations.scope.bus.everyHasOne.description',
'error', type: RECOMMENDATION_TYPES.WARNING,
]; };
return {
title: oneMaintainer,
description: 'recommendations.scope.bus.oneMaintainer',
type: RECOMMENDATION_TYPES.ALERT,
};
} }
getManyTypes(dataGrip: any) { getManyTypes(dataGrip: any) {
@ -90,22 +95,23 @@ export default class RecommendationsTeamByScope {
}).map((statistic: any) => statistic.scope); }).map((statistic: any) => statistic.scope);
const everyHasOne = oneType.length > dataGrip.scope.statistic.length * 0.6; const everyHasOne = oneType.length > dataGrip.scope.statistic.length * 0.6;
if (everyHasOne) return [
localization.get('recommendations.scope.types.process.title'),
[
localization.get('recommendations.scope.types.process.description'),
localization.get('recommendations.scope.types.common'),
].join('\n'),
'warning',
];
return [ if (everyHasOne) return {
oneType, title: 'recommendations.scope.types.process.title',
[ description: [
localization.get('recommendations.scope.types.one'), 'recommendations.scope.types.process.description',
localization.get('recommendations.scope.types.common'), 'recommendations.scope.types.common',
].join('\n'), ],
'warning', type: RECOMMENDATION_TYPES.WARNING,
]; };
return {
title: oneType,
description: [
'recommendations.scope.types.one',
'recommendations.scope.types.common',
],
type: RECOMMENDATION_TYPES.WARNING,
};
} }
} }

View file

@ -1,4 +1,5 @@
import { getDateByTimestamp } from 'ts/helpers/formatter'; import { getDateByTimestamp } from 'ts/helpers/formatter';
import RECOMMENDATION_TYPES from '../contstants';
export default class RecommendationsTeamByTimestamp { export default class RecommendationsTeamByTimestamp {
getTotalInfo(dataGrip: any) { getTotalInfo(dataGrip: any) {
@ -10,14 +11,26 @@ export default class RecommendationsTeamByTimestamp {
// TODO: all days не верный, я вывожу рабочие дни, а не выходные. // TODO: all days не верный, я вывожу рабочие дни, а не выходные.
return [ return [
workInWeek ? [`${workInWeek} дней`, `работы на выходных (workInWeek ? {
title: 'recommendations.timestamp.common.title',
description: 'recommendations.timestamp.weekendDays.description',
type: RECOMMENDATION_TYPES.ALERT,
arguments: {
title: [workInWeek],
},
} : null),
# Почему это плохо:
- заказчик платит двойную цену за работу в выходной день;
- сотрудники быстрее выгорают;
`, 'error'] : null,
this.getWorkOnWeek(byTimestamp.allCommitsByTimestamp.length, workInWeek), this.getWorkOnWeek(byTimestamp.allCommitsByTimestamp.length, workInWeek),
[`${totalDays} дней работы`, 'от первого до последнего коммита', 'fact'],
{
title: 'recommendations.timestamp.common.title',
description: 'recommendations.timestamp.allDays.description',
type: RECOMMENDATION_TYPES.FACT,
arguments: {
title: [totalDays],
},
},
this.getFirstDay(byTimestamp), this.getFirstDay(byTimestamp),
this.getLastDay(byTimestamp), this.getLastDay(byTimestamp),
].filter(item => item); ].filter(item => item);
@ -25,49 +38,60 @@ export default class RecommendationsTeamByTimestamp {
getWorkOnWeek(allWorkDays: number, workOnWeek: number) { getWorkOnWeek(allWorkDays: number, workOnWeek: number) {
const percent = (workOnWeek * 100) / allWorkDays; const percent = (workOnWeek * 100) / allWorkDays;
const description = `Вероятно, стоит сменить менеджера проекта, аналитика и архитектора.
# Почему это плохо:
- заказчик платит двойную цену за работу в выходной день;
- качество продуката, как правило, получается низкое;
- часть сотрудников увольняется;
- из-за спешки появляются новые ошибки;
# Скорее всего:
- неверно оценили сроки в самом начале;
- тех. задание отсутствует;
- слабая аналитика;
- слабая архитектура (архитектора не нанимали, а команда состоит из мидл разработчиков);
- сначала начали писать код, потом проектировать;
- нет нормальных процессов, чтобы понять ошибки;
`;
if (percent > 13) { if (percent > 13) {
return ['Регулярные переработки', description, 'error']; return {
title: 'recommendations.timestamp.regularWeekendWord.title',
description: 'recommendations.timestamp.weekendWord.description',
type: RECOMMENDATION_TYPES.ALERT,
};
} }
if (percent > 7) {
return ['Бывают переработки', description, 'error'];
}
if (percent > 2) {
return ['Обычно без переработок', `Но иногда бывают.
# Почему это плохо: if (percent > 7) {
- заказчик платит двойную цену за работу в выходной день; return {
- сотрудники быстрее выгорают; title: 'recommendations.timestamp.sometimeWeekendWord.title',
`, 'fact']; description: 'recommendations.timestamp.weekendWord.description',
type: RECOMMENDATION_TYPES.ALERT,
};
} }
if (percent > 2) {
return {
title: 'recommendations.timestamp.neverWeekendWord.title',
description: 'recommendations.timestamp.neverWeekendWord.description',
type: RECOMMENDATION_TYPES.FACT,
};
}
return null; return null;
} }
getFirstDay(byTimestamp: any) { getFirstDay(byTimestamp: any) {
const commit = byTimestamp.allCommitsByTimestamp[0]; const commit = byTimestamp.allCommitsByTimestamp[0];
const [ date, day ] = getDateByTimestamp(commit.timestamp); const [ date, day ] = getDateByTimestamp(commit.timestamp);
return [date, `был первый коммит\n\ень недели: ${day}`, 'fact'];
return {
title: date,
description: 'recommendations.timestamp.firstCommit.description',
type: RECOMMENDATION_TYPES.FACT,
arguments: {
description: [day],
},
};
} }
getLastDay(byTimestamp: any) { getLastDay(byTimestamp: any) {
const commit = byTimestamp.allCommitsByTimestamp[(byTimestamp.allCommitsByTimestamp.length - 1)]; const commit = byTimestamp.allCommitsByTimestamp[(byTimestamp.allCommitsByTimestamp.length - 1)];
const [ date, day ] = getDateByTimestamp(commit.timestamp); const [ date, day ] = getDateByTimestamp(commit.timestamp);
return [date, `был последний коммит\n\ень недели: ${day}`, 'fact'];
return {
title: date,
description: 'recommendations.timestamp.lastCommit.description',
type: RECOMMENDATION_TYPES.FACT,
arguments: {
description: [day],
},
};
} }
} }

View file

@ -1,4 +1,4 @@
import localization from 'ts/helpers/Localization'; import RECOMMENDATION_TYPES from '../contstants';
export default class RecommendationsTeamByType { export default class RecommendationsTeamByType {
getTotalInfo(dataGrip: any) { getTotalInfo(dataGrip: any) {
@ -8,21 +8,21 @@ export default class RecommendationsTeamByType {
return [ return [
this.getBusFactor(dataGrip), this.getBusFactor(dataGrip),
(fewTypes ? [ (fewTypes ? {
localization.get('recommendations.type.fewTypes.title'), title: 'recommendations.type.fewTypes.title',
localization.get('recommendations.type.fewTypes.description'), description: 'recommendations.type.fewTypes.description',
'fact', type: RECOMMENDATION_TYPES.FACT,
] : null), } : null),
[ {
localization.get('recommendations.type.diff.title'), title: 'recommendations.type.diff.title',
localization.get('recommendations.type.diff.description'), description: 'recommendations.type.diff.description',
'info', type: RECOMMENDATION_TYPES.INFO,
], },
[ {
localization.get('recommendations.type.buddy.title'), title: 'recommendations.type.buddy.title',
localization.get('recommendations.type.buddy.description'), description: 'recommendations.type.buddy.description',
'info', type: RECOMMENDATION_TYPES.INFO,
], },
].filter(item => item); ].filter(item => item);
} }
@ -37,24 +37,26 @@ export default class RecommendationsTeamByType {
if (!oneMaintainer.length) return null; if (!oneMaintainer.length) return null;
const everyHasOne = oneMaintainer.length > dataGrip.type.statistic.length * 0.6; const everyHasOne = oneMaintainer.length > dataGrip.type.statistic.length * 0.6;
if (everyHasOne) return [ if (everyHasOne) return {
localization.get('recommendations.type.everyHasOne.title'), title: 'recommendations.type.everyHasOne.title',
[ description: [
localization.get('recommendations.type.everyHasOne.description'), 'recommendations.type.everyHasOne.description',
localization.get('recommendations.type.common'), 'recommendations.type.common',
].join('\n'), ],
'warning', type: RECOMMENDATION_TYPES.WARNING,
]; };
return [ return {
localization.get('recommendations.type.oneMaintainer.title'), title: 'recommendations.type.oneMaintainer.title',
[ description: [
localization.get('recommendations.type.oneMaintainer.description'), 'recommendations.type.oneMaintainer.description',
`- ${oneMaintainer.join(';\n- ')}`, 'recommendations.type.common',
localization.get('recommendations.type.common'), ],
].join('\n'), type: RECOMMENDATION_TYPES.ALERT,
'error', arguments: {
]; description: [`- ${oneMaintainer.join(';\n- ')}`],
},
};
} }
} }

View file

@ -1,3 +1,5 @@
import RECOMMENDATION_TYPES from '../contstants';
export default class RecommendationsTeamByWeek { export default class RecommendationsTeamByWeek {
getTotalInfo(dataGrip: any) { getTotalInfo(dataGrip: any) {
if (dataGrip.author.list.length < 2) return []; if (dataGrip.author.list.length < 2) return [];
@ -12,23 +14,45 @@ export default class RecommendationsTeamByWeek {
getLazyDays(dataGrip: any, lastWeek: any) { getLazyDays(dataGrip: any, lastWeek: any) {
const lazyDays = lastWeek.map((statistic: any) => statistic.lazyDaysTotal / statistic.authorsLength); const lazyDays = lastWeek.map((statistic: any) => statistic.lazyDaysTotal / statistic.authorsLength);
if (lazyDays[0] < lazyDays[1] && lazyDays[1] < lazyDays[2]) { if (lazyDays[0] < lazyDays[1] && lazyDays[1] < lazyDays[2]) {
return ['Стало меньше прогулов', 'за последние три недели этот показатель упал', 'fact']; return {
title: 'recommendations.week.lazyDays.down.title',
description: 'recommendations.week.lazyDays.down.description',
type: RECOMMENDATION_TYPES.FACT,
};
} }
if (lazyDays[0] > lazyDays[1] && lazyDays[1] > lazyDays[2]) { if (lazyDays[0] > lazyDays[1] && lazyDays[1] > lazyDays[2]) {
return ['Стало больше прогулов', 'нет задач или нужен более жесткий контроль', 'error']; return {
title: 'recommendations.week.lazyDays.up.title',
description: 'recommendations.week.lazyDays.up.description',
type: RECOMMENDATION_TYPES.ALERT,
};
} }
return null; return null;
} }
getTasks(dataGrip: any, lastWeek: any) { // TODO: спорно, это видно по количеству изменений getTasks(dataGrip: any, lastWeek: any) { // TODO: спорно, это видно по количеству изменений
const lazyDays = lastWeek.map((statistic: any) => statistic.tasks / statistic.authorsLength); const lazyDays = lastWeek.map((statistic: any) => statistic.tasks / statistic.authorsLength);
if (lazyDays[0] < lazyDays[1] && lazyDays[1] < lazyDays[2]) { if (lazyDays[0] < lazyDays[1] && lazyDays[1] < lazyDays[2]) {
return ['Растёт производительность', 'или задачи стали слишком мелкие. Нужно проверить. Если гранулярность та же - закрепить результат.', 'fact']; return {
title: 'recommendations.week.task.up.title',
description: 'recommendations.week.task.up.description',
type: RECOMMENDATION_TYPES.FACT,
};
} }
if (lazyDays[0] > lazyDays[1] && lazyDays[1] > lazyDays[2]) { if (lazyDays[0] > lazyDays[1] && lazyDays[1] > lazyDays[2]) {
return ['Падает производительность', 'или задачи хуже разбивают. Нужно проверить. Если гранулярность та же - взять на контроль.', 'error']; return {
title: 'recommendations.week.task.down.title',
description: 'recommendations.week.task.down.description',
type: RECOMMENDATION_TYPES.ALERT,
};
} }
return null; return null;
} }
@ -39,9 +63,15 @@ export default class RecommendationsTeamByWeek {
); );
// TODO: неверный расчет // TODO: неверный расчет
// нужен человек, который встречается в трех массивах лидеров прогула // нужен человек, который встречается в трех массивах лидеров прогула
return lazyMaintainer[0] === lazyMaintainer[1] === lazyMaintainer[2] if (lazyMaintainer[0] === lazyMaintainer[1] === lazyMaintainer[2]) {
? [lazyMaintainer[0], 'стабильный лидер по прогулам. Уволить?', 'error'] return {
: null; title: lazyMaintainer[0],
description: 'recommendations.week.task.lazyMaintainer.description',
type: RECOMMENDATION_TYPES.ALERT,
};
}
return null;
} }
} }

View file

@ -0,0 +1,6 @@
export default {
ALERT: 'error',
WARNING: 'warning',
FACT: 'fact',
INFO: 'info',
};

View file

@ -1,96 +0,0 @@
class RecommendationsRender {
static list(recommendations, state) {
const list = (recommendations || []).filter(item => item);
const html = list.map(RecommendationsRender.item).join('');
const className = state.openRecommendations
? 'recommendations_full'
: 'recommendations_short';
const more = !state.openRecommendations && list.length > 5
? '<div class="recommendations_more" onclick="app.updateState({ openRecommendations: true });">»</div>'
: '';
return `
<div class="${className}">
${html}
${more}
</div>`;
}
static item(recommendation) {
const [title, subTitle] = RecommendationsRender.getTitleAndSubTitle(recommendation);
const className = {
info: 'recommendations_info',
fact: 'recommendations_fact',
warning: 'recommendations_warning',
error: 'recommendations_error',
}[recommendation[2] || ''] || '';
const description = RecommendationsRender.getFormattedDescription(recommendation[1] || '');
// const description = (recommendation[1] || '')
// .replace(/(#)([^#]*)(#)/gim, '<span style="color: #ED675F">$2</span>');
return `
<div class="recommendations_item ${className}">
<div class="recommendations_item_wrapper recommendations_item_wrapper_scroll">
<h5 class="recommendations_title">
<span class="recommendations_icon"></span>
${title}
${subTitle || ''}
</h5>
${description}
</div>
</div>`;
}
static getTitleAndSubTitle(recommendation) {
if (!Array.isArray(recommendation[0])) {
return [recommendation[0] || ''];
}
const firstTitle = recommendation[0][0] || '';
const count = recommendation[0].length;
if (count <= 1) return [firstTitle];
const mainTitle = `
${firstTitle}
<span class="recommendations_title_more">
+${count - 1}
</span>`;
const otherTitle = recommendation[0]
.slice(1, Infinity)
.join(', ');
const subTitle = `
<span class="recommendations_sub_title">
, ${otherTitle}
</span>`;
return [mainTitle, subTitle];
}
static getFormattedDescription(text) {
const className = 'recommendations_description';
const paragraphs = text.trim().split(/\n+/gm);
let prevPrefix = '';
let fullText = paragraphs.map((paragraph, index) => {
const prefix = paragraph.substring(0, 2);
let suffix = index === 1 ? `<div class="${className}_shortcut">` : '';
if (prevPrefix !== '- ' && prefix === '- ') suffix += `<ul class="${className}_list">`;
if (prevPrefix === '- ' && prefix !== '- ') suffix += '</ul>';
prevPrefix = prefix;
if (prefix === '- ') return `${suffix}<li class="${className}_item">${paragraph.substring(2)}</li>`;
if (prefix === '# ') return `${suffix}<h6 class="${className}_sub_title">${paragraph.substring(2)}</h6>`;
return `${suffix}<p class="${className}">${paragraph}</p>`;
}).join('');
return paragraphs.length > 1
? (fullText + '</div>')
: fullText;
}
}

61
src/ts/helpers/Title.ts Normal file
View file

@ -0,0 +1,61 @@
function getFormattedType(dataGrip: any): string {
const popularType = dataGrip.extension.statistic?.[0] || {};
const extension = popularType?.extension || '';
if ([
'js',
'ts',
'tsx',
'vue',
'css',
'less',
'scss',
'cjs',
'html',
].includes(extension)) {
return 'Front';
}
if ([
'swift',
].includes(extension)) {
return 'IOS';
}
if ([
'kt',
'php',
'perl',
'java',
].includes(extension)) {
const hasManifest = dataGrip.extension.statisticByName?.xml?.files?.AndroidManifest;
return hasManifest
? 'Android'
: 'Back';
}
if ([
'xml',
].includes(extension)) {
return 'Config';
}
return extension.toUpperCase();
}
export default function getTitle(dataGrip: any, commits: any) {
if (!commits.length) {
return 'Git статистика';
}
const type = getFormattedType(dataGrip) || '';
const task = dataGrip.pr.statistic?.[0]?.task || '';
const author = dataGrip.firstLastCommit.minData.author || '';
const year = commits?.[0]?.year || '';
const formattedTask = task.split('-').shift().toUpperCase() || '';
const formattedAuthor = author.split(' ').shift() || '';
const title = `${type} ${formattedTask} (${year}, ${formattedAuthor})`;
return `${title}. Git статистика`;
}

View file

@ -2,54 +2,54 @@ import ACHIEVEMENT_TYPE from './type';
export default { export default {
// готово // готово
commitsAfter1500: ['Сова', '70% коммитов после 15:00', ACHIEVEMENT_TYPE.NORMAL], commitsAfter1500: ACHIEVEMENT_TYPE.NORMAL, // Сова
commitsBefore1500: ['Ранняя пташка', '70% коммитов до обеда', ACHIEVEMENT_TYPE.NORMAL], commitsBefore1500: ACHIEVEMENT_TYPE.NORMAL, // Ранняя пташка
workEveryTime: ['Раб божий', 'есть коммит на каждый час суток', ACHIEVEMENT_TYPE.BAD], workEveryTime: ACHIEVEMENT_TYPE.BAD, // Раб божий
workNotWork: ['Стрельба холостыми', 'коммиты есть, а закрытых задач нет', ACHIEVEMENT_TYPE.BAD], workNotWork: ACHIEVEMENT_TYPE.BAD, // Стрельба холостыми
userNotWork: ['Залётный', 'это не его основной проект', ACHIEVEMENT_TYPE.NORMAL], userNotWork: ACHIEVEMENT_TYPE.NORMAL, // Залётный
userIsDied: ['Мёртвая душа', 'работал, но уволился', ACHIEVEMENT_TYPE.NORMAL], userIsDied: ACHIEVEMENT_TYPE.NORMAL, // Мёртвая душа
lessTasks: ['Зашел и вышел', 'меньше всего закрытых задач', ACHIEVEMENT_TYPE.BAD], lessTasks: ACHIEVEMENT_TYPE.BAD, // Зашел и вышел
moreTasks: ['Батя грит малаца', 'больше всего закрытых задач', ACHIEVEMENT_TYPE.GOOD], moreTasks: ACHIEVEMENT_TYPE.GOOD, // Батя грит малаца
everyMessageLong: ['Мастер красноречия', 'стабильно самые длинные подписи коммитов', ACHIEVEMENT_TYPE.NORMAL], everyMessageLong: ACHIEVEMENT_TYPE.NORMAL, // Мастер красноречия
everyMessageShort: ['Болтун находка для шпиона', 'стабильно, самые короткие подписи коммитов', ACHIEVEMENT_TYPE.BAD], everyMessageShort: ACHIEVEMENT_TYPE.BAD, // Болтун находка для шпиона
shortestName: ['Размер не главное', 'самое короткое имя', ACHIEVEMENT_TYPE.NORMAL], // нет картинки shortestName: ACHIEVEMENT_TYPE.NORMAL, // Размер не главное // нет картинки
longestName: ['Азим Азиз Иль Ам Кадир Имран II', 'самое длинное имя', ACHIEVEMENT_TYPE.NORMAL], longestName: ACHIEVEMENT_TYPE.NORMAL, // Азим Азиз Иль Ам Кадир Имран II
moreCommits: ['Мастер бекапов', 'больше всего коммитов', ACHIEVEMENT_TYPE.NORMAL], moreCommits: ACHIEVEMENT_TYPE.NORMAL, // Мастер бекапов
lessCommits: ['Редко но метко', 'меньше всего коммитов', ACHIEVEMENT_TYPE.BAD], lessCommits: ACHIEVEMENT_TYPE.BAD, // Редко но метко
oneCommitOneTask: ['Точно в цель', 'в среднем один коммит на задачу', ACHIEVEMENT_TYPE.NORMAL], oneCommitOneTask: ACHIEVEMENT_TYPE.NORMAL, // Точно в цель
moreLazyDays: ['Мысленно я с вами', 'больше всего дней без коммитов', ACHIEVEMENT_TYPE.BAD], moreLazyDays: ACHIEVEMENT_TYPE.BAD, // Мысленно я с вами
lessLazyDays: ['Папа Карло', 'меньше всего дней без коммитов', ACHIEVEMENT_TYPE.GOOD], lessLazyDays: ACHIEVEMENT_TYPE.GOOD, // Папа Карло
zeroLazyDays: ['Ни единого разрыва', 'ни одного дня без коммитов', ACHIEVEMENT_TYPE.GOOD], zeroLazyDays: ACHIEVEMENT_TYPE.GOOD, // Ни единого разрыва
moreWorkDays: ['Ценный работник', 'больше всего рабочих дней', ACHIEVEMENT_TYPE.GOOD], moreWorkDays: ACHIEVEMENT_TYPE.GOOD, // Ценный работник
moreScopes: ['Стартапер', 'сделал больше всего фичей', ACHIEVEMENT_TYPE.GOOD], // нет картинки moreScopes: ACHIEVEMENT_TYPE.GOOD, // Стартапер // нет картинки
lessScopes: ['Щегол', 'сделал меньше всего фичей', ACHIEVEMENT_TYPE.BAD], lessScopes: ACHIEVEMENT_TYPE.BAD, // Щегол
moreDaysForTask: ['Улитка на склоне', 'работа по задачам идёт медленнее чем у остальных', ACHIEVEMENT_TYPE.BAD], moreDaysForTask: ACHIEVEMENT_TYPE.BAD, // Улитка на склоне
more2DaysForTask: ['Cо слоу', 'больше двух дней на задачу', ACHIEVEMENT_TYPE.BAD], more2DaysForTask: ACHIEVEMENT_TYPE.BAD, // Cо слоу
moreDaysInProject: ['Старожил', 'больше всего дней на проекте', ACHIEVEMENT_TYPE.GOOD], moreDaysInProject: ACHIEVEMENT_TYPE.GOOD, // Старожил
lessDaysInProject: ['А это кто?', 'меньше всего дней на проекте', ACHIEVEMENT_TYPE.NORMAL], lessDaysInProject: ACHIEVEMENT_TYPE.NORMAL, // А это кто?
more90DaysInProject: ['Добро пожаловать', 'не уволили на испытательном', ACHIEVEMENT_TYPE.GOOD], more90DaysInProject: ACHIEVEMENT_TYPE.GOOD, // Добро пожаловать
lessDaysForTask: ['Скорострел', 'одна задача занимает меньше дня', ACHIEVEMENT_TYPE.GOOD], lessDaysForTask: ACHIEVEMENT_TYPE.GOOD, // Скорострел
adam: ['Адам', 'первый стабильный сотрудник на проекте', ACHIEVEMENT_TYPE.NORMAL], adam: ACHIEVEMENT_TYPE.NORMAL, // Адам
more666DaysInProject: ['Чёрт', 'отработал 666 дней на проекте', ACHIEVEMENT_TYPE.GOOD], more666DaysInProject: ACHIEVEMENT_TYPE.GOOD, // Чёрт
more777DaysInProject: ['Азино 3 топора', 'отработал 777 дней на проекте', ACHIEVEMENT_TYPE.GOOD], more777DaysInProject: ACHIEVEMENT_TYPE.GOOD, // Азино 3 топора
moreRefactoring: ['Выпускающий редактор', 'сделал больше всех меток «рефакторинг»', ACHIEVEMENT_TYPE.GOOD], moreRefactoring: ACHIEVEMENT_TYPE.GOOD, // Выпускающий редактор
// нет картинки // нет картинки
longestMessage: ['А разговоров то было...', 'самая длинная подпись коммита за все время', ACHIEVEMENT_TYPE.NORMAL], longestMessage: ACHIEVEMENT_TYPE.NORMAL, // А разговоров то было...
moreTasksInDay: ['Спиди-гонщик', 'рекорд по количеству закрытых задач в день', ACHIEVEMENT_TYPE.GOOD], moreTasksInDay: ACHIEVEMENT_TYPE.GOOD, // Спиди-гонщик
hasCommitFrom0to7: ['Ночной дозор', 'есть коммит на каждый час ночи', ACHIEVEMENT_TYPE.BAD], hasCommitFrom0to7: ACHIEVEMENT_TYPE.BAD, // Ночной дозор
noCommitOnDay: ['Технический перерыв', 'есть определенный час и день в рабочее время в который никогда не комитит', ACHIEVEMENT_TYPE.NORMAL], noCommitOnDay: ACHIEVEMENT_TYPE.NORMAL, // Технический перерыв
hasCommitEveryTime: ['Умер на работе', 'есть коммит на час каждого дня (включая выходные)', ACHIEVEMENT_TYPE.BAD], hasCommitEveryTime: ACHIEVEMENT_TYPE.BAD, // Умер на работе
commitsAfter1800: ['Делу время', 'нет ни одного коммита после 18:00', ACHIEVEMENT_TYPE.GOOD], commitsAfter1800: ACHIEVEMENT_TYPE.GOOD, // Делу время
more1488DaysInProject: ['им. Максима Марцинкевича', 'отработал 1488 дней на проекте', ACHIEVEMENT_TYPE.GOOD], more1488DaysInProject: ACHIEVEMENT_TYPE.GOOD, // им. Максима Марцинкевича
taskNumber300: ['Знаком с трактористом', 'первый взял в работу задачу с номером 300', ACHIEVEMENT_TYPE.NORMAL], taskNumber300: ACHIEVEMENT_TYPE.NORMAL, // Знаком с трактористом
// нет кода // нет кода
// moreFix: ['Bug hunter', 'больше всего закрытых багов', ACHIEVEMENT_TYPE.GOOD], // moreFix: ACHIEVEMENT_TYPE.GOOD, // Bug hunter
lessWorkDays: ['Дальше без меня', 'меньше всего рабочих дней', ACHIEVEMENT_TYPE.BAD], lessWorkDays: ACHIEVEMENT_TYPE.BAD, // Дальше без меня
moreCreateCode: ['Созидатель', 'склонен больше остальных добавлять код', ACHIEVEMENT_TYPE.NORMAL], moreCreateCode: ACHIEVEMENT_TYPE.NORMAL, // Созидатель
moreRemoveCode: ['Разрушитель', 'склонен больше остальных удалять код', ACHIEVEMENT_TYPE.NORMAL], moreRemoveCode: ACHIEVEMENT_TYPE.NORMAL, // Разрушитель
moreChangeCode: ['Реформатор', 'склонен больше остальных изменять код', ACHIEVEMENT_TYPE.NORMAL], // есть картинка moreChangeCode: ACHIEVEMENT_TYPE.NORMAL, // Реформатор // есть картинка
moreStyle: ['Полиция моды', 'склонен больше остальных изменять CSS', ACHIEVEMENT_TYPE.GOOD], moreStyle: ACHIEVEMENT_TYPE.GOOD, // Полиция моды
moreOnHoliday: ['Нет жизни', 'относительно много коммитов в нерабочее время', ACHIEVEMENT_TYPE.BAD], moreOnHoliday: ACHIEVEMENT_TYPE.BAD, // Нет жизни
}; };

View file

@ -1,5 +1,5 @@
export default { export default {
GOOD: 0, GOOD: 1,
NORMAL: 1, NORMAL: 2,
BAD: 2, BAD: 3,
}; };

View file

@ -26,7 +26,6 @@ const Success = observer((): React.ReactElement => {
onChange={(type: string, data: any[]) => { onChange={(type: string, data: any[]) => {
setShowSplashScreen(false); setShowSplashScreen(false);
if (type === 'dump') dataGripStore.setCommits(data); if (type === 'dump') dataGripStore.setCommits(data);
if (type === 'telegramm') dataGripStore.setTelegrammMessages(data);
setTimeout(() => { setTimeout(() => {
setShowSplashScreen(true); setShowSplashScreen(true);
}); });

View file

@ -54,10 +54,11 @@ function Commits({ statistic }: ICommitsProps) {
<br/> <br/>
<br/> <br/>
{} {}
<Title title={localization.get('page.common.commits.title2', [ <Title title={localization.get(
'page.common.commits.title2',
getDate(selected?.timestamp), getDate(selected?.timestamp),
selected?.commits, selected?.commits,
])} /> )} />
<PageWrapper template="box"> <PageWrapper template="box">
<DayInfo <DayInfo
day={selected} day={selected}

View file

@ -0,0 +1,32 @@
import React from 'react';
import Title from 'ts/components/Title';
import localization from 'ts/helpers/Localization';
import style from '../styles/table-of-contents.module.scss';
interface ITableOfContents {
titles?: string[];
}
function TableOfContents({ titles }: ITableOfContents) {
const items = (titles || []).map((title) => (
<li
key={title}
className={style.table_of_contents_item}
>
{localization.get(title || '')}
</li>
));
return (
<>
<Title title="page.print.tableOfContents" />
<ul className={style.table_of_contents}>
{items}
</ul>
</>
);
}
export default TableOfContents;

View file

@ -0,0 +1,21 @@
@import '../../../../styles/variables';
.table_of_contents {
margin-bottom: var(--space-xxl);
&_item {
position: relative;
padding: 0 0 0 22px;
margin: var(--space-m) 0;
list-style-type: none;
&:before {
position: absolute;
top: 0;
left: 0;
display: block;
content: "";
margin-right: 8px;
}
}
}

View file

@ -17,7 +17,7 @@ const Print = observer(() => {
}}> }}>
<Header> <Header>
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
{localization.get('page.print.title')} {localization.get('page.print.modal.title')}
</div> </div>
</Header> </Header>
<Body> <Body>
@ -31,7 +31,7 @@ const Print = observer(() => {
printStore.printPage(); printStore.printPage();
}} }}
> >
{localization.get('page.print.page')} {localization.get('page.print.modal.page')}
</UiKitButton> </UiKitButton>
<UiKitButton <UiKitButton
className={style.page_wrapper_print_button} className={style.page_wrapper_print_button}
@ -39,7 +39,7 @@ const Print = observer(() => {
printStore.printSection(); printStore.printSection();
}} }}
> >
{localization.get('page.print.type')} {localization.get('page.print.modal.type')}
</UiKitButton> </UiKitButton>
{false && ( {false && (
<UiKitButton <UiKitButton
@ -48,7 +48,7 @@ const Print = observer(() => {
printStore.printAllPages(); printStore.printAllPages();
}} }}
> >
{localization.get('page.print.all')} {localization.get('page.print.modal.all')}
</UiKitButton> </UiKitButton>
)} )}
<UiKitButton <UiKitButton
@ -58,7 +58,7 @@ const Print = observer(() => {
printStore.close(); printStore.close();
}} }}
> >
{localization.get('page.print.cancel')} {localization.get('page.print.modal.cancel')}
</UiKitButton> </UiKitButton>
</Body> </Body>
</Modal> </Modal>

View file

@ -1,7 +1,12 @@
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import Title from 'ts/components/Title';
import Description from 'ts/components/Description';
import PageBreak from 'ts/pages/Common/components/PageBreak'; import PageBreak from 'ts/pages/Common/components/PageBreak';
import TableOfContents from 'ts/pages/Common/components/TableOfContents';
import localization from 'ts/helpers/Localization';
import Hours from './Hours'; import Hours from './Hours';
import Money from './Money'; import Money from './Money';
@ -15,6 +20,19 @@ import Month from './Month';
const Print = observer((): React.ReactElement => { const Print = observer((): React.ReactElement => {
return ( return (
<> <>
<Title title={localization.get('page.print.title', document.title)} />
<Description text={localization.get('page.print.description')} />
<br />
<TableOfContents titles={[
'page.team.total.titleA',
'page.person.speed.task',
'page.person.speed.max',
'page.team.total.titleB',
'page.person.achievement.title',
'page.person.hours.title',
'page.common.words.title',
]}/>
<PageBreak/>
<Total/> <Total/>
<Speed/> <Speed/>
<Money/> <Money/>
@ -25,7 +43,6 @@ const Print = observer((): React.ReactElement => {
<Week mode="print"/> <Week mode="print"/>
<PageBreak/> <PageBreak/>
<Month/> <Month/>
<Hours/>
<PopularWords mode="print"/> <PopularWords mode="print"/>
</> </>
); );

View file

@ -1,4 +1,4 @@
import React from 'react'; import React, { useState } from 'react';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import InputString from 'ts/components/UiKit/components/InputString'; import InputString from 'ts/components/UiKit/components/InputString';
@ -8,27 +8,32 @@ import Title from 'ts/components/Title';
import localization from 'ts/helpers/Localization'; import localization from 'ts/helpers/Localization';
const Common = observer((): React.ReactElement | null => { const Common = observer((): React.ReactElement | null => {
const [title, setTitle] = useState<string>(document.title);
const [language, setLanguage] = useState<string>(localization.language);
return ( return (
<> <>
<Title title="page.settings.document.title"/> <Title title="page.settings.document.title"/>
<PageBox> <PageBox>
<InputString <InputString
title="page.settings.document.name" title="page.settings.document.name"
value={document.title} value={title}
placeholder="Git статистика" placeholder="Git статистика"
onChange={(value: string) => { onChange={(value: string) => {
setTitle(value);
document.title = value || 'Git статистика'; document.title = value || 'Git статистика';
}} }}
/> />
<Select <Select
title="page.settings.document.language" title="page.settings.document.language"
value={localization.language} value={language}
options={[ options={[
{ id: 'ru', title: 'Русский' }, { id: 'ru', title: 'Русский' },
{ id: 'en', title: 'English' }, { id: 'en', title: 'English' },
]} ]}
onChange={(value: string) => { onChange={(item: any, id: string) => {
localization.language = value; localization.language = id;
setLanguage(id);
}} }}
/> />
</PageBox> </PageBox>

View file

@ -0,0 +1,36 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import InputString from 'ts/components/UiKit/components/InputString';
import PageBox from 'ts/components/Page/Box';
import Title from 'ts/components/Title';
import formStore from '../store/Form';
const Prefixes = observer((): React.ReactElement | null => {
return (
<>
<Title title="page.settings.links.title"/>
<PageBox>
<InputString
title="page.settings.links.task"
value={formStore.state?.linksPrefix?.task}
placeholder="https://jira.com/secure/RapidBoard.jspa?task="
onChange={(value: string) => {
formStore.updateState('linksPrefix.task', value);
}}
/>
<InputString
title="page.settings.links.pr"
value={formStore.state.linksPrefix.pr}
placeholder="https://bitbucket.com/projects/assayo/repos/frontend/pull-requests/"
onChange={(value: string) => {
formStore.updateState('linksPrefix.pr', value);
}}
/>
</PageBox>
</>
);
});
export default Prefixes;

View file

@ -16,7 +16,7 @@ import getFakeLoader from 'ts/components/DataLoader/helpers/formatter';
import localization from 'ts/helpers/Localization'; import localization from 'ts/helpers/Localization';
import NothingFound from 'ts/components/NothingFound'; import NothingFound from 'ts/components/NothingFound';
import Title from 'ts/components/Title'; import Title from 'ts/components/Title';
import Table from 'ts/components/Table'; import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column'; import Column from 'ts/components/Table/components/Column';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column'; import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import LineChart from 'ts/components/LineChart'; import LineChart from 'ts/components/LineChart';
@ -42,7 +42,7 @@ function AuthorView({ response, updateSort }: IAuthorViewProps) {
const typeChart = getOptions({ order: dataGripStore.dataGrip.type.list }); const typeChart = getOptions({ order: dataGripStore.dataGrip.type.list });
return ( return (
<Table <DataView
rows={response.content} rows={response.content}
sort={response.sort} sort={response.sort}
updateSort={updateSort} updateSort={updateSort}
@ -142,7 +142,7 @@ function AuthorView({ response, updateSort }: IAuthorViewProps) {
properties="moneyLosses" properties="moneyLosses"
formatter={getMoney} formatter={getMoney}
/> />
</Table> </DataView>
); );
} }

View file

@ -4,7 +4,7 @@ import { IPagination } from 'ts/interfaces/Pagination';
import dataGripStore from 'ts/store/DataGrip'; import dataGripStore from 'ts/store/DataGrip';
import userSettings from 'ts/store/UserSettings'; import userSettings from 'ts/store/UserSettings';
import Table from 'ts/components/Table'; import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column'; import Column from 'ts/components/Table/components/Column';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column'; import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import ExternalLink from 'ts/components/ExternalLink'; import ExternalLink from 'ts/components/ExternalLink';
@ -35,7 +35,7 @@ function AllPR({
}); });
return ( return (
<Table <DataView
rows={response.content} rows={response.content}
sort={response.sort} sort={response.sort}
updateSort={updateSort} updateSort={updateSort}
@ -151,7 +151,7 @@ function AllPR({
properties="author" properties="author"
width={250} width={250}
/> />
</Table> </DataView>
); );
} }

View file

@ -2,7 +2,7 @@ import React from 'react';
import { IPagination } from 'ts/interfaces/Pagination'; import { IPagination } from 'ts/interfaces/Pagination';
import Table from 'ts/components/Table'; import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column'; import Column from 'ts/components/Table/components/Column';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column'; import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import LineChart from 'ts/components/LineChart'; import LineChart from 'ts/components/LineChart';
@ -38,7 +38,7 @@ function Authors({ response, updateSort }: IAuthorsProps) {
}); });
return ( return (
<Table <DataView
rows={response.content} rows={response.content}
sort={response.sort} sort={response.sort}
updateSort={updateSort} updateSort={updateSort}
@ -89,7 +89,7 @@ function Authors({ response, updateSort }: IAuthorsProps) {
/> />
)} )}
/> />
</Table> </DataView>
); );
} }

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import Table from 'ts/components/Table'; import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column'; import Column from 'ts/components/Table/components/Column';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column'; import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import LineChart from 'ts/components/LineChart'; import LineChart from 'ts/components/LineChart';
@ -40,7 +40,7 @@ function Total() {
]; ];
return ( return (
<Table rows={rows}> <DataView rows={rows}>
<Column <Column
title="page.team.pr.workDays" title="page.team.pr.workDays"
properties="workDays" properties="workDays"
@ -79,7 +79,7 @@ function Total() {
/> />
)} )}
/> />
</Table> </DataView>
); );
} }

View file

@ -1,8 +1,13 @@
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import Title from 'ts/components/Title';
import Description from 'ts/components/Description';
import TableOfContents from 'ts/pages/Common/components/TableOfContents';
import PageBreak from 'ts/pages/Common/components/PageBreak'; import PageBreak from 'ts/pages/Common/components/PageBreak';
import localization from 'ts/helpers/Localization';
import Author from './Author'; import Author from './Author';
import Hours from './Hours'; import Hours from './Hours';
import PopularWords from './PopularWords'; import PopularWords from './PopularWords';
@ -16,6 +21,21 @@ import Pr from './PR';
const Print = observer((): React.ReactElement => { const Print = observer((): React.ReactElement => {
return ( return (
<> <>
<Title title={localization.get('page.print.title', document.title)} />
<Description text={localization.get('page.print.description')} />
<br />
<TableOfContents titles={[
'page.team.total.titleA',
'page.team.total.titleB',
'page.team.scope.title',
'page.team.author.title',
'page.team.type.title',
'page.team.pr.oneTaskDays',
'page.team.pr.statByAuthors',
'page.team.pr.longDelay',
'page.team.hours.title',
'page.common.words.title',
]}/>
<Total/> <Total/>
<PageBreak/> <PageBreak/>
<Scope mode="print"/> <Scope mode="print"/>

View file

@ -12,7 +12,8 @@ import Pagination from 'ts/components/DataLoader/components/Pagination';
import getFakeLoader from 'ts/components/DataLoader/helpers/formatter'; import getFakeLoader from 'ts/components/DataLoader/helpers/formatter';
import NothingFound from 'ts/components/NothingFound'; import NothingFound from 'ts/components/NothingFound';
import Title from 'ts/components/Title'; import Title from 'ts/components/Title';
import Table from 'ts/components/Table'; // import Table from 'ts/components/Table';
import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column'; import Column from 'ts/components/Table/components/Column';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column'; import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import LineChart from 'ts/components/LineChart'; import LineChart from 'ts/components/LineChart';
@ -29,7 +30,7 @@ function ScopeView({ response }: IScopeViewProps) {
const authorChart = getOptions({ order: dataGripStore.dataGrip.author.list }); const authorChart = getOptions({ order: dataGripStore.dataGrip.author.list });
return ( return (
<Table rows={response.content}> <DataView rows={response.content}>
<Column <Column
isFixed isFixed
template={ColumnTypesEnum.STRING} template={ColumnTypesEnum.STRING}
@ -96,7 +97,7 @@ function ScopeView({ response }: IScopeViewProps) {
properties="cost" properties="cost"
formatter={getMoney} formatter={getMoney}
/> />
</Table> </DataView>
); );
} }

View file

@ -14,7 +14,7 @@ import Tv100And1 from 'ts/components/Tv100And1';
import ACHIEVEMENT_TYPE from 'ts/helpers/achievement/constants/type'; import ACHIEVEMENT_TYPE from 'ts/helpers/achievement/constants/type';
import getAchievementByAuthor from 'ts/helpers/achievement/byAuthor'; import getAchievementByAuthor from 'ts/helpers/achievement/byAuthor';
import Description from 'ts/components/Description'; import Description from 'ts/components/Description';
import Table from 'ts/components/Table'; import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column'; import Column from 'ts/components/Table/components/Column';
import LineChart from 'ts/components/LineChart'; import LineChart from 'ts/components/LineChart';
import getOptions from 'ts/components/LineChart/helpers/getOptions'; import getOptions from 'ts/components/LineChart/helpers/getOptions';
@ -85,7 +85,7 @@ const Top = observer((): React.ReactElement => {
<Title title="Максимальная длинна подписи коммита"/> <Title title="Максимальная длинна подписи коммита"/>
<PageWrapper template="table"> <PageWrapper template="table">
<Table rows={maxMessageLength}> <DataView rows={maxMessageLength}>
<Column <Column
isFixed isFixed
template={ColumnTypesEnum.STRING} template={ColumnTypesEnum.STRING}
@ -108,7 +108,7 @@ const Top = observer((): React.ReactElement => {
/> />
)} )}
/> />
</Table> </DataView>
</PageWrapper> </PageWrapper>
<Tv100And1 rows={maxMessageLength} /> <Tv100And1 rows={maxMessageLength} />

View file

@ -13,7 +13,7 @@ import Pagination from 'ts/components/DataLoader/components/Pagination';
import getFakeLoader from 'ts/components/DataLoader/helpers/formatter'; import getFakeLoader from 'ts/components/DataLoader/helpers/formatter';
import NothingFound from 'ts/components/NothingFound'; import NothingFound from 'ts/components/NothingFound';
import Title from 'ts/components/Title'; import Title from 'ts/components/Title';
import Table from 'ts/components/Table'; import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column'; import Column from 'ts/components/Table/components/Column';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column'; import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import LineChart from 'ts/components/LineChart'; import LineChart from 'ts/components/LineChart';
@ -37,7 +37,7 @@ function TypeView({ response, updateSort }: ITypeViewProps) {
const authorChart = getOptions({ order: dataGripStore.dataGrip.author.list }); const authorChart = getOptions({ order: dataGripStore.dataGrip.author.list });
return ( return (
<Table <DataView
rows={response.content} rows={response.content}
sort={response.sort} sort={response.sort}
updateSort={updateSort} updateSort={updateSort}
@ -102,7 +102,7 @@ function TypeView({ response, updateSort }: ITypeViewProps) {
)} )}
minWidth={500} minWidth={500}
/> />
</Table> </DataView>
); );
} }

View file

@ -13,7 +13,7 @@ import DataLoader from 'ts/components/DataLoader';
import Pagination from 'ts/components/DataLoader/components/Pagination'; import Pagination from 'ts/components/DataLoader/components/Pagination';
import getFakeLoader from 'ts/components/DataLoader/helpers/formatter'; import getFakeLoader from 'ts/components/DataLoader/helpers/formatter';
import NothingFound from 'ts/components/NothingFound'; import NothingFound from 'ts/components/NothingFound';
import Table from 'ts/components/Table'; import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column'; import Column from 'ts/components/Table/components/Column';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column'; import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import LineChart from 'ts/components/LineChart'; import LineChart from 'ts/components/LineChart';
@ -44,7 +44,7 @@ function WeekView({ response, updateSort }: IWeekViewProps) {
const workDaysChart = getOptions({ order: dataGripStore.dataGrip.author.list, suffix: 'page.team.week.days' }); const workDaysChart = getOptions({ order: dataGripStore.dataGrip.author.list, suffix: 'page.team.week.days' });
return ( return (
<Table <DataView
rows={response.content} rows={response.content}
sort={response.sort} sort={response.sort}
updateSort={updateSort} updateSort={updateSort}
@ -141,7 +141,7 @@ function WeekView({ response, updateSort }: IWeekViewProps) {
}} }}
minWidth={200} minWidth={200}
/> />
</Table> </DataView>
); );
} }

View file

@ -2,8 +2,12 @@ import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import Console from 'ts/components/Console'; import Console from 'ts/components/Console';
import {
getStringFromFileList,
getStringsForParser,
} from 'ts/components/DropZone/helpers';
import localization from 'ts/helpers/Localization'; import localization from 'ts/helpers/Localization';
import Description from 'ts/components/Description'; import dataGripStore from 'ts/store/DataGrip';
import style from './styles/index.module.scss'; import style from './styles/index.module.scss';
@ -61,7 +65,24 @@ function Welcome() {
{localization.get('page.welcome.description2')} {localization.get('page.welcome.description2')}
</p> </p>
<h2 className={style.welcome_last_title}> <h2 className={style.welcome_last_title}>
{localization.get('page.welcome.step2')} {localization.get('page.welcome.step2') === 'page.welcome.step2'
? ''
: localization.get('page.welcome.step2')}
<label className={style.welcome_title_link}>
{localization.get('page.welcome.step3')}
<input
multiple
type="file"
style={{ display: 'none' }}
onChange={async (event: any) => {
const files = Array.from(event.target.files);
const text = await getStringFromFileList(files);
const report = getStringsForParser(text);
dataGripStore.setCommits(report);
}}
/>
</label>
{localization.get('page.welcome.step4')}
</h2> </h2>
</div> </div>
</section> </section>

View file

@ -50,22 +50,6 @@
} }
} }
&_first_title,
&_last_title {
font-size: 42px;
font-weight: 100;
margin: 46px auto;
padding: 0;
}
&_first_title {
margin-top: 0;
}
&_last_title {
margin-bottom: 0;
}
&_link, &_link,
&_description { &_description {
font-size: var(--font-xs); font-size: var(--font-xs);
@ -87,6 +71,28 @@
margin: 16px 4px 0 4px; margin: 16px 4px 0 4px;
text-decoration: underline; text-decoration: underline;
} }
&_first_title,
&_last_title {
font-size: 42px;
font-weight: 100;
margin: 46px auto;
padding: 0;
}
&_first_title {
margin-top: 0;
}
&_last_title {
margin-bottom: 0;
}
&_title_link {
margin: 0 12px;
text-decoration: underline;
cursor: pointer;
}
} }
@media (max-width: 800px) { @media (max-width: 800px) {

View file

@ -6,7 +6,6 @@ import achievements from 'ts/helpers/achievement/byCompetition';
import dataGrip from 'ts/helpers/DataGrip'; import dataGrip from 'ts/helpers/DataGrip';
import getFileTreeWithStatistic from 'ts/helpers/DataGrip/helpers/tree'; import getFileTreeWithStatistic from 'ts/helpers/DataGrip/helpers/tree';
import Parser from 'ts/helpers/Parser'; import Parser from 'ts/helpers/Parser';
import ParserTelegramm from 'ts/helpers/ParserTelegramm';
import { setDefaultValues } from 'ts/pages/Settings/helpers/getEmptySettings'; import { setDefaultValues } from 'ts/pages/Settings/helpers/getEmptySettings';
import getTitle from 'ts/helpers/Title'; import getTitle from 'ts/helpers/Title';
@ -36,15 +35,12 @@ class DataGripStore implements IDataGripStore {
dataGrip: observable, dataGrip: observable,
showApplication: observable, showApplication: observable,
setCommits: action, setCommits: action,
setTelegrammMessages: action,
}); });
} }
setCommits(dump?: string[], type?: string) { setCommits(dump?: string[]) {
dataGrip.clear(); dataGrip.clear();
const parser = type === 'telegramm' const parser = Parser;
? ParserTelegramm
: Parser;
const { const {
commits, commits,
@ -77,10 +73,6 @@ class DataGripStore implements IDataGripStore {
document.title = getTitle(this.dataGrip, this.commits); document.title = getTitle(this.dataGrip, this.commits);
} }
setTelegrammMessages(dump?: any[]) {
return this.setCommits(dump, 'telegramm');
}
updateChars() { // todo: remove, never use updateChars() { // todo: remove, never use
console.log('need update data TODO'); console.log('need update data TODO');
dataGrip.updateByFilters(); dataGrip.updateByFilters();