TASK-0000 feat(main): update all project on github
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
4544
build/assets/log.txt
|
@ -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>
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 24 KiB |
|
@ -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
|
|
||||||
*/
|
|
16
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 24 KiB |
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadApplication();
|
userSettings.loadUserSettings().then(() => {
|
||||||
|
loadApplication();
|
||||||
|
});
|
||||||
|
|
|
@ -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> {
|
||||||
localStorage.setItem('settings', JSON.stringify(body));
|
const defaultSettings = getEmptySettings();
|
||||||
|
if (JSON.stringify(defaultSettings) === JSON.stringify(body)) {
|
||||||
|
localStorage.removeItem('settings');
|
||||||
|
} else {
|
||||||
|
localStorage.setItem('settings', JSON.stringify(body));
|
||||||
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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('Текст скопирован');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Копировать
|
Копировать
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}>
|
||||||
|
<ExternalLink
|
||||||
|
link={`${userSettings?.settings?.linksPrefix?.task || '/'}${task}`}
|
||||||
|
text={task}
|
||||||
|
/>
|
||||||
|
{prId && (
|
||||||
|
<ExternalLink
|
||||||
|
link={`${userSettings?.settings?.linksPrefix?.pr || '/'}${prId}`}
|
||||||
|
text="PR"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<CommitInfo commits={commits}/>
|
<CommitInfo commits={commits}/>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
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])))
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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}>
|
||||||
Он вносил правки не каждый рабочий день и получил статус Помошник.
|
Он вносил правки не каждый рабочий день и получил статус Помошник.
|
||||||
|
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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,
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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_wrapper}`}
|
||||||
className={`${style.table}`}
|
>
|
||||||
>
|
<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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 ?? ''}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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];
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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: коммитов
|
||||||
|
|
|
@ -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();
|
||||||
commitsByDayAndHour[commit.day][commit.hours] += 1;
|
try {
|
||||||
|
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)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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() {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 || 'неопределенна',
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ export default function getUserInfo(message: any): ICommit {
|
||||||
message: text || '',
|
message: text || '',
|
||||||
|
|
||||||
task: 'беседа',
|
task: 'беседа',
|
||||||
|
taskNumber: '',
|
||||||
type: 'не подписан',
|
type: 'не подписан',
|
||||||
scope: 'неопределенна',
|
scope: 'неопределенна',
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
if (statistic.hours.filter((hour: number) => hour >= 15).length > (statistic.commits * 0.7)) list.push('commitsAfter1500');
|
||||||
|
// Раняя пташка - 70% коммитов до обеда
|
||||||
|
if (statistic.hours.filter((hour: number) => hour <= 13).length > (statistic.commits * 0.7)) list.push('commitsBefore1500');
|
||||||
|
}
|
||||||
|
|
||||||
// Сова - 70% коммитов после 15:00
|
|
||||||
if (statistic.hours.filter((hour: number) => hour >= 15).length > (statistic.commits * 0.7)) list.push('commitsAfter1500');
|
|
||||||
// Раняя пташка - 70% коммитов до обеда
|
|
||||||
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);
|
||||||
|
|
|
@ -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],
|
||||||
};
|
};
|
||||||
|
|
|
@ -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',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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'}*/}
|
||||||
/>
|
{/*/>*/}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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="«Взять гет» в данном случае означает первым оставить коммит к задаче с «красивым» номером."/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</PageColumn>
|
</PageColumn>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
|
|
@ -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/>}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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[];
|
|
||||||
}
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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' }} >
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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;
|
|
|
@ -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/>}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,33 +7,58 @@ 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 (
|
||||||
<section className={style.welcome}>
|
<>
|
||||||
<div className={style.welcome_row}>
|
<h4 className={style.welcome_warning}>
|
||||||
<h2 className={style.welcome_first_title}>
|
<p>
|
||||||
Выполните команду в корне вашего проекта
|
{'Сервис '}
|
||||||
</h2>
|
<span className={style.welcome_warning_bold}>НЕ ХРАНИТ</span>
|
||||||
<Console
|
{' и '}
|
||||||
className={style.welcome_console}
|
<span className={style.welcome_warning_bold}>НЕ ПЕРЕДАЁТ</span>
|
||||||
textForCopy={command}
|
{' ваши данные. Все расчёты выполняются локально в вашем браузере прямо на вашей машине.'}
|
||||||
/>
|
|
||||||
<p className={style.welcome_description}>
|
|
||||||
Git создаст файл log.txt.
|
|
||||||
Он содержит данные для построения отчёта.
|
|
||||||
Или git shortlog -s -n -e если отчёт вам не нужен.
|
|
||||||
Добавьте файл
|
|
||||||
<Link
|
|
||||||
className={`${style.welcome_link}`}
|
|
||||||
target="_blank"
|
|
||||||
to="https://git-scm.com/docs/gitmailmap">
|
|
||||||
.mailmap
|
|
||||||
</Link>
|
|
||||||
{' в проект, чтобы обьединить статистику по пользователям.'}
|
|
||||||
</p>
|
</p>
|
||||||
<h2 className={style.welcome_last_title}>
|
<p>
|
||||||
Перетащите файл log.txt на эту страницу
|
{'Сервис '}
|
||||||
</h2>
|
<span className={style.welcome_warning_bold}>НЕ СОБИРАЕТ СТАТИСТИКУ</span>
|
||||||
</div>
|
{' по проектам. Вы можете отключить интернет, проверить трафик и даже собрать локальный билд из '}
|
||||||
</section>
|
<a
|
||||||
|
href='https://github.com/bakhirev/assayo'
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className={style.welcome_warning_link}
|
||||||
|
>
|
||||||
|
исходников
|
||||||
|
</a>
|
||||||
|
{'.'}
|
||||||
|
</p>
|
||||||
|
</h4>
|
||||||
|
<section className={style.welcome}>
|
||||||
|
<div className={style.welcome_row}>
|
||||||
|
<h2 className={style.welcome_first_title}>
|
||||||
|
Выполните команду в корне вашего проекта
|
||||||
|
</h2>
|
||||||
|
<Console
|
||||||
|
className={style.welcome_console}
|
||||||
|
textForCopy={command}
|
||||||
|
/>
|
||||||
|
<p className={style.welcome_description}>
|
||||||
|
Git создаст файл log.txt.
|
||||||
|
Он содержит данные для построения отчёта.
|
||||||
|
Или git shortlog -s -n -e если отчёт вам не нужен.
|
||||||
|
Добавьте файл
|
||||||
|
<Link
|
||||||
|
className={`${style.welcome_link}`}
|
||||||
|
target="_blank"
|
||||||
|
to="https://git-scm.com/docs/gitmailmap">
|
||||||
|
.mailmap
|
||||||
|
</Link>
|
||||||
|
{' в проект, чтобы обьединить статистику по сотрудникам.'}
|
||||||
|
</p>
|
||||||
|
<h2 className={style.welcome_last_title}>
|
||||||
|
Перетащите файл log.txt на эту страницу
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|