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

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

View file

@ -1,17 +1,17 @@
{
"files": {
"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"
]
}

File diff suppressed because it is too large Load diff

View file

@ -1 +1 @@
<!doctype html><html lang="ru"><head><meta name="viewport" content="width=device-width,height=device-height,initial-scale=1,user-scalable=no,maximum-scale=1"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><meta http-equiv="Cache-Control" content="no-cache"><meta http-equiv="cleartype" content="on"><meta name="HandheldFriendly" content="True"><meta name="format-detection" content="telephone=no"><meta name="format-detection" content="address=no"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><script type="text/javascript">var report=[]</script><script src="/log.txt"></script><script src="./log.txt"></script><script src="../log.txt"></script><script src="./log-0.txt"></script><script src="./log-1.txt"></script><script src="./log-2.txt"></script><script src="./log-3.txt"></script><script src="./log-4.txt"></script><script src="./log-5.txt"></script><script src="./log-6.txt"></script><script src="./report/log-0.txt"></script><script src="./report/log-1.txt"></script><script src="./report/log-2.txt"></script><script src="./report/log-3.txt"></script><script src="./report/log-4.txt"></script><script src="./report/log-5.txt"></script><script src="./report/log-6.txt"></script><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./logo192.png"/><link rel="manifest" href="./manifest.json"/><title>ASSAYO</title><meta name="description" content="Простой и быстрый отчёт по истории коммитов в git."><meta name="keywords" content="git, статистика, аудит, история, log, мониторинг, контроль сотрудников"><meta name="author" content="Bakhirev Aleksei"><meta name="copyright" content="(c) Bakhirev Aleksei"><meta http-equiv="Reply-to" content="alexey-bakhirev@yandex.ru"><meta name="application-name" content="GIT Статистика"><meta name="msapplication-tooltip" content="Простой и быстрый отчёт по истории коммитов в git."><meta property="og:title" content="GIT Статистика"><meta property="og:description" content="Простой и быстрый отчёт по истории коммитов в git."><meta property="og:image" content="http://assayo.jp/assets/seo/custom_icon_256.png"><meta property="og:site_name" content="Assayo"><meta property="og:url" content="http://assayo.jp/"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="GIT Статистика"><meta name="twitter:description" content="Простой и быстрый отчёт по истории коммитов в git."><meta name="twitter:creator" content="Bakhirev Aleksei"><meta name="twitter:image:src" content="http://assayo.jp/assets/seo/custom_icon_256.png"><meta name="twitter:domain" content="assayo.jp"><meta name="twitter:site" content="assayo.jp"><meta itemprop="name" content="GIT Статистика"><meta itemprop="description" content="Простой и быстрый отчёт по истории коммитов в git."><meta itemprop="image" content="http://assayo.jp/assets/seo/custom_icon_256.png"><script defer="defer" src="./static/js/main.16986165.js"></script><link href="./static/css/main.a871f7d5.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="ru"><head><meta name="viewport" content="width=device-width,height=device-height,initial-scale=1,user-scalable=no,maximum-scale=1"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><meta http-equiv="Cache-Control" content="no-cache"><meta http-equiv="cleartype" content="on"><meta name="HandheldFriendly" content="True"><meta name="format-detection" content="telephone=no"><meta name="format-detection" content="address=no"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><script type="text/javascript">var report=[]</script><script src="/log.txt"></script><script src="./log.txt"></script><script src="../log.txt"></script><script src="./log-0.txt"></script><script src="./log-1.txt"></script><script src="./log-2.txt"></script><script src="./log-3.txt"></script><script src="./log-4.txt"></script><script src="./log-5.txt"></script><script src="./log-6.txt"></script><script src="./report/log-0.txt"></script><script src="./report/log-1.txt"></script><script src="./report/log-2.txt"></script><script src="./report/log-3.txt"></script><script src="./report/log-4.txt"></script><script src="./report/log-5.txt"></script><script src="./report/log-6.txt"></script><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./logo192.png"/><link rel="manifest" href="./manifest.json"/><title>ASSAYO</title><meta name="description" content="Простой и быстрый отчёт по истории коммитов в git."><meta name="keywords" content="git, статистика, аудит, история, log, мониторинг, контроль сотрудников"><meta name="author" content="Bakhirev Aleksei"><meta name="copyright" content="(c) Bakhirev Aleksei"><meta http-equiv="Reply-to" content="alexey-bakhirev@yandex.ru"><meta name="application-name" content="GIT Статистика"><meta name="msapplication-tooltip" content="Простой и быстрый отчёт по истории коммитов в git."><meta property="og:title" content="GIT Статистика"><meta property="og:description" content="Простой и быстрый отчёт по истории коммитов в git."><meta property="og:image" content="http://assayo.jp/assets/seo/custom_icon_256.png"><meta property="og:site_name" content="Assayo"><meta property="og:url" content="http://assayo.jp/"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="GIT Статистика"><meta name="twitter:description" content="Простой и быстрый отчёт по истории коммитов в git."><meta name="twitter:creator" content="Bakhirev Aleksei"><meta name="twitter:image:src" content="http://assayo.jp/assets/seo/custom_icon_256.png"><meta name="twitter:domain" content="assayo.jp"><meta name="twitter:site" content="assayo.jp"><meta itemprop="name" content="GIT Статистика"><meta itemprop="description" content="Простой и быстрый отчёт по истории коммитов в git."><meta itemprop="image" content="http://assayo.jp/assets/seo/custom_icon_256.png"><script defer="defer" src="./static/js/main.0fa5ec0a.js"></script><link href="./static/css/main.c2f3798a.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script type="text/javascript">!function(e,t,c,n,r,a,s){e[r]=e[r]||function(){(e[r].a=e[r].a||[]).push(arguments)},e[r].l=1*new Date;for(var i=0;i<document.scripts.length;i++)if(document.scripts[i].src===n)return;a=t.createElement(c),s=t.getElementsByTagName(c)[0],a.async=1,a.src=n,s.parentNode.insertBefore(a,s)}(window,document,"script","https://mc.yandex.ru/metrika/tag.js","ym"),ym(94903985,"init",{clickmap:!0,trackLinks:!0,accurateTrackBounce:!0,webvisor:!0})</script><noscript><div><img src="https://mc.yandex.ru/watch/94903985" style="position:absolute;left:-9999px" alt=""/></div></noscript></body></html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

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

File diff suppressed because one or more lines are too long

16
package-lock.json generated
View file

@ -5699,9 +5699,9 @@
}
},
"node_modules/caniuse-lite": {
"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",

File diff suppressed because it is too large Load diff

View file

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -2,8 +2,11 @@ import React from 'react';
import { HashRouter } from 'react-router-dom';
import { 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();
});

View file

@ -1,16 +1,34 @@
import { ISetting } from 'ts/pages/Settings/interfaces/Setting';
import { IUserSetting } from 'ts/interfaces/UserSetting';
import getEmptySettings from 'ts/pages/Settings/helpers/getEmptySettings';
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();
},
};

View file

@ -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('Текст скопирован');
}}
>
Копировать

View file

@ -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,

View file

@ -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])))

View file

@ -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;
}

View file

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

View file

@ -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;

View file

@ -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),
};
}

View file

@ -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;

View file

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

View file

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

View file

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

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,7 +26,7 @@ function Body({
? column.formatter(value)
: value;
const content: any = typeof column.template === 'function'
? column.template(formattedValue)
? column.template(formattedValue, row)
: `${column.prefixes ?? ''}${formattedValue ?? ''}${column.suffixes ?? ''}`;
return (

View file

@ -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);
}

View file

@ -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;

View file

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

View file

@ -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>

View file

@ -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,

View file

@ -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 ?? ''}

View file

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

View file

@ -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];

View file

@ -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}

View file

@ -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;

View file

@ -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;

View file

@ -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: коммитов

View file

@ -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)

View file

@ -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;

View file

@ -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,
},
};
});
}
}

View file

@ -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() {

View file

@ -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);

View file

@ -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 || 'неопределенна',

View file

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

View file

@ -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);

View file

@ -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],
};

View file

@ -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',
};

View file

@ -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}

View file

@ -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>
);
}

View file

@ -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>
);

View file

@ -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"

View file

@ -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'}*/}
{/*/>*/}
</>
);
}

View file

@ -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

View file

@ -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="&laquo;Взять гет&raquo; в данном случае означает первым оставить коммит к&nbsp;задаче с&nbsp;&laquo;красивым&raquo; номером."/>
</>
) : null}
</PageColumn>
</PageWrapper>
);

View file

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

View file

@ -2,6 +2,7 @@ import React from 'react';
import { useParams } from 'react-router-dom';
import 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/>}
</>
);
}

View file

@ -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>

View file

@ -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>
)}

View file

@ -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>
</>
);

View file

@ -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),
],
});
}}

View file

@ -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 {

View file

@ -1,30 +1,31 @@
import { ISetting, IEmployees, IEmployeesSalary } from '../interfaces/Setting';
import { IUserSetting, IEmployees, IEmployeesSalary } from 'ts/interfaces/UserSetting';
import ICommit from 'ts/interfaces/Commit';
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: [],
};
}

View file

@ -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;
}
}

View file

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

View file

@ -1,8 +1,10 @@
import { action, makeObservable } from 'mobx';
import { 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);
});

View file

@ -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 });

View file

@ -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

View file

@ -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' }} >

View file

@ -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 (

View file

@ -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">

View file

@ -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 (

View file

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

View file

@ -12,8 +12,9 @@ import Total from './components/Total';
import Tree from './components/Tree';
import 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/>}
</>
);

View file

@ -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>
</>
);
}

View file

@ -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,

View file

@ -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);

View file

@ -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;