test $ -.&? ${}

This commit is contained in:
bakhirev 2024-10-06 23:47:37 +03:00
parent 6f6ac5a749
commit 81cb8a1b40
54 changed files with 8779 additions and 8116 deletions

View file

@ -1 +1 @@
<!doctype html><html><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="default"><meta name="theme-color" content="white"/><meta name="defaultLanguage" content="ru"><meta name="availableLanguages" content="en, es, fr, ja, pt, de, zh, ru"><link rel="canonical" href="https://assayo.online/demo/"><script type="text/javascript">var report=[];function r(r){report.push(r)}var f=String.raw.bind(String)</script><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./logo192.png"/><link rel="manifest" href="./manifest.json"/><title>Git Statistics</title><script src='./log.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='../log.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='../../log.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='/log.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><meta name="description" content="Simple and fast report on git commit history."><meta name="keywords" content="git, statistics, audit, history, log, monitoring, employee control"><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 statistics"><meta name="msapplication-tooltip" content="Simple and fast report on Git commit history."><meta property="og:title" content="Git Statistics"><meta property="og:description" content="Simple and fast report on Git commit history."><meta property="og:image" content="https://assayo.online/assets/seo/custom_icon_256.png"><meta property="og:site_name" content="Assayo"><meta property="og:url" content="https://assayo.online/"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="Git Statistics"><meta name="twitter:description" content="Simple and fast report on Git commit history."><meta name="twitter:creator" content="Bakhirev Aleksei"><meta name="twitter:image:src" content="https://assayo.online/assets/seo/custom_icon_256.png"><meta name="twitter:domain" content="assayo.online"><meta name="twitter:site" content="assayo.online"><meta itemprop="name" content="Git Statistics"><meta itemprop="description" content="Simple and fast report on Git commit history."><meta itemprop="image" content="https://assayo.online/assets/seo/custom_icon_256.png"><script defer="defer" src="./static/index.js"></script><link href="./static/index.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><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="mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="theme-color" content="white"/><meta name="defaultLanguage" content="ru"><meta name="availableLanguages" content="en, es, fr, ja, pt, de, zh, ru"><link rel="canonical" href="https://assayo.online/demo/"><script type="text/javascript">var report=[];function r(r){report.push(r)}function R(r){report=report.concat(r.split("\n"))}var f=String.raw.bind(String)</script><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./logo192.png"/><title>Git Statistics</title><script src='./log.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='../log.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='../../log.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='/log.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><meta name="description" content="Simple and fast report on git commit history."><meta name="keywords" content="git, statistics, audit, history, log, monitoring, employee control"><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 statistics"><meta name="msapplication-tooltip" content="Simple and fast report on Git commit history."><meta property="og:title" content="Git Statistics"><meta property="og:description" content="Simple and fast report on Git commit history."><meta property="og:image" content="https://assayo.online/assets/seo/custom_icon_256.png"><meta property="og:site_name" content="Assayo"><meta property="og:url" content="https://assayo.online/"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="Git Statistics"><meta name="twitter:description" content="Simple and fast report on Git commit history."><meta name="twitter:creator" content="Bakhirev Aleksei"><meta name="twitter:image:src" content="https://assayo.online/assets/seo/custom_icon_256.png"><meta name="twitter:domain" content="assayo.online"><meta name="twitter:site" content="assayo.online"><meta itemprop="name" content="Git Statistics"><meta itemprop="description" content="Simple and fast report on Git commit history."><meta itemprop="image" content="https://assayo.online/assets/seo/custom_icon_256.png"><script defer="defer" src="./static/index.js"></script><link href="./static/index.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -145,7 +145,7 @@
}, },
{ {
"pre": [ "pre": [
"git --no-pager log --raw --numstat --oneline --all --reverse --date=iso-strict --pretty=format:\"%ad>%aN>%aE>%s\" | sed -e 's/\\\\/\\\\\\\\/g' | sed -e 's/`/\"/g' | sed -e 's/^/r(f\\`/g' | sed 's/$/\\`\\);/g' | sed 's/\\$/_/g' > log.txt" "git --no-pager log --raw --numstat --oneline --all --reverse --date=iso-strict --pretty=format:\"%ad>%aN>%aE>%s\" | sed -e 's/\\\\/\\\\\\\\/g' | sed -e 's/`/\"/g' | sed -e 's/\\$/S/g' | sed -e '1s/^/R(f\\`/' | sed -e '$s/$/\\`\\);/' > log.txt",
] ]
}, },
{ {

View file

@ -18,10 +18,13 @@
<script type="text/javascript"> <script type="text/javascript">
var report = []; var report = [];
var f = String.raw.bind(String);
function r(t) { function r(t) {
report.push(t); report.push(t);
} }
var f = String.raw.bind(String); function R(t) {
report = report.concat(t.split("\n"));
}
</script> </script>
<link rel="icon" href="%PUBLIC_URL%/favicon.svg" /> <link rel="icon" href="%PUBLIC_URL%/favicon.svg" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />

File diff suppressed because it is too large Load diff

View file

@ -16,6 +16,7 @@ import PageWrapper from '../Page/wrapper';
interface IDataViewProps { interface IDataViewProps {
rowsForExcel?: any[]; rowsForExcel?: any[];
rows: any[]; rows: any[];
mode?: string;
type?: string; type?: string;
sort?: ISort[]; sort?: ISort[];
columnCount?: number, columnCount?: number,
@ -32,6 +33,7 @@ function DataView({
rows = [], rows = [],
sort = [], sort = [],
type, type,
mode,
columnCount, columnCount,
className, className,
fullScreenMode = '', fullScreenMode = '',
@ -58,6 +60,7 @@ function DataView({
return ( return (
<> <>
{mode !== 'details' && (
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<div className={style.data_view_buttons}> <div className={style.data_view_buttons}>
{!isMobile && ( {!isMobile && (
@ -95,8 +98,9 @@ function DataView({
)} )}
</div> </div>
</div> </div>
)}
{localType === 'table' && ( {localType === 'table' && mode !== 'details' && (
<PageWrapper template="table"> <PageWrapper template="table">
<Table <Table
rows={rows} rows={rows}
@ -109,6 +113,17 @@ function DataView({
</PageWrapper> </PageWrapper>
)} )}
{localType === 'table' && mode === 'details' && (
<Table
rows={rows}
sort={sort}
disabledRow={disabledRow}
updateSort={updateSort}
>
{children}
</Table>
)}
{localType === 'cards' && ( {localType === 'cards' && (
<Cards <Cards
items={rows} items={rows}

View file

@ -39,7 +39,7 @@ function CommitInfo({ commits }: { commits: ICommit[] }): React.ReactElement {
function TaskInfo({ tasks }: { tasks: ITask }): React.ReactElement { function TaskInfo({ tasks }: { tasks: ITask }): React.ReactElement {
const items = Object.entries(tasks) const items = Object.entries(tasks)
.map(([task, commits]: [string, any]) => { .map(([task, commits]: [string, any]) => {
const prId = dataGrip.pr.prByTask[task]; const prId = dataGrip.pr.prByTask.get(task);
return ( return (
<> <>
<div className={style.day_info_link}> <div className={style.day_info_link}>

View file

@ -8,28 +8,31 @@
// onChange('meta', { byTaskId }); // onChange('meta', { byTaskId });
// } // }
import splashScreenStore from 'ts/components/SplashScreen/store';
function getGlobalValue() { // @ts-ignore function getGlobalValue() { // @ts-ignore
return window.report; return window.report;
} }
function setGlobalValue(value?: any) { // @ts-ignore function setGlobalValue(value?: any) { // @ts-ignore
window.report = value || []; window.report = value || [];
splashScreenStore.setDelay((value || [])?.length);
} }
export function getStringsForParser(text: string) { export function getStringsForParser(text: string) {
let temp = getGlobalValue();
setGlobalValue([]); setGlobalValue([]);
const firstText = text.slice(0, 3); const firstText = text.slice(0, 3);
if (firstText === 'rep' || firstText === 'r(f') { const isNeedClear = {
try { 'rep': true,
eval(text); 'r(f': true,
} catch (e) { 'R(f': true,
setGlobalValue(temp); }[firstText];
return;
if (isNeedClear) {
text = text.replace(/(R\(f`)|(r\(f`)|(report\.push\(`)|(`\);)/gim, '');
} }
} else {
setGlobalValue(text.split('\n')); setGlobalValue(text.split('\n'));
}
return getGlobalValue(); return getGlobalValue();
} }
@ -56,7 +59,6 @@ export function getOnDrop(setLoading: Function, onChange: Function) {
.map((file: any) => file.kind === 'file' ? file?.getAsFile() : null) .map((file: any) => file.kind === 'file' ? file?.getAsFile() : null)
.filter(file => file); .filter(file => file);
console.log(files);
setLoading(false); setLoading(false);
if (!files.length) return; if (!files.length) return;

View file

@ -17,7 +17,7 @@ function GetItem({ commit, mode }: IGetItemProps) {
const className = size > 5 const className = size > 5
? style.get_list_big_number ? style.get_list_big_number
: ''; : '';
const prId = dataGrip.pr.prByTask[commit.task]; const prId = dataGrip.pr.prByTask.get(commit.task);
return ( return (
<div className={style.get_list}> <div className={style.get_list}>

View file

@ -12,16 +12,27 @@ const SplashScreen = observer((): React.ReactElement | null => {
if (!splashScreenStore.isOpen) return; if (!splashScreenStore.isOpen) return;
setTimeout(() => { setTimeout(() => {
splashScreenStore.hide(); splashScreenStore.hide();
}, 5400); }, splashScreenStore.delay);
}, [splashScreenStore.isOpen]); }, [splashScreenStore.isOpen]);
if (!splashScreenStore.isOpen) return null; if (!splashScreenStore.isOpen) return null;
return ( return (
<div className={style.splash_screen}> <div
<div className={style.splash_screen_container}> className={style.splash_screen}
style={{ animationDelay: splashScreenStore.getDelay(100) }}
>
<div
className={style.splash_screen_container}
style={{ animationDelay: splashScreenStore.getDelay(-1400) }}
>
<Logo center /> <Logo center />
<div className={progress.progress_bar}></div> <div className={progress.progress_bar}>
<div
className={progress.progress_bar_line}
style={{ animationDuration: splashScreenStore.getDelay(-1100) }}
/>
</div>
</div> </div>
</div> </div>
); );

View file

@ -7,8 +7,7 @@
background-color: #404148; background-color: #404148;
&:after { &_line {
content: '';
font-size: 0.8em; font-size: 0.8em;
display: block; display: block;
width: 0; width: 0;
@ -33,11 +32,11 @@
} }
.progress_bar, .progress_bar,
.progress_bar:after { .progress_bar_line {
transition: background-color 0.7s, width 0.5s; transition: background-color 0.7s, width 0.5s;
} }
.progress_bar:after { .progress_bar_line {
animation: progress_bar 4.3s linear forwards; animation: progress_bar 4.3s linear forwards;
} }

View file

@ -1,25 +1,41 @@
import { observable, action, makeObservable } from 'mobx'; import { observable, action, makeObservable } from 'mobx';
import globalScroll from 'ts/helpers/globalScroll'; import globalScroll from 'ts/helpers/globalScroll';
const DEFAULT_DELAY = 3400;
class SplashScreenStore { class SplashScreenStore {
isOpen: boolean = false; isOpen: boolean = false;
delay: number = DEFAULT_DELAY;
constructor() { constructor() {
makeObservable(this, { makeObservable(this, {
isOpen: observable, isOpen: observable,
delay: observable,
show: action, show: action,
hide: action, hide: action,
setDelay: action,
}); });
} }
show() { show() {
this.isOpen = true; this.isOpen = true;
globalScroll.off(5400); globalScroll.off(this.delay);
} }
hide() { hide() {
this.isOpen = false; this.isOpen = false;
} }
setDelay(logSize: number) {
const delay = (logSize / 190) + 400;
this.delay = Math.max(DEFAULT_DELAY, delay);
}
getDelay(diff?: number) {
const delay = this.delay + (diff || 0);
return (delay / 1000).toFixed(1) + 's';
}
} }
const splashScreen = new SplashScreenStore(); const splashScreen = new SplashScreenStore();

View file

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { IColumn, IRowsConfig } from '../../interfaces/Column'; import { IColumn, IRowsConfig } from '../../interfaces/Column';
import getClassName from '../../helpers/getClassName';
import style from '../../styles/index.module.scss'; import style from '../../styles/index.module.scss';
interface IDefaultCellProps { interface IDefaultCellProps {
@ -24,14 +25,12 @@ function DetailsCell({
const left = column?.isFixed ? marginLeft : 0; const left = column?.isFixed ? marginLeft : 0;
const columnClassName = typeof column.className === 'function'
? column.className('body', row)
: column.className;
const iconClassName = config?.details const iconClassName = config?.details
? style.table_cell_icon_open ? style.table_cell_icon_open
: style.table_cell_icon_close; : style.table_cell_icon_close;
const localClassName = getClassName(style.table_cell, column, ['body', row], className);
const hasIcon = ((column.properties && row[column.properties]) const hasIcon = ((column.properties && row[column.properties])
|| !column.properties || !column.properties
|| !column.properties?.length) || !column.properties?.length)
@ -47,13 +46,13 @@ function DetailsCell({
return ( return (
<div <div
key={column.title} // @ts-ignore key={column.title}
className={`${style.table_cell} ${className || ''} ${columnClassName || ''}`} className={localClassName}
style={{ style={{
left,
width: column.width, width: column.width,
cursor: 'pointer', cursor: 'pointer',
left, }}
}} // @ts-ignore
onClick={onClick} onClick={onClick}
> >
{hasIcon && ( {hasIcon && (

View file

@ -41,11 +41,13 @@
position: relative; position: relative;
font-weight: 100; font-weight: 100;
display: block; display: block;
padding-top: 24px; padding: 0 var(--space-m) var(--space-xxl) 42px;
padding-left: 12px;
white-space: nowrap; white-space: nowrap;
border-bottom: 1px solid #EEEEEE;
break-inside: auto; break-inside: auto;
border-bottom: 1px solid #EEEEEE;
border-radius: var(--border-radius-m);
} }
&_cell, &_cell,
@ -62,6 +64,8 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
vertical-align: top; vertical-align: top;
background-color: var(--color-white);
} }
&_fixed { &_fixed {
@ -97,11 +101,11 @@
margin-top: 8px; margin-top: 8px;
cursor: pointer; cursor: pointer;
transition: transform 0.5s; transition: transform 0.5s;
transform: rotate(0); transform: rotate(-90deg);
} }
&_open { &_open {
transform: rotate(-180deg); transform: rotate(0);
} }
} }
} }

View file

@ -43,7 +43,7 @@ interface ITaskProps {
function Task({ title, commits }: ITaskProps) { function Task({ title, commits }: ITaskProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const prId = dataGrip.pr.prByTask[title]; const prId = dataGrip.pr.prByTask.get(title);
return ( return (
<div <div
key={title} key={title}

View file

@ -1,16 +1,16 @@
import ICommit from 'ts/interfaces/Commit'; import ICommit from 'ts/interfaces/Commit';
import IHashMap from 'ts/interfaces/HashMap'; import IHashMap, { HashMap } from 'ts/interfaces/HashMap';
import { ONE_DAY } from 'ts/helpers/formatter'; import { ONE_DAY } from 'ts/helpers/formatter';
import { increment } from 'ts/helpers/Math'; import { createHashMap, createIncrement, increment } from 'ts/helpers/Math';
import getCompany from '../helpers/getCompany'; import getCompany from 'ts/helpers/Parser/getCompany';
import userSettings from 'ts/store/UserSettings'; import userSettings from 'ts/store/UserSettings';
export default class DataGripByAuthor { export default class DataGripByAuthor {
list: string[] = []; list: string[] = [];
commits: IHashMap<any> = {}; commits: HashMap<any> = new Map();
statistic: any = []; statistic: any = [];
@ -20,22 +20,22 @@ export default class DataGripByAuthor {
clear() { clear() {
this.list = []; this.list = [];
this.commits = {}; this.commits.clear();
this.statistic = []; this.statistic = [];
this.statisticByName = {}; this.statisticByName = {};
} }
addCommit(commit: ICommit) { addCommit(commit: ICommit) {
if (this.commits.hasOwnProperty(commit.author)) { const statistic = this.commits.get(commit.author);
this.#updateCommitByAuthor(commit); if (statistic) {
this.#updateCommitByAuthor(statistic, commit);
} else { } else {
this.#addCommitByAuthor(commit); this.#addCommitByAuthor(commit);
} }
this.#setMoneyByMonth(commit); this.#setMoneyByMonth(commit);
} }
#updateCommitByAuthor(commit: ICommit) { #updateCommitByAuthor(statistic: any, commit: ICommit) {
const statistic = this.commits[commit.author];
statistic.commits += 1; statistic.commits += 1;
statistic.lastCommit = commit; statistic.lastCommit = commit;
statistic.days[commit.timestamp] = true; statistic.days[commit.timestamp] = true;
@ -56,6 +56,11 @@ export default class DataGripByAuthor {
} }
statistic.commitsByHour[commit.hours] += 1; statistic.commitsByHour[commit.hours] += 1;
statistic.wordStatistics = DataGripByAuthor.#updateWordStatistics(commit, statistic.wordStatistics); statistic.wordStatistics = DataGripByAuthor.#updateWordStatistics(commit, statistic.wordStatistics);
if (commit.company && statistic.lastCompany !== commit.company) {
statistic.lastCompany = commit.company;
statistic.company.push({ title: commit.company, from: commit.timestamp });
}
} }
#addCommitByAuthor(commit: ICommit) { #addCommitByAuthor(commit: ICommit) {
@ -65,16 +70,20 @@ export default class DataGripByAuthor {
const commitsByHour = new Array(24).fill(0); const commitsByHour = new Array(24).fill(0);
commitsByHour[commit.hours] += 1; commitsByHour[commit.hours] += 1;
this.commits[commit.author] = { this.commits.set(commit.author, {
author: commit.author, author: commit.author,
commits: 1, commits: 1,
firstCommit: commit, firstCommit: commit,
lastCommit: commit, lastCommit: commit,
days: { [commit.timestamp]: true }, days: createHashMap(commit.timestamp),
tasks: { [commit.task]: commit.added + commit.changes + commit.removed }, tasks: { [commit.task]: commit.added + commit.changes + commit.removed },
types: { [commit.type]: 1 }, types: createIncrement(commit.type),
scopes: { [commit.scope]: 1 }, scopes: createIncrement(commit.scope),
hours: [commit.hours], hours: [commit.hours],
company: commit.company
? [{ title: commit.company, from: commit.timestamp }]
: [],
lastCompany: commit.company,
commitsByDayAndHour, commitsByDayAndHour,
commitsByHour, commitsByHour,
messageLength: [commit.text.length || 0], messageLength: [commit.text.length || 0],
@ -82,12 +91,12 @@ export default class DataGripByAuthor {
maxMessageLength: commit.text.length || 0, maxMessageLength: commit.text.length || 0,
wordStatistics: DataGripByAuthor.#updateWordStatistics(commit), wordStatistics: DataGripByAuthor.#updateWordStatistics(commit),
moneyByMonth: {}, moneyByMonth: {},
}; });
} }
#setMoneyByMonth(commit: ICommit) { #setMoneyByMonth(commit: ICommit) {
const key = `${commit.year}-${commit.month}`; const key = `${commit.year}-${commit.month}`;
if (this.commits[commit.author].moneyByMonth[key]) { if (this.commits.get(commit.author).moneyByMonth[key]) {
this.#updateMoneyByMonth(commit, key); this.#updateMoneyByMonth(commit, key);
} else { } else {
this.#addMoneyByMonth(commit, key); this.#addMoneyByMonth(commit, key);
@ -95,7 +104,7 @@ export default class DataGripByAuthor {
} }
#updateMoneyByMonth(commit: ICommit, key: string) { #updateMoneyByMonth(commit: ICommit, key: string) {
const statistic = this.commits[commit.author].moneyByMonth[key]; const statistic = this.commits.get(commit.author).moneyByMonth[key];
if (statistic.alreadyAdded[commit.milliseconds]) return; if (statistic.alreadyAdded[commit.milliseconds]) return;
statistic.alreadyAdded[commit.milliseconds] = true; statistic.alreadyAdded[commit.milliseconds] = true;
@ -110,7 +119,7 @@ export default class DataGripByAuthor {
#addMoneyByMonth(commit: ICommit, key: string) { #addMoneyByMonth(commit: ICommit, key: string) {
const contract = userSettings.getEmploymentContract(commit.author, commit.milliseconds); const contract = userSettings.getEmploymentContract(commit.author, commit.milliseconds);
const isWorkDay = contract.workDaysInWeek[commit.day]; const isWorkDay = contract.workDaysInWeek[commit.day];
this.commits[commit.author].moneyByMonth[key] = { this.commits.get(commit.author).moneyByMonth[key] = {
workDay: isWorkDay ? 1 : 0, workDay: isWorkDay ? 1 : 0,
weekDay: isWorkDay ? 0 : 1, weekDay: isWorkDay ? 0 : 1,
alreadyAdded: { alreadyAdded: {
@ -147,7 +156,7 @@ export default class DataGripByAuthor {
active: [], active: [],
}; };
this.statistic = Object.values(this.commits) this.statistic = Array.from(this.commits.values())
.sort((dotA: any, dotB: any) => dotB.commits - dotA.commits) .sort((dotA: any, dotB: any) => dotB.commits - dotA.commits)
.map((dot: any) => { .map((dot: any) => {
const from = dot.firstCommit.milliseconds; const from = dot.firstCommit.milliseconds;
@ -186,7 +195,7 @@ export default class DataGripByAuthor {
daysForTask: isStaff ? 0 : workDays / tasks.length, daysForTask: isStaff ? 0 : workDays / tasks.length,
taskInDay: isStaff ? 0 : tasks.length / workDays, taskInDay: isStaff ? 0 : tasks.length / workDays,
changesForTask: DataGripByAuthor.getMiddleValue(tasksSize), changesForTask: DataGripByAuthor.getMiddleValue(tasksSize),
company: getCompany(dot.author, dot.lastCommit.email), lastCompany: getCompany(dot.author, dot.lastCommit.email),
days: workDays, days: workDays,
money: isStaff ? 0 : moneyWorked, money: isStaff ? 0 : moneyWorked,

View file

@ -0,0 +1,87 @@
import ICommit from 'ts/interfaces/Commit';
import IHashMap, { HashMap } from 'ts/interfaces/HashMap';
import { createIncrement, increment } from 'ts/helpers/Math';
export default class DataGripByCompany {
commits: HashMap<any> = new Map();
statistic: any = [];
statisticByName: IHashMap<any> = {};
clear() {
this.commits.clear();
this.statistic = [];
this.statisticByName = {};
}
addCommit(commit: ICommit) {
if (!commit.company) return;
const statistic = this.commits.get(commit.company);
if (statistic) {
this.#updateCommitByCompany(statistic, commit);
} else {
this.#addCommitByCompany(commit);
}
}
#addCommitByCompany(commit: ICommit) {
this.commits.set(commit.company, {
company: commit.company,
commits: 1,
firstCommit: commit,
lastCommit: commit,
days: createIncrement(commit.timestamp),
employments: createIncrement(commit.author),
tasks: createIncrement(commit.task),
types: createIncrement(commit.type),
scopes: createIncrement(commit.scope),
});
}
#updateCommitByCompany(statistic: any, commit: ICommit) {
statistic.commits += 1;
statistic.lastCommit = commit;
statistic.days[commit.timestamp] = true;
statistic.employments[commit.author] = true;
increment(statistic.tasks, commit.task);
increment(statistic.types, commit.type);
increment(statistic.scopes, commit.scope);
}
updateTotalInfo(dataGripByAuthor: any) {
console.dir(dataGripByAuthor);
this.statistic = Array.from(this.commits.values())
.sort((dotA: any, dotB: any) => dotB.commits - dotA.commits)
.map((statistic: any) => {
const tasks = Object.keys(statistic.tasks);
const days = Object.keys(statistic.days);
const employments = Object.keys(statistic.employments);
let isActive = false;
employments.forEach((name) => {
const author = dataGripByAuthor.statisticByName[name];
if (!author) return;
if (author.lastCompany === statistic.company) isActive = true;
});
const companyInfo = {
...statistic,
employments,
tasks,
totalTasks: tasks.length,
totalDays: days.length,
totalEmployments: employments.length,
isActive,
};
delete companyInfo.days;
this.statisticByName[statistic.company] = companyInfo;
return companyInfo;
});
this.commits.clear();
}
}

View file

@ -1,6 +1,6 @@
import { COMMIT_TYPE, ISystemCommit } from 'ts/interfaces/Commit'; import { COMMIT_TYPE, ISystemCommit } from 'ts/interfaces/Commit';
import IHashMap from 'ts/interfaces/HashMap'; import IHashMap, { HashMap } from 'ts/interfaces/HashMap';
import { increment, WeightedAverage } from 'ts/helpers/Math'; import { createIncrement, increment, WeightedAverage } from 'ts/helpers/Math';
const IS_PR = { const IS_PR = {
[COMMIT_TYPE.PR_BITBUCKET]: true, [COMMIT_TYPE.PR_BITBUCKET]: true,
@ -9,71 +9,69 @@ const IS_PR = {
}; };
export default class DataGripByPR { export default class DataGripByPR {
pr: IHashMap<any> = {}; pr: HashMap<any> = new Map();
prByTask: IHashMap<string> = {}; prByTask: HashMap<any> = new Map();
lastCommitByTaskNumber: IHashMap<any> = {}; lastCommitByTaskNumber: HashMap<any> = new Map();
statistic: any[] = []; statistic: any[] = [];
statisticByName: IHashMap<any> = []; statisticByName: IHashMap<any> = [];
clear() { clear() {
this.pr = {}; this.pr.clear();
this.prByTask = {}; this.prByTask.clear();
this.lastCommitByTaskNumber = {}; this.lastCommitByTaskNumber.clear();
this.statistic = []; this.statistic = [];
} }
addCommit(commit: ISystemCommit) { addCommit(commit: ISystemCommit) {
if (!commit.commitType) { if (!commit.commitType) {
if (!this.lastCommitByTaskNumber[commit.task]) { const commitByTaskNumber = this.lastCommitByTaskNumber.get(commit.task);
this.#addCommitByTaskNumber(commit); if (commitByTaskNumber) {
this.#updateCommitByTaskNumber(commitByTaskNumber, commit);
} else { } else {
this.#updateCommitByTaskNumber(commit); this.#addCommitByTaskNumber(commit);
} }
} else if (!this.pr[commit.prId] && IS_PR[commit.commitType || '']) { } else if (!this.pr.has(commit.prId) && IS_PR[commit.commitType || '']) {
this.#addCommitByPR(commit); this.#addCommitByPR(commit);
} }
} }
#addCommitByTaskNumber(commit: ISystemCommit) { #addCommitByTaskNumber(commit: ISystemCommit) {
this.lastCommitByTaskNumber[commit.task] = { this.lastCommitByTaskNumber.set(commit.task, {
commits : 1, commits : 1,
beginTaskTime: commit.milliseconds, beginTaskTime: commit.milliseconds,
endTaskTime: commit.milliseconds, endTaskTime: commit.milliseconds,
commitsByAuthors: { commitsByAuthors: createIncrement(commit.author),
[commit.author]: 1,
},
firstCommit: commit, firstCommit: commit,
}; });
} }
#updateCommitByTaskNumber(commit: ISystemCommit) { #updateCommitByTaskNumber(statistic: any, commit: ISystemCommit) {
const statistic = this.lastCommitByTaskNumber[commit.task];
statistic.endTaskTime = commit.milliseconds; statistic.endTaskTime = commit.milliseconds;
statistic.commits += 1; statistic.commits += 1;
increment(statistic.commitsByAuthors, commit.author); increment(statistic.commitsByAuthors, commit.author);
} }
#addCommitByPR(commit: ISystemCommit) { #addCommitByPR(commit: ISystemCommit) {
const lastCommit = this.lastCommitByTaskNumber[commit.task]; const lastCommit = this.lastCommitByTaskNumber.get(commit.task);
if (lastCommit) { if (lastCommit) {
// коммиты после влития PR сгорают, чтобы не засчитать технические PR мержи веток // коммиты после влития PR сгорают, чтобы не засчитать технические PR мержи веток
delete this.lastCommitByTaskNumber[commit.task]; this.lastCommitByTaskNumber.delete(commit.task);
const delay = commit.milliseconds - lastCommit.endTaskTime; const delay = commit.milliseconds - lastCommit.endTaskTime;
const work = lastCommit.endTaskTime - lastCommit.beginTaskTime; const work = lastCommit.endTaskTime - lastCommit.beginTaskTime;
this.pr[commit.prId] = { this.pr.set(commit.prId, {
...commit, ...commit,
...lastCommit, ...lastCommit,
delay, delay,
delayDays: delay / (24 * 60 * 60 * 1000), delayDays: delay / (24 * 60 * 60 * 1000),
workDays: work === 0 ? 1 : (work / (24 * 60 * 60 * 1000)), workDays: work === 0 ? 1 : (work / (24 * 60 * 60 * 1000)),
}; });
this.prByTask[commit.task] = commit.prId; this.prByTask.set(commit.task, commit.prId);
} else { } else {
this.pr[commit.prId] = { ...commit }; this.pr.set(commit.prId, { ...commit });
} }
} }
@ -101,7 +99,7 @@ export default class DataGripByPR {
this.statistic.sort((a: any, b: any) => b.delay - a.delay); this.statistic.sort((a: any, b: any) => b.delay - a.delay);
this.updateTotalByAuthor(authors, refAuthorPR); this.updateTotalByAuthor(authors, refAuthorPR);
this.lastCommitByTaskNumber = {}; this.lastCommitByTaskNumber.clear();
} }
static getPRByGroups(list: any, propertyName: string) { static getPRByGroups(list: any, propertyName: string) {

View file

@ -1,7 +1,7 @@
import ICommit from 'ts/interfaces/Commit'; import ICommit from 'ts/interfaces/Commit';
import IHashMap from 'ts/interfaces/HashMap'; import IHashMap from 'ts/interfaces/HashMap';
import userSettings from 'ts/store/UserSettings'; import userSettings from 'ts/store/UserSettings';
import { increment } from 'ts/helpers/Math'; import { createHashMap, createIncrement, increment } from 'ts/helpers/Math';
interface IStatByAuthor { interface IStatByAuthor {
commits: number; // number of commits by author in this scope commits: number; // number of commits by author in this scope
@ -60,10 +60,10 @@ export default class DataGripByScope {
this.commits[commit.scope] = { this.commits[commit.scope] = {
scope: commit.scope, scope: commit.scope,
commits: 1, commits: 1,
days: { [commit.timestamp]: true }, days: createHashMap(commit.timestamp),
tasks: { [commit.task]: true }, tasks: createHashMap(commit.task),
types: { [commit.type]: 1 }, types: createIncrement(commit.type),
authors: { [commit.author]: this.#getDefaultAuthorForScope(commit) }, authors: createIncrement(commit.author, this.#getDefaultAuthorForScope(commit)),
}; };
} }

View file

@ -40,7 +40,7 @@ export default class DataGripByTasks {
const firstCommit = commits[0]; const firstCommit = commits[0];
const lastCommit = commits[commits.length - 1]; const lastCommit = commits[commits.length - 1];
const from = firstCommit.milliseconds; const from = firstCommit.milliseconds;
const pr = PRs.prByTask[task] ? PRs.pr[PRs.prByTask[task]] : null; const pr = PRs.prByTask.get(task) ? PRs.pr.get(PRs.prByTask.get(task)) : null;
const shortInfo = { const shortInfo = {
task, task,

View file

@ -1,14 +1,14 @@
import ICommit from 'ts/interfaces/Commit'; import ICommit from 'ts/interfaces/Commit';
import IHashMap from 'ts/interfaces/HashMap'; import { HashMap } from 'ts/interfaces/HashMap';
import userSettings from 'ts/store/UserSettings'; import userSettings from 'ts/store/UserSettings';
import { increment } from 'ts/helpers/Math'; import { increment } from 'ts/helpers/Math';
import MinMaxCounter from './counter'; import MinMaxCounter from './counter';
export default class DataGripByTimestamp { export default class DataGripByTimestamp {
commits: IHashMap<any> = {}; commits: HashMap<any> = new Map();
commitsByAuthor: IHashMap<any> = {}; commitsByAuthor: HashMap<any> = new Map();
statistic: any = []; statistic: any = [];
@ -19,29 +19,35 @@ export default class DataGripByTimestamp {
} }
clear() { clear() {
this.commits = {}; this.commits.clear();
this.commitsByAuthor = {}; this.commitsByAuthor.clear();
this.statistic = []; this.statistic = [];
this.statisticByAuthor = {}; this.statisticByAuthor = {};
} }
addCommit(commit: ICommit) { addCommit(commit: ICommit) {
if (this.commits[commit.milliseconds]) { const commitByMilliseconds = this.commits.get(commit.milliseconds);
this.#updateCommitByTimestamp(commit, this.commits[commit.milliseconds]); if (commitByMilliseconds) {
this.#updateCommitByTimestamp(commitByMilliseconds, commit);
} else { } else {
this.commits[commit.milliseconds] = this.#getDefaultCommitByTimestamp(commit); this.commits.set(commit.milliseconds, this.#getDefaultCommitByTimestamp(commit));
} }
if (!this.commitsByAuthor[commit.author]) {
this.commitsByAuthor[commit.author] = {}; let commitsByAuthor = this.commitsByAuthor.get(commit.author);
if (!commitsByAuthor) {
commitsByAuthor = new Map();
this.commitsByAuthor.set(commit.author, commitsByAuthor);
} }
if (this.commitsByAuthor[commit.author][commit.milliseconds]) {
this.#updateCommitByTimestamp(commit, this.commitsByAuthor[commit.author][commit.milliseconds]); const commitByAuthorMilliseconds = commitsByAuthor.get(commit.milliseconds);
if (commitByAuthorMilliseconds) {
this.#updateCommitByTimestamp(commitByAuthorMilliseconds, commit);
} else { } else {
this.commitsByAuthor[commit.author][commit.milliseconds] = this.#getDefaultCommitByTimestamp(commit); commitsByAuthor.set(commit.milliseconds, this.#getDefaultCommitByTimestamp(commit));
} }
} }
#updateCommitByTimestamp(commit: ICommit, statistic: any) { #updateCommitByTimestamp(statistic: any, commit: ICommit) {
statistic.commits += 1; statistic.commits += 1;
statistic.addedAndChanges += commit.added + commit.changes; statistic.addedAndChanges += commit.added + commit.changes;
increment(statistic.tasks, commit.task); increment(statistic.tasks, commit.task);
@ -76,16 +82,16 @@ export default class DataGripByTimestamp {
updateTotalInfo(dataGripByAuthor: any) { updateTotalInfo(dataGripByAuthor: any) {
this.statistic = this.#getTotalInfo(this.commits); this.statistic = this.#getTotalInfo(this.commits);
this.statistic.weekendPayment = 0; this.statistic.weekendPayment = 0;
for (let author in this.commitsByAuthor) { for (let author of this.commitsByAuthor.keys()) {
const statistic = this.#getTotalInfo(this.commitsByAuthor[author]); const statistic = this.#getTotalInfo(this.commitsByAuthor.get(author));
statistic.weekendPayment = this.#getWeekendPaymentByAuthor(statistic, dataGripByAuthor.statisticByName[author]); statistic.weekendPayment = this.#getWeekendPaymentByAuthor(statistic, dataGripByAuthor.statisticByName[author || '']);
this.statisticByAuthor[author] = statistic; // TODO: странный результат, неверный расчёт? this.statisticByAuthor[author || ''] = statistic; // TODO: странный результат, неверный расчёт?
this.statistic.weekendPayment += statistic.weekendPayment; this.statistic.weekendPayment += statistic.weekendPayment;
} }
} }
#getTotalInfo(uniqCommitsByTimestamp: any) { #getTotalInfo(uniqCommitsByTimestamp: HashMap<any>) {
const allCommitsByTimestamp = Object.values(uniqCommitsByTimestamp); const allCommitsByTimestamp = Array.from(uniqCommitsByTimestamp.values());
const commitsCounter = new MinMaxCounter(); const commitsCounter = new MinMaxCounter();
const changesCounter = new MinMaxCounter(); const changesCounter = new MinMaxCounter();
@ -111,12 +117,6 @@ export default class DataGripByTimestamp {
}; };
} }
#getMiddleValue(list: any, property: string) {
const sortList = list.sort((a: any, b: any) => b[property] - a[property]);
const gap = Math.floor(sortList.length * 0.05);
return sortList.slice(gap, sortList.length - gap);
}
#getWeekendPaymentByAuthor(statistic: any, dataGripByAuthor: any) { #getWeekendPaymentByAuthor(statistic: any, dataGripByAuthor: any) {
if (dataGripByAuthor.isStaff) return 0; if (dataGripByAuthor.isStaff) return 0;
const salaryInMonth = userSettings.getCurrentSalaryInMonth(dataGripByAuthor.author); const salaryInMonth = userSettings.getCurrentSalaryInMonth(dataGripByAuthor.author);

View file

@ -1,6 +1,6 @@
import ICommit from 'ts/interfaces/Commit'; import ICommit from 'ts/interfaces/Commit';
import IHashMap from 'ts/interfaces/HashMap'; import IHashMap from 'ts/interfaces/HashMap';
import { increment } from 'ts/helpers/Math'; import { createIncrement, increment } from 'ts/helpers/Math';
import { POPULAR_TYPES } from 'ts/helpers/Parser/getTypeAndScope'; import { POPULAR_TYPES } from 'ts/helpers/Parser/getTypeAndScope';
export default class DataGripByType { export default class DataGripByType {
@ -39,10 +39,12 @@ export default class DataGripByType {
this.commits[commit.type] = { this.commits[commit.type] = {
type: commit.type, type: commit.type,
commits: 1, commits: 1,
days: { [commit.timestamp]: true }, days: createIncrement(commit.timestamp, true),
tasks: { [commit.task]: true }, tasks: createIncrement(commit.task, true),
commitsByAuthors: { [commit.author]: 1 }, commitsByAuthors: createIncrement(commit.author, true),
daysByAuthors: { [commit.author]: { [commit.timestamp]: true } }, daysByAuthors: {
[commit.author]: createIncrement(commit.timestamp, true),
},
}; };
} }

View file

@ -14,12 +14,15 @@ import DataGripByPR from './components/pr';
import DataGripByTasks from './components/tasks'; import DataGripByTasks from './components/tasks';
import DataGripByRelease from './components/release'; import DataGripByRelease from './components/release';
import DataGripByScoring from './components/scoring'; import DataGripByScoring from './components/scoring';
import DataGripByCompany from './components/company';
class DataGrip { class DataGrip {
firstLastCommit: any = new MinMaxCounter(); firstLastCommit: any = new MinMaxCounter();
author: any = new DataGripByAuthor(); author: any = new DataGripByAuthor();
company: any = new DataGripByCompany();
team: any = new DataGripByTeam(); team: any = new DataGripByTeam();
scope: any = new DataGripByScope(); scope: any = new DataGripByScope();
@ -45,6 +48,7 @@ class DataGrip {
clear() { clear() {
this.firstLastCommit.clear(); this.firstLastCommit.clear();
this.author.clear(); this.author.clear();
this.company.clear();
this.team.clear(); this.team.clear();
this.scope.clear(); this.scope.clear();
this.type.clear(); this.type.clear();
@ -71,6 +75,7 @@ class DataGrip {
this.get.addCommit(commit); this.get.addCommit(commit);
this.week.addCommit(commit); this.week.addCommit(commit);
this.tasks.addCommit(commit); this.tasks.addCommit(commit);
this.company.addCommit(commit);
} }
} }
@ -86,6 +91,7 @@ class DataGrip {
this.tasks.updateTotalInfo(this.pr); this.tasks.updateTotalInfo(this.pr);
this.release.updateTotalInfo(); this.release.updateTotalInfo();
this.scoring.updateTotalInfo(this.author, this.timestamp); this.scoring.updateTotalInfo(this.author, this.timestamp);
this.company.updateTotalInfo(this.author);
} }
} }

View file

@ -1,6 +1,7 @@
import ICommit, { IFileChange } from 'ts/interfaces/Commit'; import ICommit, { IFileChange } from 'ts/interfaces/Commit';
import IHashMap from 'ts/interfaces/HashMap'; import IHashMap, { HashMap } from 'ts/interfaces/HashMap';
import { IDirtyFile } from 'ts/interfaces/FileInfo'; import { IDirtyFile } from 'ts/interfaces/FileInfo';
import { increment } from 'ts/helpers/Math';
import FileBuilderCommon from './Common'; import FileBuilderCommon from './Common';
import FileBuilderLineStat from './LineStat'; import FileBuilderLineStat from './LineStat';
@ -8,25 +9,26 @@ import FileBuilderLineStat from './LineStat';
export default class FileGripByPaths { export default class FileGripByPaths {
list: IDirtyFile[] = []; list: IDirtyFile[] = [];
refFileIds: IHashMap<IDirtyFile> = {}; refFileIds: HashMap<IDirtyFile> = new Map();
refRemovedFileIds: IHashMap<IDirtyFile> = {}; refRemovedFileIds: HashMap<IDirtyFile> = new Map();
refExtensionType: IHashMap<IHashMap<number>> = {}; // TODO: remove me? refExtensionType: HashMap<IHashMap<number>> = new Map(); // TODO: remove me?
clear() { clear() {
this.list = []; this.list = [];
this.refFileIds = {}; this.refFileIds.clear();
this.refRemovedFileIds = {}; this.refRemovedFileIds.clear();
this.refExtensionType.clear();
} }
addCommit(fileChange: IFileChange, commit: ICommit) { addCommit(fileChange: IFileChange, commit: ICommit) {
let file = this.refFileIds[fileChange.id] || this.refFileIds[fileChange.newId || '']; let file = this.refFileIds.get(fileChange.id) || this.refFileIds.get(fileChange.newId);
if (file) { if (file) {
this.#updateDirtyFile(file, fileChange, commit); this.#updateDirtyFile(file, fileChange, commit);
} else { } else {
file = this.#getNewDirtyFile(fileChange, commit) as IDirtyFile; file = this.#getNewDirtyFile(fileChange, commit) as IDirtyFile;
this.refFileIds[fileChange.id] = file; this.refFileIds.set(fileChange.id, file);
} }
if (fileChange.newId) { if (fileChange.newId) {
@ -51,20 +53,22 @@ export default class FileGripByPaths {
} }
#renameFile(file: any, newId: string) { #renameFile(file: any, newId: string) {
this.refFileIds[newId] = this.refFileIds[file.id]; const oldFile = this.refFileIds.get(file.id) as IDirtyFile;
delete this.refFileIds[file.id]; this.refFileIds.set(newId, oldFile);
this.refFileIds.delete(file.id);
file.id = newId; file.id = newId;
} }
#removeFile(file: any) { #removeFile(file: any) {
file.action = 'D'; file.action = 'D';
this.refRemovedFileIds[file.id] = this.refFileIds[file.id]; const oldFile = this.refFileIds.get(file.id) as IDirtyFile;
this.refRemovedFileIds[file.id].action = 'D'; oldFile.action = 'D';
delete this.refFileIds[file.id]; this.refRemovedFileIds.set(file.id, oldFile);
this.refFileIds.delete(file.id);
} }
updateTotalInfo(callback?: Function) { updateTotalInfo(callback?: Function) {
this.list = Object.values(this.refFileIds); this.list = Array.from(this.refFileIds.values());
this.list.forEach((temp: any) => { this.list.forEach((temp: any) => {
const file = temp; const file = temp;
@ -72,10 +76,12 @@ export default class FileGripByPaths {
FileBuilderLineStat.updateTotal(file); FileBuilderLineStat.updateTotal(file);
if (file.type) { if (file.type) {
if (!this.refExtensionType[file.extension]) this.refExtensionType[file.extension] = {}; let refExtensionType = this.refExtensionType.get(file.extension);
this.refExtensionType[file.extension][file.type] = this.refExtensionType[file.extension][file.type] if (!refExtensionType) {
? (this.refExtensionType[file.extension][file.type] + 1) refExtensionType = {};
: 1; this.refExtensionType.set(file.extension, refExtensionType);
}
increment(refExtensionType, file.type);
} }
if (file.lines === 0 if (file.lines === 0

View file

@ -1,4 +1,4 @@
import IHashMap from 'ts/interfaces/HashMap'; import { HashMap } from 'ts/interfaces/HashMap';
import { IDirtyFile } from 'ts/interfaces/FileInfo'; import { IDirtyFile } from 'ts/interfaces/FileInfo';
interface IStatByAuthor { interface IStatByAuthor {
@ -8,12 +8,12 @@ interface IStatByAuthor {
} }
export default class FileGripByAuthor { export default class FileGripByAuthor {
statisticByName: IHashMap<IStatByAuthor> = {}; statisticByName: HashMap<IStatByAuthor> = new Map();
totalAddedFiles: number = 0; totalAddedFiles: number = 0;
clear() { clear() {
this.statisticByName = {}; this.statisticByName.clear();
} }
addFile(file: IDirtyFile) { addFile(file: IDirtyFile) {
@ -28,17 +28,17 @@ export default class FileGripByAuthor {
} }
#addCommitByAuthor(author: string) { #addCommitByAuthor(author: string) {
if (this.statisticByName[author]) return; if (this.statisticByName.has(author)) return;
this.statisticByName[author] = { this.statisticByName.set(author, {
addedFiles: 0, addedFiles: 0,
removedFiles: 0, removedFiles: 0,
addedWithoutRemoveFiles: 0, addedWithoutRemoveFiles: 0,
}; });
} }
#updateCommitByAuthor(file: IDirtyFile, firstAuthor: string, lastAuthor: string) { #updateCommitByAuthor(file: IDirtyFile, firstAuthor: string, lastAuthor: string) {
const createStatistic = this.statisticByName[firstAuthor]; const createStatistic = this.statisticByName.get(firstAuthor) as IStatByAuthor;
const removeStatistic = this.statisticByName[lastAuthor]; const removeStatistic = this.statisticByName.get(lastAuthor) as IStatByAuthor;
createStatistic.addedWithoutRemoveFiles += 1; createStatistic.addedWithoutRemoveFiles += 1;
if (file.action === 'D') { if (file.action === 'D') {
@ -49,7 +49,7 @@ export default class FileGripByAuthor {
} }
updateTotalInfo() { updateTotalInfo() {
this.totalAddedFiles = Object.values(this.statisticByName) this.totalAddedFiles = Array.from(this.statisticByName.values())
.reduce((sum: number, stat: any) => sum + stat.addedFiles, 0); .reduce((sum: number, stat: any) => sum + stat.addedFiles, 0);
} }
} }

View file

@ -1,4 +1,4 @@
import IHashMap from 'ts/interfaces/HashMap'; import { HashMap } from 'ts/interfaces/HashMap';
import { IDirtyFile } from 'ts/interfaces/FileInfo'; import { IDirtyFile } from 'ts/interfaces/FileInfo';
interface IStatByExtension { interface IStatByExtension {
@ -22,7 +22,7 @@ const IGNORE_LIST = [
export default class FileGripByExtension { export default class FileGripByExtension {
statistic: IStatByExtension[] = []; statistic: IStatByExtension[] = [];
statisticByName: IHashMap<IStatByExtension> = {}; statisticByName: HashMap<IStatByExtension> = new Map();
property: string = ''; property: string = '';
@ -32,7 +32,7 @@ export default class FileGripByExtension {
clear() { clear() {
this.statistic = []; this.statistic = [];
this.statisticByName = {}; this.statisticByName.clear();
} }
addFile(file: IDirtyFile) { addFile(file: IDirtyFile) {
@ -40,17 +40,18 @@ export default class FileGripByExtension {
if (!key || IGNORE_LIST.includes(file.name)) return; if (!key || IGNORE_LIST.includes(file.name)) return;
if (!this.statisticByName[key]) { let extension = this.statisticByName.get(key);
this.statisticByName[key] = this.#getNewExtension(file); if (!extension) {
extension = this.#getNewExtension(file);
this.statisticByName.set(key, extension);
} }
const extensions = this.statisticByName[key];
if (file.action === 'D') { if (file.action === 'D') {
extensions.removedFiles.push(file); extension.removedFiles.push(file);
extensions.removedCount += 1; extension.removedCount += 1;
} else { } else {
extensions.files.push(file); extension.files.push(file);
extensions.count += 1; extension.count += 1;
} }
} }
@ -67,8 +68,7 @@ export default class FileGripByExtension {
} }
updateTotalInfo() { updateTotalInfo() {
this.statistic = Object.entries(this.statisticByName) this.statistic = Array.from(this.statisticByName.values())
.sort((a: any, b: any) => b[1].count - a[1].count) .sort((a: any, b: any) => b.count - a.count);
.map((item: any) => item[1]);
} }
} }

View file

@ -30,6 +30,16 @@ function getFolder(name?: string, path?: string[], file?: IDirtyFile): IFolder {
}; };
} }
function updateFolderBy(folder: any, file: IDirtyFile, property: string) {
for (let author in file[property]) {
const folderAddedLinesByAuthor = folder[property][author];
const fileAddedLinesByAuthor = file[property][author];
folder[property][author] = folderAddedLinesByAuthor
? (folderAddedLinesByAuthor + fileAddedLinesByAuthor)
: fileAddedLinesByAuthor;
}
}
function updateFolder(folder: any, file: IDirtyFile) { function updateFolder(folder: any, file: IDirtyFile) {
folder.lastCommit = file.lastCommit; folder.lastCommit = file.lastCommit;
folder.lines += file.lines; folder.lines += file.lines;
@ -38,23 +48,9 @@ function updateFolder(folder: any, file: IDirtyFile) {
folder.removedLines += file.removedLines || 0; folder.removedLines += file.removedLines || 0;
folder.changedLines += file.changedLines || 0; folder.changedLines += file.changedLines || 0;
for (let author in file.addedLinesByAuthor) { updateFolderBy(folder, file, 'addedLinesByAuthor');
folder.addedLinesByAuthor[author] = folder.addedLinesByAuthor[author] updateFolderBy(folder, file, 'removedLinesByAuthor');
? (folder.addedLinesByAuthor[author] + file.addedLinesByAuthor[author]) updateFolderBy(folder, file, 'changedLinesByAuthor');
: file.addedLinesByAuthor[author];
}
for (let author in file.removedLinesByAuthor) {
folder.removedLinesByAuthor[author] = folder.removedLinesByAuthor[author]
? (folder.removedLinesByAuthor[author] + file.removedLinesByAuthor[author])
: file.removedLinesByAuthor[author];
}
for (let author in file.changedLinesByAuthor) {
folder.changedLinesByAuthor[author] = folder.changedLinesByAuthor[author]
? (folder.changedLinesByAuthor[author] + file.changedLinesByAuthor[author])
: file.changedLinesByAuthor[author];
}
} }
export default class FileGripByFolder { export default class FileGripByFolder {

View file

@ -1,5 +1,6 @@
import IHashMap from 'ts/interfaces/HashMap'; import IHashMap, { HashMap } from 'ts/interfaces/HashMap';
import { IDirtyFile } from 'ts/interfaces/FileInfo'; import { IDirtyFile } from 'ts/interfaces/FileInfo';
import { increment } from 'ts/helpers/Math';
interface IStatByType { interface IStatByType {
type: string; // type name type: string; // type name
@ -15,11 +16,11 @@ interface IStatByType {
export default class FileGripByType { export default class FileGripByType {
statistic: IStatByType[] = []; statistic: IStatByType[] = [];
statisticByName: IHashMap<IStatByType> = {}; statisticByName: HashMap<IStatByType> = new Map();
clear() { clear() {
this.statistic = []; this.statistic = [];
this.statisticByName = {}; this.statisticByName.clear();
} }
addFile(file: IDirtyFile) { addFile(file: IDirtyFile) {
@ -27,14 +28,13 @@ export default class FileGripByType {
if (!key || file?.name?.[0] === '.') return; if (!key || file?.name?.[0] === '.') return;
if (!this.statisticByName.hasOwnProperty(key)) { let type = this.statisticByName.get(key);
this.statisticByName[key] = this.#getNewType(file); if (!type) {
type = this.#getNewType(file);
this.statisticByName.set(key, type);
} }
const type = this.statisticByName[key]; increment(type.extension, file?.extension);
type.extension[file?.extension] = type.extension[file?.extension]
? (type.extension[file?.extension] + 1)
: 1;
if (file.action === 'D') { if (file.action === 'D') {
type.removedFiles.push(file); type.removedFiles.push(file);
@ -50,7 +50,7 @@ export default class FileGripByType {
type: file?.type, type: file?.type,
task: file?.firstCommit?.task, task: file?.firstCommit?.task,
path: file?.name, path: file?.name,
extension: { [file?.extension]: 1 }, extension: {},
files: [], files: [],
count: 0, count: 0,
removedFiles: [], removedFiles: [],
@ -59,8 +59,7 @@ export default class FileGripByType {
} }
updateTotalInfo() { updateTotalInfo() {
this.statistic = Object.entries(this.statisticByName) this.statistic = Array.from(this.statisticByName.values())
.sort((a: any, b: any) => b[1].count - a[1].count) .sort((a: any, b: any) => b.count - a.count);
.map((item: any) => item[1]);
} }
} }

View file

@ -37,5 +37,13 @@ export class WeightedAverage {
} }
export function increment(object: Object, path: string) { export function increment(object: Object, path: string) {
object[path] = (object[path] || 0) + 1; if (path) object[path] = (object[path] || 0) + 1;
}
export function createIncrement(key?: string, firstValue?: any) {
return key ? { [key]: firstValue || 1 } : {};
}
export function createHashMap(key?: string) {
return createIncrement(key, true);
} }

View file

@ -2,6 +2,7 @@ import ICommit, { COMMIT_TYPE, ISystemCommit } from 'ts/interfaces/Commit';
import IHashMap from 'ts/interfaces/HashMap'; import IHashMap from 'ts/interfaces/HashMap';
import { getTypeAndScope, getTask, getTaskNumber } from './getTypeAndScope'; import { getTypeAndScope, getTask, getTaskNumber } from './getTypeAndScope';
import getCompany from './getCompany';
const MASTER_BRANCH = { const MASTER_BRANCH = {
master: true, master: true,
@ -11,11 +12,11 @@ const MASTER_BRANCH = {
let prevDate = new Date(); let prevDate = new Date();
let refTimestampTime = {}; let refTimestampTime = new Map();
export function clearCache() { export function clearCache() {
prevDate = new Date(); prevDate = new Date();
refTimestampTime = {}; refTimestampTime.clear();
} }
export default function getCommitInfo( export default function getCommitInfo(
@ -28,33 +29,44 @@ export default function getCommitInfo(
const sourceDate = parts[0] || ''; const sourceDate = parts[0] || '';
let date = new Date(sourceDate); let date = new Date(sourceDate);
if (isNaN(date.getDay())) { if (isNaN(date.getDay())) {
console.log(`PARSE ERROR: Date parse error for: "${logString}"`); // console.log(`PARSE ERROR: Date parse error for: "${logString}"`);
date = prevDate; date = prevDate;
} }
prevDate = date; prevDate = date;
const day = date.getDay() - 1; const day = date.getDay() - 1;
const timestamp = sourceDate.substring(0, 10); // split('T')[0]; const timestamp = sourceDate.substring(0, 10); // split('T')[0];
if (!refTimestampTime[timestamp]) { let milliseconds = refTimestampTime.get(timestamp);
refTimestampTime[timestamp] = (new Date(timestamp)).getTime(); if (!milliseconds) {
milliseconds = (new Date(timestamp)).getTime();
refTimestampTime.set(timestamp, milliseconds);
} }
let author = parts[1]?.replace(/[._]/gm, ' ') || ''; let author = parts[1]?.replace(/[._]/gm, ' ') || '';
let email = parts[2] || ''; let email = parts[2] || '';
if (email.indexOf('@') === -1) email = ''; if (email.indexOf('@') === -1) email = '';
const companyKey = `${author}>in>${email}`;
if (!refEmailAuthor[companyKey]) {
const companyForKey = getCompany(author, email);
// @ts-ignore
refEmailAuthor[companyKey] = { company: companyForKey };
}
// @ts-ignore
const company = refEmailAuthor[companyKey].company;
const authorID = author.replace(/\s|\t/gm, ''); const authorID = author.replace(/\s|\t/gm, '');
if (authorID && refEmailAuthor[authorID] && refEmailAuthor[authorID] !== author) { if (authorID && refEmailAuthor[authorID] && refEmailAuthor[authorID] !== author) {
console.log(`PARSE WARNING: Rename "${author}" to "${refEmailAuthor[authorID]}"`); // console.log(`PARSE WARNING: Rename "${author}" to "${refEmailAuthor[authorID]}"`);
author = refEmailAuthor[authorID]; author = refEmailAuthor[authorID];
} }
if (email && refEmailAuthor[email] && refEmailAuthor[email] !== author) { if (email && refEmailAuthor[email] && refEmailAuthor[email] !== author) {
console.log(`PARSE WARNING: Rename "${author}" to "${refEmailAuthor[email]}" by "${email}"`); // console.log(`PARSE WARNING: Rename "${author}" to "${refEmailAuthor[email]}" by "${email}"`);
author = refEmailAuthor[email]; author = refEmailAuthor[email];
} }
if (author && refEmailAuthor[author] && refEmailAuthor[author] !== email) { if (author && refEmailAuthor[author] && refEmailAuthor[author] !== email) {
console.log(`PARSE WARNING: Rename "${email}" to "${refEmailAuthor[author]}" by "${author}"`); // console.log(`PARSE WARNING: Rename "${email}" to "${refEmailAuthor[author]}" by "${author}"`);
email = refEmailAuthor[author]; email = refEmailAuthor[author];
} }
@ -75,11 +87,12 @@ export default function getCommitInfo(
year: date.getUTCFullYear(), year: date.getUTCFullYear(),
week: 0, week: 0,
timestamp, timestamp,
milliseconds: refTimestampTime[timestamp], milliseconds,
author, author,
email, email,
message, message,
company,
text: '', text: '',
type: '—', type: '—',

View file

@ -14,6 +14,11 @@ const PUBLIC_SERVICES = [
'rambler', 'rambler',
'github', 'github',
'gitlab', 'gitlab',
'com',
'me',
'qq',
'dev',
'localhost',
]; ];
const isPublicService = Object.fromEntries( const isPublicService = Object.fromEntries(
@ -36,14 +41,32 @@ function getCompanyByName(author?: string): string {
function getCompanyByEmail(email?: string) { function getCompanyByEmail(email?: string) {
const domain = (email || '').split('@').pop() || ''; const domain = (email || '').split('@').pop() || '';
const company = domain.split('.').shift() || ''; const parts = domain.split('.');
return company.toUpperCase(); parts.pop();
return (parts.pop() || '').toUpperCase();
}
function getClearText(text: string) {
return (text || '')
.replace(/(\[[^\]]])+/gim, '')
.replace(/[\s\t._0-9]+/gim, '')
.toLowerCase();
}
function isUserName(author?: string, company?: string): boolean {
if (!author || !company) return false;
const clearAuthor = getClearText(author);
const clearCompany = getClearText(company);
if (!clearAuthor || !clearCompany) return false;
return !!clearAuthor.match(clearCompany);
} }
function getCompany(author?: string, email?: string) { function getCompany(author?: string, email?: string) {
const company = getCompanyByName(author) || getCompanyByEmail(email) || ''; const company = getCompanyByName(author) || getCompanyByEmail(email) || '';
const isMailService = company.indexOf('MAIL') !== -1; const isMailService = company.indexOf('MAIL') !== -1;
return isPublicService[company] || isMailService return isPublicService[company] || isMailService || isUserName(author, company)
? '' ? ''
: company; : company;
} }

View file

@ -5,6 +5,8 @@ function getFilePath(path: string): string[] {
.replace(/"/gm, '') .replace(/"/gm, '')
.replace(/\/\//gm, '/'); .replace(/\/\//gm, '/');
if (formattedPath.indexOf('{') === -1) return [formattedPath];
const parts = formattedPath.split(/(?:\{)|(?:\s=>\s)|(?:})/gm); const parts = formattedPath.split(/(?:\{)|(?:\s=>\s)|(?:})/gm);
if (parts.length !== 2 && parts.length !== 4) return [formattedPath]; if (parts.length !== 2 && parts.length !== 4) return [formattedPath];
@ -19,9 +21,32 @@ function getFilePath(path: string): string[] {
return [oldPath, newPath]; return [oldPath, newPath];
} }
function fastNumStatSplit(message: string) {
let firstIndex = 0;
if (message[1] === '\t') firstIndex = 1;
else if (message[2] === '\t') firstIndex = 2;
else if (message[3] === '\t') firstIndex = 3;
else if (message[4] === '\t') firstIndex = 4;
else if (message[5] === '\t') firstIndex = 5;
let secondIndex = firstIndex + 2;
if (message[firstIndex + 2] === '\t') secondIndex = firstIndex + 2;
else if (message[firstIndex + 3] === '\t') secondIndex = firstIndex + 3;
else if (message[firstIndex + 4] === '\t') secondIndex = firstIndex + 4;
else if (message[firstIndex + 5] === '\t') secondIndex = firstIndex + 5;
else if (message[firstIndex + 6] === '\t') secondIndex = firstIndex + 6;
return [
message.substring(0, firstIndex),
message.substring(firstIndex + 1, secondIndex),
message.substring(secondIndex + 1),
];
}
// "38 9 src/app.css" -> [38, 9, 'src/app.css'] // "38 9 src/app.css" -> [38, 9, 'src/app.css']
export function getNumStatInfo(message: string) { export function getNumStatInfo(message: string) {
let [addedRaw, removedRaw, path] = message.split('\t'); let [addedRaw, removedRaw, path] = fastNumStatSplit(message);
// let [addedRaw, removedRaw, path] = message.split('\t');
let added = parseInt(addedRaw, 10) || 0; let added = parseInt(addedRaw, 10) || 0;
let removed = parseInt(removedRaw, 10) || 0; let removed = parseInt(removedRaw, 10) || 0;
@ -51,7 +76,7 @@ export function getNumStatInfo(message: string) {
// ":000000 100644 000000000 fc44b0a37 A public/logo192.png" -> ['A', 'public/logo192.png'] // ":000000 100644 000000000 fc44b0a37 A public/logo192.png" -> ['A', 'public/logo192.png']
export function getRawInfo(message: string) { export function getRawInfo(message: string) {
return { return {
action:message[35], action: message[35],
path: message.substring(37), path: message.substring(37),
}; };
} }

View file

@ -4,7 +4,11 @@ import IHashMap from 'ts/interfaces/HashMap';
import { ONE_DAY, ONE_WEEK } from 'ts/helpers/formatter'; import { ONE_DAY, ONE_WEEK } from 'ts/helpers/formatter';
import getCommitInfo, { clearCache } from './getCommitInfo'; import getCommitInfo, { clearCache } from './getCommitInfo';
import { getInfoFromPath, getNumStatInfo, getRawInfo } from './getFileChanges'; import {
getInfoFromPath,
getNumStatInfo,
getRawInfo,
} from './getFileChanges';
function updateLineTotal(commit: any, line: any) { function updateLineTotal(commit: any, line: any) {
commit.added += line.addedLines || 0; commit.added += line.addedLines || 0;
@ -12,50 +16,60 @@ function updateLineTotal(commit: any, line: any) {
commit.changes += line.changedLines || 0; commit.changes += line.changedLines || 0;
} }
function isNumStatLine(message: string) {
return message[1] === '\t'
|| message[2] === '\t'
|| message[3] === '\t'
|| message[4] === '\t'
|| message[5] === '\t'
|| message[6] === '\t'
|| message[7] === '\t';
}
export default function Parser(report: string[]) { export default function Parser(report: string[]) {
let commit = null; let commit = null;
const commits: Array<ICommit | ISystemCommit> = []; const commits: Array<ICommit | ISystemCommit> = [];
let refEmailAuthor: IHashMap<string> = {}; let refEmailAuthor: IHashMap<string> = {};
let files: IHashMap<IFileChange> = {}; let files: Map<string, IFileChange> = new Map();
let fileChanges: IFileChange | null = null; let fileChanges: IFileChange | null = null;
let firstMonday = 0; let firstMonday = 0;
clearCache();
for (let i = 0, l = report.length; i < l; i += 1) { for (let i = 0, l = report.length; i < l; i += 1) {
const message = report[i]; const message = report[i];
if (!message) continue; if (!message) continue;
const index = message.indexOf('\t'); if (message[0] === ':') {
if (index > 0 && index < 10) { // парсинг файлов формата --raw
// ":000000 100644 0000000 496d1ef A .browserlistrc"
const line = getRawInfo(message);
fileChanges = files.get(line.path) as IFileChange;
if (!fileChanges) {
fileChanges = getInfoFromPath(line.path);
files.set(line.path, fileChanges);
}
fileChanges.action = line.action;
} else if (isNumStatLine(message)) {
// парсинг файлов формата --num-stat // парсинг файлов формата --num-stat
// "1 0 .browserlistrc" // "1 0 .browserlistrc"
const line = getNumStatInfo(message); const line = getNumStatInfo(message);
if (!files[line.path]) { fileChanges = files.get(line.path) as IFileChange;
files[line.path] = getInfoFromPath(line.path); if (!fileChanges) {
fileChanges = getInfoFromPath(line.path);
files.set(line.path, fileChanges);
} }
fileChanges = files[line.path];
fileChanges.addedLines = line.addedLines; fileChanges.addedLines = line.addedLines;
fileChanges.removedLines = line.removedLines; fileChanges.removedLines = line.removedLines;
fileChanges.changedLines = line.changedLines; fileChanges.changedLines = line.changedLines;
updateLineTotal(commit, line); updateLineTotal(commit, line);
} else if (message[0] === ':') {
// парсинг файлов формата --raw
// ":000000 100644 0000000 496d1ef A .browserlistrc"
const line = getRawInfo(message);
if (!files[line.path]) {
files[line.path] = getInfoFromPath(line.path);
}
fileChanges = files[line.path];
fileChanges.action = line.action;
} else { } else {
// парсинг коммита // парсинг коммита
// "2021-02-09T16:08:15+03:00>Albert>instein@mail.de>feat(init): added the speed of light" // "2021-02-09T16:08:15+03:00>Albert>instein@mail.de>feat(init): added the speed of light"
if (commit) commit.fileChanges = Object.values(files); if (commit) commit.fileChanges = Array.from(files.values());
files = {}; files.clear();
commit = getCommitInfo(message, refEmailAuthor); commit = getCommitInfo(message, refEmailAuthor);
const monday = commit.milliseconds - commit.day * ONE_DAY; const monday = commit.milliseconds - commit.day * ONE_DAY;
@ -69,5 +83,7 @@ export default function Parser(report: string[]) {
} }
} }
clearCache();
return commits; return commits;
} }

View file

@ -26,8 +26,9 @@ export interface ILog {
week: number; // 42, week: number; // 42,
// user // user
author: string; // "Frolov Ivan", author: string; // "Dart Vader",
email: string; // "frolov@mail.ru", email: string; // "d.vader@emap.com",
company: string; // "emap",
// task // task
message: string; // "JIRA-0000 fix(profile): add new avatar", message: string; // "JIRA-0000 fix(profile): add new avatar",

View file

@ -1,3 +1,5 @@
export default interface IHashMap<T> { export default interface IHashMap<T> {
[key: string | number]: T; [key: string | number]: T;
} }
export type HashMap<T> = Map<string | number | undefined | null, T>;

View file

@ -28,14 +28,14 @@ import Recommendations from 'ts/components/Recommendations';
import { getMax, getMaxByLength } from 'ts/pages/Common/helpers/getMax'; import { getMax, getMaxByLength } from 'ts/pages/Common/helpers/getMax';
import Description from 'ts/components/Description'; import Description from 'ts/components/Description';
interface IAuthorViewProps { interface AuthorViewProps {
response?: IPagination<any>; response?: IPagination<any>;
updateSort?: Function; updateSort?: Function;
rowsForExcel?: any[]; rowsForExcel?: any[];
mode?: string; mode?: string;
} }
function AuthorView({ response, updateSort, rowsForExcel, mode }: IAuthorViewProps) { export function AuthorView({ response, updateSort, rowsForExcel, mode }: AuthorViewProps) {
const { t } = useTranslation(); const { t } = useTranslation();
if (!response) return null; if (!response) return null;
@ -58,6 +58,7 @@ function AuthorView({ response, updateSort, rowsForExcel, mode }: IAuthorViewPro
rows={response.content} rows={response.content}
sort={response.sort} sort={response.sort}
updateSort={updateSort} updateSort={updateSort}
mode={mode}
type={mode === 'print' ? 'cards' : undefined} type={mode === 'print' ? 'cards' : undefined}
columnCount={mode === 'print' ? 3 : undefined} columnCount={mode === 'print' ? 3 : undefined}
> >
@ -84,6 +85,13 @@ function AuthorView({ response, updateSort, rowsForExcel, mode }: IAuthorViewPro
template={(value: string) => <UiKitTags value={value} />} template={(value: string) => <UiKitTags value={value} />}
width={100} width={100}
/> />
<Column
isSortable="company"
title="page.team.author.company"
properties="lastCompany"
template={(value: string) => <UiKitTags value={value} />}
width={150}
/>
<Column <Column
template={ColumnTypesEnum.STRING} template={ColumnTypesEnum.STRING}
properties="firstCommit" properties="firstCommit"

View file

@ -0,0 +1,143 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import ICommit from 'ts/interfaces/Commit';
import { IPagination } from 'ts/interfaces/Pagination';
import { getDate } from 'ts/helpers/formatter';
import dataGripStore from 'ts/store/DataGrip';
import UiKitTags from 'ts/components/UiKit/components/Tags';
import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import LineChart from 'ts/components/LineChart';
import getOptions from 'ts/components/LineChart/helpers/getOptions';
import { getMax } from 'ts/pages/Common/helpers/getMax';
import Employments from './Employments';
interface CompaniesProps {
response?: IPagination<any>;
updateSort?: Function;
rowsForExcel?: any[];
mode?: string;
}
function Companies({ response, updateSort, rowsForExcel, mode }: CompaniesProps) {
const { t } = useTranslation();
if (!response) return null;
const [works, dismissed] = [
t('page.team.author.type.work'),
t('page.team.author.type.dismissed'),
];
const taskChart = getOptions({ max: getMax(response, 'totalTasks'), suffix: 'page.team.author.tasksSmall' });
const daysChart = getOptions({ max: getMax(response, 'totalDays'), suffix: 'page.team.author.days' });
return (
<DataView
rowsForExcel={rowsForExcel}
rows={response.content}
sort={response.sort}
updateSort={updateSort}
type={mode === 'print' ? 'cards' : undefined}
columnCount={mode === 'print' ? 3 : undefined}
>
<Column
isFixed
template={ColumnTypesEnum.DETAILS}
width={40}
formatter={(row: any) => {
const content = row.employments.map((name: string) => (
dataGripStore?.dataGrip?.author?.statisticByName?.[name]
)).filter((v: any) => v);
return (
<Employments // @ts-ignore
response={{ content }}
mode="details"
/>
);
}}
/>
<Column
isFixed
template={ColumnTypesEnum.STRING}
properties="company"
title="page.team.pr.author"
width={200}
/>
<Column
title="page.team.author.status"
formatter={(row: any) => (row.isActive ? works : dismissed)}
template={(value: string) => <UiKitTags value={value} />}
width={100}
/>
{/*<Column*/}
{/* template={ColumnTypesEnum.SHORT_NUMBER}*/}
{/* title="page.team.company.people"*/}
{/* properties="totalEmployments"*/}
{/* width={90}*/}
{/*/>*/}
<Column
template={ColumnTypesEnum.STRING}
properties="firstCommit"
title="page.team.author.firstCommit"
width={130}
formatter={(commit: ICommit) => getDate(commit.timestamp)}
/>
<Column
template={ColumnTypesEnum.STRING}
properties="lastCommit"
title="page.team.author.lastCommit"
width={130}
formatter={(commit: ICommit) => getDate(commit.timestamp)}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
properties="totalDays"
width={90}
/>
<Column
isSortable="totalDays"
title="page.team.author.daysAll"
properties="totalDays"
width={150}
template={(value: number) => (
<LineChart
options={daysChart}
value={value}
/>
)}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
properties="totalTasks"
width={90}
/>
<Column
isSortable="totalTasks"
title="page.team.author.tasks"
properties="totalTasks"
width={150}
template={(value: number) => (
<LineChart
options={taskChart}
value={value}
/>
)}
/>
<Column
properties="emptyCell"
minWidth={40}
/>
</DataView>
);
}
Companies.defaultProps = {
response: undefined,
};
export default Companies;

View file

@ -0,0 +1,146 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import ICommit from 'ts/interfaces/Commit';
import { IPagination } from 'ts/interfaces/Pagination';
import { getDate, getMoney } from 'ts/helpers/formatter';
import dataGripStore from 'ts/store/DataGrip';
import UiKitTags from 'ts/components/UiKit/components/Tags';
import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import LineChart from 'ts/components/LineChart';
import getOptions from 'ts/components/LineChart/helpers/getOptions';
interface EmploymentsProps {
response?: IPagination<any>;
updateSort?: Function;
rowsForExcel?: any[];
mode?: string;
}
export function Employments({ response, updateSort, rowsForExcel, mode }: EmploymentsProps) {
const { t } = useTranslation();
if (!response) return null;
const [works, dismissed, staff] = [
t('page.team.author.type.work'),
t('page.team.author.type.dismissed'),
t('page.team.author.type.staff'),
];
const textWork = t('page.team.author.worked');
const textLosses = t('page.team.author.losses');
const daysWorked = getOptions({ order: [textWork, textLosses], suffix: 'page.team.author.days' });
const typeChart = getOptions({
suffix: 'page.team.author.tasksSmall',
order: dataGripStore.dataGrip.type.list,
});
return (
<DataView
rowsForExcel={rowsForExcel}
rows={response.content}
sort={response.sort}
updateSort={updateSort}
mode={mode}
type={mode === 'print' ? 'cards' : undefined}
columnCount={mode === 'print' ? 3 : undefined}
>
<Column
isFixed
template={ColumnTypesEnum.STRING}
formatter={(row: any, index: number) => (index + 1)}
width={40}
/>
<Column
isFixed
template={ColumnTypesEnum.STRING}
properties="author"
title="page.team.pr.author"
width={158}
/>
<Column
formatter={(row: any) => {
if (row.isStaff) return staff;
if (row.isDismissed) return dismissed;
return works;
}}
template={(value: string) => <UiKitTags value={value} />}
width={100}
/>
<Column
template={ColumnTypesEnum.STRING}
properties="firstCommit"
width={130}
formatter={(commit: ICommit) => getDate(commit.timestamp)}
/>
<Column
template={ColumnTypesEnum.STRING}
properties="lastCommit"
width={130}
formatter={(commit: ICommit) => getDate(commit.timestamp)}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
properties="daysAll"
formatter={(value: number) => value || 1}
width={90}
/>
<Column
isSortable="daysWorked"
width={150}
template={(details: any) => (
<LineChart
options={daysWorked}
details={details}
/>
)}
formatter={(row: any) => {
return { [textWork]: row.daysWorked, [textLosses]: row.daysLosses };
}}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
properties="tasks"
formatter={(tasks: string[]) => (tasks?.length || 0)}
width={90}
/>
<Column
isSortable
width={150}
template={(row: any) => (
<LineChart
options={typeChart}
details={row.types}
/>
)}
/>
<Column
template={ColumnTypesEnum.NUMBER}
title="page.team.author.moneyAll"
properties="moneyAll"
formatter={getMoney}
/>
<Column
template={ColumnTypesEnum.NUMBER}
title="page.team.author.moneyWorked"
properties="moneyWorked"
formatter={getMoney}
/>
<Column
template={ColumnTypesEnum.NUMBER}
title="page.team.author.moneyLosses"
properties="moneyLosses"
formatter={getMoney}
/>
</DataView>
);
}
Employments.defaultProps = {
response: undefined,
};
export default Employments;

View file

@ -0,0 +1,45 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import ISort from 'ts/interfaces/Sort';
import { IPaginationRequest } from 'ts/interfaces/Pagination';
import dataGripStore from 'ts/store/DataGrip';
import ICommonPageProps from 'ts/components/Page/interfaces/CommonPageProps';
import DataLoader from 'ts/components/DataLoader';
import Pagination from 'ts/components/DataLoader/components/Pagination';
import getFakeLoader from 'ts/components/DataLoader/helpers/formatter';
import NothingFound from 'ts/components/NothingFound';
import Title from 'ts/components/Title';
import Companies from './Companies';
const Company = observer(({
mode,
}: ICommonPageProps): React.ReactElement | null => {
const rows = dataGripStore.dataGrip.company.statistic;
if (!rows?.length) {
return mode !== 'print' ? (<NothingFound />) : null;
}
return (
<>
<Title title="page.team.author.title"/>
<DataLoader
to="response"
loader={(pagination?: IPaginationRequest, sort?: ISort[]) => getFakeLoader({
content: rows, pagination, sort, mode,
})}
watch={`${mode}${dataGripStore.hash}`}
>
<Companies
mode={mode}
rowsForExcel={rows}
/>
<Pagination />
</DataLoader>
</>
);
});
export default Company;

View file

@ -50,7 +50,7 @@ function ReleaseView({ response, updateSort, rowsForExcel, mode }: IReleaseViewP
width={40} width={40}
formatter={(row: any) => { formatter={(row: any) => {
const content = row.pr.map((commit: any) => ( const content = row.pr.map((commit: any) => (
dataGripStore?.dataGrip?.pr?.pr?.[commit.prId] dataGripStore?.dataGrip?.pr?.pr?.get(commit.prId)
)).filter((item: any) => item?.firstCommit); )).filter((item: any) => item?.firstCommit);
return ( return (
<AllPR // @ts-ignore <AllPR // @ts-ignore

View file

@ -1,4 +1,4 @@
import React from 'react'; import React, { useState } from 'react';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import ISort from 'ts/interfaces/Sort'; import ISort from 'ts/interfaces/Sort';
@ -17,10 +17,14 @@ import LineChart from 'ts/components/LineChart';
import getOptions from 'ts/components/LineChart/helpers/getOptions'; import getOptions from 'ts/components/LineChart/helpers/getOptions';
import UiKitTags from 'ts/components/UiKit/components/Tags'; import UiKitTags from 'ts/components/UiKit/components/Tags';
import { PRLink, TaskLink } from 'ts/components/ExternalLink'; import { PRLink, TaskLink } from 'ts/components/ExternalLink';
import Title from 'ts/components/Title';
import PageWrapper from 'ts/components/Page/wrapper';
import { getMax } from 'ts/pages/Common/helpers/getMax'; import { getMax } from 'ts/pages/Common/helpers/getMax';
import { getDate } from 'ts/helpers/formatter'; import { getDate } from 'ts/helpers/formatter';
import TasksFilters from './TasksFilters';
interface ITasksViewProps { interface ITasksViewProps {
response?: IPagination<any>; response?: IPagination<any>;
updateSort?: Function; updateSort?: Function;
@ -139,9 +143,19 @@ const Tasks = observer(({
mode, mode,
}: ICommonPageProps): React.ReactElement | null => { }: ICommonPageProps): React.ReactElement | null => {
const rows = dataGripStore.dataGrip.tasks.statistic; const rows = dataGripStore.dataGrip.tasks.statistic;
const [filters, setFilters] = useState<any>({ user: 0, company: 0 });
if (!rows?.length) return mode !== 'print' ? (<NothingFound />) : null; if (!rows?.length) return mode !== 'print' ? (<NothingFound />) : null;
return ( return (
<>
<Title title="common.filters" />
<PageWrapper>
<TasksFilters
filters={filters}
onChange={setFilters}
/>
</PageWrapper>
<DataLoader <DataLoader
to="response" to="response"
loader={(pagination?: IPaginationRequest, sort?: ISort[]) => getFakeLoader({ loader={(pagination?: IPaginationRequest, sort?: ISort[]) => getFakeLoader({
@ -149,15 +163,13 @@ const Tasks = observer(({
})} })}
watch={`${mode}${dataGripStore.hash}`} watch={`${mode}${dataGripStore.hash}`}
> >
<br/>
<br/>
<br/>
<TasksView <TasksView
mode={mode} mode={mode}
rowsForExcel={rows} rowsForExcel={rows}
/> />
<Pagination /> <Pagination />
</DataLoader> </DataLoader>
</>
); );
}); });

View file

@ -0,0 +1,60 @@
import React, { useMemo } from 'react';
import { observer } from 'mobx-react-lite';
import { useTranslation } from 'react-i18next';
import SelectWithButtons from 'ts/components/UiKit/components/SelectWithButtons';
import dataGripStore from 'ts/store/DataGrip';
import style from '../styles/filters.module.scss';
function getFormattedUsers(rows: any[], t: Function) {
const options = rows.map((title: string, id: number) => ({ id: id + 1, title }));
options.unshift({ id: 0, title: t('page.team.tree.filters.all') });
return options;
}
interface ITempoFiltersProps {
filters: {
company?: number;
user?: number;
};
onChange: Function;
}
const TasksFilters = observer(({
filters,
onChange,
}: ITempoFiltersProps): React.ReactElement => {
const { t } = useTranslation();
const users = dataGripStore.dataGrip.author.list;
const userOptions = useMemo(() => getFormattedUsers(users, t), [users]);
const companies = dataGripStore.dataGrip.company.statistic.map((v: any) => v.company);
const companyOptions = useMemo(() => getFormattedUsers(companies, t), [companies]);
return (
<div className={style.table_filters}>
<SelectWithButtons
title="page.team.tree.filters.author"
value={filters.user}
className={style.table_filters_item}
options={userOptions}
onChange={(user: number) => {
onChange({ ...filters, user, company: 0 });
}}
/>
<SelectWithButtons
title="page.team.tree.filters.author"
value={filters.company}
className={style.table_filters_item}
options={companyOptions}
onChange={(company: number) => {
onChange({ ...filters, user: 0, company });
}}
/>
</div>
);
});
export default TasksFilters;

View file

@ -8,6 +8,7 @@ import fullScreen from 'ts/store/FullScreen';
import Author from './components/Author'; import Author from './components/Author';
import Commits from './components/Commits'; import Commits from './components/Commits';
import Company from './components/Company';
import Changes from './components/Changes'; import Changes from './components/Changes';
import Hours from './components/Hours'; import Hours from './components/Hours';
import PopularWords from './components/PopularWords'; import PopularWords from './components/PopularWords';
@ -37,6 +38,7 @@ const View = observer(({ page }: ViewProps): React.ReactElement => {
if (page === 'total') return <Total/>; if (page === 'total') return <Total/>;
if (page === 'scope') return <Scope mode={mode}/>; if (page === 'scope') return <Scope mode={mode}/>;
if (page === 'author') return <Author mode={mode}/>; if (page === 'author') return <Author mode={mode}/>;
if (page === 'company') return <Company mode={mode}/>;
if (page === 'type') return <Type mode={mode}/>; if (page === 'type') return <Type mode={mode}/>;
if (page === 'pr') return <Pr mode={mode}/>; if (page === 'pr') return <Pr mode={mode}/>;
if (page === 'day') return <Tempo/>; if (page === 'day') return <Tempo/>;

View file

@ -94,7 +94,6 @@ const Main = observer(() => {
const view = viewNameStore.view; const view = viewNameStore.view;
useEffect(() => { useEffect(() => {
console.log('main');
// @ts-ignore // @ts-ignore
const list = window?.report || []; const list = window?.report || [];
if (list?.length && bugInReactWithDoubleInit !== list?.length) { if (list?.length && bugInReactWithDoubleInit !== list?.length) {

View file

@ -37,8 +37,8 @@ class DataGripStore {
hash: observable, hash: observable,
isDepersonalized: observable, isDepersonalized: observable,
asyncSetCommits: action, asyncSetCommits: action,
processingStep01: action, processingStringToCommit: action,
processingStep03: action, processingDataAnalysis: action,
depersonalized: action, depersonalized: action,
updateStatistic: action, updateStatistic: action,
}); });
@ -47,10 +47,10 @@ class DataGripStore {
asyncSetCommits(dump?: string[]) { asyncSetCommits(dump?: string[]) {
if (!dump?.length) return; if (!dump?.length) return;
splashScreenStore.show(); splashScreenStore.show();
setTimeout(() => this.processingStep01(dump), PROCESSING_DELAY); setTimeout(() => this.processingStringToCommit(dump), PROCESSING_DELAY);
} }
processingStep01(dump?: string[]) { processingStringToCommit(dump?: string[]) {
dataGrip.clear(); dataGrip.clear();
fileGrip.clear(); fileGrip.clear();
@ -60,20 +60,20 @@ class DataGripStore {
return; return;
} }
setTimeout(() => this.processingStep02(commits), PROCESSING_DELAY); setTimeout(() => this.processingCommitGrouping(commits), PROCESSING_DELAY);
} }
processingStep02(commits: (ICommit | ISystemCommit)[]) { processingCommitGrouping(commits: (ICommit | ISystemCommit)[]) {
commits.sort((a, b) => a.milliseconds - b.milliseconds); commits.sort((a, b) => a.milliseconds - b.milliseconds);
commits.forEach((commit: ICommit | ISystemCommit) => { commits.forEach((commit: ICommit | ISystemCommit) => {
dataGrip.addCommit(commit); dataGrip.addCommit(commit);
fileGrip.addCommit(commit); fileGrip.addCommit(commit);
}); });
setTimeout(() => this.processingStep03(commits), PROCESSING_DELAY); setTimeout(() => this.processingDataAnalysis(commits), PROCESSING_DELAY);
} }
processingStep03(commits: (ICommit | ISystemCommit)[]) { processingDataAnalysis(commits: (ICommit | ISystemCommit)[]) {
fileGrip.updateTotalInfo(); fileGrip.updateTotalInfo();
this.commits = commits; this.commits = commits;

View file

@ -24,6 +24,7 @@ export default `
§ page.team.author.description1: *Part of the statistics* (work speed, costs, etc.) *for employees with the 'Assistant' type is not counted*, as it is an episodic role in the project. It is assumed that they do not affect the project, and their edits can be disregarded in the context of the overall volume of work. § page.team.author.description1: *Part of the statistics* (work speed, costs, etc.) *for employees with the 'Assistant' type is not counted*, as it is an episodic role in the project. It is assumed that they do not affect the project, and their edits can be disregarded in the context of the overall volume of work.
§ page.team.author.description2: *Default sorting* is by the number of tasks and groups (current, fired, assisting employees). § page.team.author.description2: *Default sorting* is by the number of tasks and groups (current, fired, assisting employees).
§ page.team.author.status: Status § page.team.author.status: Status
§ page.team.author.company: Company
§ page.team.author.firstCommit: First commit § page.team.author.firstCommit: First commit
§ page.team.author.lastCommit: Last § page.team.author.lastCommit: Last
§ page.team.author.daysAll: Total days § page.team.author.daysAll: Total days

View file

@ -24,6 +24,7 @@ export default `
§ page.team.author.description1: *Part of the statistics* (work speed, costs, etc.) *for employees with the 'Assistant' type is not counted*, as it is an episodic role in the project. It is assumed that they do not affect the project, and their edits can be disregarded in the context of the overall volume of work. § page.team.author.description1: *Part of the statistics* (work speed, costs, etc.) *for employees with the 'Assistant' type is not counted*, as it is an episodic role in the project. It is assumed that they do not affect the project, and their edits can be disregarded in the context of the overall volume of work.
§ page.team.author.description2: *Default sorting* is by the number of tasks and groups (current, fired, assisting employees). § page.team.author.description2: *Default sorting* is by the number of tasks and groups (current, fired, assisting employees).
§ page.team.author.status: Status § page.team.author.status: Status
§ page.team.author.company: Company
§ page.team.author.firstCommit: First commit § page.team.author.firstCommit: First commit
§ page.team.author.lastCommit: Last § page.team.author.lastCommit: Last
§ page.team.author.daysAll: Total days § page.team.author.daysAll: Total days

View file

@ -24,6 +24,7 @@ export default `
§ page.team.author.description1: Parte de las estadísticas (la velocidad del trabajo, el dinero gastado, etc.) para los empleados con el tipo de "Asistente" no cuenta, ya que no es un rol permanente en el proyecto. Su trabajo es insignificante y puede ser ignorado. § page.team.author.description1: Parte de las estadísticas (la velocidad del trabajo, el dinero gastado, etc.) para los empleados con el tipo de "Asistente" no cuenta, ya que no es un rol permanente en el proyecto. Su trabajo es insignificante y puede ser ignorado.
§ page.team.author.description2: La clasificación predeterminada es la clasificación por número de tareas y grupos(empleados actuales, despedidos, ayudantes). § page.team.author.description2: La clasificación predeterminada es la clasificación por número de tareas y grupos(empleados actuales, despedidos, ayudantes).
§ page.team.author.status: Status § page.team.author.status: Status
§ page.team.author.company: Company
§ page.team.author.firstCommit: First commit § page.team.author.firstCommit: First commit
§ page.team.author.lastCommit: Last § page.team.author.lastCommit: Last
§ page.team.author.daysAll: Total days § page.team.author.daysAll: Total days

View file

@ -24,6 +24,7 @@ export default `
§ page.team.author.description1: Partie des statistiques (vitesse de travail, argent dépensé, etc.) pour les collaborateurs de type Assistant, ce nest pas une rôle permanente dans le projet. Leur travail est insignifiant et peut être ignoré. § page.team.author.description1: Partie des statistiques (vitesse de travail, argent dépensé, etc.) pour les collaborateurs de type Assistant, ce nest pas une rôle permanente dans le projet. Leur travail est insignifiant et peut être ignoré.
§ page.team.author.description2: Le tri par défaut est le tri par nombre de tâches et de groupes (employés actuels, licenciés et aidants). § page.team.author.description2: Le tri par défaut est le tri par nombre de tâches et de groupes (employés actuels, licenciés et aidants).
§ page.team.author.status: Status § page.team.author.status: Status
§ page.team.author.company: Company
§ page.team.author.firstCommit: First commit § page.team.author.firstCommit: First commit
§ page.team.author.lastCommit: Last § page.team.author.lastCommit: Last
§ page.team.author.daysAll: Total days § page.team.author.daysAll: Total days

View file

@ -24,6 +24,7 @@ export default `
§ page.team.author.description1: *Part of the statistics* (work speed, costs, etc.) *for employees with the 'Assistant' type is not counted*, as it is an episodic role in the project. It is assumed that they do not affect the project, and their edits can be disregarded in the context of the overall volume of work. § page.team.author.description1: *Part of the statistics* (work speed, costs, etc.) *for employees with the 'Assistant' type is not counted*, as it is an episodic role in the project. It is assumed that they do not affect the project, and their edits can be disregarded in the context of the overall volume of work.
§ page.team.author.description2: *Default sorting* is by the number of tasks and groups (current, fired, assisting employees). § page.team.author.description2: *Default sorting* is by the number of tasks and groups (current, fired, assisting employees).
§ page.team.author.status: Status § page.team.author.status: Status
§ page.team.author.company: Company
§ page.team.author.firstCommit: First commit § page.team.author.firstCommit: First commit
§ page.team.author.lastCommit: Last § page.team.author.lastCommit: Last
§ page.team.author.daysAll: Total days § page.team.author.daysAll: Total days

View file

@ -24,6 +24,7 @@ export default `
§ page.team.author.description1: *Part of the statistics* (work speed, costs, etc.) *for employees with the 'Assistant' type is not counted*, as it is an episodic role in the project. It is assumed that they do not affect the project, and their edits can be disregarded in the context of the overall volume of work. § page.team.author.description1: *Part of the statistics* (work speed, costs, etc.) *for employees with the 'Assistant' type is not counted*, as it is an episodic role in the project. It is assumed that they do not affect the project, and their edits can be disregarded in the context of the overall volume of work.
§ page.team.author.description2: *Default sorting* is by the number of tasks and groups (current, fired, assisting employees). § page.team.author.description2: *Default sorting* is by the number of tasks and groups (current, fired, assisting employees).
§ page.team.author.status: Status § page.team.author.status: Status
§ page.team.author.company: Company
§ page.team.author.firstCommit: First commit § page.team.author.firstCommit: First commit
§ page.team.author.lastCommit: Last § page.team.author.lastCommit: Last
§ page.team.author.daysAll: Total days § page.team.author.daysAll: Total days

View file

@ -24,6 +24,7 @@ export default `
§ page.team.author.description1: *Часть статистики* (скорость работы, затраченные деньги и т.п.) *по сотрудникам с типом «Помощник» не считается*, т.к. это эпизодическая роль в проекте. Предполагаем, что они не влияют на проект, а их правками можно пренебречь на фоне общего объема работы. § page.team.author.description1: *Часть статистики* (скорость работы, затраченные деньги и т.п.) *по сотрудникам с типом «Помощник» не считается*, т.к. это эпизодическая роль в проекте. Предполагаем, что они не влияют на проект, а их правками можно пренебречь на фоне общего объема работы.
§ page.team.author.description2: *Сортировка по умолчанию* это сортировка по количеству задач и группам (текущие, уволенные, помогающие сотрудники). § page.team.author.description2: *Сортировка по умолчанию* это сортировка по количеству задач и группам (текущие, уволенные, помогающие сотрудники).
§ page.team.author.status: Статус § page.team.author.status: Статус
§ page.team.author.company: Компания
§ page.team.author.firstCommit: Первый коммит § page.team.author.firstCommit: Первый коммит
§ page.team.author.lastCommit: Последний § page.team.author.lastCommit: Последний
§ page.team.author.daysAll: Всего дней § page.team.author.daysAll: Всего дней

View file

@ -24,6 +24,7 @@ export default `
§ page.team.author.description1: Часть статистики (скорость работы, затраченные деньги и т.п.) по сотрудникам с типом «Помощник» не считается, т.к. это не постоянная роль в проекте. Их работа незначительно и её можно не учитывать. § page.team.author.description1: Часть статистики (скорость работы, затраченные деньги и т.п.) по сотрудникам с типом «Помощник» не считается, т.к. это не постоянная роль в проекте. Их работа незначительно и её можно не учитывать.
§ page.team.author.description2: Сортировка по умолчанию — это сортировка по количеству задач и группам (текущие, уволенные, помогающие сотрудники). § page.team.author.description2: Сортировка по умолчанию — это сортировка по количеству задач и группам (текущие, уволенные, помогающие сотрудники).
§ page.team.author.status: Status § page.team.author.status: Status
§ page.team.author.company: Company
§ page.team.author.firstCommit: First commit § page.team.author.firstCommit: First commit
§ page.team.author.lastCommit: Last § page.team.author.lastCommit: Last
§ page.team.author.daysAll: Total days § page.team.author.daysAll: Total days