TASK-0000 feat(main): update all project on github
|
@ -1,17 +1,17 @@
|
|||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.a871f7d5.css",
|
||||
"main.js": "./static/js/main.16986165.js",
|
||||
"main.css": "./static/css/main.c2f3798a.css",
|
||||
"main.js": "./static/js/main.0fa5ec0a.js",
|
||||
"static/media/car.png": "./static/media/car.b8dd8738e37fe866285f.png",
|
||||
"index.html": "./index.html",
|
||||
"static/media/warning.svg": "./static/media/warning.e39a87773603f3ab157f.svg",
|
||||
"static/media/info.svg": "./static/media/info.954631f6b19e3fe9c495.svg",
|
||||
"static/media/alert.svg": "./static/media/alert.41e2b99c481139c13074.svg",
|
||||
"main.a871f7d5.css.map": "./static/css/main.a871f7d5.css.map",
|
||||
"main.16986165.js.map": "./static/js/main.16986165.js.map"
|
||||
"main.c2f3798a.css.map": "./static/css/main.c2f3798a.css.map",
|
||||
"main.0fa5ec0a.js.map": "./static/js/main.0fa5ec0a.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.a871f7d5.css",
|
||||
"static/js/main.16986165.js"
|
||||
"static/css/main.c2f3798a.css",
|
||||
"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": {
|
||||
"version": "1.0.30001449",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001449.tgz",
|
||||
"integrity": "sha512-CPB+UL9XMT/Av+pJxCKGhdx+yg1hzplvFJQlJ2n68PyQGMz9L/E2zCyLdOL8uasbouTUgnPl+y0tccI/se+BEw==",
|
||||
"version": "1.0.30001519",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz",
|
||||
"integrity": "sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
@ -5710,6 +5710,10 @@
|
|||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -22205,9 +22209,9 @@
|
|||
}
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001449",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001449.tgz",
|
||||
"integrity": "sha512-CPB+UL9XMT/Av+pJxCKGhdx+yg1hzplvFJQlJ2n68PyQGMz9L/E2zCyLdOL8uasbouTUgnPl+y0tccI/se+BEw=="
|
||||
"version": "1.0.30001519",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz",
|
||||
"integrity": "sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg=="
|
||||
},
|
||||
"case-sensitive-paths-webpack-plugin": {
|
||||
"version": "2.4.0",
|
||||
|
|
|
@ -63,5 +63,23 @@
|
|||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<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>
|
||||
</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 { render } from 'react-dom';
|
||||
|
||||
import ru from './ts/config/translations/ru';
|
||||
import Authorization from './ts/pages/Authorization';
|
||||
import ru from 'ts/config/translations/ru';
|
||||
import Authorization from 'ts/pages/Authorization';
|
||||
import userSettings from 'ts/store/UserSettings';
|
||||
import Notifications from 'ts/components/Notifications';
|
||||
|
||||
import './styles/index.scss';
|
||||
|
||||
// eslint-disable-next-line
|
||||
|
@ -31,6 +34,7 @@ function renderReactApplication() {
|
|||
<React.StrictMode>
|
||||
<HashRouter>
|
||||
<Authorization/>
|
||||
<Notifications/>
|
||||
</HashRouter>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root'),
|
||||
|
@ -55,4 +59,6 @@ function loadApplication() {
|
|||
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';
|
||||
|
||||
export default {
|
||||
loadSettings(): Promise<ISetting> {
|
||||
loadSettings(): Promise<IUserSetting> {
|
||||
const defaultSettings = getEmptySettings();
|
||||
const response = localStorage.getItem('settings');
|
||||
return response
|
||||
? Promise.resolve(JSON.parse(response))
|
||||
: Promise.resolve(getEmptySettings());
|
||||
const defaultResponse = () => {
|
||||
localStorage.removeItem('settings');
|
||||
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> {
|
||||
localStorage.setItem('settings', JSON.stringify(body));
|
||||
saveSettings(body: IUserSetting): Promise<any> {
|
||||
const defaultSettings = getEmptySettings();
|
||||
if (JSON.stringify(defaultSettings) === JSON.stringify(body)) {
|
||||
localStorage.removeItem('settings');
|
||||
} else {
|
||||
localStorage.setItem('settings', JSON.stringify(body));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
|
||||
import Button from 'ts/components/UiKit/components/Button';
|
||||
import notificationsStore from 'ts/components/Notifications/store';
|
||||
|
||||
function copyInBuffer(value?: string) {
|
||||
if (!value) return;
|
||||
|
@ -39,6 +40,7 @@ function Console({ className, textForCopy, children }: IConsoleProps) {
|
|||
className={`${style.console_copy}`}
|
||||
onClick={() => {
|
||||
copyInBuffer(textForCopy);
|
||||
notificationsStore.show('Текст скопирован');
|
||||
}}
|
||||
>
|
||||
Копировать
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
}
|
||||
|
||||
&_author,
|
||||
&_task,
|
||||
&_date,
|
||||
&_message {
|
||||
font-weight: 100;
|
||||
|
@ -27,17 +26,10 @@
|
|||
line-height: var(--font-m);
|
||||
}
|
||||
|
||||
&_task {
|
||||
font-size: var(--font-s);
|
||||
&_link {
|
||||
display: block;
|
||||
padding: 0 0 0 var(--space-l);
|
||||
margin: 0 0 var(--space-s) 0;
|
||||
|
||||
line-height: var(--font-s);
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
|
||||
color: var(--color-first);
|
||||
}
|
||||
|
||||
&_date,
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import React from 'react';
|
||||
|
||||
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 dataGrip from 'ts/helpers/DataGrip';
|
||||
|
||||
import style from './index.module.scss';
|
||||
|
||||
|
@ -36,11 +39,23 @@ function CommitInfo({ commits }: { commits: ICommit[] }): React.ReactElement {
|
|||
function TaskInfo({ tasks }: { tasks: ITask }): React.ReactElement {
|
||||
const items = Object.entries(tasks)
|
||||
.map(([task, commits]: [string, any]) => {
|
||||
const prId = dataGrip.pr.prByTask[task];
|
||||
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}/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
return (<>{items}</>);
|
||||
|
@ -57,7 +72,6 @@ function DayInfo({ day, order, events, timestamp }: IDayInfoProps): React.ReactE
|
|||
const firstCommit = events?.firstCommit?.[timestamp || ''] || [];
|
||||
const lastCommit = events?.lastCommit?.[timestamp || ''] || [];
|
||||
let taskNumber = 0;
|
||||
console.dir(firstCommit);
|
||||
|
||||
const items = Object.entries(day?.tasksByAuthor)
|
||||
.sort((a: any, b: any) => (order.indexOf(a[0]) - order.indexOf(b[0])))
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
@import '../../../styles/variables';
|
||||
|
||||
.description {
|
||||
white-space: normal;
|
||||
|
||||
&_title,
|
||||
&_text,
|
||||
&_list {
|
||||
|
@ -16,6 +18,7 @@
|
|||
line-height: var(--font-m);
|
||||
text-decoration: none;
|
||||
vertical-align: bottom;
|
||||
white-space: normal;
|
||||
color: #73809F;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
align-items: center;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 1;
|
||||
z-index: 3;
|
||||
background-color: white;
|
||||
|
||||
&_icon {
|
||||
|
|
|
@ -8,6 +8,7 @@ interface ILineProps {
|
|||
suffix?: string;
|
||||
color?: { first: string, second: string } | null;
|
||||
className?: string;
|
||||
formatter?: Function;
|
||||
}
|
||||
|
||||
function Line({
|
||||
|
@ -18,12 +19,15 @@ function Line({
|
|||
suffix,
|
||||
color,
|
||||
className,
|
||||
formatter,
|
||||
}: ILineProps): React.ReactElement | null {
|
||||
if (!width || width <= 0) return null;
|
||||
|
||||
const formattedTitle = title || '';
|
||||
const formattedValue = formatter?.(value);
|
||||
const formattedSuffix = suffix ? ` ${suffix}` : '';
|
||||
const formattedDescription = value
|
||||
? `${width}% (${value} ${suffix}) ${description || formattedTitle}`
|
||||
? `${width}% (${formattedValue}${formattedSuffix}) ${description || formattedTitle}`
|
||||
: `${width}% ${description || formattedTitle}`;
|
||||
|
||||
return (
|
||||
|
@ -50,6 +54,7 @@ Line.defaultProps = {
|
|||
suffix: '',
|
||||
color: null,
|
||||
className: '',
|
||||
formatter: (v: any) => v,
|
||||
};
|
||||
|
||||
export default Line;
|
||||
|
|
|
@ -7,6 +7,7 @@ interface IOptionsProps {
|
|||
other?: string;
|
||||
max?: number[] | number;
|
||||
limit?: number;
|
||||
formatter?: Function;
|
||||
}
|
||||
|
||||
export default function getOptions({
|
||||
|
@ -15,13 +16,15 @@ export default function getOptions({
|
|||
other,
|
||||
max,
|
||||
limit,
|
||||
formatter,
|
||||
}: IOptionsProps): IOptions {
|
||||
return {
|
||||
max: max instanceof Array ? Math.max(...max) : (max || 100),
|
||||
order: order || [],
|
||||
suffix: suffix || 'коммитов',
|
||||
otherTitle: other || 'Остальные',
|
||||
suffix: suffix ?? 'коммитов',
|
||||
otherTitle: other ?? 'Остальные',
|
||||
color: order?.length ? (new ColorGenerator(order)) : null,
|
||||
limit: limit || 15,
|
||||
formatter: formatter || ((v: any) => v),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -12,25 +12,27 @@ interface ILineChartProps {
|
|||
options: IOptions;
|
||||
value?: number;
|
||||
details?: IHashMap<number>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function LineChart({
|
||||
options,
|
||||
value,
|
||||
details,
|
||||
className,
|
||||
}: ILineChartProps): React.ReactElement | null {
|
||||
|
||||
if (value === 0) return null;
|
||||
|
||||
const width = Math.round((value ?? 100) * (100 / options.max));
|
||||
|
||||
if (!details) {
|
||||
return (
|
||||
<div className={style.line_chart}>
|
||||
<div className={`${style.line_chart} ${className || ''}`}>
|
||||
<Line
|
||||
value={value ?? 100}
|
||||
width={width}
|
||||
suffix={options.suffix}
|
||||
formatter={options.formatter}
|
||||
className={style.line_chart_item}
|
||||
/>
|
||||
</div>
|
||||
|
@ -46,13 +48,14 @@ function LineChart({
|
|||
width={item.width}
|
||||
color={options.color.get(item.title)}
|
||||
suffix={options.suffix}
|
||||
formatter={options.formatter}
|
||||
description={item.description}
|
||||
className={style.line_chart_sub_item}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className={style.line_chart}>
|
||||
<div className={`${style.line_chart} ${className || ''}`}>
|
||||
<div
|
||||
className={style.line_chart_item}
|
||||
style={{ width: `${width}%` }}
|
||||
|
@ -66,6 +69,7 @@ function LineChart({
|
|||
LineChart.defaultProps = {
|
||||
value: 100,
|
||||
details: undefined,
|
||||
className: '',
|
||||
};
|
||||
|
||||
export default LineChart;
|
||||
|
|
|
@ -5,6 +5,7 @@ export interface IOptions {
|
|||
otherTitle: string;
|
||||
color: any;
|
||||
limit: number;
|
||||
formatter?: Function;
|
||||
}
|
||||
|
||||
export interface ISubLine {
|
||||
|
|
|
@ -6,7 +6,7 @@ function IsStaff() {
|
|||
return (
|
||||
<>
|
||||
<p className={style.nothing_found_title}>
|
||||
Нет данных для этого пользователя
|
||||
Нет данных для этого сотрудника
|
||||
</p>
|
||||
<p className={style.nothing_found_text}>
|
||||
Он вносил правки не каждый рабочий день и получил статус Помошник.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export default interface IMessage {
|
||||
id?: number;
|
||||
title?: string;
|
||||
id: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
type?: 'error' | 'warning' | 'success' | 'info';
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ interface INotificationsStore {
|
|||
class NotificationsStore implements INotificationsStore {
|
||||
timer: any = null;
|
||||
|
||||
limit: number = 6;
|
||||
|
||||
messages: IMessage[] = [];
|
||||
|
||||
constructor() {
|
||||
|
@ -22,24 +24,32 @@ class NotificationsStore implements INotificationsStore {
|
|||
});
|
||||
}
|
||||
|
||||
static getTime() {
|
||||
return (new Date()).getTime();
|
||||
}
|
||||
|
||||
show(message?: any) {
|
||||
this.messages.push({
|
||||
id: Math.random(),
|
||||
id: NotificationsStore.getTime(),
|
||||
title: message?.title || message || 'Изменения сохранены',
|
||||
description: message?.description || '',
|
||||
type: message?.type || 'success',
|
||||
});
|
||||
if (this.messages.length > this.limit) {
|
||||
this.messages.shift();
|
||||
}
|
||||
this.startClearTimer();
|
||||
}
|
||||
|
||||
startClearTimer() {
|
||||
if (this.timer) return;
|
||||
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;
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}, 3500);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,26 +8,29 @@
|
|||
|
||||
&_item {
|
||||
display: block;
|
||||
width: 250px;
|
||||
padding: 12px;
|
||||
width: 300px;
|
||||
padding: 24px;
|
||||
margin: 0 12px 12px;
|
||||
|
||||
box-sizing: border-box;
|
||||
box-shadow: 2px 2px 3px var(--color-grey);
|
||||
border: 1px solid var(--color-border);
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 4px;
|
||||
border-left: 8px solid var(--color-border);
|
||||
border-radius: var(--border-radius-s);
|
||||
background-color: white;
|
||||
|
||||
animation: notification_item 3s linear 0.5s forwards;
|
||||
|
||||
&_error {
|
||||
background-color: var(--color-12);
|
||||
border-color: var(--color-12);
|
||||
}
|
||||
|
||||
&_warning {
|
||||
background-color: var(--color-32);
|
||||
border-color: var(--color-32);
|
||||
}
|
||||
|
||||
&_success {
|
||||
background-color: var(--color-13);
|
||||
border-color: var(--color-13);
|
||||
}
|
||||
|
||||
&_info {
|
||||
|
@ -36,20 +39,43 @@
|
|||
|
||||
&_title,
|
||||
&_description {
|
||||
font-size: var(--font-s);
|
||||
font-size: var(--font-m);
|
||||
font-weight: 100;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
color: var(--color-black);
|
||||
animation: notification_title 3s linear 0.5s forwards;
|
||||
}
|
||||
|
||||
&_title {
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&_description {
|
||||
font-weight: 100;
|
||||
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)
|
||||
: value;
|
||||
const content: any = typeof column.template === 'function'
|
||||
? column.template(formattedValue)
|
||||
? column.template(formattedValue, row)
|
||||
: `${column.prefixes ?? ''}${formattedValue ?? ''}${column.suffixes ?? ''}`;
|
||||
|
||||
return (
|
||||
|
|
|
@ -2,9 +2,9 @@ import type { IColumn } from '../interfaces/Column';
|
|||
|
||||
export default function getDefaultColumnWidth(
|
||||
columns: IColumn[],
|
||||
tableRef: any,
|
||||
offsetWidth: number,
|
||||
): number {
|
||||
if (!tableRef?.current?.offsetWidth) return 150;
|
||||
if (!offsetWidth) return 150;
|
||||
const visibleColumns = columns.filter(({ isShow }: IColumn) => isShow);
|
||||
|
||||
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 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;
|
||||
console.dir(adaptiveColumnsWidth);
|
||||
|
||||
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';
|
||||
|
||||
|
@ -26,20 +26,27 @@ function Table({
|
|||
updateSort,
|
||||
children,
|
||||
}: ITableProps): React.ReactElement | null {
|
||||
const [offsetWidth, setOffsetWidth] = useState<number>(0);
|
||||
|
||||
if (!rows || !rows.length) return null;
|
||||
|
||||
const refTable = React.useRef() as React.MutableRefObject<HTMLDivElement>;
|
||||
const currentWidth = refTable?.current?.offsetWidth;
|
||||
|
||||
useEffect(() => {
|
||||
setOffsetWidth(currentWidth);
|
||||
}, [currentWidth]);
|
||||
|
||||
const defaultColumns = getDefaultProps(children) as IColumn[];
|
||||
const defaultWidth = getDefaultColumnWidth(defaultColumns, refTable);
|
||||
const defaultWidth = getDefaultColumnWidth(defaultColumns, offsetWidth);
|
||||
const columns = getColumnConfigs(defaultColumns, defaultWidth, sort);
|
||||
|
||||
return (
|
||||
<div className={`${style.table_wrapper}`}>
|
||||
<div
|
||||
ref={refTable}
|
||||
className={`${style.table}`}
|
||||
>
|
||||
<div
|
||||
ref={refTable}
|
||||
className={`${style.table_wrapper}`}
|
||||
>
|
||||
<div className={`${style.table}`}>
|
||||
<Header
|
||||
columns={columns}
|
||||
updateSort={updateSort}
|
||||
|
@ -57,7 +64,8 @@ function Table({
|
|||
Table.defaultProps = {
|
||||
rows: [],
|
||||
sort: [],
|
||||
updateSort: () => {},
|
||||
updateSort: () => {
|
||||
},
|
||||
};
|
||||
|
||||
export default Table;
|
||||
|
|
|
@ -39,7 +39,7 @@ function Column({ dayInfo, order, author }: IColumnProps) {
|
|||
) : (
|
||||
<NothingFound
|
||||
icon="./assets/cards/commits.png"
|
||||
message="В этот день у этого пользователя не было ни одного коммита."
|
||||
message="В этот день у этого сотрудника не было ни одного коммита."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
import IHashMap from 'ts/interfaces/HashMap';
|
||||
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 dataGrip from 'ts/helpers/DataGrip';
|
||||
|
||||
import style from '../styles/task.module.scss';
|
||||
|
||||
function getFormattedTime(time: any) {
|
||||
|
@ -37,15 +40,23 @@ interface ITaskProps {
|
|||
}
|
||||
|
||||
function Task({ title, commits }: ITaskProps) {
|
||||
const prId = dataGrip.pr.prByTask[title];
|
||||
return (
|
||||
<div
|
||||
key={title}
|
||||
className={style.tempo_task}
|
||||
>
|
||||
<div className={style.tempo_task_header}>
|
||||
<p className={style.tempo_task_link}>
|
||||
{title}
|
||||
</p>
|
||||
<div>
|
||||
<ExternalLink
|
||||
text={title}
|
||||
link={`${userSettings?.settings?.linksPrefix?.task || '/'}${title}`}
|
||||
/>
|
||||
<ExternalLink
|
||||
text="PR"
|
||||
link={`${userSettings?.settings?.linksPrefix?.pr || '/'}${prId}`}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.tempo_task_tags}>
|
||||
{getTags(commits)}
|
||||
</div>
|
||||
|
|
|
@ -53,14 +53,17 @@
|
|||
|
||||
&_tags {
|
||||
display: inline-block;
|
||||
white-space: normal;
|
||||
text-align: right;
|
||||
margin: 0 0 -6px 0;
|
||||
}
|
||||
|
||||
&_tag {
|
||||
display: inline-block;
|
||||
padding: var(--space-xxxs) var(--space-sm);
|
||||
margin: 0 0 var(--space-xs) var(--space-xs);
|
||||
border-radius: var(--border-radius-l);
|
||||
background-color: var(--color-border);
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
&_commits,
|
||||
|
|
|
@ -4,6 +4,7 @@ import Wrapper, { IUiKitWrapperProps } from './Wrapper';
|
|||
import style from '../styles/switch.module.scss';
|
||||
|
||||
interface IUiKitSwitchProps extends IUiKitWrapperProps {
|
||||
multiple?: boolean;
|
||||
value: any;
|
||||
options: any[];
|
||||
onChange: Function;
|
||||
|
@ -20,20 +21,33 @@ function UiKitSwitch({
|
|||
options,
|
||||
onChange,
|
||||
}: IUiKitSwitchProps) {
|
||||
const hasValue = value || value === 0 || value === false;
|
||||
let selectedIds = value;
|
||||
if (hasValue && !Array.isArray(value)) {
|
||||
selectedIds = [value];
|
||||
}
|
||||
|
||||
const items = (options || [])
|
||||
.map((option: any, index: number) => {
|
||||
const formattedOption = typeof option !== 'object'
|
||||
? ({ id: option, title: option })
|
||||
: option;
|
||||
const isSelected = hasValue && selectedIds.includes(formattedOption?.id);
|
||||
|
||||
return (
|
||||
<button
|
||||
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}
|
||||
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 ?? ''}
|
||||
|
|
|
@ -9,7 +9,7 @@ export interface IUiKitWrapperProps {
|
|||
example?: string;
|
||||
error?: string;
|
||||
className?: string;
|
||||
disabled?: boolean,
|
||||
disabled?: boolean;
|
||||
children?: ReactNode | string | null;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,11 +10,13 @@ import { getEvents } from '../helpers/day';
|
|||
interface IBodyProps {
|
||||
month: IMonth;
|
||||
maxCommits: number;
|
||||
showEvents: boolean;
|
||||
}
|
||||
|
||||
function Body({
|
||||
month,
|
||||
maxCommits,
|
||||
showEvents,
|
||||
}: IBodyProps): React.ReactElement | null {
|
||||
const firstDay = month.date.getDay() - 1;
|
||||
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);
|
||||
let currentDay = 0;
|
||||
|
||||
const events = getEvents(dataGripStore);
|
||||
const events = showEvents ? getEvents(dataGripStore) : {};
|
||||
const days = allDays.map((v: any, index: number) => {
|
||||
const dayInfo = month.commits[currentDay];
|
||||
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import DayInfo from 'ts/components/DayInfo';
|
||||
import dataGripStore from 'ts/store/DataGrip';
|
||||
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 style from '../styles/index.module.scss';
|
||||
|
@ -50,9 +53,10 @@ function Day({
|
|||
>
|
||||
{showInfo ? (
|
||||
<>
|
||||
{'📌'}
|
||||
{'◉'}
|
||||
<div className={style.year_chart_month_body_day_arrow} />
|
||||
<div className={style.year_chart_month_body_day_info}>
|
||||
<Title title={getDate(dayInfo.timestamp)} />
|
||||
<DayInfo // @ts-ignore
|
||||
day={dayInfo}
|
||||
events={events}
|
||||
|
|
|
@ -1,43 +1,81 @@
|
|||
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 cssDescription from 'ts/components/Description/index.module.scss';
|
||||
|
||||
import IMonth from '../interfaces/Month';
|
||||
import Header from './Header';
|
||||
import Body from './Body';
|
||||
|
||||
import styleChart from '../styles/line.module.scss';
|
||||
import style from '../styles/index.module.scss';
|
||||
|
||||
interface IMonthProps {
|
||||
month: IMonth;
|
||||
maxCommits: number;
|
||||
interface IMonthTotalProps {
|
||||
title: string;
|
||||
options: any;
|
||||
value: any;
|
||||
}
|
||||
|
||||
function Month({
|
||||
month,
|
||||
maxCommits,
|
||||
}: IMonthProps): React.ReactElement | null {
|
||||
function MonthTotal({
|
||||
title,
|
||||
options,
|
||||
value,
|
||||
}: IMonthTotalProps) {
|
||||
return (
|
||||
<div className={`${style.year_chart_month}`}>
|
||||
<Header month={month} />
|
||||
<Body
|
||||
month={month}
|
||||
maxCommits={maxCommits}
|
||||
<div className={styleChart.year_chart_month_info}>
|
||||
<span className={styleChart.year_chart_month_text}>
|
||||
{title}
|
||||
</span>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
Month.defaultProps = {
|
||||
rows: [],
|
||||
};
|
||||
interface IMonthProps {
|
||||
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;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import MinMaxCounter from 'ts/helpers/DataGrip/components/counter';
|
||||
|
||||
import getCommitsByMonth from './helpers/getCommitsByMonth';
|
||||
import getAuthorByDate from './helpers/getAuthorByDate';
|
||||
import Month from './components/Month';
|
||||
|
@ -7,12 +9,14 @@ import IMonth from './interfaces/Month';
|
|||
|
||||
interface IYearChartProps {
|
||||
maxCommits: number;
|
||||
showEvents?: boolean;
|
||||
wordDays: any[];
|
||||
authors: any[];
|
||||
}
|
||||
|
||||
function YearChart({
|
||||
maxCommits = 100,
|
||||
showEvents = true,
|
||||
wordDays = [],
|
||||
authors = [],
|
||||
}: IYearChartProps): React.ReactElement | null {
|
||||
|
@ -21,11 +25,26 @@ function YearChart({
|
|||
const authorsByDate = getAuthorByDate(authors);
|
||||
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) => (
|
||||
<Month
|
||||
key={month.id}
|
||||
max={{
|
||||
tasks: max.tasks.max,
|
||||
money: max.money.max,
|
||||
commits: maxCommits,
|
||||
}}
|
||||
month={month}
|
||||
maxCommits={maxCommits}
|
||||
showEvents={showEvents}
|
||||
/>
|
||||
));
|
||||
|
||||
|
@ -37,7 +56,7 @@ function YearChart({
|
|||
}
|
||||
|
||||
YearChart.defaultProps = {
|
||||
rows: [],
|
||||
showEvents: true,
|
||||
};
|
||||
|
||||
export default YearChart;
|
||||
|
|
|
@ -1,29 +1,33 @@
|
|||
import localization from 'ts/helpers/Localization';
|
||||
|
||||
localization.parse('ru', `
|
||||
§ common.filters: Фильтры
|
||||
§ sidebar.team.total: Общая информация
|
||||
§ sidebar.team.scope: Оценка проекта
|
||||
§ sidebar.team.author: Оценка сотрудников
|
||||
§ sidebar.team.scope: Фичи
|
||||
§ sidebar.team.author: Сотрудники
|
||||
§ sidebar.team.type: Типы задач
|
||||
§ sidebar.team.sprint: По неделям
|
||||
§ sidebar.team.month: По месяцу
|
||||
§ sidebar.team.pr: Влитие кода
|
||||
§ sidebar.team.day: По дням
|
||||
§ sidebar.team.week: По неделям
|
||||
§ sidebar.team.month: По месяцам
|
||||
§ sidebar.team.tree: Анализ файлов
|
||||
§ sidebar.team.heatmap: График работы
|
||||
§ sidebar.team.hours: Расписание
|
||||
§ sidebar.team.timestamp: Все коммиты
|
||||
§ sidebar.team.commits: Все коммиты
|
||||
§ sidebar.team.changes: Все изменения
|
||||
§ sidebar.team.words: Популярные слова
|
||||
§ sidebar.team.top: Викторина
|
||||
§ sidebar.team.settings: Настройки
|
||||
§ sidebar.person.total: Общая информация
|
||||
§ sidebar.person.money: Стоимость работы
|
||||
§ sidebar.person.speed: Скорость
|
||||
§ sidebar.person.day: По дням
|
||||
§ sidebar.person.week: По неделям
|
||||
§ sidebar.person.month: По месяцам
|
||||
§ sidebar.person.frequency: График работы
|
||||
§ sidebar.person.hours: Расписание
|
||||
§ sidebar.person.commits: Все коммиты
|
||||
§ sidebar.person.changes: Все изменения
|
||||
§ sidebar.person.words: Популярные слова
|
||||
§ sidebar.person.settings: Настройки
|
||||
§ page.team.author.types: Тип работ
|
||||
§ page.team.author.commits: Коммитов
|
||||
§ page.team.author.commitsSmall: коммитов
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import ICommit from 'ts/interfaces/Commit';
|
||||
import settingsStore from 'ts/store/Settings';
|
||||
import IHashMap from 'ts/interfaces/HashMap';
|
||||
import settingsStore from 'ts/store/Settings';
|
||||
import userSettings from 'ts/store/UserSettings';
|
||||
|
||||
export default class DataGripByAuthor {
|
||||
list: string[] = [];
|
||||
|
@ -26,6 +27,7 @@ export default class DataGripByAuthor {
|
|||
} else {
|
||||
this.#addCommitByAuthor(commit);
|
||||
}
|
||||
this.#setMoneyByMonth(commit);
|
||||
}
|
||||
|
||||
#updateCommitByAuthor(commit: ICommit) {
|
||||
|
@ -44,12 +46,20 @@ export default class DataGripByAuthor {
|
|||
? commit.message.length
|
||||
: statistic.maxMessageLength;
|
||||
statistic.commitsByDayAndHour[commit.day][commit.hours] += 1;
|
||||
statistic.commitsByHour[commit.hours] += 1;
|
||||
statistic.wordStatistics = DataGripByAuthor.#updateWordStatistics(commit, statistic.wordStatistics);
|
||||
}
|
||||
|
||||
#addCommitByAuthor(commit: ICommit) {
|
||||
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] = {
|
||||
author: commit.author,
|
||||
commits: 1,
|
||||
|
@ -61,13 +71,52 @@ export default class DataGripByAuthor {
|
|||
scopes: { [commit.scope]: 1 },
|
||||
hours: [commit.hours],
|
||||
commitsByDayAndHour,
|
||||
commitsByHour,
|
||||
messageLength: [commit.message.length || 0],
|
||||
totalMessageLength: commit.message.length || 0,
|
||||
maxMessageLength: commit.message.length || 0,
|
||||
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() {
|
||||
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 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 moneyLosses = lazyDays > 0
|
||||
? Math.ceil(lazyDays * middleSalaryInDay)
|
||||
|
|
|
@ -7,7 +7,8 @@ export default class MinMaxCounter {
|
|||
|
||||
maxData: any = undefined;
|
||||
|
||||
update(value: number, data: any) {
|
||||
update(value?: number, data?: any) {
|
||||
if (!value && value !== 0) return;
|
||||
if (this.min > value) {
|
||||
this.min = value;
|
||||
this.minData = data;
|
||||
|
|
|
@ -1,41 +1,163 @@
|
|||
import { COMMIT_TYPE, ISystemCommit } from 'ts/interfaces/Commit';
|
||||
import IHashMap from 'ts/interfaces/HashMap';
|
||||
import { WeightedAverage } from 'ts/helpers/Math';
|
||||
|
||||
export default class DataGripByPr {
|
||||
list: string[] = [];
|
||||
|
||||
export default class DataGripByPR {
|
||||
pr: IHashMap<any> = {};
|
||||
|
||||
statistic: any = [];
|
||||
prByTask: IHashMap<string> = {};
|
||||
|
||||
lastCommitByTaskNumber: IHashMap<any> = {};
|
||||
|
||||
statistic: any[] = [];
|
||||
|
||||
statisticByName: IHashMap<any> = [];
|
||||
|
||||
clear() {
|
||||
this.list = [];
|
||||
this.pr = {};
|
||||
this.prByTask = {};
|
||||
this.lastCommitByTaskNumber = {};
|
||||
this.statistic = [];
|
||||
}
|
||||
|
||||
addCommit(commit: ISystemCommit) {
|
||||
if (commit.commitType === COMMIT_TYPE.AUTO_MERGE) return;
|
||||
if (this.pr[commit.prId]) {
|
||||
this.#updateCommitByPR(commit);
|
||||
} else {
|
||||
if (!commit.commitType) {
|
||||
if (!this.lastCommitByTaskNumber[commit.task]) {
|
||||
this.#addCommitByTaskNumber(commit);
|
||||
} else {
|
||||
this.#updateCommitByTaskNumber(commit);
|
||||
}
|
||||
} else if (commit.commitType !== COMMIT_TYPE.AUTO_MERGE && !this.pr[commit.prId]) {
|
||||
this.#addCommitByPR(commit);
|
||||
}
|
||||
}
|
||||
|
||||
#updateCommitByPR(commit: ISystemCommit) {
|
||||
const statistic = this.pr[commit.prId];
|
||||
const property = commit.commitType === COMMIT_TYPE.MERGE ? 'close' : 'open';
|
||||
statistic[property] = commit;
|
||||
statistic.delay = statistic.open.milliseconds - statistic.close.milliseconds;
|
||||
#addCommitByTaskNumber(commit: ISystemCommit) {
|
||||
this.lastCommitByTaskNumber[commit.task] = {
|
||||
commits : 1,
|
||||
beginTaskTime: commit.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) {
|
||||
const property = commit.commitType === COMMIT_TYPE.MERGE ? 'close' : 'open';
|
||||
this.pr[commit.prId] = { [property]: commit };
|
||||
const lastCommit = this.lastCommitByTaskNumber[commit.task];
|
||||
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.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 MinMaxCounter from './components/counter';
|
||||
import DataGripByExtension from './components/extension';
|
||||
import DataGripByGet from './components/get';
|
||||
import DataGripByPR from './components/pr';
|
||||
|
||||
class DataGrip {
|
||||
|
@ -31,6 +32,8 @@ class DataGrip {
|
|||
|
||||
extension: any = new DataGripByExtension();
|
||||
|
||||
get: any = new DataGripByGet();
|
||||
|
||||
pr: any = new DataGripByPR();
|
||||
|
||||
initializationInfo: any = {};
|
||||
|
@ -45,19 +48,20 @@ class DataGrip {
|
|||
this.week.clear();
|
||||
this.recommendations.clear();
|
||||
this.extension.clear();
|
||||
this.get.clear();
|
||||
this.pr.clear();
|
||||
}
|
||||
|
||||
addCommit(commit: ICommit | ISystemCommit) {
|
||||
if (commit.author === 'GitHub') return; // @ts-ignore
|
||||
if (commit.commitType) {
|
||||
this.pr.addCommit(commit);
|
||||
} else {
|
||||
if (commit.author === 'GitHub') return;
|
||||
this.pr.addCommit(commit); // @ts-ignore
|
||||
if (!commit.commitType) {
|
||||
this.firstLastCommit.update(commit.milliseconds, commit);
|
||||
this.author.addCommit(commit);
|
||||
this.scope.addCommit(commit);
|
||||
this.type.addCommit(commit);
|
||||
this.timestamp.addCommit(commit);
|
||||
this.get.addCommit(commit);
|
||||
this.week.addCommit(commit);
|
||||
}
|
||||
}
|
||||
|
@ -70,7 +74,7 @@ class DataGrip {
|
|||
this.timestamp.updateTotalInfo(this.author);
|
||||
this.week.updateTotalInfo(this.author);
|
||||
this.recommendations.updateTotalInfo(this);
|
||||
this.pr.updateTotalInfo(this);
|
||||
this.pr.updateTotalInfo(this.author);
|
||||
}
|
||||
|
||||
updateByInitialization() {
|
||||
|
|
|
@ -18,16 +18,12 @@ export default function Parser(
|
|||
let weekEndTime: number = 0;
|
||||
|
||||
let prev = null;
|
||||
let isFileInfo = false; // [ name, file, empty ];
|
||||
|
||||
for (let i = 0, l = report.length; i < l; i += 1) {
|
||||
const message = report[i];
|
||||
if (!message) {
|
||||
isFileInfo = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isFileInfo) {
|
||||
if (!message) continue;
|
||||
const index = message.indexOf('\t');
|
||||
if (index > 0 && index < 10) {
|
||||
let [addedRaw, removedRaw, fileName] = message.split('\t');
|
||||
fileName = getNewFileName(fileName, allFiles);
|
||||
let added = parseInt(addedRaw, 10) || 0;
|
||||
|
@ -104,7 +100,6 @@ export default function Parser(
|
|||
|
||||
prev = next;
|
||||
commits.push(prev); // @ts-ignore
|
||||
isFileInfo = !prev.commitType;
|
||||
}
|
||||
}
|
||||
if (prev) parseCommit(prev);
|
||||
|
|
|
@ -1,22 +1,6 @@
|
|||
import ICommit, { COMMIT_TYPE, ISystemCommit } from 'ts/interfaces/Commit';
|
||||
|
||||
function getTypeAndScope(messageParts: string[], task: string) {
|
||||
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] || '';
|
||||
}
|
||||
import { getTypeAndScope, getTask, getTaskNumber } from './getTypeAndScope';
|
||||
|
||||
export default function getUserInfo(logString: string): ICommit | ISystemCommit {
|
||||
// "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 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
|
||||
|| isSystemMerge
|
||||
|| fromGitHubToBitBucket
|
||||
|| message.indexOf('Automatic merge from') === 0;
|
||||
|| isAutoMerge;
|
||||
|
||||
if (isSystemCommit) {
|
||||
let commitType = COMMIT_TYPE.AUTO_MERGE;
|
||||
let prId, repository, branch, toBranch, task;
|
||||
let prId, repository, branch, toBranch, task, taskNumber;
|
||||
if (isSystemMerge) {
|
||||
commitType = COMMIT_TYPE.MERGE;
|
||||
commitType = COMMIT_TYPE.PR_GITHUB;
|
||||
[, prId, repository, branch, toBranch ] = message
|
||||
.replace(/(Merge\spull\srequest\s#)|(\sfrom\s)|(\sin\s)|(\sto\s)/gim, ',')
|
||||
.split(',');
|
||||
task = getTask(branch);
|
||||
} else if (isSystemPR) {
|
||||
commitType = COMMIT_TYPE.PR;
|
||||
commitType = COMMIT_TYPE.PR_BITBUCKET;
|
||||
const messageParts = message.substring(14, Infinity).split(':');
|
||||
prId = messageParts.shift();
|
||||
task = getTask(messageParts.join(':'));
|
||||
}
|
||||
taskNumber = getTaskNumber(task);
|
||||
|
||||
return {
|
||||
...commonInfo,
|
||||
prId: prId || '',
|
||||
task: task || '',
|
||||
taskNumber: taskNumber || '',
|
||||
repository: repository || '',
|
||||
branch: branch || '',
|
||||
toBranch: toBranch || '',
|
||||
|
@ -87,12 +75,13 @@ export default function getUserInfo(logString: string): ICommit | ISystemCommit
|
|||
};
|
||||
}
|
||||
|
||||
const messageParts = message.split(':');
|
||||
const task = getTask(message);
|
||||
const [type, scope] = getTypeAndScope(messageParts, task);
|
||||
const taskNumber = getTaskNumber(task);
|
||||
const [type, scope] = getTypeAndScope(message, task);
|
||||
return {
|
||||
...commonInfo,
|
||||
task,
|
||||
taskNumber,
|
||||
type: type || 'не подписан',
|
||||
scope: scope || 'неопределенна',
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ export default function getUserInfo(message: any): ICommit {
|
|||
message: text || '',
|
||||
|
||||
task: 'беседа',
|
||||
taskNumber: '',
|
||||
type: 'не подписан',
|
||||
scope: 'неопределенна',
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import ICommit from 'ts/interfaces/Commit';
|
||||
import dataGrip from 'ts/helpers/DataGrip';
|
||||
|
||||
import ALL_ACHIEVEMENTS from './constants/list';
|
||||
|
@ -5,21 +6,34 @@ import byCompetition from './byCompetition';
|
|||
|
||||
export default function getAchievementByAuthor(author: string) {
|
||||
const statistic = dataGrip.author.statisticByName[author];
|
||||
const getList = dataGrip.get.getsByAuthor[author];
|
||||
if (!statistic) return;
|
||||
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) {
|
||||
// Залётный - это не его основной проект
|
||||
list.push('userNotWork');
|
||||
} 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');
|
||||
// Скорострел - меньше дня на задачу
|
||||
|
@ -30,8 +44,10 @@ export default function getAchievementByAuthor(author: string) {
|
|||
if (statistic.allDaysInProject > 90) list.push('more90DaysInProject');
|
||||
// Чёрт - отработал 666 дней на проекте
|
||||
if (statistic.allDaysInProject > 666) list.push('more666DaysInProject');
|
||||
// Флеш-рояль - отработал 777 дней на проекте
|
||||
// Азино - отработал 777 дней на проекте
|
||||
if (statistic.allDaysInProject > 777) list.push('more777DaysInProject');
|
||||
// Тесак - отработал 1488 дней на проекте
|
||||
if (statistic.allDaysInProject > 1488) list.push('more1488DaysInProject');
|
||||
}
|
||||
// Ни единого разрыва - 0 дней без коммитов
|
||||
if (statistic.lazyDays === 0) list.push('zeroLazyDays');
|
||||
|
@ -40,6 +56,8 @@ export default function getAchievementByAuthor(author: string) {
|
|||
// сказал как отрезал - в среднем 1 коммит на таск
|
||||
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) => {
|
||||
const index = ALL_ACHIEVEMENTS[type][2];
|
||||
acc[index].push(type);
|
||||
|
|
|
@ -4,7 +4,6 @@ export default {
|
|||
// готово
|
||||
commitsAfter1500: ['Сова', '70% коммитов после 15:00', ACHIEVEMENT_TYPE.NORMAL],
|
||||
commitsBefore1500: ['Раняя пташка', '70% коммитов до обеда', ACHIEVEMENT_TYPE.NORMAL],
|
||||
commitsAfter1800: ['Делу время', 'нет ни одного коммита после 18:00', ACHIEVEMENT_TYPE.GOOD],
|
||||
workEveryTime: ['Раб божий', 'есть коммит на каждый час суток', ACHIEVEMENT_TYPE.BAD],
|
||||
workNotWork: ['Стрельба холостыми', 'коммиты есть, а закрытых задач нет', ACHIEVEMENT_TYPE.BAD],
|
||||
userNotWork: ['Залётный', 'это не его основной проект', ACHIEVEMENT_TYPE.NORMAL],
|
||||
|
@ -30,14 +29,20 @@ export default {
|
|||
lessDaysInProject: ['А это кто?', 'меньше всего дней на проекте', ACHIEVEMENT_TYPE.NORMAL],
|
||||
more90DaysInProject: ['Добро пожаловать', 'не уволили на испытательном', 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],
|
||||
// нет картинки
|
||||
longestMessage: ['А разговоров то было...', 'самая длинная подпись коммита за все время', ACHIEVEMENT_TYPE.NORMAL],
|
||||
adam: ['Адам', 'первый стабильны сотрудник на проекте', ACHIEVEMENT_TYPE.NORMAL],
|
||||
more666DaysInProject: ['Чёрт', 'отработал 666 дней на проекте', ACHIEVEMENT_TYPE.GOOD],
|
||||
more777DaysInProject: ['Азино 3 топора', 'отработал 777 дней на проекте', 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],
|
||||
|
@ -46,4 +51,5 @@ export default {
|
|||
moreRemoveCode: ['Разрушитель', 'склонен больше остальных удалять код', ACHIEVEMENT_TYPE.NORMAL],
|
||||
moreChangeCode: ['Реформатор', 'склонен больше остальных изменять код', ACHIEVEMENT_TYPE.NORMAL], // есть картинка
|
||||
moreStyle: ['Полиция моды', 'склонен больше остальных изменять CSS', ACHIEVEMENT_TYPE.GOOD],
|
||||
moreOnHoliday: ['Нет жизни', 'относительно много коммитов в нерабочее время', ACHIEVEMENT_TYPE.BAD],
|
||||
};
|
||||
|
|
|
@ -17,14 +17,15 @@ export interface ILog {
|
|||
|
||||
// task
|
||||
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
|
||||
scope: string; // table, sale, profile and etc.
|
||||
}
|
||||
|
||||
export const COMMIT_TYPE = {
|
||||
PR: 'PR',
|
||||
MERGE: 'MERGE',
|
||||
PR_BITBUCKET: 'PR_BITBUCKET',
|
||||
PR_GITHUB: 'PR_GITHUB',
|
||||
AUTO_MERGE: 'AUTO_MERGE',
|
||||
};
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ function Commits({ statistic }: ICommitsProps) {
|
|||
<br/>
|
||||
<br/>
|
||||
{}
|
||||
<Title title={`${getDate(selected?.timestamp)} сделано ${selected?.commits || '_'} коммитов`}/>
|
||||
<Title title={`${getDate(selected?.timestamp)} сделано коммитов: ${selected?.commits || '_'}`}/>
|
||||
<PageWrapper template="box">
|
||||
<DayInfo
|
||||
day={selected}
|
||||
|
|
|
@ -1,48 +1,18 @@
|
|||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import localization from 'ts/helpers/Localization';
|
||||
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() {
|
||||
const { type, page } = useParams<any>();
|
||||
const title = type && page
|
||||
? localization.get(`sidebar.${type}.${page}`)
|
||||
: localization.get('sidebar.team.total');
|
||||
|
||||
return (
|
||||
<h2 className={style.header_title}>
|
||||
{TITLES[type || '']?.[page || ''] || 'Настройки'}
|
||||
{title}
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,11 +21,13 @@ function SideBarMenuItem({
|
|||
icon,
|
||||
isSelected,
|
||||
}: ISideBarMenuItemProps) {
|
||||
const formattedTitle = localization.get(title);
|
||||
return (
|
||||
<Link
|
||||
key={id}
|
||||
className={`${style.sidebar_item} ${isSelected ? style.selected : ''}`}
|
||||
to={link}
|
||||
title={formattedTitle}
|
||||
id={`sidebar-menu-${id}`}
|
||||
onClick={() => {
|
||||
if (settingsForm.isEdited) {
|
||||
|
@ -40,7 +42,7 @@ function SideBarMenuItem({
|
|||
alt={title || ''}
|
||||
/>
|
||||
<figcaption className={style.sidebar_item_title}>
|
||||
{localization.get(title)}
|
||||
{formattedTitle}
|
||||
</figcaption>
|
||||
</Link>
|
||||
);
|
||||
|
|
|
@ -35,6 +35,13 @@ function SideBarPerson({ page }: ISideBarProps) {
|
|||
isSelected={page === 'speed'}
|
||||
/>
|
||||
<SideBarMenuGap/>
|
||||
<SideBarMenuItem
|
||||
id="day"
|
||||
link={`/person/day/${formattedUserId}`}
|
||||
title="sidebar.person.day"
|
||||
icon="./assets/menu/team_week.svg"
|
||||
isSelected={page === 'day'}
|
||||
/>
|
||||
<SideBarMenuItem
|
||||
id="week"
|
||||
link={`/person/week/${formattedUserId}`}
|
||||
|
@ -46,15 +53,8 @@ function SideBarPerson({ page }: ISideBarProps) {
|
|||
id="month"
|
||||
link={`/person/month/${formattedUserId}`}
|
||||
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"
|
||||
isSelected={page === 'year'}
|
||||
isSelected={page === 'month'}
|
||||
/>
|
||||
<SideBarMenuItem
|
||||
id="hours"
|
||||
|
|
|
@ -38,27 +38,34 @@ function SideBarTeam({ page }: ISideBarProps) {
|
|||
icon="./assets/menu/team_type.svg"
|
||||
isSelected={page === 'type'}
|
||||
/>
|
||||
<SideBarMenuItem
|
||||
id="type"
|
||||
link="/team/pr"
|
||||
title="sidebar.team.pr"
|
||||
icon="./assets/menu/pull_request.svg"
|
||||
isSelected={page === 'pr'}
|
||||
/>
|
||||
<SideBarMenuGap/>
|
||||
<SideBarMenuItem
|
||||
id="sprint"
|
||||
link="/team/sprint"
|
||||
title="sidebar.team.sprint"
|
||||
id="day"
|
||||
link="/team/day"
|
||||
title="sidebar.team.day"
|
||||
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
|
||||
id="month"
|
||||
link="/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"
|
||||
isSelected={page === 'year'}
|
||||
isSelected={page === 'month'}
|
||||
/>
|
||||
<SideBarMenuItem
|
||||
id="hours"
|
||||
|
@ -76,11 +83,11 @@ function SideBarTeam({ page }: ISideBarProps) {
|
|||
isSelected={page === 'tree'}
|
||||
/>
|
||||
<SideBarMenuItem
|
||||
id="timestamp"
|
||||
link="/team/timestamp"
|
||||
title="sidebar.team.timestamp"
|
||||
id="commits"
|
||||
link="/team/commits"
|
||||
title="sidebar.team.commits"
|
||||
icon="./assets/menu/pull-request.svg"
|
||||
isSelected={page === 'timestamp'}
|
||||
isSelected={page === 'commits'}
|
||||
/>
|
||||
<SideBarMenuItem
|
||||
id="changes"
|
||||
|
@ -96,14 +103,14 @@ function SideBarTeam({ page }: ISideBarProps) {
|
|||
icon="./assets/menu/team_words.svg"
|
||||
isSelected={page === 'words'}
|
||||
/>
|
||||
<SideBarMenuGap/>
|
||||
<SideBarMenuItem
|
||||
id="top"
|
||||
link="/team/top"
|
||||
title="sidebar.team.top"
|
||||
icon="./assets/menu/team_words.svg"
|
||||
isSelected={page === 'top'}
|
||||
/>
|
||||
{/*<SideBarMenuGap/>*/}
|
||||
{/*<SideBarMenuItem*/}
|
||||
{/* id="top"*/}
|
||||
{/* link="/team/top"*/}
|
||||
{/* title="sidebar.team.top"*/}
|
||||
{/* icon="./assets/menu/team_words.svg"*/}
|
||||
{/* isSelected={page === 'top'}*/}
|
||||
{/*/>*/}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { observer } from 'mobx-react-lite';
|
|||
import { IPagination } from 'ts/interfaces/Pagination';
|
||||
import dataGripStore from 'ts/store/DataGrip';
|
||||
import { getShortDateRange } from 'ts/helpers/formatter';
|
||||
import localization from 'ts/helpers/Localization';
|
||||
|
||||
import UiKitButton from 'ts/components/UiKit/components/Button';
|
||||
import PageWrapper from 'ts/components/Page/wrapper';
|
||||
|
@ -59,7 +60,7 @@ const Tempo = observer((): React.ReactElement => {
|
|||
if (!partOfData?.length) return (<NothingFound />);
|
||||
return (
|
||||
<>
|
||||
<Title title="Фильтры"/>
|
||||
<Title title={localization.get('common.filters')} />
|
||||
<PageWrapper>
|
||||
<div className={style.tempo_page_filters}>
|
||||
<UiKitButton
|
||||
|
|
|
@ -12,6 +12,7 @@ import Description from 'ts/components/Description';
|
|||
import PageWrapper from 'ts/components/Page/wrapper';
|
||||
import PageColumn from 'ts/components/Page/column';
|
||||
import Title from 'ts/components/Title';
|
||||
import GetList from 'ts/components/GetList';
|
||||
|
||||
import dataGripStore from 'ts/store/DataGrip';
|
||||
|
||||
|
@ -33,6 +34,7 @@ function AchievementBlock({ title, achievements }: IAchievementBlockProps) {
|
|||
const Total = observer((): React.ReactElement => {
|
||||
const { userId } = useParams<any>();
|
||||
const statistic = dataGripStore.dataGrip.author.statistic[userId || 0];
|
||||
const commitsWithGet = dataGripStore.dataGrip.get.getsByAuthor[statistic.author];
|
||||
const taskNumber = statistic.tasks.length;
|
||||
const achievements = getAchievementByAuthor(statistic.author);
|
||||
|
||||
|
@ -82,6 +84,15 @@ const Total = observer((): React.ReactElement => {
|
|||
achievements={achievements[ACHIEVEMENT_TYPE.BAD]}
|
||||
/>
|
||||
<Description text="Чем больше сотрудник набрал отрицательных достижений, тем больше вероятность, что ситуация нестандартная. Возможно, стоит изменить режим его работы, задачи или отчётность. Следует поговорить с ним и узнать, какие проблемы мешают его работе."/>
|
||||
<br />
|
||||
<br />
|
||||
{commitsWithGet?.length ? (
|
||||
<>
|
||||
<Title title={localization.get('Взятые геты:')}/>
|
||||
<GetList list={commitsWithGet} />
|
||||
<Description text="«Взять гет» в данном случае означает первым оставить коммит к задаче с «красивым» номером."/>
|
||||
</>
|
||||
) : null}
|
||||
</PageColumn>
|
||||
</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 Title from 'ts/components/Title';
|
||||
import localization from 'ts/helpers/Localization';
|
||||
|
||||
import UserSelect from './components/UserSelect';
|
||||
import Changes from './components/Changes';
|
||||
|
@ -12,7 +13,7 @@ import PopularWords from './components/PopularWords';
|
|||
import Speed from './components/Speed';
|
||||
import Total from './components/Total';
|
||||
import Week from './components/Week';
|
||||
import Year from './components/Year';
|
||||
import Month from './components/Month';
|
||||
import Tempo from './components/Tempo';
|
||||
|
||||
function Person() {
|
||||
|
@ -22,21 +23,20 @@ function Person() {
|
|||
<>
|
||||
{page !== 'week' && (
|
||||
<>
|
||||
<Title title="Фильтры"/>
|
||||
<Title title={localization.get('common.filters')} />
|
||||
<UserSelect />
|
||||
</>
|
||||
)}
|
||||
|
||||
{page === 'changes' && <Changes/>}
|
||||
{page === 'commits' && <Commits/>}
|
||||
{page === 'total' && <Total/>}
|
||||
{page === 'hours' && <Hours/>}
|
||||
{page === 'money' && <Money/>}
|
||||
{page === 'week' && <Week/>}
|
||||
{page === 'month' && <Month/>}
|
||||
{page === 'commits' && <Commits/>}
|
||||
{page === 'changes' && <Changes/>}
|
||||
{page === 'words' && <PopularWords/>}
|
||||
{page === 'speed' && <Speed/>}
|
||||
{page === 'total' && <Total/>}
|
||||
{page === 'month' && <Week/>}
|
||||
{page === 'week' && <Tempo/>}
|
||||
{page === 'year' && <Year/>}
|
||||
{page === 'day' && <Tempo/>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
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 Title from 'ts/components/Title';
|
||||
|
||||
|
@ -10,13 +10,22 @@ import formStore from '../store/Form';
|
|||
const Common = observer((): React.ReactElement | null => {
|
||||
return (
|
||||
<>
|
||||
<Title title="Другие данные"/>
|
||||
<Title title="Префиксы ссылок"/>
|
||||
<PageBox>
|
||||
<UiKitInputNumber
|
||||
title="Ссылка на таск-трекер"
|
||||
value={formStore.state.linksPrefixForTasks}
|
||||
onChange={(jiraLink: number) => {
|
||||
formStore.updateState('linksPrefixForTasks', jiraLink);
|
||||
<InputString
|
||||
title="Для номеров задач"
|
||||
value={formStore.state?.linksPrefix?.task}
|
||||
placeholder="https://jira.com/secure/RapidBoard.jspa?task="
|
||||
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>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { IEmployees } from 'ts/interfaces/UserSetting';
|
||||
import UiKitButtonMenu from 'ts/components/UiKit/components/ButtonMenu';
|
||||
import PageWrapper from 'ts/components/Page/wrapper';
|
||||
import PageColumn from 'ts/components/Page/column';
|
||||
|
@ -12,9 +13,7 @@ import dataGripStore from 'ts/store/DataGrip';
|
|||
import UserSetting from './User';
|
||||
import Salary from './Salary';
|
||||
import Common from './Common';
|
||||
import Filter from './Filter';
|
||||
|
||||
import { IEmployees } from '../interfaces/Setting';
|
||||
import { getNewEmployeesSettings } from '../helpers/getEmptySettings';
|
||||
import MailMap from './MailMap';
|
||||
import formStore from '../store/Form';
|
||||
|
@ -49,9 +48,8 @@ const SettingForm = observer((response: any): React.ReactElement | null => {
|
|||
<>
|
||||
<PageWrapper>
|
||||
<PageColumn>
|
||||
<Filter />
|
||||
<Salary />
|
||||
<Common />
|
||||
<Salary />
|
||||
</PageColumn>
|
||||
<PageColumn>
|
||||
<Title title="Индивидуальные настройки"/>
|
||||
|
@ -73,7 +71,7 @@ const SettingForm = observer((response: any): React.ReactElement | null => {
|
|||
]);
|
||||
}}
|
||||
>
|
||||
Добавить пользователя
|
||||
Добавить сотрудника
|
||||
</UiKitButtonMenu>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -69,6 +69,25 @@ const Common = observer((): React.ReactElement | null => {
|
|||
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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
import { IEmployees, IEmployeesSalary } from 'ts/interfaces/UserSetting';
|
||||
import UiKitButton from 'ts/components/UiKit/components/Button';
|
||||
import confirm from 'ts/components/ModalWindow/store/Confirm';
|
||||
import PageBox from 'ts/components/Page/Box';
|
||||
import Title from 'ts/components/Title';
|
||||
|
||||
import { IEmployees, IEmployeesSalary } from '../interfaces/Setting';
|
||||
import { getNewSalarySettings } from '../helpers/getEmptySettings';
|
||||
import { getNewEmploymentContract } from '../helpers/getEmptySettings';
|
||||
import UserSalary from './UserSalary';
|
||||
import formStore from '../store/Form';
|
||||
import style from '../styles/index.module.scss';
|
||||
|
@ -60,7 +60,7 @@ function UserSetting({
|
|||
...user,
|
||||
salary: [
|
||||
...user.salary,
|
||||
getNewSalarySettings(formStore.state),
|
||||
getNewEmploymentContract(formStore.state),
|
||||
],
|
||||
});
|
||||
}}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { IEmployeesSalary } from 'ts/interfaces/UserSetting';
|
||||
import UiKitInputNumber from 'ts/components/UiKit/components/InputNumber';
|
||||
import UiKitColumns from 'ts/components/UiKit/components/Columns';
|
||||
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 Title from 'ts/components/Title';
|
||||
|
||||
import { IEmployeesSalary } from '../interfaces/Setting';
|
||||
import style from '../styles/index.module.scss';
|
||||
|
||||
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';
|
||||
|
||||
let DEFAULT_VALUES: any = {};
|
||||
|
||||
export function setDefaultValues(firstCommit: ICommit, lastCommit: ICommit) {
|
||||
DEFAULT_VALUES = {
|
||||
minCommits: 20,
|
||||
from: firstCommit.timestamp,
|
||||
to: lastCommit.timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
export function getNewSalarySettings(settings: ISetting): IEmployeesSalary {
|
||||
export function getNewEmploymentContract(settings: IUserSetting): IEmployeesSalary {
|
||||
return {
|
||||
id: Math.random(),
|
||||
value: settings.defaultSalary.value,
|
||||
currency: settings.defaultSalary.currency,
|
||||
workDaysInYear: settings.defaultSalary.workDaysInYear,
|
||||
vacationDaysInYear: settings.defaultSalary.vacationDaysInYear,
|
||||
workDaysInWeek: settings.defaultSalary.workDaysInWeek,
|
||||
from: settings.defaultFilters.from,
|
||||
workDaysInWeek: [...settings.defaultSalary.workDaysInWeek],
|
||||
from: DEFAULT_VALUES.from,
|
||||
type: 'full',
|
||||
};
|
||||
}
|
||||
|
||||
export function getNewEmployeesSettings(
|
||||
name: string,
|
||||
settings: ISetting,
|
||||
settings: IUserSetting,
|
||||
order: number,
|
||||
): IEmployees {
|
||||
return {
|
||||
|
@ -32,24 +33,26 @@ export function getNewEmployeesSettings(
|
|||
name,
|
||||
order,
|
||||
salary: [
|
||||
getNewSalarySettings(settings),
|
||||
getNewEmploymentContract(settings),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export default function getEmptySettings(): ISetting {
|
||||
export default function getEmptySettings(): IUserSetting {
|
||||
return {
|
||||
defaultFilters: { ...DEFAULT_VALUES },
|
||||
filters: { ...DEFAULT_VALUES },
|
||||
version: 1,
|
||||
defaultSalary: {
|
||||
value: 180000,
|
||||
currency: 'RUB',
|
||||
workDaysInYear: 247,
|
||||
vacationDaysInYear: 28,
|
||||
workDaysInWeek: 5,
|
||||
workDaysInWeek: [1, 1, 1, 1, 1, 0, 0],
|
||||
type: 'full',
|
||||
},
|
||||
linksPrefixForTasks: '',
|
||||
linksPrefix: {
|
||||
task: 'https://jira.com/secure/RapidBoard.jspa?task=',
|
||||
pr: 'https://bitbucket.com/projects/assayo/repos/frontend/pull-requests/',
|
||||
},
|
||||
employees: [],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import IHashMap from 'ts/interfaces/HashMap';
|
||||
import { IEmployees, IUserSetting } from 'ts/interfaces/UserSetting';
|
||||
|
||||
import { IEmployees, ISetting } from '../interfaces/Setting';
|
||||
import getEmptySettings from '../helpers/getEmptySettings';
|
||||
|
||||
class Settings {
|
||||
customSettings: ISetting = getEmptySettings();
|
||||
customSettings: IUserSetting = getEmptySettings();
|
||||
|
||||
employeesByName: IHashMap<IEmployees> = {};
|
||||
|
||||
|
@ -12,7 +12,7 @@ class Settings {
|
|||
|
||||
salaryInDay: number = 180000 / 22;
|
||||
|
||||
update(customSettings?: ISetting) {
|
||||
update(customSettings?: IUserSetting) {
|
||||
this.customSettings = customSettings || getEmptySettings();
|
||||
|
||||
this.employeesByName = this.customSettings.employees.reduce((acc, user) => {
|
||||
|
@ -21,7 +21,7 @@ class Settings {
|
|||
}, {});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -40,7 +40,7 @@ class Settings {
|
|||
const user = this.employeesByName[name];
|
||||
if (!user) return this.salaryInDay;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 { IUserSetting } from 'ts/interfaces/UserSetting';
|
||||
import Form from 'ts/store/Form';
|
||||
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 {
|
||||
constructor() {
|
||||
|
@ -12,10 +14,12 @@ class FormStore extends Form {
|
|||
});
|
||||
}
|
||||
|
||||
save(body: ISetting): Promise<any> {
|
||||
save(body: IUserSetting): Promise<any> {
|
||||
const { saveSettings } = settingsApi;
|
||||
return this.submit(saveSettings, body, false)
|
||||
.then((response: any) => {
|
||||
notificationsStore.show('Настройки сохранены');
|
||||
userSettings.loadUserSettings();
|
||||
this.setInitState(this.state);
|
||||
return Promise.resolve(response);
|
||||
});
|
||||
|
|
|
@ -35,7 +35,7 @@ function AuthorView({ response, updateSort }: IAuthorViewProps) {
|
|||
|
||||
const textWork = localization.get('page.team.author.worked');
|
||||
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 commitsChart = getOptions({ max: getMax(response, 'commits') });
|
||||
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 dataGripStore from 'ts/store/DataGrip';
|
||||
import { getShortDateRange } from 'ts/helpers/formatter';
|
||||
import localization from 'ts/helpers/Localization';
|
||||
|
||||
import UiKitButton from 'ts/components/UiKit/components/Button';
|
||||
import UiKitSelect from 'ts/components/UiKit/components/Select';
|
||||
|
@ -60,7 +61,7 @@ const Tempo = observer((): React.ReactElement => {
|
|||
if (!partOfData?.length) return (<NothingFound />);
|
||||
return (
|
||||
<>
|
||||
<Title title="Фильтры"/>
|
||||
<Title title={localization.get('common.filters')} />
|
||||
<PageWrapper>
|
||||
<div className={style.tempo_page_filters}>
|
||||
<UiKitButton
|
||||
|
|
|
@ -14,6 +14,12 @@ import Tv100And1 from 'ts/components/Tv100And1';
|
|||
import ACHIEVEMENT_TYPE from 'ts/helpers/achievement/constants/type';
|
||||
import getAchievementByAuthor from 'ts/helpers/achievement/byAuthor';
|
||||
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 style from '../styles/quiz.module.scss';
|
||||
|
@ -41,6 +47,7 @@ const Top = observer((): React.ReactElement => {
|
|||
const maxMessageLength = [...tracksAuth]
|
||||
.sort((a: any, b: any) => b.maxMessageLength - a.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 achievements = getAchievementByAuthor(statistic.author);
|
||||
|
@ -75,8 +82,37 @@ const Top = observer((): React.ReactElement => {
|
|||
<>
|
||||
<Title title="Скорость закрытия задач"/>
|
||||
<Races tracks={tracks} />
|
||||
|
||||
<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} />
|
||||
|
||||
{authors}
|
||||
<PageWrapper>
|
||||
<div style={{ whiteSpace: 'normal' }} >
|
||||
|
|
|
@ -9,7 +9,7 @@ import CardWithIcon from 'ts/components/CardWithIcon';
|
|||
import Description from 'ts/components/Description';
|
||||
|
||||
import dataGripStore from 'ts/store/DataGrip';
|
||||
import settingsStore from 'ts/store/Settings';
|
||||
import userSettings from 'ts/store/UserSettings';
|
||||
import { getShortMoney } from 'ts/helpers/formatter';
|
||||
|
||||
const Total = observer((): React.ReactElement => {
|
||||
|
@ -20,7 +20,7 @@ const Total = observer((): React.ReactElement => {
|
|||
return speed + dataGripStore.dataGrip.author.statisticByName[name].taskInDay;
|
||||
}, 0).toFixed(1);
|
||||
const moneySpeed = employment.active.reduce((speed: number, name: string) => {
|
||||
return speed + (settingsStore.salary[name] || settingsStore.defaultSalary);
|
||||
return speed + userSettings.getCurrentSalaryInMonth(name);
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
|
|
|
@ -3,6 +3,7 @@ import { observer } from 'mobx-react-lite';
|
|||
|
||||
import { IPaginationRequest, IPagination } from 'ts/interfaces/Pagination';
|
||||
import dataGripStore from 'ts/store/DataGrip';
|
||||
import localization from 'ts/helpers/Localization';
|
||||
|
||||
import PageWrapper from 'ts/components/Page/wrapper';
|
||||
import DataLoader from 'ts/components/DataLoader';
|
||||
|
@ -125,7 +126,7 @@ const Tree = observer((): React.ReactElement => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Title title="Фильтры"/>
|
||||
<Title title={localization.get('common.filters')} />
|
||||
<TreeFilters/>
|
||||
<Title title="Дерево проекта с учётом выбранных фильтров"/>
|
||||
<PageWrapper template="table">
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
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 treeStore from '../store/Tree';
|
||||
|
@ -10,7 +10,7 @@ import style from '../styles/filters.module.scss';
|
|||
|
||||
const TreeFilters = observer((): React.ReactElement => {
|
||||
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: 'Все сотрудники' });
|
||||
|
||||
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 Type from './components/Type';
|
||||
import Week from './components/Week';
|
||||
import Year from './components/Year';
|
||||
import Month from './components/Month';
|
||||
import Top from './components/Top';
|
||||
import Pr from './components/PR';
|
||||
|
||||
function Team() {
|
||||
const { type, page } = useParams<any>();
|
||||
|
@ -23,18 +24,19 @@ function Team() {
|
|||
|
||||
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 === 'tree' && <Tree/>}
|
||||
{page === 'scope' && <Scope/>}
|
||||
{page === 'author' && <Author/>}
|
||||
{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/>}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -7,33 +7,58 @@ import style from './styles/index.module.scss';
|
|||
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';
|
||||
return (
|
||||
<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>
|
||||
{' в проект, чтобы обьединить статистику по пользователям.'}
|
||||
<>
|
||||
<h4 className={style.welcome_warning}>
|
||||
<p>
|
||||
{'Сервис '}
|
||||
<span className={style.welcome_warning_bold}>НЕ ХРАНИТ</span>
|
||||
{' и '}
|
||||
<span className={style.welcome_warning_bold}>НЕ ПЕРЕДАЁТ</span>
|
||||
{' ваши данные. Все расчёты выполняются локально в вашем браузере прямо на вашей машине.'}
|
||||
</p>
|
||||
<h2 className={style.welcome_last_title}>
|
||||
Перетащите файл log.txt на эту страницу
|
||||
</h2>
|
||||
</div>
|
||||
</section>
|
||||
<p>
|
||||
{'Сервис '}
|
||||
<span className={style.welcome_warning_bold}>НЕ СОБИРАЕТ СТАТИСТИКУ</span>
|
||||
{' по проектам. Вы можете отключить интернет, проверить трафик и даже собрать локальный билд из '}
|
||||
<a
|
||||
href='https://github.com/bakhirev/assayo'
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={style.welcome_warning_link}
|
||||
>
|
||||
исходников
|
||||
</a>
|
||||
{'.'}
|
||||
</p>
|
||||
</h4>
|
||||
<section className={style.welcome}>
|
||||
<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;
|
||||
justify-content: center;
|
||||
|
||||
width: calc(100vw - 16px);
|
||||
height: 100vh;
|
||||
width: calc(100vw - 20px);
|
||||
height: calc(100vh - 60px);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
|
@ -22,6 +22,29 @@
|
|||
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,
|
||||
&_last_title {
|
||||
font-size: 42px;
|
||||
|
@ -65,6 +88,7 @@
|
|||
.welcome {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding: 32px 0 0 0;
|
||||
|
||||
&_first_title,
|
||||
|
|
|
@ -80,7 +80,6 @@ class DataGripStore implements IDataGripStore {
|
|||
|
||||
updateChars() { // todo: remove, never use
|
||||
console.log('need update data TODO');
|
||||
return;
|
||||
dataGrip.updateByFilters();
|
||||
if (!dataGrip.author.list.length) return;
|
||||
achievements.updateByDataGrip(dataGrip.author.statistic);
|
||||
|
|
|
@ -156,7 +156,6 @@ class FormStore implements IFormStore {
|
|||
if (this.isLocked) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
console.log(data);
|
||||
return this.validation(data)
|
||||
.then(action(() => {
|
||||
this.isLoading = true;
|
||||
|
|