TASK-0000 feat(main): update all project on github

This commit is contained in:
bakhirev 2023-09-14 10:14:45 +03:00
parent 42a6fbf363
commit 29e548b154
94 changed files with 5409 additions and 5640 deletions

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1 +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>ASSAYO</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.16986165.js"></script><link href="./static/css/main.a871f7d5.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html> <!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>ASSAYO</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.0fa5ec0a.js"></script><link href="./static/css/main.c2f3798a.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script type="text/javascript">!function(e,t,c,n,r,a,s){e[r]=e[r]||function(){(e[r].a=e[r].a||[]).push(arguments)},e[r].l=1*new Date;for(var i=0;i<document.scripts.length;i++)if(document.scripts[i].src===n)return;a=t.createElement(c),s=t.getElementsByTagName(c)[0],a.async=1,a.src=n,s.parentNode.insertBefore(a,s)}(window,document,"script","https://mc.yandex.ru/metrika/tag.js","ym"),ym(94903985,"init",{clickmap:!0,trackLinks:!0,accurateTrackBounce:!0,webvisor:!0})</script><noscript><div><img src="https://mc.yandex.ru/watch/94903985" style="position:absolute;left:-9999px" alt=""/></div></noscript></body></html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

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

File diff suppressed because one or more lines are too long

16
package-lock.json generated
View file

@ -5699,9 +5699,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001449", "version": "1.0.30001519",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001449.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz",
"integrity": "sha512-CPB+UL9XMT/Av+pJxCKGhdx+yg1hzplvFJQlJ2n68PyQGMz9L/E2zCyLdOL8uasbouTUgnPl+y0tccI/se+BEw==", "integrity": "sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@ -5710,6 +5710,10 @@
{ {
"type": "tidelift", "type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite" "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
} }
] ]
}, },
@ -22205,9 +22209,9 @@
} }
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001449", "version": "1.0.30001519",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001449.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz",
"integrity": "sha512-CPB+UL9XMT/Av+pJxCKGhdx+yg1hzplvFJQlJ2n68PyQGMz9L/E2zCyLdOL8uasbouTUgnPl+y0tccI/se+BEw==" "integrity": "sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg=="
}, },
"case-sensitive-paths-webpack-plugin": { "case-sensitive-paths-webpack-plugin": {
"version": "2.4.0", "version": "2.4.0",

File diff suppressed because it is too large Load diff

View file

@ -63,5 +63,23 @@
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<!-- Yandex.Metrika counter -->
<script type="text/javascript" >
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym(94903985, "init", {
clickmap:true,
trackLinks:true,
accurateTrackBounce:true,
webvisor:true
});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/94903985" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body> </body>
</html> </html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -2,8 +2,11 @@ import React from 'react';
import { HashRouter } from 'react-router-dom'; 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 Authorization from './ts/pages/Authorization'; import Authorization from 'ts/pages/Authorization';
import userSettings from 'ts/store/UserSettings';
import Notifications from 'ts/components/Notifications';
import './styles/index.scss'; import './styles/index.scss';
// eslint-disable-next-line // eslint-disable-next-line
@ -31,6 +34,7 @@ function renderReactApplication() {
<React.StrictMode> <React.StrictMode>
<HashRouter> <HashRouter>
<Authorization/> <Authorization/>
<Notifications/>
</HashRouter> </HashRouter>
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root'), document.getElementById('root'),
@ -55,4 +59,6 @@ function loadApplication() {
document.body.appendChild(script); document.body.appendChild(script);
} }
userSettings.loadUserSettings().then(() => {
loadApplication(); loadApplication();
});

View file

@ -1,16 +1,34 @@
import { ISetting } from 'ts/pages/Settings/interfaces/Setting'; import { IUserSetting } from 'ts/interfaces/UserSetting';
import getEmptySettings from 'ts/pages/Settings/helpers/getEmptySettings'; import getEmptySettings from 'ts/pages/Settings/helpers/getEmptySettings';
export default { export default {
loadSettings(): Promise<ISetting> { loadSettings(): Promise<IUserSetting> {
const defaultSettings = getEmptySettings();
const response = localStorage.getItem('settings'); const response = localStorage.getItem('settings');
return response const defaultResponse = () => {
? Promise.resolve(JSON.parse(response)) localStorage.removeItem('settings');
: Promise.resolve(getEmptySettings()); return Promise.resolve(defaultSettings);
};
if (!response || response === JSON.stringify(defaultSettings)) {
return defaultResponse();
}
const jsonFromMemory = JSON.parse(response);
if (jsonFromMemory.version !== defaultSettings.version) {
return defaultResponse();
}
return Promise.resolve(jsonFromMemory);
}, },
saveSettings(body: ISetting): Promise<any> { saveSettings(body: IUserSetting): Promise<any> {
const defaultSettings = getEmptySettings();
if (JSON.stringify(defaultSettings) === JSON.stringify(body)) {
localStorage.removeItem('settings');
} else {
localStorage.setItem('settings', JSON.stringify(body)); localStorage.setItem('settings', JSON.stringify(body));
}
return Promise.resolve(); return Promise.resolve();
}, },
}; };

View file

@ -1,6 +1,7 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import Button from 'ts/components/UiKit/components/Button'; import Button from 'ts/components/UiKit/components/Button';
import notificationsStore from 'ts/components/Notifications/store';
function copyInBuffer(value?: string) { function copyInBuffer(value?: string) {
if (!value) return; if (!value) return;
@ -39,6 +40,7 @@ function Console({ className, textForCopy, children }: IConsoleProps) {
className={`${style.console_copy}`} className={`${style.console_copy}`}
onClick={() => { onClick={() => {
copyInBuffer(textForCopy); copyInBuffer(textForCopy);
notificationsStore.show('Текст скопирован');
}} }}
> >
Копировать Копировать

View file

@ -6,7 +6,6 @@
} }
&_author, &_author,
&_task,
&_date, &_date,
&_message { &_message {
font-weight: 100; font-weight: 100;
@ -27,17 +26,10 @@
line-height: var(--font-m); line-height: var(--font-m);
} }
&_task { &_link {
font-size: var(--font-s);
display: block; display: block;
padding: 0 0 0 var(--space-l); padding: 0 0 0 var(--space-l);
margin: 0 0 var(--space-s) 0; margin: 0 0 var(--space-s) 0;
line-height: var(--font-s);
cursor: pointer;
text-decoration: underline;
color: var(--color-first);
} }
&_date, &_date,

View file

@ -1,7 +1,10 @@
import React from 'react'; import React from 'react';
import IHashMap from 'ts/interfaces/HashMap'; import IHashMap from 'ts/interfaces/HashMap';
import ExternalLink from 'ts/components/ExternalLink';
import userSettings from 'ts/store/UserSettings';
import { getShortTime } from 'ts/helpers/formatter'; import { getShortTime } from 'ts/helpers/formatter';
import dataGrip from 'ts/helpers/DataGrip';
import style from './index.module.scss'; import style from './index.module.scss';
@ -36,11 +39,23 @@ function CommitInfo({ commits }: { commits: ICommit[] }): React.ReactElement {
function TaskInfo({ tasks }: { tasks: ITask }): React.ReactElement { function TaskInfo({ tasks }: { tasks: ITask }): React.ReactElement {
const items = Object.entries(tasks) const items = Object.entries(tasks)
.map(([task, commits]: [string, any]) => { .map(([task, commits]: [string, any]) => {
const prId = dataGrip.pr.prByTask[task];
return ( return (
<div key={task}> <>
<div className={style.day_info_task}>{task}</div> <div className={style.day_info_link}>
<CommitInfo commits={commits}/> <ExternalLink
link={`${userSettings?.settings?.linksPrefix?.task || '/'}${task}`}
text={task}
/>
{prId && (
<ExternalLink
link={`${userSettings?.settings?.linksPrefix?.pr || '/'}${prId}`}
text="PR"
/>
)}
</div> </div>
<CommitInfo commits={commits}/>
</>
); );
}); });
return (<>{items}</>); return (<>{items}</>);
@ -57,7 +72,6 @@ function DayInfo({ day, order, events, timestamp }: IDayInfoProps): React.ReactE
const firstCommit = events?.firstCommit?.[timestamp || ''] || []; const firstCommit = events?.firstCommit?.[timestamp || ''] || [];
const lastCommit = events?.lastCommit?.[timestamp || ''] || []; const lastCommit = events?.lastCommit?.[timestamp || ''] || [];
let taskNumber = 0; let taskNumber = 0;
console.dir(firstCommit);
const items = Object.entries(day?.tasksByAuthor) const items = Object.entries(day?.tasksByAuthor)
.sort((a: any, b: any) => (order.indexOf(a[0]) - order.indexOf(b[0]))) .sort((a: any, b: any) => (order.indexOf(a[0]) - order.indexOf(b[0])))

View file

@ -1,6 +1,8 @@
@import '../../../styles/variables'; @import '../../../styles/variables';
.description { .description {
white-space: normal;
&_title, &_title,
&_text, &_text,
&_list { &_list {
@ -16,6 +18,7 @@
line-height: var(--font-m); line-height: var(--font-m);
text-decoration: none; text-decoration: none;
vertical-align: bottom; vertical-align: bottom;
white-space: normal;
color: #73809F; color: #73809F;
} }

View file

@ -10,7 +10,7 @@
align-items: center; align-items: center;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
z-index: 1; z-index: 3;
background-color: white; background-color: white;
&_icon { &_icon {

View file

@ -8,6 +8,7 @@ interface ILineProps {
suffix?: string; suffix?: string;
color?: { first: string, second: string } | null; color?: { first: string, second: string } | null;
className?: string; className?: string;
formatter?: Function;
} }
function Line({ function Line({
@ -18,12 +19,15 @@ function Line({
suffix, suffix,
color, color,
className, className,
formatter,
}: ILineProps): React.ReactElement | null { }: ILineProps): React.ReactElement | null {
if (!width || width <= 0) return null; if (!width || width <= 0) return null;
const formattedTitle = title || ''; const formattedTitle = title || '';
const formattedValue = formatter?.(value);
const formattedSuffix = suffix ? ` ${suffix}` : '';
const formattedDescription = value const formattedDescription = value
? `${width}% (${value} ${suffix}) ${description || formattedTitle}` ? `${width}% (${formattedValue}${formattedSuffix}) ${description || formattedTitle}`
: `${width}% ${description || formattedTitle}`; : `${width}% ${description || formattedTitle}`;
return ( return (
@ -50,6 +54,7 @@ Line.defaultProps = {
suffix: '', suffix: '',
color: null, color: null,
className: '', className: '',
formatter: (v: any) => v,
}; };
export default Line; export default Line;

View file

@ -7,6 +7,7 @@ interface IOptionsProps {
other?: string; other?: string;
max?: number[] | number; max?: number[] | number;
limit?: number; limit?: number;
formatter?: Function;
} }
export default function getOptions({ export default function getOptions({
@ -15,13 +16,15 @@ export default function getOptions({
other, other,
max, max,
limit, limit,
formatter,
}: IOptionsProps): IOptions { }: IOptionsProps): IOptions {
return { return {
max: max instanceof Array ? Math.max(...max) : (max || 100), max: max instanceof Array ? Math.max(...max) : (max || 100),
order: order || [], order: order || [],
suffix: suffix || 'коммитов', suffix: suffix ?? 'коммитов',
otherTitle: other || 'Остальные', otherTitle: other ?? 'Остальные',
color: order?.length ? (new ColorGenerator(order)) : null, color: order?.length ? (new ColorGenerator(order)) : null,
limit: limit || 15, limit: limit || 15,
formatter: formatter || ((v: any) => v),
}; };
} }

View file

@ -12,25 +12,27 @@ interface ILineChartProps {
options: IOptions; options: IOptions;
value?: number; value?: number;
details?: IHashMap<number>; details?: IHashMap<number>;
className?: string;
} }
function LineChart({ function LineChart({
options, options,
value, value,
details, details,
className,
}: ILineChartProps): React.ReactElement | null { }: ILineChartProps): React.ReactElement | null {
if (value === 0) return null; if (value === 0) return null;
const width = Math.round((value ?? 100) * (100 / options.max)); const width = Math.round((value ?? 100) * (100 / options.max));
if (!details) { if (!details) {
return ( return (
<div className={style.line_chart}> <div className={`${style.line_chart} ${className || ''}`}>
<Line <Line
value={value ?? 100} value={value ?? 100}
width={width} width={width}
suffix={options.suffix} suffix={options.suffix}
formatter={options.formatter}
className={style.line_chart_item} className={style.line_chart_item}
/> />
</div> </div>
@ -46,13 +48,14 @@ function LineChart({
width={item.width} width={item.width}
color={options.color.get(item.title)} color={options.color.get(item.title)}
suffix={options.suffix} suffix={options.suffix}
formatter={options.formatter}
description={item.description} description={item.description}
className={style.line_chart_sub_item} className={style.line_chart_sub_item}
/> />
)); ));
return ( return (
<div className={style.line_chart}> <div className={`${style.line_chart} ${className || ''}`}>
<div <div
className={style.line_chart_item} className={style.line_chart_item}
style={{ width: `${width}%` }} style={{ width: `${width}%` }}
@ -66,6 +69,7 @@ function LineChart({
LineChart.defaultProps = { LineChart.defaultProps = {
value: 100, value: 100,
details: undefined, details: undefined,
className: '',
}; };
export default LineChart; export default LineChart;

View file

@ -5,6 +5,7 @@ export interface IOptions {
otherTitle: string; otherTitle: string;
color: any; color: any;
limit: number; limit: number;
formatter?: Function;
} }
export interface ISubLine { export interface ISubLine {

View file

@ -6,7 +6,7 @@ function IsStaff() {
return ( return (
<> <>
<p className={style.nothing_found_title}> <p className={style.nothing_found_title}>
Нет данных для этого пользователя Нет данных для этого сотрудника
</p> </p>
<p className={style.nothing_found_text}> <p className={style.nothing_found_text}>
Он вносил правки не каждый рабочий день и получил статус Помошник. Он вносил правки не каждый рабочий день и получил статус Помошник.

View file

@ -1,6 +1,6 @@
export default interface IMessage { export default interface IMessage {
id?: number; id: number;
title?: string; title: string;
description?: string; description?: string;
type?: 'error' | 'warning' | 'success' | 'info'; type?: 'error' | 'warning' | 'success' | 'info';
} }

View file

@ -12,6 +12,8 @@ interface INotificationsStore {
class NotificationsStore implements INotificationsStore { class NotificationsStore implements INotificationsStore {
timer: any = null; timer: any = null;
limit: number = 6;
messages: IMessage[] = []; messages: IMessage[] = [];
constructor() { constructor() {
@ -22,24 +24,32 @@ class NotificationsStore implements INotificationsStore {
}); });
} }
static getTime() {
return (new Date()).getTime();
}
show(message?: any) { show(message?: any) {
this.messages.push({ this.messages.push({
id: Math.random(), id: NotificationsStore.getTime(),
title: message?.title || message || 'Изменения сохранены', title: message?.title || message || 'Изменения сохранены',
description: message?.description || '', description: message?.description || '',
type: message?.type || 'success', type: message?.type || 'success',
}); });
if (this.messages.length > this.limit) {
this.messages.shift();
}
this.startClearTimer(); this.startClearTimer();
} }
startClearTimer() { startClearTimer() {
if (this.timer) return; if (this.timer) return;
this.timer = setInterval(() => { this.timer = setInterval(() => {
this.messages.shift(); const time = NotificationsStore.getTime() - 3500;
this.messages = this.messages.filter((item: IMessage) => item?.id > time);
if (this.messages.length) return; if (this.messages.length) return;
clearInterval(this.timer); clearInterval(this.timer);
this.timer = null; this.timer = null;
}, 3500); }, 500);
} }
} }

View file

@ -8,26 +8,29 @@
&_item { &_item {
display: block; display: block;
width: 250px; width: 300px;
padding: 12px; padding: 24px;
margin: 0 12px 12px; margin: 0 12px 12px;
box-sizing: border-box; box-sizing: border-box;
box-shadow: 2px 2px 3px var(--color-grey); box-shadow: 2px 2px 3px var(--color-grey);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
background-color: #FFFFFF; border-left: 8px solid var(--color-border);
border-radius: 4px; border-radius: var(--border-radius-s);
background-color: white;
animation: notification_item 3s linear 0.5s forwards;
&_error { &_error {
background-color: var(--color-12); border-color: var(--color-12);
} }
&_warning { &_warning {
background-color: var(--color-32); border-color: var(--color-32);
} }
&_success { &_success {
background-color: var(--color-13); border-color: var(--color-13);
} }
&_info { &_info {
@ -36,20 +39,43 @@
&_title, &_title,
&_description { &_description {
font-size: var(--font-s); font-size: var(--font-m);
font-weight: 100;
padding: 0; padding: 0;
text-align: left; text-align: left;
color: var(--color-black); color: var(--color-black);
animation: notification_title 3s linear 0.5s forwards;
} }
&_title { &_title {
font-weight: bold;
margin: 0; margin: 0;
} }
&_description { &_description {
font-weight: 100;
margin: 0 0 4px 0; margin: 0 0 4px 0;
} }
} }
} }
@keyframes notification_item {
80% {
overflow: hidden;
height: auto;
padding: 24px;
opacity: 1;
}
to {
height: 0;
padding: 0 24px;
opacity: 0;
}
}
@keyframes notification_title {
80% {
font-size: var(--font-m);
}
to {
font-size: 2px;
}
}

View file

@ -1,68 +0,0 @@
import React from 'react';
import { IColumn } from '../interfaces/Column';
import style from '../styles/index.module.scss';
import DefaultCell from './cells/CellDefault';
interface IBodyProps {
rows: any[];
columns: IColumn[];
disabledRow?: (row: any) => boolean;
className?: string;
}
function Body({
rows,
disabledRow,
columns,
className,
}: IBodyProps) {
const formattedRows = rows?.map((row: any, index: number) => {
const cells = columns.map((column: IColumn, columnIndex: number) => {
const value = column.properties
? row[column.properties]
: row;
const formattedValue = column.formatter
? column.formatter(value)
: value;
const content: any = typeof column.template === 'function'
? column.template(formattedValue)
: `${column.prefixes ?? ''}${formattedValue ?? ''}${column.suffixes ?? ''}`;
return (
<DefaultCell
key={`${column.title}_${columnIndex}`}
column={column}
row={row}
>
{content}
</DefaultCell>
);
});
const rowClassName = disabledRow && disabledRow(row)
? style.disabled
: '';
return (
<div
key={index}
className={`${style.table_row} ${rowClassName} ${className}`}
>
{cells}
</div>
);
});
return (
<>
{formattedRows}
</>
);
}
Body.defaultProps = {
className: '',
};
export default Body;

View file

@ -1,61 +0,0 @@
import React from 'react';
import { IColumn } from '../interfaces/Column';
function Column({
template,
title,
properties,
prefixes,
suffixes,
formatter,
className,
style,
isFixed,
isSortable,
isResizable,
isDraggable,
isShow,
width,
onClick,
}: IColumn): JSX.Element {
return (
<>
{{
template,
title,
properties,
prefixes,
suffixes,
formatter,
className,
style,
isFixed,
isSortable,
isResizable,
isDraggable,
isShow,
width,
onClick,
}}
</>
);
}
Column.defaultProps = {
title: '',
prefixes: [''],
suffixes: [''],
formatter: (value: any) => value,
className: '',
isDisabled: false,
isFixed: false,
isSortable: false,
isResizable: false,
isDraggable: false,
isShow: true,
width: undefined,
onClick: undefined,
};
export default Column;

View file

@ -1,64 +0,0 @@
import React from 'react';
import localization from 'ts/helpers/Localization';
import { IColumn } from '../interfaces/Column';
import headerStyle from '../styles/header.module.scss';
import style from '../styles/index.module.scss';
interface ITitleProps {
columns: IColumn[];
className?: string;
updateSort?: Function;
}
function Header({
columns,
className,
updateSort,
}: ITitleProps) {
const cells = columns.map((column: IColumn, columnIndex: number) => {
const columnClassName = typeof column.className === 'function'
? column.className('header', columnIndex)
: column.className;
return (
<div
key={`${column.title}_${columnIndex}`}
className={`${style.table_header_cell} ${className} ${columnClassName || ''}`}
style={{ width: column.width }}
>
<span
onClick={() => {
if (!column.isSortable || !updateSort) return;
updateSort([{
property: typeof column.isSortable === 'string' ? column.isSortable : column.properties,
direction: [1, -1][column.sortDirection || 0] || 0,
}]);
}}
>
{localization.get(column.title)}
</span>
{column.title && column.sortDirection === -1 && (
<div className={headerStyle.sort_down} />
)}
{column.title && column.sortDirection === 1 && (
<div className={headerStyle.sort_up} />
)}
</div>
);
});
return (
<div className={`${style.table_row} ${className}`}>
{cells}
</div>
);
}
Header.defaultProps = {
className: '',
updateSort: () => {},
};
export default Header;

View file

@ -1,45 +0,0 @@
import React, { ReactNode } from 'react';
import { IColumn } from '../../interfaces/Column';
import style from '../../styles/index.module.scss';
interface IDefaultCellProps {
column: IColumn,
row: any,
className?: string,
children?: ReactNode | string | number | boolean | null;
}
function DefaultCell({
column,
row,
className,
children,
}: IDefaultCellProps): JSX.Element {
const columnClassName = typeof column.className === 'function'
? column.className('body', row)
: column.className;
const onClick = column.onClick
? (() => { if (column.onClick) column.onClick(row); })
: undefined;
return (
<div
key={column.title}
className={`${style.table_cell} ${className || ''} ${columnClassName || ''}`}
style={{
width: column.width,
cursor: onClick ? 'pointer' : 'auto',
}} // @ts-ignore
onClick={onClick}
>
{children}
</div>
);
}
DefaultCell.defaultPeops = {
className: '',
};
export default DefaultCell;

View file

@ -1,30 +0,0 @@
import ISort from 'ts/interfaces/Sort';
import { IColumn } from '../interfaces/Column';
function getColumnConfigs(
dirtyColumns: IColumn[] = [],
defaultWidth?: number,
sort?: ISort[],
): IColumn[] {
const sortByColumns = sort?.reduce((ref: any, item: ISort) => {
ref[item.property] = item.direction;
return ref;
}, {});
const columns: IColumn[] = dirtyColumns.map((column: IColumn) => ({
...column,
sortDirection: typeof column?.isSortable === 'string'
? (sortByColumns[column?.isSortable || ''] || 0)
: (sortByColumns[column?.properties || ''] || 0),
width: column.userWidth || column.defaultWidth || defaultWidth || column.width || 150,
}));
const middle = Math.floor(columns.length / 2);
return [
...columns.filter((column: IColumn, index: number) => column.isFixed && index <= middle),
...columns.filter((column: IColumn) => !column.isFixed),
...columns.filter((column: IColumn, index: number) => column.isFixed && index > middle),
];
}
export default getColumnConfigs;

View file

@ -1,20 +0,0 @@
import type { IColumn } from '../interfaces/Column';
export default function getDefaultColumnWidth(
columns: IColumn[],
tableRef: any,
): number {
if (!tableRef?.current?.offsetWidth) return 150;
const visibleColumns = columns.filter(({ isShow }: IColumn) => isShow);
const columnsWidth = visibleColumns.map((column: IColumn) => (
column.userWidth || column.defaultWidth || 0
));
const fixedWidth = columnsWidth.reduce((sum: number, width: number) => sum + width, 0);
const adaptiveColumnsCount = columnsWidth.filter((width: number) => !width).length;
const tableWidth = tableRef?.current?.offsetWidth - fixedWidth;
const adaptiveColumnsWidth = tableWidth / adaptiveColumnsCount;
return Math.max(adaptiveColumnsWidth, 40);
}

View file

@ -1,41 +0,0 @@
import React from 'react';
import { ColumnTypesEnum } from '../interfaces/Column';
import style from '../styles/index.module.scss';
export default function getDefaultProps(children: React.ReactNode) {
return React.Children.map(children, (child: React.ReactNode) => {
if (!React.isValidElement(child)) return null;
const template = child?.props?.template || ColumnTypesEnum.STRING;
// @ts-ignore
const className = child?.props?.className || {
[ColumnTypesEnum.STRING]: '',
[ColumnTypesEnum.NUMBER]: style.table_cell_number,
[ColumnTypesEnum.SHORT_NUMBER]: style.table_cell_number,
}[template || ''] || '';
// @ts-ignore
const defaultWidth = child?.props?.width || {
[ColumnTypesEnum.STRING]: 200,
[ColumnTypesEnum.NUMBER]: 110,
[ColumnTypesEnum.SHORT_NUMBER]: 70,
}[template || ''] || 0;
// @ts-ignore
const isSortable = child?.props?.isSortable // @ts-ignore
? child?.props?.isSortable
: [ColumnTypesEnum.STRING, ColumnTypesEnum.NUMBER, ColumnTypesEnum.SHORT_NUMBER].includes(template);
return {
...child.props as object,
className,
template,
isSortable,
width: undefined,
userWidth: undefined,
defaultWidth,
};
});
}

View file

@ -1,63 +0,0 @@
import React from 'react';
import ISort from 'ts/interfaces/Sort';
import { IColumn } from './interfaces/Column';
import Header from './components/Header';
import Body from './components/Body';
import getDefaultColumnWidth from './helpers/getDefaultColumnWidth';
import getColumnConfigs from './helpers/getColumnConfigs';
import getDefaultProps from './helpers/getDefaultProps';
import style from './styles/index.module.scss';
interface ITableProps {
rows: any[];
sort?: ISort[];
disabledRow?: (row: any) => boolean;
updateSort?: Function,
children: React.ReactNode | React.ReactNode[];
}
function Table({
rows = [],
sort = [],
disabledRow,
updateSort,
children,
}: ITableProps): React.ReactElement | null {
if (!rows || !rows.length) return null;
const refTable = React.useRef() as React.MutableRefObject<HTMLDivElement>;
const defaultColumns = getDefaultProps(children) as IColumn[];
const defaultWidth = getDefaultColumnWidth(defaultColumns, refTable);
const columns = getColumnConfigs(defaultColumns, defaultWidth, sort);
return (
<div className={`${style.table_wrapper}`}>
<div
ref={refTable}
className={`${style.table}`}
>
<Header
columns={columns}
updateSort={updateSort}
/>
<Body
rows={rows}
columns={columns}
disabledRow={disabledRow}
/>
</div>
</div>
);
}
Table.defaultProps = {
rows: [],
sort: [],
updateSort: () => {},
};
export default Table;

View file

@ -1,49 +0,0 @@
export type ColumnType = 'STRING' | 'NUMBER' | 'SHORT_NUMBER';
/** Тип столбца определяет тип содержимого всех ячеек столбца */
export enum ColumnTypesEnum {
STRING = 'STRING',
NUMBER = 'NUMBER',
SHORT_NUMBER = 'SHORT_NUMBER',
}
export interface IColumn {
/** Тип столбца */
template?: ColumnTypesEnum | Function,
/** Уникальный ключ столбца */
properties?: string,
/** Заголовок столбца */
title?: string,
/** Префиксы для заголовка столбца */
prefixes?: string,
/** Суффиксы для заголовка столбца (%, $ и т.д.) */
suffixes?: string,
/** Функция для форматирования данных в столбце */
formatter?: Function,
/** Направление сортировки */
sortDirection?: number,
/** Фиксированный столбец */
isFixed?: boolean,
/** Сортировка столбца */
isSortable?: boolean | string,
/** Изменение ширины столбца */
isResizable?: boolean,
/** Drag-and-Drop столбца */
isDraggable?: boolean,
/** Видимость столбца */
isShow?: boolean,
/** Клас для колонки */
className?: string | Function
/** Стилья для колонки */
style?: Function,
/** Ширина столбца заданная в верстке */
defaultWidth?: number,
/** Ширина столбца установленная пользователем */
userWidth?: number,
/** Ширина столбца итоговая */
width?: number,
/** Клик на ячейку */
onClick?: Function,
}

View file

@ -1,28 +0,0 @@
@import '../../../../styles/variables';
.title {
font-size: var(--font-l);
font-weight: 100;
margin: 24px 0;
color: var(--color-black);
}
.sort_up,
.sort_down {
display: inline-block;
height: 0;
width: 0;
margin: 0 0 -5px 8px;
cursor: pointer;
transform: rotateZ(-45deg);
border: 6px solid var(--color-grey);
border-left-color: white;
border-bottom-color: white;
}
.sort_down {
margin: 0 0 3px 8px;
transform: rotateZ(135deg);
}

View file

@ -1,92 +0,0 @@
@import '../../../../styles/variables';
.table_wrapper {
display: block;
overflow-x: auto;
&::-webkit-scrollbar {
height: 8px;
background-color: #DDDDDD;
}
&::-webkit-scrollbar-thumb {
background: #AAAAAA;
}
}
.table {
display: block;
width: fit-content;
--table-cell-height: 48px;
--table-bar-width: 350px;
}
.table_tree {
--table-cell-height: 22px;
--table-bar-width: 200px;
}
.table_row {
position: relative;
font-weight: 100;
display: block;
white-space: nowrap;
border-bottom: 1px solid #EEEEEE;
}
.table_row:last-child {
border-bottom: none;
}
.table_row_hide {
opacity: 0.2;
}
.table_cell,
.table_header_cell {
font-size: var(--font-xs);
z-index: 0;
display: inline-block;
height: var(--table-cell-height);
line-height: var(--table-cell-height);
box-sizing: border-box;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
}
.table_cell {
padding: 0 4px;
}
.table_header_cell {
font-weight: bold;
height: var(--table-cell-height);
padding: 0 4px;
line-height: var(--table-cell-height);
background-color: #FFFFFF;
}
.table_cell:first-child,
.table_header_cell:first-child {
position: sticky;
top: 0;
left: 0;
z-index: 1;
}
.table_cell:first-child {
background-color: rgba(255, 255, 255, 0.9);
}
.table_cell_number {
text-align: right;
}
.disabled {
opacity: 0.4;
filter: grayscale(0.6);
}

View file

@ -26,7 +26,7 @@ function Body({
? 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) ? column.template(formattedValue, row)
: `${column.prefixes ?? ''}${formattedValue ?? ''}${column.suffixes ?? ''}`; : `${column.prefixes ?? ''}${formattedValue ?? ''}${column.suffixes ?? ''}`;
return ( return (

View file

@ -2,9 +2,9 @@ import type { IColumn } from '../interfaces/Column';
export default function getDefaultColumnWidth( export default function getDefaultColumnWidth(
columns: IColumn[], columns: IColumn[],
tableRef: any, offsetWidth: number,
): number { ): number {
if (!tableRef?.current?.offsetWidth) return 150; if (!offsetWidth) return 150;
const visibleColumns = columns.filter(({ isShow }: IColumn) => isShow); const visibleColumns = columns.filter(({ isShow }: IColumn) => isShow);
const columnsWidth = visibleColumns.map((column: IColumn) => ( const columnsWidth = visibleColumns.map((column: IColumn) => (
@ -12,9 +12,11 @@ export default function getDefaultColumnWidth(
)); ));
const fixedWidth = columnsWidth.reduce((sum: number, width: number) => sum + width, 0); const fixedWidth = columnsWidth.reduce((sum: number, width: number) => sum + width, 0);
const adaptiveColumnsCount = columnsWidth.filter((width: number) => !width).length; const adaptiveColumnsCount = columnsWidth.filter((width: number) => !width).length;
if (!adaptiveColumnsCount) return 40;
const tableWidth = tableRef?.current?.offsetWidth - fixedWidth; const tableWidth = offsetWidth - fixedWidth;
const adaptiveColumnsWidth = tableWidth / adaptiveColumnsCount; const adaptiveColumnsWidth = tableWidth / adaptiveColumnsCount;
console.dir(adaptiveColumnsWidth);
return Math.max(adaptiveColumnsWidth, 40); return Math.max(adaptiveColumnsWidth, 40);
} }

View file

@ -1,4 +1,4 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import ISort from 'ts/interfaces/Sort'; import ISort from 'ts/interfaces/Sort';
@ -26,20 +26,27 @@ function Table({
updateSort, updateSort,
children, children,
}: ITableProps): React.ReactElement | null { }: ITableProps): React.ReactElement | null {
const [offsetWidth, setOffsetWidth] = useState<number>(0);
if (!rows || !rows.length) return null; if (!rows || !rows.length) return null;
const refTable = React.useRef() as React.MutableRefObject<HTMLDivElement>; const refTable = React.useRef() as React.MutableRefObject<HTMLDivElement>;
const currentWidth = refTable?.current?.offsetWidth;
useEffect(() => {
setOffsetWidth(currentWidth);
}, [currentWidth]);
const defaultColumns = getDefaultProps(children) as IColumn[]; const defaultColumns = getDefaultProps(children) as IColumn[];
const defaultWidth = getDefaultColumnWidth(defaultColumns, refTable); const defaultWidth = getDefaultColumnWidth(defaultColumns, offsetWidth);
const columns = getColumnConfigs(defaultColumns, defaultWidth, sort); const columns = getColumnConfigs(defaultColumns, defaultWidth, sort);
return ( return (
<div className={`${style.table_wrapper}`}>
<div <div
ref={refTable} ref={refTable}
className={`${style.table}`} className={`${style.table_wrapper}`}
> >
<div className={`${style.table}`}>
<Header <Header
columns={columns} columns={columns}
updateSort={updateSort} updateSort={updateSort}
@ -57,7 +64,8 @@ function Table({
Table.defaultProps = { Table.defaultProps = {
rows: [], rows: [],
sort: [], sort: [],
updateSort: () => {}, updateSort: () => {
},
}; };
export default Table; export default Table;

View file

@ -39,7 +39,7 @@ function Column({ dayInfo, order, author }: IColumnProps) {
) : ( ) : (
<NothingFound <NothingFound
icon="./assets/cards/commits.png" icon="./assets/cards/commits.png"
message="В этот день у этого пользователя не было ни одного коммита." message="В этот день у этого сотрудника не было ни одного коммита."
/> />
)} )}
</div> </div>

View file

@ -1,9 +1,12 @@
import React from 'react'; import React from 'react';
import IHashMap from 'ts/interfaces/HashMap';
import ICommit from 'ts/interfaces/Commit'; import ICommit from 'ts/interfaces/Commit';
import IHashMap from 'ts/interfaces/HashMap';
import ExternalLink from 'ts/components/ExternalLink';
import userSettings from 'ts/store/UserSettings';
import { get2Number } from 'ts/helpers/formatter'; import { get2Number } from 'ts/helpers/formatter';
import dataGrip from 'ts/helpers/DataGrip';
import style from '../styles/task.module.scss'; import style from '../styles/task.module.scss';
function getFormattedTime(time: any) { function getFormattedTime(time: any) {
@ -37,15 +40,23 @@ interface ITaskProps {
} }
function Task({ title, commits }: ITaskProps) { function Task({ title, commits }: ITaskProps) {
const prId = dataGrip.pr.prByTask[title];
return ( return (
<div <div
key={title} key={title}
className={style.tempo_task} className={style.tempo_task}
> >
<div className={style.tempo_task_header}> <div className={style.tempo_task_header}>
<p className={style.tempo_task_link}> <div>
{title} <ExternalLink
</p> text={title}
link={`${userSettings?.settings?.linksPrefix?.task || '/'}${title}`}
/>
<ExternalLink
text="PR"
link={`${userSettings?.settings?.linksPrefix?.pr || '/'}${prId}`}
/>
</div>
<div className={style.tempo_task_tags}> <div className={style.tempo_task_tags}>
{getTags(commits)} {getTags(commits)}
</div> </div>

View file

@ -53,14 +53,17 @@
&_tags { &_tags {
display: inline-block; display: inline-block;
white-space: normal;
text-align: right;
margin: 0 0 -6px 0;
} }
&_tag { &_tag {
display: inline-block; display: inline-block;
padding: var(--space-xxxs) var(--space-sm); padding: var(--space-xxxs) var(--space-sm);
margin: 0 0 var(--space-xs) var(--space-xs);
border-radius: var(--border-radius-l); border-radius: var(--border-radius-l);
background-color: var(--color-border); background-color: var(--color-border);
margin-left: 8px;
} }
&_commits, &_commits,

View file

@ -4,6 +4,7 @@ import Wrapper, { IUiKitWrapperProps } from './Wrapper';
import style from '../styles/switch.module.scss'; import style from '../styles/switch.module.scss';
interface IUiKitSwitchProps extends IUiKitWrapperProps { interface IUiKitSwitchProps extends IUiKitWrapperProps {
multiple?: boolean;
value: any; value: any;
options: any[]; options: any[];
onChange: Function; onChange: Function;
@ -20,20 +21,33 @@ function UiKitSwitch({
options, options,
onChange, onChange,
}: IUiKitSwitchProps) { }: IUiKitSwitchProps) {
const hasValue = value || value === 0 || value === false;
let selectedIds = value;
if (hasValue && !Array.isArray(value)) {
selectedIds = [value];
}
const items = (options || []) const items = (options || [])
.map((option: any, index: number) => { .map((option: any, index: number) => {
const formattedOption = typeof option !== 'object' const formattedOption = typeof option !== 'object'
? ({ id: option, title: option }) ? ({ id: option, title: option })
: option; : option;
const isSelected = hasValue && selectedIds.includes(formattedOption?.id);
return ( return (
<button <button
key={`${formattedOption?.id}_${index}`} key={`${formattedOption?.id}_${index}`}
className={value === formattedOption?.id className={isSelected
? `${style.ui_kit_switch_item} ${style.ui_kit_switch_item_selected}` ? `${style.ui_kit_switch_item} ${style.ui_kit_switch_item_selected}`
: style.ui_kit_switch_item} : style.ui_kit_switch_item}
onClick={() => { onClick={() => {
if (onChange) onChange(option); if (!onChange) return;
const newSelected = isSelected
? selectedIds.filter((id: any) => id !== formattedOption?.id)
: [...selectedIds, formattedOption?.id].sort();
onChange(newSelected);
}} }}
> >
{formattedOption?.title ?? formattedOption?.id ?? ''} {formattedOption?.title ?? formattedOption?.id ?? ''}

View file

@ -9,7 +9,7 @@ export interface IUiKitWrapperProps {
example?: string; example?: string;
error?: string; error?: string;
className?: string; className?: string;
disabled?: boolean, disabled?: boolean;
children?: ReactNode | string | null; children?: ReactNode | string | null;
} }

View file

@ -10,11 +10,13 @@ import { getEvents } from '../helpers/day';
interface IBodyProps { interface IBodyProps {
month: IMonth; month: IMonth;
maxCommits: number; maxCommits: number;
showEvents: boolean;
} }
function Body({ function Body({
month, month,
maxCommits, maxCommits,
showEvents,
}: IBodyProps): React.ReactElement | null { }: IBodyProps): React.ReactElement | null {
const firstDay = month.date.getDay() - 1; const firstDay = month.date.getDay() - 1;
const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
@ -22,7 +24,7 @@ function Body({
const allDays = (new Array(6 * 7)).fill(0); const allDays = (new Array(6 * 7)).fill(0);
let currentDay = 0; let currentDay = 0;
const events = getEvents(dataGripStore); const events = showEvents ? getEvents(dataGripStore) : {};
const days = allDays.map((v: any, index: number) => { const days = allDays.map((v: any, index: number) => {
const dayInfo = month.commits[currentDay]; const dayInfo = month.commits[currentDay];

View file

@ -1,8 +1,11 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import DayInfo from 'ts/components/DayInfo';
import dataGripStore from 'ts/store/DataGrip';
import IHashMap from 'ts/interfaces/HashMap'; import IHashMap from 'ts/interfaces/HashMap';
import dataGripStore from 'ts/store/DataGrip';
import DayInfo from 'ts/components/DayInfo';
import Title from 'ts/components/Title';
import { getDate } from 'ts/helpers/formatter';
import IMonth from '../interfaces/Month'; import IMonth from '../interfaces/Month';
import style from '../styles/index.module.scss'; import style from '../styles/index.module.scss';
@ -50,9 +53,10 @@ function Day({
> >
{showInfo ? ( {showInfo ? (
<> <>
{'📌'} {''}
<div className={style.year_chart_month_body_day_arrow} /> <div className={style.year_chart_month_body_day_arrow} />
<div className={style.year_chart_month_body_day_info}> <div className={style.year_chart_month_body_day_info}>
<Title title={getDate(dayInfo.timestamp)} />
<DayInfo // @ts-ignore <DayInfo // @ts-ignore
day={dayInfo} day={dayInfo}
events={events} events={events}

View file

@ -1,43 +1,81 @@
import React from 'react'; import React from 'react';
import IHashMap from 'ts/interfaces/HashMap';
import LineChart from 'ts/components/LineChart';
import getOptions from 'ts/components/LineChart/helpers/getOptions';
import { getShortMoney } from 'ts/helpers/formatter'; import { getShortMoney } from 'ts/helpers/formatter';
import cssDescription from 'ts/components/Description/index.module.scss';
import IMonth from '../interfaces/Month'; import IMonth from '../interfaces/Month';
import Header from './Header'; import Header from './Header';
import Body from './Body'; import Body from './Body';
import styleChart from '../styles/line.module.scss';
import style from '../styles/index.module.scss'; import style from '../styles/index.module.scss';
interface IMonthProps { interface IMonthTotalProps {
month: IMonth; title: string;
maxCommits: number; options: any;
value: any;
} }
function Month({ function MonthTotal({
month, title,
maxCommits, options,
}: IMonthProps): React.ReactElement | null { value,
}: IMonthTotalProps) {
return ( return (
<div className={`${style.year_chart_month}`}> <div className={styleChart.year_chart_month_info}>
<Header month={month} /> <span className={styleChart.year_chart_month_text}>
<Body {title}
month={month} </span>
maxCommits={maxCommits} <LineChart
options={options}
value={value}
className={styleChart.year_chart_month_chart}
/> />
<p className={cssDescription.description_text}>
<span title="Задач за месяц">
{`${month.tasks}`}
</span>
<span title="Затраты на зарплату сотрудникам">
{` за ${getShortMoney(month.money || 0, 0)}`}
</span>
</p>
</div> </div>
); );
} }
Month.defaultProps = { interface IMonthProps {
rows: [], max: IHashMap<number>;
}; month: IMonth;
showEvents: boolean;
}
function Month({
max,
month,
showEvents,
}: IMonthProps): React.ReactElement | null {
const tasksChart = getOptions({ max: max.tasks, suffix: 'задач' });
const moneyChart = getOptions({
max: max.money,
suffix: '',
formatter: getShortMoney,
});
console.dir(month);
return (
<div className={style.year_chart_month}>
<Header month={month}/>
<Body
month={month}
maxCommits={max.commits}
showEvents={showEvents}
/>
<MonthTotal
title="$"
options={moneyChart}
value={month.money}
/>
<MonthTotal
title="☑"
options={tasksChart}
value={month.tasks}
/>
</div>
);
}
export default Month; export default Month;

View file

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import MinMaxCounter from 'ts/helpers/DataGrip/components/counter';
import getCommitsByMonth from './helpers/getCommitsByMonth'; import getCommitsByMonth from './helpers/getCommitsByMonth';
import getAuthorByDate from './helpers/getAuthorByDate'; import getAuthorByDate from './helpers/getAuthorByDate';
import Month from './components/Month'; import Month from './components/Month';
@ -7,12 +9,14 @@ import IMonth from './interfaces/Month';
interface IYearChartProps { interface IYearChartProps {
maxCommits: number; maxCommits: number;
showEvents?: boolean;
wordDays: any[]; wordDays: any[];
authors: any[]; authors: any[];
} }
function YearChart({ function YearChart({
maxCommits = 100, maxCommits = 100,
showEvents = true,
wordDays = [], wordDays = [],
authors = [], authors = [],
}: IYearChartProps): React.ReactElement | null { }: IYearChartProps): React.ReactElement | null {
@ -21,11 +25,26 @@ function YearChart({
const authorsByDate = getAuthorByDate(authors); const authorsByDate = getAuthorByDate(authors);
const months = getCommitsByMonth(wordDays, authorsByDate); const months = getCommitsByMonth(wordDays, authorsByDate);
const max = {
tasks: new MinMaxCounter(),
money: new MinMaxCounter(),
};
months.forEach((month: IMonth) => {
max.tasks.update(month.tasks);
max.money.update(month.money);
});
const elements = months.map((month: IMonth) => ( const elements = months.map((month: IMonth) => (
<Month <Month
key={month.id} key={month.id}
max={{
tasks: max.tasks.max,
money: max.money.max,
commits: maxCommits,
}}
month={month} month={month}
maxCommits={maxCommits} showEvents={showEvents}
/> />
)); ));
@ -37,7 +56,7 @@ function YearChart({
} }
YearChart.defaultProps = { YearChart.defaultProps = {
rows: [], showEvents: true,
}; };
export default YearChart; export default YearChart;

View file

@ -1,29 +1,33 @@
import localization from 'ts/helpers/Localization'; import localization from 'ts/helpers/Localization';
localization.parse('ru', ` localization.parse('ru', `
§ common.filters: Фильтры
§ sidebar.team.total: Общая информация § sidebar.team.total: Общая информация
§ sidebar.team.scope: Оценка проекта § sidebar.team.scope: Фичи
§ sidebar.team.author: Оценка сотрудников § sidebar.team.author: Сотрудники
§ sidebar.team.type: Типы задач § sidebar.team.type: Типы задач
§ sidebar.team.sprint: По неделям § sidebar.team.pr: Влитие кода
§ sidebar.team.month: По месяцу § sidebar.team.day: По дням
§ sidebar.team.week: По неделям
§ sidebar.team.month: По месяцам
§ sidebar.team.tree: Анализ файлов § sidebar.team.tree: Анализ файлов
§ sidebar.team.heatmap: График работы
§ sidebar.team.hours: Расписание § sidebar.team.hours: Расписание
§ sidebar.team.timestamp: Все коммиты § sidebar.team.commits: Все коммиты
§ sidebar.team.changes: Все изменения § sidebar.team.changes: Все изменения
§ sidebar.team.words: Популярные слова § sidebar.team.words: Популярные слова
§ sidebar.team.top: Викторина § sidebar.team.top: Викторина
§ sidebar.team.settings: Настройки
§ sidebar.person.total: Общая информация § sidebar.person.total: Общая информация
§ sidebar.person.money: Стоимость работы § sidebar.person.money: Стоимость работы
§ sidebar.person.speed: Скорость § sidebar.person.speed: Скорость
§ sidebar.person.day: По дням
§ sidebar.person.week: По неделям § sidebar.person.week: По неделям
§ sidebar.person.month: По месяцам § sidebar.person.month: По месяцам
§ sidebar.person.frequency: График работы
§ sidebar.person.hours: Расписание § sidebar.person.hours: Расписание
§ sidebar.person.commits: Все коммиты § sidebar.person.commits: Все коммиты
§ sidebar.person.changes: Все изменения § sidebar.person.changes: Все изменения
§ sidebar.person.words: Популярные слова § sidebar.person.words: Популярные слова
§ sidebar.person.settings: Настройки
§ page.team.author.types: Тип работ § page.team.author.types: Тип работ
§ page.team.author.commits: Коммитов § page.team.author.commits: Коммитов
§ page.team.author.commitsSmall: коммитов § page.team.author.commitsSmall: коммитов

View file

@ -1,6 +1,7 @@
import ICommit from 'ts/interfaces/Commit'; import ICommit from 'ts/interfaces/Commit';
import settingsStore from 'ts/store/Settings';
import IHashMap from 'ts/interfaces/HashMap'; import IHashMap from 'ts/interfaces/HashMap';
import settingsStore from 'ts/store/Settings';
import userSettings from 'ts/store/UserSettings';
export default class DataGripByAuthor { export default class DataGripByAuthor {
list: string[] = []; list: string[] = [];
@ -26,6 +27,7 @@ export default class DataGripByAuthor {
} else { } else {
this.#addCommitByAuthor(commit); this.#addCommitByAuthor(commit);
} }
this.#setMoneyByMonth(commit);
} }
#updateCommitByAuthor(commit: ICommit) { #updateCommitByAuthor(commit: ICommit) {
@ -44,12 +46,20 @@ export default class DataGripByAuthor {
? commit.message.length ? commit.message.length
: statistic.maxMessageLength; : statistic.maxMessageLength;
statistic.commitsByDayAndHour[commit.day][commit.hours] += 1; statistic.commitsByDayAndHour[commit.day][commit.hours] += 1;
statistic.commitsByHour[commit.hours] += 1;
statistic.wordStatistics = DataGripByAuthor.#updateWordStatistics(commit, statistic.wordStatistics); statistic.wordStatistics = DataGripByAuthor.#updateWordStatistics(commit, statistic.wordStatistics);
} }
#addCommitByAuthor(commit: ICommit) { #addCommitByAuthor(commit: ICommit) {
const commitsByDayAndHour = DataGripByAuthor.getDefaultCommitsByDayAndHour(); const commitsByDayAndHour = DataGripByAuthor.getDefaultCommitsByDayAndHour();
try {
commitsByDayAndHour[commit.day][commit.hours] += 1; commitsByDayAndHour[commit.day][commit.hours] += 1;
} catch (e: any) {
debugger;
}
const commitsByHour = new Array(24).fill(0);
commitsByHour[commit.hours] += 1;
this.commits[commit.author] = { this.commits[commit.author] = {
author: commit.author, author: commit.author,
commits: 1, commits: 1,
@ -61,13 +71,52 @@ export default class DataGripByAuthor {
scopes: { [commit.scope]: 1 }, scopes: { [commit.scope]: 1 },
hours: [commit.hours], hours: [commit.hours],
commitsByDayAndHour, commitsByDayAndHour,
commitsByHour,
messageLength: [commit.message.length || 0], messageLength: [commit.message.length || 0],
totalMessageLength: commit.message.length || 0, totalMessageLength: commit.message.length || 0,
maxMessageLength: commit.message.length || 0, maxMessageLength: commit.message.length || 0,
wordStatistics: DataGripByAuthor.#updateWordStatistics(commit), wordStatistics: DataGripByAuthor.#updateWordStatistics(commit),
moneyByMonth: {},
}; };
} }
#setMoneyByMonth(commit: ICommit) {
const key = `${commit.year}-${commit.month}`;
if (this.commits[commit.author].moneyByMonth[key]) {
this.#updateMoneyByMonth(commit, key);
} else {
this.#addMoneyByMonth(commit, key);
}
}
#updateMoneyByMonth(commit: ICommit, key: string) {
const statistic = this.commits[commit.author].moneyByMonth[key];
if (statistic.alreadyAdded[commit.milliseconds]) return;
statistic.alreadyAdded[commit.milliseconds] = true;
const isWorkDay = statistic.contract.workDaysInWeek[commit.day];
if (isWorkDay) {
statistic.workDay += 1;
} else {
statistic.weekDay += 1;
}
}
#addMoneyByMonth(commit: ICommit, key: string) {
const contract = userSettings.getEmploymentContract(commit.author, commit.milliseconds);
const isWorkDay = contract.workDaysInWeek[commit.day];
this.commits[commit.author].moneyByMonth[key] = {
workDay: isWorkDay ? 1 : 0,
weekDay: isWorkDay ? 0 : 1,
alreadyAdded: {
[commit.milliseconds]: true,
},
contract,
};
}
static getDefaultCommitsByDayAndHour() { static getDefaultCommitsByDayAndHour() {
return (new Array(7)).fill(1).map(() => (new Array(24)).fill(0)); return (new Array(7)).fill(1).map(() => (new Array(24)).fill(0));
} }
@ -107,7 +156,8 @@ export default class DataGripByAuthor {
const allDaysInProject = Math.ceil((to - from) / settingsStore.ONE_DAY); const allDaysInProject = Math.ceil((to - from) / settingsStore.ONE_DAY);
const lazyDays = Math.floor((allDaysInProject * WORK_AND_HOLIDAYS) - workDays) + 1; const lazyDays = Math.floor((allDaysInProject * WORK_AND_HOLIDAYS) - workDays) + 1;
const middleSalaryInDay = settingsStore.getMiddleSalaryInDay(dot.author); const middleSalaryInMonth = userSettings.getMiddleSalaryInMonth(dot.author, from, to);
const middleSalaryInDay = middleSalaryInMonth / 22;
const moneyWorked = Math.ceil(workDays * middleSalaryInDay); const moneyWorked = Math.ceil(workDays * middleSalaryInDay);
const moneyLosses = lazyDays > 0 const moneyLosses = lazyDays > 0
? Math.ceil(lazyDays * middleSalaryInDay) ? Math.ceil(lazyDays * middleSalaryInDay)

View file

@ -7,7 +7,8 @@ export default class MinMaxCounter {
maxData: any = undefined; maxData: any = undefined;
update(value: number, data: any) { update(value?: number, data?: any) {
if (!value && value !== 0) return;
if (this.min > value) { if (this.min > value) {
this.min = value; this.min = value;
this.minData = data; this.minData = data;

View file

@ -1,41 +1,163 @@
import { COMMIT_TYPE, ISystemCommit } from 'ts/interfaces/Commit'; import { COMMIT_TYPE, ISystemCommit } from 'ts/interfaces/Commit';
import IHashMap from 'ts/interfaces/HashMap'; import IHashMap from 'ts/interfaces/HashMap';
import { WeightedAverage } from 'ts/helpers/Math';
export default class DataGripByPr { export default class DataGripByPR {
list: string[] = [];
pr: IHashMap<any> = {}; pr: IHashMap<any> = {};
statistic: any = []; prByTask: IHashMap<string> = {};
lastCommitByTaskNumber: IHashMap<any> = {};
statistic: any[] = [];
statisticByName: IHashMap<any> = [];
clear() { clear() {
this.list = [];
this.pr = {}; this.pr = {};
this.prByTask = {};
this.lastCommitByTaskNumber = {};
this.statistic = []; this.statistic = [];
} }
addCommit(commit: ISystemCommit) { addCommit(commit: ISystemCommit) {
if (commit.commitType === COMMIT_TYPE.AUTO_MERGE) return; if (!commit.commitType) {
if (this.pr[commit.prId]) { if (!this.lastCommitByTaskNumber[commit.task]) {
this.#updateCommitByPR(commit); this.#addCommitByTaskNumber(commit);
} else { } else {
this.#updateCommitByTaskNumber(commit);
}
} else if (commit.commitType !== COMMIT_TYPE.AUTO_MERGE && !this.pr[commit.prId]) {
this.#addCommitByPR(commit); this.#addCommitByPR(commit);
} }
} }
#updateCommitByPR(commit: ISystemCommit) { #addCommitByTaskNumber(commit: ISystemCommit) {
const statistic = this.pr[commit.prId]; this.lastCommitByTaskNumber[commit.task] = {
const property = commit.commitType === COMMIT_TYPE.MERGE ? 'close' : 'open'; commits : 1,
statistic[property] = commit; beginTaskTime: commit.milliseconds,
statistic.delay = statistic.open.milliseconds - statistic.close.milliseconds; endTaskTime: commit.milliseconds,
commitsByAuthors: {
[commit.author]: 1,
},
firstCommit: commit,
};
}
#updateCommitByTaskNumber(commit: ISystemCommit) {
const statistic = this.lastCommitByTaskNumber[commit.task];
statistic.endTaskTime = commit.milliseconds;
statistic.commits += 1;
statistic.commitsByAuthors[commit.author] = statistic.commitsByAuthors[commit.author]
? (statistic.commitsByAuthors[commit.author] + 1)
: 1;
} }
#addCommitByPR(commit: ISystemCommit) { #addCommitByPR(commit: ISystemCommit) {
const property = commit.commitType === COMMIT_TYPE.MERGE ? 'close' : 'open'; const lastCommit = this.lastCommitByTaskNumber[commit.task];
this.pr[commit.prId] = { [property]: commit }; if (lastCommit) {
// коммиты после влития PR сгорают, чтобы не засчитать технические PR мержи веток
delete this.lastCommitByTaskNumber[commit.task];
const delay = commit.milliseconds - lastCommit.endTaskTime;
const work = lastCommit.endTaskTime - lastCommit.beginTaskTime;
this.pr[commit.prId] = {
...commit,
...lastCommit,
delay,
delayDays: delay / (24 * 60 * 60 * 1000),
workDays: work === 0 ? 1 : (work / (24 * 60 * 60 * 1000)),
};
this.prByTask[commit.task] = commit.prId;
} else {
this.pr[commit.prId] = { ...commit };
}
} }
updateTotalInfo() { updateTotalInfo(dataGripByAuthor: any) {
const employment = dataGripByAuthor.employment;
const authors = [...employment.active, ...employment.dismissed];
const refAuthorPR: any = Object.fromEntries(authors.map((name: string) => ([name, []])));
this.statistic = Object.values(this.pr)
.filter((item: any) => item.delay && item.task)
.sort((a: any, b: any) => b.delay - a.delay);
this.statistic = []; this.statistic = [];
this.statisticByName = {};
Object.values(this.pr).forEach((item: any) => {
if (!item.delay || !item.task) return;
this.statistic.push(item);
if (refAuthorPR[item.firstCommit.author]) {
refAuthorPR[item.firstCommit.author].push(item);
}
});
this.statistic.sort((a: any, b: any) => b.delay - a.delay);
this.updateTotalByAuthor(authors, refAuthorPR);
this.lastCommitByTaskNumber = {};
}
static getPRByGroups(list: any, propertyName: string) {
const TITLES = {
DAY: 'день',
THREE_DAY: 'три дня',
WEEK: 'неделя',
TWO_WEEK: 'две недели',
MONTH: 'месяц',
MORE: 'более',
};
const details = {
[TITLES.DAY]: 0,
[TITLES.THREE_DAY]: 0,
[TITLES.WEEK]: 0,
[TITLES.TWO_WEEK]: 0,
[TITLES.MONTH]: 0,
[TITLES.MORE]: 0,
};
const weightedAverage = new WeightedAverage();
list.forEach((pr: any) => {
const value = pr[propertyName];
weightedAverage.update(value);
if (value <= 1) details[TITLES.DAY]++;
else if (value <= 2) details[TITLES.THREE_DAY]++;
else if (value <= 7) details[TITLES.WEEK]++;
else if (value <= 14) details[TITLES.TWO_WEEK]++;
else if (value <= 30) details[TITLES.MONTH]++;
else details[TITLES.MORE]++;
});
const order = Object.keys(details);
return { details, order, weightedAverage: weightedAverage.get() };
}
updateTotalByAuthor(authors: any, refAuthorPR: IHashMap<any>) {
this.statisticByName = {};
authors.map((name: string) => {
const delayDays = DataGripByPR.getPRByGroups(refAuthorPR[name], 'delayDays');
const delayDaysWeightedAverage = parseInt(delayDays.weightedAverage.toFixed(1), 10);
const workDays = DataGripByPR.getPRByGroups(refAuthorPR[name], 'workDays');
const workDaysWeightedAverage = parseInt(workDays.weightedAverage.toFixed(1), 10);
this.statisticByName[name] = {
author: name,
workDays: workDays.details,
delayDays: delayDays.details,
weightedAverage: workDaysWeightedAverage + delayDaysWeightedAverage,
weightedAverageDetails: {
workDays: workDaysWeightedAverage,
delayDays: delayDaysWeightedAverage,
},
};
});
} }
} }

View file

@ -10,6 +10,7 @@ import DataGripByTimestamp from './components/timestamp';
import DataGripByWeek from './components/week'; import DataGripByWeek from './components/week';
import MinMaxCounter from './components/counter'; import MinMaxCounter from './components/counter';
import DataGripByExtension from './components/extension'; import DataGripByExtension from './components/extension';
import DataGripByGet from './components/get';
import DataGripByPR from './components/pr'; import DataGripByPR from './components/pr';
class DataGrip { class DataGrip {
@ -31,6 +32,8 @@ class DataGrip {
extension: any = new DataGripByExtension(); extension: any = new DataGripByExtension();
get: any = new DataGripByGet();
pr: any = new DataGripByPR(); pr: any = new DataGripByPR();
initializationInfo: any = {}; initializationInfo: any = {};
@ -45,19 +48,20 @@ class DataGrip {
this.week.clear(); this.week.clear();
this.recommendations.clear(); this.recommendations.clear();
this.extension.clear(); this.extension.clear();
this.get.clear();
this.pr.clear(); this.pr.clear();
} }
addCommit(commit: ICommit | ISystemCommit) { addCommit(commit: ICommit | ISystemCommit) {
if (commit.author === 'GitHub') return; // @ts-ignore if (commit.author === 'GitHub') return;
if (commit.commitType) { this.pr.addCommit(commit); // @ts-ignore
this.pr.addCommit(commit); if (!commit.commitType) {
} else {
this.firstLastCommit.update(commit.milliseconds, commit); this.firstLastCommit.update(commit.milliseconds, commit);
this.author.addCommit(commit); this.author.addCommit(commit);
this.scope.addCommit(commit); this.scope.addCommit(commit);
this.type.addCommit(commit); this.type.addCommit(commit);
this.timestamp.addCommit(commit); this.timestamp.addCommit(commit);
this.get.addCommit(commit);
this.week.addCommit(commit); this.week.addCommit(commit);
} }
} }
@ -70,7 +74,7 @@ class DataGrip {
this.timestamp.updateTotalInfo(this.author); this.timestamp.updateTotalInfo(this.author);
this.week.updateTotalInfo(this.author); this.week.updateTotalInfo(this.author);
this.recommendations.updateTotalInfo(this); this.recommendations.updateTotalInfo(this);
this.pr.updateTotalInfo(this); this.pr.updateTotalInfo(this.author);
} }
updateByInitialization() { updateByInitialization() {

View file

@ -18,16 +18,12 @@ export default function Parser(
let weekEndTime: number = 0; let weekEndTime: number = 0;
let prev = null; let prev = null;
let isFileInfo = false; // [ name, file, empty ];
for (let i = 0, l = report.length; i < l; i += 1) { for (let i = 0, l = report.length; i < l; i += 1) {
const message = report[i]; const message = report[i];
if (!message) { if (!message) continue;
isFileInfo = false; const index = message.indexOf('\t');
continue; if (index > 0 && index < 10) {
}
if (isFileInfo) {
let [addedRaw, removedRaw, fileName] = message.split('\t'); let [addedRaw, removedRaw, fileName] = message.split('\t');
fileName = getNewFileName(fileName, allFiles); fileName = getNewFileName(fileName, allFiles);
let added = parseInt(addedRaw, 10) || 0; let added = parseInt(addedRaw, 10) || 0;
@ -104,7 +100,6 @@ export default function Parser(
prev = next; prev = next;
commits.push(prev); // @ts-ignore commits.push(prev); // @ts-ignore
isFileInfo = !prev.commitType;
} }
} }
if (prev) parseCommit(prev); if (prev) parseCommit(prev);

View file

@ -1,22 +1,6 @@
import ICommit, { COMMIT_TYPE, ISystemCommit } from 'ts/interfaces/Commit'; import ICommit, { COMMIT_TYPE, ISystemCommit } from 'ts/interfaces/Commit';
function getTypeAndScope(messageParts: string[], task: string) { import { getTypeAndScope, getTask, getTaskNumber } from './getTypeAndScope';
if (messageParts.length < 2) return ['', ''];
const [type, scope] = (messageParts.shift() || '')
.replace(task, '')
.split(/[()]/g)
.map(v => v.trim());
return (!type && scope)
? [scope, '']
: [type, scope];
}
// ABC-123, #123, gh-123
function getTask(message: string) {
return ((message || '').match(/(([A-Z]+-)|(#)|(gh-)|(GH-))([0-9]+)/gm) || [])[0] || '';
}
export default function getUserInfo(logString: string): ICommit | ISystemCommit { export default function getUserInfo(logString: string): ICommit | ISystemCommit {
// "2021-02-09T12:59:17+03:00>Frolov Ivan>frolov@mail.ru>profile" // "2021-02-09T12:59:17+03:00>Frolov Ivan>frolov@mail.ru>profile"
@ -54,32 +38,36 @@ export default function getUserInfo(logString: string): ICommit | ISystemCommit
const isSystemPR = message.indexOf('Pull request #') === 0; const isSystemPR = message.indexOf('Pull request #') === 0;
const isSystemMerge = message.indexOf('Merge pull request #') === 0; const isSystemMerge = message.indexOf('Merge pull request #') === 0;
const fromGitHubToBitBucket = message.indexOf('Merge branch ') === 0; const isAutoMerge = message.indexOf('Merge branch ') === 0
|| message.indexOf('Merge remote-tracking branch') === 0
|| message.indexOf('Merge commit ') === 0
|| message.indexOf('Automatic merge from') === 0;
const isSystemCommit = isSystemPR const isSystemCommit = isSystemPR
|| isSystemMerge || isSystemMerge
|| fromGitHubToBitBucket || isAutoMerge;
|| message.indexOf('Automatic merge from') === 0;
if (isSystemCommit) { if (isSystemCommit) {
let commitType = COMMIT_TYPE.AUTO_MERGE; let commitType = COMMIT_TYPE.AUTO_MERGE;
let prId, repository, branch, toBranch, task; let prId, repository, branch, toBranch, task, taskNumber;
if (isSystemMerge) { if (isSystemMerge) {
commitType = COMMIT_TYPE.MERGE; commitType = COMMIT_TYPE.PR_GITHUB;
[, prId, repository, branch, toBranch ] = message [, prId, repository, branch, toBranch ] = message
.replace(/(Merge\spull\srequest\s#)|(\sfrom\s)|(\sin\s)|(\sto\s)/gim, ',') .replace(/(Merge\spull\srequest\s#)|(\sfrom\s)|(\sin\s)|(\sto\s)/gim, ',')
.split(','); .split(',');
task = getTask(branch); task = getTask(branch);
} else if (isSystemPR) { } else if (isSystemPR) {
commitType = COMMIT_TYPE.PR; commitType = COMMIT_TYPE.PR_BITBUCKET;
const messageParts = message.substring(14, Infinity).split(':'); const messageParts = message.substring(14, Infinity).split(':');
prId = messageParts.shift(); prId = messageParts.shift();
task = getTask(messageParts.join(':')); task = getTask(messageParts.join(':'));
} }
taskNumber = getTaskNumber(task);
return { return {
...commonInfo, ...commonInfo,
prId: prId || '', prId: prId || '',
task: task || '', task: task || '',
taskNumber: taskNumber || '',
repository: repository || '', repository: repository || '',
branch: branch || '', branch: branch || '',
toBranch: toBranch || '', toBranch: toBranch || '',
@ -87,12 +75,13 @@ export default function getUserInfo(logString: string): ICommit | ISystemCommit
}; };
} }
const messageParts = message.split(':');
const task = getTask(message); const task = getTask(message);
const [type, scope] = getTypeAndScope(messageParts, task); const taskNumber = getTaskNumber(task);
const [type, scope] = getTypeAndScope(message, task);
return { return {
...commonInfo, ...commonInfo,
task, task,
taskNumber,
type: type || 'не подписан', type: type || 'не подписан',
scope: scope || 'неопределенна', scope: scope || 'неопределенна',

View file

@ -44,6 +44,7 @@ export default function getUserInfo(message: any): ICommit {
message: text || '', message: text || '',
task: 'беседа', task: 'беседа',
taskNumber: '',
type: 'не подписан', type: 'не подписан',
scope: 'неопределенна', scope: 'неопределенна',

View file

@ -1,3 +1,4 @@
import ICommit from 'ts/interfaces/Commit';
import dataGrip from 'ts/helpers/DataGrip'; import dataGrip from 'ts/helpers/DataGrip';
import ALL_ACHIEVEMENTS from './constants/list'; import ALL_ACHIEVEMENTS from './constants/list';
@ -5,21 +6,34 @@ import byCompetition from './byCompetition';
export default function getAchievementByAuthor(author: string) { export default function getAchievementByAuthor(author: string) {
const statistic = dataGrip.author.statisticByName[author]; const statistic = dataGrip.author.statisticByName[author];
const getList = dataGrip.get.getsByAuthor[author];
if (!statistic) return; if (!statistic) return;
const list = byCompetition.get(author); const list = byCompetition.get(author);
const commitByHours = statistic.commitsByHour;
if (statistic.commits > 20) {
// Сова - 70% коммитов после 15:00 // Сова - 70% коммитов после 15:00
if (statistic.hours.filter((hour: number) => hour >= 15).length > (statistic.commits * 0.7)) list.push('commitsAfter1500'); if (statistic.hours.filter((hour: number) => hour >= 15).length > (statistic.commits * 0.7)) list.push('commitsAfter1500');
// Раняя пташка - 70% коммитов до обеда // Раняя пташка - 70% коммитов до обеда
if (statistic.hours.filter((hour: number) => hour <= 13).length > (statistic.commits * 0.7)) list.push('commitsBefore1500'); if (statistic.hours.filter((hour: number) => hour <= 13).length > (statistic.commits * 0.7)) list.push('commitsBefore1500');
// Делу время - ни одного коммита после 18:00 }
if (statistic.hours.filter((hour: number) => hour > 18 || hour < 5).length === 0) list.push('commitsAfter1800');
// Раб божий - есть коммит на каждый час суток
if ((new Set(statistic.hours)).size === 24) list.push('workEveryTime');
if (statistic.isStaff) { if (statistic.isStaff) {
// Залётный - это не его основной проект // Залётный - это не его основной проект
list.push('userNotWork'); list.push('userNotWork');
} else { } else {
// Ночной дозор
if (commitByHours.slice(0, 7).every((commits: number) => commits)) list.push('hasCommitFrom0to7');
// Технический перерыв
if (commitByHours.slice(10, 18).some((commits: number) => !commits)) list.push('noCommitOnDay');
// Делу время - ни одного коммита после 18:00
if (commitByHours.slice(0, 5).every((commits: number) => !commits)
&& commitByHours.slice(18, 24).every((commits: number) => !commits)) list.push('commitsAfter1800');
// Раб божий - есть коммит на каждый час суток
if (commitByHours.every((commits: number) => commits)) list.push('workEveryTime');
// Умер на работе
if (statistic.commitsByDayAndHour.every((day: any) => day.every((v: any) => v))) list.push('hasCommitEveryTime');
// Мёртвая душа - работал, но уволился // Мёртвая душа - работал, но уволился
if (statistic.isDismissed) list.push('userIsDied'); if (statistic.isDismissed) list.push('userIsDied');
// Скорострел - меньше дня на задачу // Скорострел - меньше дня на задачу
@ -30,8 +44,10 @@ export default function getAchievementByAuthor(author: string) {
if (statistic.allDaysInProject > 90) list.push('more90DaysInProject'); if (statistic.allDaysInProject > 90) list.push('more90DaysInProject');
// Чёрт - отработал 666 дней на проекте // Чёрт - отработал 666 дней на проекте
if (statistic.allDaysInProject > 666) list.push('more666DaysInProject'); if (statistic.allDaysInProject > 666) list.push('more666DaysInProject');
// Флеш-рояль - отработал 777 дней на проекте // Азино - отработал 777 дней на проекте
if (statistic.allDaysInProject > 777) list.push('more777DaysInProject'); if (statistic.allDaysInProject > 777) list.push('more777DaysInProject');
// Тесак - отработал 1488 дней на проекте
if (statistic.allDaysInProject > 1488) list.push('more1488DaysInProject');
} }
// Ни единого разрыва - 0 дней без коммитов // Ни единого разрыва - 0 дней без коммитов
if (statistic.lazyDays === 0) list.push('zeroLazyDays'); if (statistic.lazyDays === 0) list.push('zeroLazyDays');
@ -40,6 +56,8 @@ export default function getAchievementByAuthor(author: string) {
// сказал как отрезал - в среднем 1 коммит на таск // сказал как отрезал - в среднем 1 коммит на таск
if (statistic.tasks / statistic.commits) list.push('oneCommitOneTask'); if (statistic.tasks / statistic.commits) list.push('oneCommitOneTask');
if (getList?.some((commit: ICommit) => commit.taskNumber === '300')) list.push('taskNumber300');
return list.reduce((acc: any, type: string) => { return list.reduce((acc: any, type: string) => {
const index = ALL_ACHIEVEMENTS[type][2]; const index = ALL_ACHIEVEMENTS[type][2];
acc[index].push(type); acc[index].push(type);

View file

@ -4,7 +4,6 @@ export default {
// готово // готово
commitsAfter1500: ['Сова', '70% коммитов после 15:00', ACHIEVEMENT_TYPE.NORMAL], commitsAfter1500: ['Сова', '70% коммитов после 15:00', ACHIEVEMENT_TYPE.NORMAL],
commitsBefore1500: ['Раняя пташка', '70% коммитов до обеда', ACHIEVEMENT_TYPE.NORMAL], commitsBefore1500: ['Раняя пташка', '70% коммитов до обеда', ACHIEVEMENT_TYPE.NORMAL],
commitsAfter1800: ['Делу время', 'нет ни одного коммита после 18:00', ACHIEVEMENT_TYPE.GOOD],
workEveryTime: ['Раб божий', 'есть коммит на каждый час суток', ACHIEVEMENT_TYPE.BAD], workEveryTime: ['Раб божий', 'есть коммит на каждый час суток', ACHIEVEMENT_TYPE.BAD],
workNotWork: ['Стрельба холостыми', 'коммиты есть, а закрытых задач нет', ACHIEVEMENT_TYPE.BAD], workNotWork: ['Стрельба холостыми', 'коммиты есть, а закрытых задач нет', ACHIEVEMENT_TYPE.BAD],
userNotWork: ['Залётный', 'это не его основной проект', ACHIEVEMENT_TYPE.NORMAL], userNotWork: ['Залётный', 'это не его основной проект', ACHIEVEMENT_TYPE.NORMAL],
@ -30,14 +29,20 @@ export default {
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],
more666DaysInProject: ['Чёрт', 'отработал 666 дней на проекте', ACHIEVEMENT_TYPE.GOOD],
more777DaysInProject: ['Азино 3 топора', 'отработал 777 дней на проекте', ACHIEVEMENT_TYPE.GOOD],
moreRefactoring: ['Выпускающий редактор', 'сделал больше всех меток «рефакторинг»', ACHIEVEMENT_TYPE.GOOD], moreRefactoring: ['Выпускающий редактор', 'сделал больше всех меток «рефакторинг»', ACHIEVEMENT_TYPE.GOOD],
// нет картинки // нет картинки
longestMessage: ['А разговоров то было...', 'самая длинная подпись коммита за все время', ACHIEVEMENT_TYPE.NORMAL], longestMessage: ['А разговоров то было...', 'самая длинная подпись коммита за все время', ACHIEVEMENT_TYPE.NORMAL],
adam: ['Адам', 'первый стабильны сотрудник на проекте', ACHIEVEMENT_TYPE.NORMAL],
more666DaysInProject: ['Чёрт', 'отработал 666 дней на проекте', ACHIEVEMENT_TYPE.GOOD],
more777DaysInProject: ['Азино 3 топора', 'отработал 777 дней на проекте', ACHIEVEMENT_TYPE.GOOD],
moreTasksInDay: ['Спиди-гонщик', 'рекорд по количеству закрытых задач в день', ACHIEVEMENT_TYPE.GOOD], moreTasksInDay: ['Спиди-гонщик', 'рекорд по количеству закрытых задач в день', ACHIEVEMENT_TYPE.GOOD],
hasCommitFrom0to7: ['Ночной дозор', 'есть коммит на каждый час ночи', ACHIEVEMENT_TYPE.BAD],
noCommitOnDay: ['Технический перерыв', 'есть определенный час и день в рабочее время в который никогда не комитит', ACHIEVEMENT_TYPE.NORMAL],
hasCommitEveryTime: ['Умер на работе', 'есть коммит на час каждого дня (включая выходные)', ACHIEVEMENT_TYPE.BAD],
commitsAfter1800: ['Делу время', 'нет ни одного коммита после 18:00', ACHIEVEMENT_TYPE.GOOD],
more1488DaysInProject: ['им. Максима Марцинкевича', 'отработал 1488 дней на проекте', ACHIEVEMENT_TYPE.GOOD],
taskNumber300: ['Знаком с трактористом', 'первый взял в работу задачу с номером 300', ACHIEVEMENT_TYPE.NORMAL],
// нет кода // нет кода
// moreFix: ['Bug hunter', 'больше всего закрытых багов', ACHIEVEMENT_TYPE.GOOD], // moreFix: ['Bug hunter', 'больше всего закрытых багов', ACHIEVEMENT_TYPE.GOOD],
@ -46,4 +51,5 @@ export default {
moreRemoveCode: ['Разрушитель', 'склонен больше остальных удалять код', ACHIEVEMENT_TYPE.NORMAL], moreRemoveCode: ['Разрушитель', 'склонен больше остальных удалять код', ACHIEVEMENT_TYPE.NORMAL],
moreChangeCode: ['Реформатор', 'склонен больше остальных изменять код', ACHIEVEMENT_TYPE.NORMAL], // есть картинка moreChangeCode: ['Реформатор', 'склонен больше остальных изменять код', ACHIEVEMENT_TYPE.NORMAL], // есть картинка
moreStyle: ['Полиция моды', 'склонен больше остальных изменять CSS', ACHIEVEMENT_TYPE.GOOD], moreStyle: ['Полиция моды', 'склонен больше остальных изменять CSS', ACHIEVEMENT_TYPE.GOOD],
moreOnHoliday: ['Нет жизни', 'относительно много коммитов в нерабочее время', ACHIEVEMENT_TYPE.BAD],
}; };

View file

@ -17,14 +17,15 @@ export interface ILog {
// task // task
message: string; // "JIRA-0000 fix(profile): add new avatar", message: string; // "JIRA-0000 fix(profile): add new avatar",
task: string; // "JIRA-0000, task: string; // "JIRA-0000",
taskNumber: string; // "0000",
type: string; // feat|fix|docs|style|refactor|test|chore type: string; // feat|fix|docs|style|refactor|test|chore
scope: string; // table, sale, profile and etc. scope: string; // table, sale, profile and etc.
} }
export const COMMIT_TYPE = { export const COMMIT_TYPE = {
PR: 'PR', PR_BITBUCKET: 'PR_BITBUCKET',
MERGE: 'MERGE', PR_GITHUB: 'PR_GITHUB',
AUTO_MERGE: 'AUTO_MERGE', AUTO_MERGE: 'AUTO_MERGE',
}; };

View file

@ -49,7 +49,7 @@ function Commits({ statistic }: ICommitsProps) {
<br/> <br/>
<br/> <br/>
{} {}
<Title title={`${getDate(selected?.timestamp)} сделано ${selected?.commits || '_'} коммитов`}/> <Title title={`${getDate(selected?.timestamp)} сделано коммитов: ${selected?.commits || '_'}`}/>
<PageWrapper template="box"> <PageWrapper template="box">
<DayInfo <DayInfo
day={selected} day={selected}

View file

@ -1,48 +1,18 @@
import React from 'react'; import React from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import localization from 'ts/helpers/Localization';
import style from '../../styles/header.module.scss'; import style from '../../styles/header.module.scss';
const TITLES = {
team: {
total: 'Общая информация',
sprint: 'Динамика работы в течении недели',
month: 'Список задач за месяц',
scope: 'Оценка проекта',
author: 'Оценка сотрудников',
type: 'Типы задач и их оценка',
tree: 'Анализ файлов',
year: 'График работы',
hours: 'Распределение коммитов в течении каждого дня недели',
commits: 'Количество коммитов',
changes: 'Все изменения',
timestamp: 'Все коммиты',
week: 'Распределение коммитов по дням недели',
words: 'Популярные слова в комментарии к коммиту',
top: 'Викторина',
settings: 'Настройки',
},
person: {
total: 'Общая информация',
speed: 'Скорость работы',
money: 'Стоимость работы',
week: 'Динамика работы в течении недели',
month: 'Список задач за месяц',
frequency: 'График работы',
hours: 'Распределение коммитов в течении каждого дня недели',
absolute: 'Распределение коммитов по дням недели',
commits: 'Все коммиты',
changes: 'Все изменения',
words: 'Популярные слова в комментарии к коммиту',
settings: 'Настройки',
},
};
function Logo() { function Logo() {
const { type, page } = useParams<any>(); const { type, page } = useParams<any>();
const title = type && page
? localization.get(`sidebar.${type}.${page}`)
: localization.get('sidebar.team.total');
return ( return (
<h2 className={style.header_title}> <h2 className={style.header_title}>
{TITLES[type || '']?.[page || ''] || 'Настройки'} {title}
</h2> </h2>
); );
} }

View file

@ -21,11 +21,13 @@ function SideBarMenuItem({
icon, icon,
isSelected, isSelected,
}: ISideBarMenuItemProps) { }: ISideBarMenuItemProps) {
const formattedTitle = localization.get(title);
return ( return (
<Link <Link
key={id} key={id}
className={`${style.sidebar_item} ${isSelected ? style.selected : ''}`} className={`${style.sidebar_item} ${isSelected ? style.selected : ''}`}
to={link} to={link}
title={formattedTitle}
id={`sidebar-menu-${id}`} id={`sidebar-menu-${id}`}
onClick={() => { onClick={() => {
if (settingsForm.isEdited) { if (settingsForm.isEdited) {
@ -40,7 +42,7 @@ function SideBarMenuItem({
alt={title || ''} alt={title || ''}
/> />
<figcaption className={style.sidebar_item_title}> <figcaption className={style.sidebar_item_title}>
{localization.get(title)} {formattedTitle}
</figcaption> </figcaption>
</Link> </Link>
); );

View file

@ -35,6 +35,13 @@ function SideBarPerson({ page }: ISideBarProps) {
isSelected={page === 'speed'} isSelected={page === 'speed'}
/> />
<SideBarMenuGap/> <SideBarMenuGap/>
<SideBarMenuItem
id="day"
link={`/person/day/${formattedUserId}`}
title="sidebar.person.day"
icon="./assets/menu/team_week.svg"
isSelected={page === 'day'}
/>
<SideBarMenuItem <SideBarMenuItem
id="week" id="week"
link={`/person/week/${formattedUserId}`} link={`/person/week/${formattedUserId}`}
@ -46,15 +53,8 @@ function SideBarPerson({ page }: ISideBarProps) {
id="month" id="month"
link={`/person/month/${formattedUserId}`} link={`/person/month/${formattedUserId}`}
title="sidebar.person.month" title="sidebar.person.month"
icon="./assets/menu/team_week.svg"
isSelected={page === 'month'}
/>
<SideBarMenuItem
id="year"
link={`/person/year/${formattedUserId}`}
title="sidebar.person.frequency"
icon="./assets/menu/team_date_1.svg" icon="./assets/menu/team_date_1.svg"
isSelected={page === 'year'} isSelected={page === 'month'}
/> />
<SideBarMenuItem <SideBarMenuItem
id="hours" id="hours"

View file

@ -38,27 +38,34 @@ function SideBarTeam({ page }: ISideBarProps) {
icon="./assets/menu/team_type.svg" icon="./assets/menu/team_type.svg"
isSelected={page === 'type'} isSelected={page === 'type'}
/> />
<SideBarMenuItem
id="type"
link="/team/pr"
title="sidebar.team.pr"
icon="./assets/menu/pull_request.svg"
isSelected={page === 'pr'}
/>
<SideBarMenuGap/> <SideBarMenuGap/>
<SideBarMenuItem <SideBarMenuItem
id="sprint" id="day"
link="/team/sprint" link="/team/day"
title="sidebar.team.sprint" title="sidebar.team.day"
icon="./assets/menu/team_week.svg" icon="./assets/menu/team_week.svg"
isSelected={page === 'sprint'} isSelected={page === 'day'}
/>
<SideBarMenuItem
id="week"
link="/team/week"
title="sidebar.team.week"
icon="./assets/menu/team_week.svg"
isSelected={page === 'week'}
/> />
<SideBarMenuItem <SideBarMenuItem
id="month" id="month"
link="/team/month" link="/team/month"
title="sidebar.team.month" title="sidebar.team.month"
icon="./assets/menu/team_week.svg"
isSelected={page === 'month'}
/>
<SideBarMenuItem
id="year"
link="/team/year"
title="sidebar.team.heatmap"
icon="./assets/menu/team_date_1.svg" icon="./assets/menu/team_date_1.svg"
isSelected={page === 'year'} isSelected={page === 'month'}
/> />
<SideBarMenuItem <SideBarMenuItem
id="hours" id="hours"
@ -76,11 +83,11 @@ function SideBarTeam({ page }: ISideBarProps) {
isSelected={page === 'tree'} isSelected={page === 'tree'}
/> />
<SideBarMenuItem <SideBarMenuItem
id="timestamp" id="commits"
link="/team/timestamp" link="/team/commits"
title="sidebar.team.timestamp" title="sidebar.team.commits"
icon="./assets/menu/pull-request.svg" icon="./assets/menu/pull-request.svg"
isSelected={page === 'timestamp'} isSelected={page === 'commits'}
/> />
<SideBarMenuItem <SideBarMenuItem
id="changes" id="changes"
@ -96,14 +103,14 @@ function SideBarTeam({ page }: ISideBarProps) {
icon="./assets/menu/team_words.svg" icon="./assets/menu/team_words.svg"
isSelected={page === 'words'} isSelected={page === 'words'}
/> />
<SideBarMenuGap/> {/*<SideBarMenuGap/>*/}
<SideBarMenuItem {/*<SideBarMenuItem*/}
id="top" {/* id="top"*/}
link="/team/top" {/* link="/team/top"*/}
title="sidebar.team.top" {/* title="sidebar.team.top"*/}
icon="./assets/menu/team_words.svg" {/* icon="./assets/menu/team_words.svg"*/}
isSelected={page === 'top'} {/* isSelected={page === 'top'}*/}
/> {/*/>*/}
</> </>
); );
} }

View file

@ -5,6 +5,7 @@ import { observer } from 'mobx-react-lite';
import { IPagination } from 'ts/interfaces/Pagination'; import { IPagination } from 'ts/interfaces/Pagination';
import dataGripStore from 'ts/store/DataGrip'; import dataGripStore from 'ts/store/DataGrip';
import { getShortDateRange } from 'ts/helpers/formatter'; import { getShortDateRange } from 'ts/helpers/formatter';
import localization from 'ts/helpers/Localization';
import UiKitButton from 'ts/components/UiKit/components/Button'; import UiKitButton from 'ts/components/UiKit/components/Button';
import PageWrapper from 'ts/components/Page/wrapper'; import PageWrapper from 'ts/components/Page/wrapper';
@ -59,7 +60,7 @@ const Tempo = observer((): React.ReactElement => {
if (!partOfData?.length) return (<NothingFound />); if (!partOfData?.length) return (<NothingFound />);
return ( return (
<> <>
<Title title="Фильтры"/> <Title title={localization.get('common.filters')} />
<PageWrapper> <PageWrapper>
<div className={style.tempo_page_filters}> <div className={style.tempo_page_filters}>
<UiKitButton <UiKitButton

View file

@ -12,6 +12,7 @@ import Description from 'ts/components/Description';
import PageWrapper from 'ts/components/Page/wrapper'; import PageWrapper from 'ts/components/Page/wrapper';
import PageColumn from 'ts/components/Page/column'; import PageColumn from 'ts/components/Page/column';
import Title from 'ts/components/Title'; import Title from 'ts/components/Title';
import GetList from 'ts/components/GetList';
import dataGripStore from 'ts/store/DataGrip'; import dataGripStore from 'ts/store/DataGrip';
@ -33,6 +34,7 @@ function AchievementBlock({ title, achievements }: IAchievementBlockProps) {
const Total = observer((): React.ReactElement => { const Total = observer((): React.ReactElement => {
const { userId } = useParams<any>(); const { userId } = useParams<any>();
const statistic = dataGripStore.dataGrip.author.statistic[userId || 0]; const statistic = dataGripStore.dataGrip.author.statistic[userId || 0];
const commitsWithGet = dataGripStore.dataGrip.get.getsByAuthor[statistic.author];
const taskNumber = statistic.tasks.length; const taskNumber = statistic.tasks.length;
const achievements = getAchievementByAuthor(statistic.author); const achievements = getAchievementByAuthor(statistic.author);
@ -82,6 +84,15 @@ const Total = observer((): React.ReactElement => {
achievements={achievements[ACHIEVEMENT_TYPE.BAD]} achievements={achievements[ACHIEVEMENT_TYPE.BAD]}
/> />
<Description text="Чем больше сотрудник набрал отрицательных достижений, тем больше вероятность, что ситуация нестандартная. Возможно, стоит изменить режим его работы, задачи или отчётность. Следует поговорить с ним и узнать, какие проблемы мешают его работе."/> <Description text="Чем больше сотрудник набрал отрицательных достижений, тем больше вероятность, что ситуация нестандартная. Возможно, стоит изменить режим его работы, задачи или отчётность. Следует поговорить с ним и узнать, какие проблемы мешают его работе."/>
<br />
<br />
{commitsWithGet?.length ? (
<>
<Title title={localization.get('Взятые геты:')}/>
<GetList list={commitsWithGet} />
<Description text="&laquo;Взять гет&raquo; в данном случае означает первым оставить коммит к&nbsp;задаче с&nbsp;&laquo;красивым&raquo; номером."/>
</>
) : null}
</PageColumn> </PageColumn>
</PageWrapper> </PageWrapper>
); );

View file

@ -1,27 +0,0 @@
import React from 'react';
import { useParams } from 'react-router-dom';
import { observer } from 'mobx-react-lite';
import dataGripStore from 'ts/store/DataGrip';
import YearChart from 'ts/components/YearChart';
import PageWrapper from 'ts/components/Page/wrapper';
const Year = observer((): React.ReactElement => {
const { userId } = useParams<any>();
const author = dataGripStore.dataGrip.author.statistic[userId || 0];
const statistic = dataGripStore.dataGrip.timestamp.statisticByAuthor[author.author];
const max = statistic.commitsByTimestampCounter.max;
return (
<PageWrapper template="table">
<YearChart
maxCommits={max}
authors={[author]}
wordDays={statistic.allCommitsByTimestamp}
/>
</PageWrapper>
);
});
export default Year;

View file

@ -2,6 +2,7 @@ import React from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import Title from 'ts/components/Title'; import Title from 'ts/components/Title';
import localization from 'ts/helpers/Localization';
import UserSelect from './components/UserSelect'; import UserSelect from './components/UserSelect';
import Changes from './components/Changes'; import Changes from './components/Changes';
@ -12,7 +13,7 @@ import PopularWords from './components/PopularWords';
import Speed from './components/Speed'; import Speed from './components/Speed';
import Total from './components/Total'; import Total from './components/Total';
import Week from './components/Week'; import Week from './components/Week';
import Year from './components/Year'; import Month from './components/Month';
import Tempo from './components/Tempo'; import Tempo from './components/Tempo';
function Person() { function Person() {
@ -22,21 +23,20 @@ function Person() {
<> <>
{page !== 'week' && ( {page !== 'week' && (
<> <>
<Title title="Фильтры"/> <Title title={localization.get('common.filters')} />
<UserSelect /> <UserSelect />
</> </>
)} )}
{page === 'total' && <Total/>}
{page === 'changes' && <Changes/>}
{page === 'commits' && <Commits/>}
{page === 'hours' && <Hours/>} {page === 'hours' && <Hours/>}
{page === 'money' && <Money/>} {page === 'money' && <Money/>}
{page === 'week' && <Week/>}
{page === 'month' && <Month/>}
{page === 'commits' && <Commits/>}
{page === 'changes' && <Changes/>}
{page === 'words' && <PopularWords/>} {page === 'words' && <PopularWords/>}
{page === 'speed' && <Speed/>} {page === 'speed' && <Speed/>}
{page === 'total' && <Total/>} {page === 'day' && <Tempo/>}
{page === 'month' && <Week/>}
{page === 'week' && <Tempo/>}
{page === 'year' && <Year/>}
</> </>
); );
} }

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import UiKitInputNumber from 'ts/components/UiKit/components/InputNumber'; import InputString from 'ts/components/UiKit/components/InputString';
import PageBox from 'ts/components/Page/Box'; import PageBox from 'ts/components/Page/Box';
import Title from 'ts/components/Title'; import Title from 'ts/components/Title';
@ -10,13 +10,22 @@ import formStore from '../store/Form';
const Common = observer((): React.ReactElement | null => { const Common = observer((): React.ReactElement | null => {
return ( return (
<> <>
<Title title="Другие данные"/> <Title title="Префиксы ссылок"/>
<PageBox> <PageBox>
<UiKitInputNumber <InputString
title="Ссылка на таск-трекер" title="Для номеров задач"
value={formStore.state.linksPrefixForTasks} value={formStore.state?.linksPrefix?.task}
onChange={(jiraLink: number) => { placeholder="https://jira.com/secure/RapidBoard.jspa?task="
formStore.updateState('linksPrefixForTasks', jiraLink); onChange={(value: string) => {
formStore.updateState('linksPrefix.task', value);
}}
/>
<InputString
title="Для 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> </PageBox>

View file

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { IEmployees } from 'ts/interfaces/UserSetting';
import UiKitButtonMenu from 'ts/components/UiKit/components/ButtonMenu'; import UiKitButtonMenu from 'ts/components/UiKit/components/ButtonMenu';
import PageWrapper from 'ts/components/Page/wrapper'; import PageWrapper from 'ts/components/Page/wrapper';
import PageColumn from 'ts/components/Page/column'; import PageColumn from 'ts/components/Page/column';
@ -12,9 +13,7 @@ import dataGripStore from 'ts/store/DataGrip';
import UserSetting from './User'; import UserSetting from './User';
import Salary from './Salary'; import Salary from './Salary';
import Common from './Common'; import Common from './Common';
import Filter from './Filter';
import { IEmployees } from '../interfaces/Setting';
import { getNewEmployeesSettings } from '../helpers/getEmptySettings'; import { getNewEmployeesSettings } from '../helpers/getEmptySettings';
import MailMap from './MailMap'; import MailMap from './MailMap';
import formStore from '../store/Form'; import formStore from '../store/Form';
@ -49,9 +48,8 @@ const SettingForm = observer((response: any): React.ReactElement | null => {
<> <>
<PageWrapper> <PageWrapper>
<PageColumn> <PageColumn>
<Filter />
<Salary />
<Common /> <Common />
<Salary />
</PageColumn> </PageColumn>
<PageColumn> <PageColumn>
<Title title="Индивидуальные настройки"/> <Title title="Индивидуальные настройки"/>
@ -73,7 +71,7 @@ const SettingForm = observer((response: any): React.ReactElement | null => {
]); ]);
}} }}
> >
Добавить пользователя Добавить сотрудника
</UiKitButtonMenu> </UiKitButtonMenu>
</div> </div>
)} )}

View file

@ -69,6 +69,25 @@ const Common = observer((): React.ReactElement | null => {
formStore.updateState('defaultSalary.workDaysInWeek', workDaysInWeek); formStore.updateState('defaultSalary.workDaysInWeek', workDaysInWeek);
}} }}
/> />
<UiKitSwitch
title="Рабочие дни"
value={defaultSalary.workDaysInWeek.map((v: number, i: number) => v ? (i + 1) : null)}
options={[
{ id: 1, title: 'Пн' },
{ id: 2, title: 'Вт' },
{ id: 3, title: 'Ср' },
{ id: 4, title: 'Чт' },
{ id: 5, title: 'Пт' },
{ id: 6, title: 'Сб' },
{ id: 7, title: 'Вс' },
]}
onChange={(workDaysInWeek: number[]) => {
const formattedValue = (new Array(7)).fill(0)
.map((v: number, i: number) => workDaysInWeek.includes(i + 1));
console.log(formattedValue);
formStore.updateState('defaultSalary.workDaysInWeek', formattedValue);
}}
/>
</PageBox> </PageBox>
</> </>
); );

View file

@ -1,12 +1,12 @@
import React from 'react'; import React from 'react';
import { IEmployees, IEmployeesSalary } from 'ts/interfaces/UserSetting';
import UiKitButton from 'ts/components/UiKit/components/Button'; import UiKitButton from 'ts/components/UiKit/components/Button';
import confirm from 'ts/components/ModalWindow/store/Confirm'; import confirm from 'ts/components/ModalWindow/store/Confirm';
import PageBox from 'ts/components/Page/Box'; import PageBox from 'ts/components/Page/Box';
import Title from 'ts/components/Title'; import Title from 'ts/components/Title';
import { IEmployees, IEmployeesSalary } from '../interfaces/Setting'; import { getNewEmploymentContract } from '../helpers/getEmptySettings';
import { getNewSalarySettings } from '../helpers/getEmptySettings';
import UserSalary from './UserSalary'; import UserSalary from './UserSalary';
import formStore from '../store/Form'; import formStore from '../store/Form';
import style from '../styles/index.module.scss'; import style from '../styles/index.module.scss';
@ -60,7 +60,7 @@ function UserSetting({
...user, ...user,
salary: [ salary: [
...user.salary, ...user.salary,
getNewSalarySettings(formStore.state), getNewEmploymentContract(formStore.state),
], ],
}); });
}} }}

View file

@ -1,5 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { IEmployeesSalary } from 'ts/interfaces/UserSetting';
import UiKitInputNumber from 'ts/components/UiKit/components/InputNumber'; import UiKitInputNumber from 'ts/components/UiKit/components/InputNumber';
import UiKitColumns from 'ts/components/UiKit/components/Columns'; import UiKitColumns from 'ts/components/UiKit/components/Columns';
import UiKitSwitch from 'ts/components/UiKit/components/Switch'; import UiKitSwitch from 'ts/components/UiKit/components/Switch';
@ -8,7 +9,6 @@ import UiKitDate from 'ts/components/UiKit/components/Date';
import confirm from 'ts/components/ModalWindow/store/Confirm'; import confirm from 'ts/components/ModalWindow/store/Confirm';
import Title from 'ts/components/Title'; import Title from 'ts/components/Title';
import { IEmployeesSalary } from '../interfaces/Setting';
import style from '../styles/index.module.scss'; import style from '../styles/index.module.scss';
interface IUserSalaryProps { interface IUserSalaryProps {

View file

@ -1,30 +1,31 @@
import { ISetting, IEmployees, IEmployeesSalary } from '../interfaces/Setting'; import { IUserSetting, IEmployees, IEmployeesSalary } from 'ts/interfaces/UserSetting';
import ICommit from 'ts/interfaces/Commit'; import ICommit from 'ts/interfaces/Commit';
let DEFAULT_VALUES: any = {}; let DEFAULT_VALUES: any = {};
export function setDefaultValues(firstCommit: ICommit, lastCommit: ICommit) { export function setDefaultValues(firstCommit: ICommit, lastCommit: ICommit) {
DEFAULT_VALUES = { DEFAULT_VALUES = {
minCommits: 20,
from: firstCommit.timestamp, from: firstCommit.timestamp,
to: lastCommit.timestamp, to: lastCommit.timestamp,
}; };
} }
export function getNewSalarySettings(settings: ISetting): IEmployeesSalary { export function getNewEmploymentContract(settings: IUserSetting): IEmployeesSalary {
return { return {
id: Math.random(), id: Math.random(),
value: settings.defaultSalary.value, value: settings.defaultSalary.value,
currency: settings.defaultSalary.currency, currency: settings.defaultSalary.currency,
workDaysInYear: settings.defaultSalary.workDaysInYear, workDaysInYear: settings.defaultSalary.workDaysInYear,
vacationDaysInYear: settings.defaultSalary.vacationDaysInYear, vacationDaysInYear: settings.defaultSalary.vacationDaysInYear,
workDaysInWeek: settings.defaultSalary.workDaysInWeek, workDaysInWeek: [...settings.defaultSalary.workDaysInWeek],
from: settings.defaultFilters.from, from: DEFAULT_VALUES.from,
type: 'full',
}; };
} }
export function getNewEmployeesSettings( export function getNewEmployeesSettings(
name: string, name: string,
settings: ISetting, settings: IUserSetting,
order: number, order: number,
): IEmployees { ): IEmployees {
return { return {
@ -32,24 +33,26 @@ export function getNewEmployeesSettings(
name, name,
order, order,
salary: [ salary: [
getNewSalarySettings(settings), getNewEmploymentContract(settings),
], ],
}; };
} }
export default function getEmptySettings(): ISetting { export default function getEmptySettings(): IUserSetting {
return { return {
defaultFilters: { ...DEFAULT_VALUES }, version: 1,
filters: { ...DEFAULT_VALUES },
defaultSalary: { defaultSalary: {
value: 180000, value: 180000,
currency: 'RUB', currency: 'RUB',
workDaysInYear: 247, workDaysInYear: 247,
vacationDaysInYear: 28, vacationDaysInYear: 28,
workDaysInWeek: 5, workDaysInWeek: [1, 1, 1, 1, 1, 0, 0],
type: 'full', type: 'full',
}, },
linksPrefixForTasks: '', linksPrefix: {
task: 'https://jira.com/secure/RapidBoard.jspa?task=',
pr: 'https://bitbucket.com/projects/assayo/repos/frontend/pull-requests/',
},
employees: [], employees: [],
}; };
} }

View file

@ -1,10 +1,10 @@
import IHashMap from 'ts/interfaces/HashMap'; import IHashMap from 'ts/interfaces/HashMap';
import { IEmployees, IUserSetting } from 'ts/interfaces/UserSetting';
import { IEmployees, ISetting } from '../interfaces/Setting';
import getEmptySettings from '../helpers/getEmptySettings'; import getEmptySettings from '../helpers/getEmptySettings';
class Settings { class Settings {
customSettings: ISetting = getEmptySettings(); customSettings: IUserSetting = getEmptySettings();
employeesByName: IHashMap<IEmployees> = {}; employeesByName: IHashMap<IEmployees> = {};
@ -12,7 +12,7 @@ class Settings {
salaryInDay: number = 180000 / 22; salaryInDay: number = 180000 / 22;
update(customSettings?: ISetting) { update(customSettings?: IUserSetting) {
this.customSettings = customSettings || getEmptySettings(); this.customSettings = customSettings || getEmptySettings();
this.employeesByName = this.customSettings.employees.reduce((acc, user) => { this.employeesByName = this.customSettings.employees.reduce((acc, user) => {
@ -21,7 +21,7 @@ class Settings {
}, {}); }, {});
const salary = this.customSettings.defaultSalary; const salary = this.customSettings.defaultSalary;
this.workDaysInMonth = Math.ceil(4.3 * salary.workDaysInWeek); this.workDaysInMonth = Math.ceil(4.3 * 5); // TODO: salary.workDaysInWeek);
this.salaryInDay = salary.value / this.workDaysInMonth; this.salaryInDay = salary.value / this.workDaysInMonth;
} }
@ -40,7 +40,7 @@ class Settings {
const user = this.employeesByName[name]; const user = this.employeesByName[name];
if (!user) return this.salaryInDay; if (!user) return this.salaryInDay;
const salary = user.salary[user.salary.length - 1]; const salary = user.salary[user.salary.length - 1];
const workDaysInMonth = Math.ceil(4.3 * salary.workDaysInWeek); const workDaysInMonth = Math.ceil(4.3 * 5); // TODO: * salary.workDaysInWeek);
return salary.value / workDaysInMonth; return salary.value / workDaysInMonth;
} }
} }

View file

@ -1,39 +0,0 @@
export interface IEmployeesSalary {
id: number;
value: number;
currency: string;
workDaysInYear: number;
vacationDaysInYear: number;
workDaysInWeek: number;
from: string;
}
export interface IEmployees {
id: number;
name: string;
order: number;
salary: IEmployeesSalary[];
}
export interface ISetting {
defaultFilters: {
minCommits: number;
from: string;
to: string;
};
filters: {
minCommits: number;
from: string;
to: string;
};
defaultSalary: {
value: number;
currency: string;
workDaysInYear: number;
vacationDaysInYear: number;
workDaysInWeek: number;
type: 'full' | 'part';
};
linksPrefixForTasks: string;
employees: IEmployees[];
}

View file

@ -1,8 +1,10 @@
import { action, makeObservable } from 'mobx'; import { action, makeObservable } from 'mobx';
import { IUserSetting } from 'ts/interfaces/UserSetting';
import Form from 'ts/store/Form'; import Form from 'ts/store/Form';
import settingsApi from 'ts/api/settings'; import settingsApi from 'ts/api/settings';
import { ISetting } from '../interfaces/Setting'; import userSettings from 'ts/store/UserSettings';
import notificationsStore from 'ts/components/Notifications/store';
class FormStore extends Form { class FormStore extends Form {
constructor() { constructor() {
@ -12,10 +14,12 @@ class FormStore extends Form {
}); });
} }
save(body: ISetting): Promise<any> { save(body: IUserSetting): Promise<any> {
const { saveSettings } = settingsApi; const { saveSettings } = settingsApi;
return this.submit(saveSettings, body, false) return this.submit(saveSettings, body, false)
.then((response: any) => { .then((response: any) => {
notificationsStore.show('Настройки сохранены');
userSettings.loadUserSettings();
this.setInitState(this.state); this.setInitState(this.state);
return Promise.resolve(response); return Promise.resolve(response);
}); });

View file

@ -35,7 +35,7 @@ function AuthorView({ response, updateSort }: IAuthorViewProps) {
const textWork = localization.get('page.team.author.worked'); const textWork = localization.get('page.team.author.worked');
const textLosses = localization.get('page.team.author.losses'); const textLosses = localization.get('page.team.author.losses');
const daysWorked = getOptions({ order: [textWork, textLosses] }); const daysWorked = getOptions({ order: [textWork, textLosses], suffix: 'дней' });
const taskChart = getOptions({ max: getMaxByLength(response, 'tasks'), suffix: 'задач' }); const taskChart = getOptions({ max: getMaxByLength(response, 'tasks'), suffix: 'задач' });
const commitsChart = getOptions({ max: getMax(response, 'commits') }); const commitsChart = getOptions({ max: getMax(response, 'commits') });
const typeChart = getOptions({ order: dataGripStore.dataGrip.type.list }); const typeChart = getOptions({ order: dataGripStore.dataGrip.type.list });

View file

@ -4,6 +4,7 @@ import { observer } from 'mobx-react-lite';
import { IPagination } from 'ts/interfaces/Pagination'; import { IPagination } from 'ts/interfaces/Pagination';
import dataGripStore from 'ts/store/DataGrip'; import dataGripStore from 'ts/store/DataGrip';
import { getShortDateRange } from 'ts/helpers/formatter'; import { getShortDateRange } from 'ts/helpers/formatter';
import localization from 'ts/helpers/Localization';
import UiKitButton from 'ts/components/UiKit/components/Button'; import UiKitButton from 'ts/components/UiKit/components/Button';
import UiKitSelect from 'ts/components/UiKit/components/Select'; import UiKitSelect from 'ts/components/UiKit/components/Select';
@ -60,7 +61,7 @@ const Tempo = observer((): React.ReactElement => {
if (!partOfData?.length) return (<NothingFound />); if (!partOfData?.length) return (<NothingFound />);
return ( return (
<> <>
<Title title="Фильтры"/> <Title title={localization.get('common.filters')} />
<PageWrapper> <PageWrapper>
<div className={style.tempo_page_filters}> <div className={style.tempo_page_filters}>
<UiKitButton <UiKitButton

View file

@ -14,6 +14,12 @@ 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 Column from 'ts/components/Table/components/Column';
import LineChart from 'ts/components/LineChart';
import getOptions from 'ts/components/LineChart/helpers/getOptions';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import { getDate } from 'ts/helpers/formatter'; import { getDate } from 'ts/helpers/formatter';
import style from '../styles/quiz.module.scss'; import style from '../styles/quiz.module.scss';
@ -41,6 +47,7 @@ const Top = observer((): React.ReactElement => {
const maxMessageLength = [...tracksAuth] const maxMessageLength = [...tracksAuth]
.sort((a: any, b: any) => b.maxMessageLength - a.maxMessageLength) .sort((a: any, b: any) => b.maxMessageLength - a.maxMessageLength)
.map((item: any) => ({ title: item.author, value: item.maxMessageLength })); .map((item: any) => ({ title: item.author, value: item.maxMessageLength }));
const chartMessageLength = getOptions({ max: maxMessageLength[0].value, suffix: 'сиволов' });
const authors = dataGripStore.dataGrip.author.statistic.map((statistic: any) => { const authors = dataGripStore.dataGrip.author.statistic.map((statistic: any) => {
const achievements = getAchievementByAuthor(statistic.author); const achievements = getAchievementByAuthor(statistic.author);
@ -75,8 +82,37 @@ const Top = observer((): React.ReactElement => {
<> <>
<Title title="Скорость закрытия задач"/> <Title title="Скорость закрытия задач"/>
<Races tracks={tracks} /> <Races tracks={tracks} />
<Title title="Максимальная длинна подписи коммита"/> <Title title="Максимальная длинна подписи коммита"/>
<PageWrapper template="table">
<Table rows={maxMessageLength}>
<Column
isFixed
template={ColumnTypesEnum.STRING}
title="Сотрудник"
properties="title"
width={260}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
properties="value"
width={40}
/>
<Column
title="Количество символов"
properties="value"
template={(messageLength: number) => (
<LineChart
options={chartMessageLength}
value={messageLength}
/>
)}
/>
</Table>
</PageWrapper>
<Tv100And1 rows={maxMessageLength} /> <Tv100And1 rows={maxMessageLength} />
{authors} {authors}
<PageWrapper> <PageWrapper>
<div style={{ whiteSpace: 'normal' }} > <div style={{ whiteSpace: 'normal' }} >

View file

@ -9,7 +9,7 @@ import CardWithIcon from 'ts/components/CardWithIcon';
import Description from 'ts/components/Description'; import Description from 'ts/components/Description';
import dataGripStore from 'ts/store/DataGrip'; import dataGripStore from 'ts/store/DataGrip';
import settingsStore from 'ts/store/Settings'; import userSettings from 'ts/store/UserSettings';
import { getShortMoney } from 'ts/helpers/formatter'; import { getShortMoney } from 'ts/helpers/formatter';
const Total = observer((): React.ReactElement => { const Total = observer((): React.ReactElement => {
@ -20,7 +20,7 @@ const Total = observer((): React.ReactElement => {
return speed + dataGripStore.dataGrip.author.statisticByName[name].taskInDay; return speed + dataGripStore.dataGrip.author.statisticByName[name].taskInDay;
}, 0).toFixed(1); }, 0).toFixed(1);
const moneySpeed = employment.active.reduce((speed: number, name: string) => { const moneySpeed = employment.active.reduce((speed: number, name: string) => {
return speed + (settingsStore.salary[name] || settingsStore.defaultSalary); return speed + userSettings.getCurrentSalaryInMonth(name);
}, 0); }, 0);
return ( return (

View file

@ -3,6 +3,7 @@ import { observer } from 'mobx-react-lite';
import { IPaginationRequest, IPagination } from 'ts/interfaces/Pagination'; import { IPaginationRequest, IPagination } from 'ts/interfaces/Pagination';
import dataGripStore from 'ts/store/DataGrip'; import dataGripStore from 'ts/store/DataGrip';
import localization from 'ts/helpers/Localization';
import PageWrapper from 'ts/components/Page/wrapper'; import PageWrapper from 'ts/components/Page/wrapper';
import DataLoader from 'ts/components/DataLoader'; import DataLoader from 'ts/components/DataLoader';
@ -125,7 +126,7 @@ const Tree = observer((): React.ReactElement => {
return ( return (
<> <>
<Title title="Фильтры"/> <Title title={localization.get('common.filters')} />
<TreeFilters/> <TreeFilters/>
<Title title="Дерево проекта с учётом выбранных фильтров"/> <Title title="Дерево проекта с учётом выбранных фильтров"/>
<PageWrapper template="table"> <PageWrapper template="table">

View file

@ -2,7 +2,7 @@ import React from 'react';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import dataGripStore from 'ts/store/DataGrip'; import dataGripStore from 'ts/store/DataGrip';
import UiKitSelect from 'ts/components/UiKit/components/Select'; import UiKitSelect from 'ts/components/UiKit/components/SelectWithButtons';
import UiKitInputNumber from 'ts/components/UiKit/components/InputNumber'; import UiKitInputNumber from 'ts/components/UiKit/components/InputNumber';
import treeStore from '../store/Tree'; import treeStore from '../store/Tree';
@ -10,7 +10,7 @@ import style from '../styles/filters.module.scss';
const TreeFilters = observer((): React.ReactElement => { const TreeFilters = observer((): React.ReactElement => {
const authors = dataGripStore.dataGrip.author.list; const authors = dataGripStore.dataGrip.author.list;
const options = authors.map((title: string, id: number) => ({ id, title })); const options = authors.map((title: string, id: number) => ({ id: id + 1, title }));
options.unshift({ id: 0, title: 'Все сотрудники' }); options.unshift({ id: 0, title: 'Все сотрудники' });
return ( return (

View file

@ -1,32 +0,0 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import dataGripStore from 'ts/store/DataGrip';
import RecommendationsWrapper from 'ts/components/Recommendations/wrapper';
import YearChart from 'ts/components/YearChart';
import Title from 'ts/components/Title';
import PageWrapper from 'ts/components/Page/wrapper';
const Year = observer((): React.ReactElement => {
const authors = dataGripStore.dataGrip.author.statistic;
const statistic = dataGripStore.dataGrip.timestamp.statistic;
const max = statistic.commitsByTimestampCounter.max;
const recommendations = dataGripStore.dataGrip.recommendations.team?.byTimestamp;
return (
<>
<RecommendationsWrapper recommendations={recommendations} />
<Title title="Фильтры"/>
<PageWrapper template="table">
<YearChart
maxCommits={max}
authors={authors}
wordDays={statistic.allCommitsByTimestamp}
/>
</PageWrapper>
</>
);
});
export default Year;

View file

@ -12,8 +12,9 @@ import Total from './components/Total';
import Tree from './components/Tree'; import Tree from './components/Tree';
import Type from './components/Type'; import Type from './components/Type';
import Week from './components/Week'; import Week from './components/Week';
import Year from './components/Year'; import Month from './components/Month';
import Top from './components/Top'; import Top from './components/Top';
import Pr from './components/PR';
function Team() { function Team() {
const { type, page } = useParams<any>(); const { type, page } = useParams<any>();
@ -23,18 +24,19 @@ function Team() {
return ( return (
<> <>
{page === 'author' && <Author/>}
{page === 'changes' && <Changes/>}
{page === 'timestamp' && <Commits/>}
{page === 'hours' && <Hours/>}
{page === 'words' && <PopularWords/>}
{page === 'scope' && <Scope/>}
{page === 'month' && <Week/>}
{page === 'year' && <Year/>}
{page === 'total' && <Total/>} {page === 'total' && <Total/>}
{page === 'tree' && <Tree/>} {page === 'scope' && <Scope/>}
{page === 'author' && <Author/>}
{page === 'type' && <Type/>} {page === 'type' && <Type/>}
{page === 'sprint' && <Tempo/>} {page === 'pr' && <Pr/>}
{page === 'day' && <Tempo/>}
{page === 'week' && <Week/>}
{page === 'month' && <Month/>}
{page === 'hours' && <Hours/>}
{page === 'tree' && <Tree/>}
{page === 'commits' && <Commits/>}
{page === 'changes' && <Changes/>}
{page === 'words' && <PopularWords/>}
{page === 'top' && <Top/>} {page === 'top' && <Top/>}
</> </>
); );

View file

@ -7,6 +7,30 @@ import style from './styles/index.module.scss';
function Welcome() { function Welcome() {
const command = 'git --no-pager log --numstat --oneline --all --reverse --date=iso-strict --pretty=format:"%ad>%cN>%cE>%s" | sed -e \'s/\\\\/\\\\\\\\/g\' | sed -e \'s/`/"/g\' | sed -e \'s/^/report.push(\\`/g\' | sed \'s/$/\\`\\);/g\' | sed \'s/\\$/_/g\' > log.txt\n'; const command = 'git --no-pager log --numstat --oneline --all --reverse --date=iso-strict --pretty=format:"%ad>%cN>%cE>%s" | sed -e \'s/\\\\/\\\\\\\\/g\' | sed -e \'s/`/"/g\' | sed -e \'s/^/report.push(\\`/g\' | sed \'s/$/\\`\\);/g\' | sed \'s/\\$/_/g\' > log.txt\n';
return ( return (
<>
<h4 className={style.welcome_warning}>
<p>
{'Сервис '}
<span className={style.welcome_warning_bold}>НЕ ХРАНИТ</span>
{' и '}
<span className={style.welcome_warning_bold}>НЕ ПЕРЕДАЁТ</span>
{' ваши данные. Все расчёты выполняются локально в вашем браузере прямо на вашей машине.'}
</p>
<p>
{'Сервис '}
<span className={style.welcome_warning_bold}>НЕ СОБИРАЕТ СТАТИСТИКУ</span>
{' по проектам. Вы можете отключить интернет, проверить трафик и даже собрать локальный билд из '}
<a
href='https://github.com/bakhirev/assayo'
target="_blank"
rel="noreferrer"
className={style.welcome_warning_link}
>
исходников
</a>
{'.'}
</p>
</h4>
<section className={style.welcome}> <section className={style.welcome}>
<div className={style.welcome_row}> <div className={style.welcome_row}>
<h2 className={style.welcome_first_title}> <h2 className={style.welcome_first_title}>
@ -27,13 +51,14 @@ function Welcome() {
to="https://git-scm.com/docs/gitmailmap"> to="https://git-scm.com/docs/gitmailmap">
.mailmap .mailmap
</Link> </Link>
{' в проект, чтобы обьединить статистику по пользователям.'} {' в проект, чтобы обьединить статистику по сотрудникам.'}
</p> </p>
<h2 className={style.welcome_last_title}> <h2 className={style.welcome_last_title}>
Перетащите файл log.txt на эту страницу Перетащите файл log.txt на эту страницу
</h2> </h2>
</div> </div>
</section> </section>
</>
); );
} }

View file

@ -6,8 +6,8 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: calc(100vw - 16px); width: calc(100vw - 20px);
height: 100vh; height: calc(100vh - 60px);
padding: 0; padding: 0;
margin: 0; margin: 0;
@ -22,6 +22,29 @@
width: auto; width: auto;
} }
&_warning {
font-weight: 100;
font-size: var(--font-s);
width: 100%;
margin: 0 auto;
padding: 6px;
box-sizing: border-box;
line-height: 1.5;
text-align: center;
border-bottom: 3px solid red;
background-color: #FCDADA;
&_bold {
font-weight: bold;
color: red;
}
&_link {
text-decoration: underline;
color: var(--color-button);
}
}
&_first_title, &_first_title,
&_last_title { &_last_title {
font-size: 42px; font-size: 42px;
@ -65,6 +88,7 @@
.welcome { .welcome {
display: block; display: block;
width: 100%; width: 100%;
height: auto;
padding: 32px 0 0 0; padding: 32px 0 0 0;
&_first_title, &_first_title,

View file

@ -80,7 +80,6 @@ class DataGripStore implements IDataGripStore {
updateChars() { // todo: remove, never use updateChars() { // todo: remove, never use
console.log('need update data TODO'); console.log('need update data TODO');
return;
dataGrip.updateByFilters(); dataGrip.updateByFilters();
if (!dataGrip.author.list.length) return; if (!dataGrip.author.list.length) return;
achievements.updateByDataGrip(dataGrip.author.statistic); achievements.updateByDataGrip(dataGrip.author.statistic);

View file

@ -156,7 +156,6 @@ class FormStore implements IFormStore {
if (this.isLocked) { if (this.isLocked) {
return Promise.resolve(); return Promise.resolve();
} }
console.log(data);
return this.validation(data) return this.validation(data)
.then(action(() => { .then(action(() => {
this.isLoading = true; this.isLoading = true;