TEST-1234 test(test): test
17
build/asset-manifest.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
3
build/assets/icons/Cards.svg
Normal 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 |
3
build/assets/icons/Table.svg
Normal 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
|
@ -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>
|
2
build/static/css/main.bd89a1c5.css
Normal file
1
build/static/css/main.bd89a1c5.css.map
Normal file
3
build/static/js/main.b114e843.js
Normal file
74
build/static/js/main.b114e843.js.LICENSE.txt
Normal 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
|
||||||
|
*/
|
1
build/static/js/main.b114e843.js.map
Normal file
1
build/static/media/alert.41e2b99c481139c13074.svg
Normal 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 |
BIN
build/static/media/car.b8dd8738e37fe866285f.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
1
build/static/media/info.954631f6b19e3fe9c495.svg
Normal 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 |
1
build/static/media/warning.e39a87773603f3ab157f.svg
Normal 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 |
3
public/assets/icons/Cards.svg
Normal 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 |
3
public/assets/icons/Table.svg
Normal 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 |
|
@ -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 || '')
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
67
src/ts/components/Cards/components/Card.tsx
Normal 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;
|
44
src/ts/components/Cards/components/Line.tsx
Normal 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;
|
38
src/ts/components/Cards/components/Title.tsx
Normal 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;
|
41
src/ts/components/Cards/helpers/getCardConfigs.ts
Normal 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;
|
47
src/ts/components/Cards/index.tsx
Normal 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;
|
84
src/ts/components/Cards/styles/index.module.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
16
src/ts/components/DataView/index.module.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
84
src/ts/components/DataView/index.tsx
Normal 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;
|
|
@ -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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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' }}
|
||||||
|
|
|
@ -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 ?? ''}`;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
747
src/ts/config/translations/en.ts
Normal 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: «Взять гет» в данном случае означает первым оставить коммит к задаче с «красивым» номером.
|
||||||
|
§ 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 {};
|
|
@ -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: «Взять гет» в данном случае означает первым оставить коммит к задаче с «красивым» номером.
|
§ page.person.gets.description: «Взять гет» в данном случае означает первым оставить коммит к задаче с «красивым» номером.
|
||||||
§ 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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,13 +7,43 @@ 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;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
@ -20,13 +51,27 @@ export default class RecommendationsPersonByTimestamp {
|
||||||
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\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\nДень недели: ${day}`, 'fact'];
|
return {
|
||||||
|
title: date,
|
||||||
|
description: 'recommendations.timestamp.lastCommit.description',
|
||||||
|
type: RECOMMENDATION_TYPES.FACT,
|
||||||
|
arguments: {
|
||||||
|
description: [day],
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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\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\nДень недели: ${day}`, 'fact'];
|
|
||||||
|
return {
|
||||||
|
title: date,
|
||||||
|
description: 'recommendations.timestamp.lastCommit.description',
|
||||||
|
type: RECOMMENDATION_TYPES.FACT,
|
||||||
|
arguments: {
|
||||||
|
description: [day],
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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- ')}`],
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
6
src/ts/helpers/Recommendations/contstants.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
ALERT: 'error',
|
||||||
|
WARNING: 'warning',
|
||||||
|
FACT: 'fact',
|
||||||
|
INFO: 'info',
|
||||||
|
};
|
|
@ -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
|
@ -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 статистика`;
|
||||||
|
}
|
|
@ -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, // Нет жизни
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export default {
|
export default {
|
||||||
GOOD: 0,
|
GOOD: 1,
|
||||||
NORMAL: 1,
|
NORMAL: 2,
|
||||||
BAD: 2,
|
BAD: 3,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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}
|
||||||
|
|
32
src/ts/pages/Common/components/TableOfContents.tsx
Normal 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;
|
21
src/ts/pages/Common/styles/table-of-contents.module.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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"/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
36
src/ts/pages/Settings/components/Prefixes.tsx
Normal 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;
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
|
|