update
BIN
build/assets/map/2x1.png
Normal file
After Width: | Height: | Size: 129 B |
BIN
build/assets/map/map.png
Normal file
After Width: | Height: | Size: 822 KiB |
3
build/assets/menu/company.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke="#84858D" d="M12 7V3H2v18h20V7zM6 19H4v-2h2zm0-4H4v-2h2zm0-4H4V9h2zm0-4H4V5h2zm4 12H8v-2h2zm0-4H8v-2h2zm0-4H8V9h2zm0-4H8V5h2zm10 12h-8v-2h2v-2h-2v-2h2v-2h-2V9h8zm-2-8h-2v2h2zm0 4h-2v2h2z"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 315 B |
4
build/assets/menu/country.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#84858D" d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7M7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 2.88-2.88 7.19-5 9.88C9.92 16.21 7 11.85 7 9"></path>
|
||||
<circle fill="#84858D" cx="12" cy="9" r="2.5"></circle>
|
||||
</svg>
|
After Width: | Height: | Size: 347 B |
3
build/assets/menu/refactor.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke="#84858D" d="M4 7v2c0 .55-.45 1-1 1s-1 .45-1 1v2c0 .55.45 1 1 1s1 .45 1 1v2c0 1.66 1.34 3 3 3h2c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1-.45-1-1v-2c0-1.3-.84-2.42-2-2.83v-.34C5.16 11.42 6 10.3 6 9V7c0-.55.45-1 1-1h2c.55 0 1-.45 1-1s-.45-1-1-1H7C5.34 4 4 5.34 4 7m17 3c-.55 0-1-.45-1-1V7c0-1.66-1.34-3-3-3h-2c-.55 0-1 .45-1 1s.45 1 1 1h2c.55 0 1 .45 1 1v2c0 1.3.84 2.42 2 2.83v.34c-1.16.41-2 1.52-2 2.83v2c0 .55-.45 1-1 1h-2c-.55 0-1 .45-1 1s.45 1 1 1h2c1.66 0 3-1.34 3-3v-2c0-.55.45-1 1-1s1-.45 1-1v-2c0-.55-.45-1-1-1"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 643 B |
|
@ -1,18 +1,16 @@
|
|||
module.exports = {
|
||||
webpack: (config) => {
|
||||
|
||||
|
||||
const oneOfs = config.module.rules.find((rule) => !!rule.oneOf).oneOf;
|
||||
for (const oneOf of oneOfs) {
|
||||
oneOf?.use?.forEach((someUse) => {
|
||||
if (!someUse?.options?.modules?.mode) return;
|
||||
// someUse.options.modules.localIdentName = '[local]_';
|
||||
someUse.options.modules.getLocalIdent = (context, localIdentName, localName, options) => {
|
||||
return localName;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
};
|
||||
module.exports = {
|
||||
webpack: (config) => {
|
||||
const oneOfs = config.module.rules.find((rule) => !!rule.oneOf).oneOf;
|
||||
for (const oneOf of oneOfs) {
|
||||
oneOf?.use?.forEach((someUse) => {
|
||||
if (!someUse?.options?.modules?.mode) return;
|
||||
// someUse.options.modules.localIdentName = '[local]';
|
||||
someUse.options.modules.getLocalIdent = (context, localIdentName, localName) => {
|
||||
return localName;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
|
BIN
public/assets/map/2x1.png
Normal file
After Width: | Height: | Size: 129 B |
BIN
public/assets/map/map.png
Normal file
After Width: | Height: | Size: 822 KiB |
3
public/assets/menu/company.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke="#84858D" d="M12 7V3H2v18h20V7zM6 19H4v-2h2zm0-4H4v-2h2zm0-4H4V9h2zm0-4H4V5h2zm4 12H8v-2h2zm0-4H8v-2h2zm0-4H8V9h2zm0-4H8V5h2zm10 12h-8v-2h2v-2h-2v-2h2v-2h-2V9h8zm-2-8h-2v2h2zm0 4h-2v2h2z"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 315 B |
4
public/assets/menu/country.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#84858D" d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7M7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 2.88-2.88 7.19-5 9.88C9.92 16.21 7 11.85 7 9"></path>
|
||||
<circle fill="#84858D" cx="12" cy="9" r="2.5"></circle>
|
||||
</svg>
|
After Width: | Height: | Size: 347 B |
3
public/assets/menu/refactor.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke="#84858D" d="M4 7v2c0 .55-.45 1-1 1s-1 .45-1 1v2c0 .55.45 1 1 1s1 .45 1 1v2c0 1.66 1.34 3 3 3h2c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1-.45-1-1v-2c0-1.3-.84-2.42-2-2.83v-.34C5.16 11.42 6 10.3 6 9V7c0-.55.45-1 1-1h2c.55 0 1-.45 1-1s-.45-1-1-1H7C5.34 4 4 5.34 4 7m17 3c-.55 0-1-.45-1-1V7c0-1.66-1.34-3-3-3h-2c-.55 0-1 .45-1 1s.45 1 1 1h2c.55 0 1 .45 1 1v2c0 1.3.84 2.42 2 2.83v.34c-1.16.41-2 1.52-2 2.83v2c0 .55-.45 1-1 1h-2c-.55 0-1 .45-1 1s.45 1 1 1h2c1.66 0 3-1.34 3-3v-2c0-.55.45-1 1-1s1-.45 1-1v-2c0-.55-.45-1-1-1"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 643 B |
32
src/ts/components/TimeZoneMap/components/Point.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
|
||||
import { getClassNameForTimeZone } from '../helpers';
|
||||
|
||||
import style from '../styles/index.module.scss';
|
||||
|
||||
interface PointProps {
|
||||
timezone: string;
|
||||
authors: string[]
|
||||
}
|
||||
|
||||
function Point({
|
||||
timezone,
|
||||
authors,
|
||||
}: PointProps): React.ReactElement | null {
|
||||
const className = getClassNameForTimeZone(timezone);
|
||||
return (
|
||||
<div
|
||||
title={authors.join(', ')}
|
||||
className={`${style.time_zone_map_point} ${className}`}
|
||||
>
|
||||
{authors.length}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Point.defaultProps = {
|
||||
timezone: '',
|
||||
authors: [],
|
||||
};
|
||||
|
||||
export default Point;
|
16
src/ts/components/TimeZoneMap/helpers/index.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
export function getGroupsByTimeZone(authors: any[]) {
|
||||
return authors.reduce((acc: any, author: any) => {
|
||||
const key = author.lastCommit.timezone;
|
||||
if (!acc[key]) acc[key] = [];
|
||||
acc[key].push(author.author);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function getClassNameForTimeZone(timezone?: string) {
|
||||
const suffix = (timezone || '')
|
||||
.replace('+', 'p')
|
||||
.replace('-', 'm')
|
||||
.replace(':', '');
|
||||
return `time_zone_map_point_${suffix || 'hide'}`;
|
||||
}
|
39
src/ts/components/TimeZoneMap/index.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
|
||||
import { getGroupsByTimeZone } from './helpers';
|
||||
import Point from './components/Point';
|
||||
import style from './styles/index.module.scss';
|
||||
|
||||
interface TimeZoneMapProps {
|
||||
authors: any[]
|
||||
}
|
||||
|
||||
function TimeZoneMap({
|
||||
authors = [],
|
||||
}: TimeZoneMapProps): React.ReactElement | null {
|
||||
const groups = getGroupsByTimeZone(authors);
|
||||
const points = Object.entries(groups).map((item: any) => (
|
||||
<Point
|
||||
key={item[0]}
|
||||
timezone={item[0]}
|
||||
authors={item[1]}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className={style.time_zone_map}>
|
||||
<img
|
||||
src="./assets/map/2x1.png"
|
||||
className={style.time_zone_map_gap}
|
||||
/>
|
||||
<div
|
||||
style={{ backgroundImage: 'url(./assets/map/map.png)' }}
|
||||
className={style.time_zone_map_points}
|
||||
>
|
||||
{points}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TimeZoneMap;
|
84
src/ts/components/TimeZoneMap/styles/index.module.scss
Normal file
|
@ -0,0 +1,84 @@
|
|||
@import 'src/styles/variables';
|
||||
@import 'src/ts/components/Description/index.module';
|
||||
|
||||
.time_zone_map {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
|
||||
&_gap {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&_points {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: center center;
|
||||
}
|
||||
|
||||
&_point {
|
||||
font-size: var(--font-xxs);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
display: block;
|
||||
width: var(--space-xl);
|
||||
height: var(--space-xl);
|
||||
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
box-shadow: 0 0 5px var(--color-white), 0 0 5px var(--color-white), 0 0 15px var(--color-white);
|
||||
|
||||
color: var(--color-white);
|
||||
border-radius: var(--border-radius-l);
|
||||
background-color: var(--color-second);
|
||||
}
|
||||
}
|
||||
|
||||
.time_zone_map_point {
|
||||
&_p1300 { top: 72%; left: 97.5%; }
|
||||
&_p1200 { top: 87%; left: 94%; }
|
||||
&_p1100 { top: 81%; left: 88%; } // { top: 26%; left: 88%; }
|
||||
&_p1000 { top: 83.5%; left: 86.5%; }
|
||||
&_p1030 { top: 82%; left: 84%; }
|
||||
&_p0930 { top: 82%; left: 84%; }
|
||||
&_p0900 { top: 42%; left: 83%; }
|
||||
&_p0800 { top: 47%; left: 78%; }
|
||||
&_p0700 { top: 29%; left: 69%; }
|
||||
&_p0630 { top: 53%; left: 73%; }
|
||||
&_p0600 { top: 29%; left: 66.5%; }
|
||||
&_p0545 { top: 47%; left: 69%; }
|
||||
&_p0430 { top: 43%; left: 65%; }
|
||||
&_p0530 { top: 49.5%; left: 67%; }
|
||||
&_p0500 { top: 30%; left: 64%; }
|
||||
&_p0400 { top: 31%; left: 60%; }
|
||||
&_p0330 { top: 44%; left: 61%; }
|
||||
&_p0300 { top: 29%; left: 57%; }
|
||||
&_p0200 { top: 33.5%; left: 54%; }
|
||||
&_p0100 { top: 35%; left: 48%; }
|
||||
&_p0000 { top: 32%; left: 45%; }
|
||||
&_m0100 { top: 41%; left: 39%; }
|
||||
&_m0200 { top: 75%; left: 33.5%; } // { top: 18%; left: 37%; }
|
||||
&_m0300 { top: 81%; left: 30%; }
|
||||
&_m0330 { top: 34%; left: 30.5%; }
|
||||
&_m0400 { top: 68%; left: 28.5%; }
|
||||
&_m0500 { top: 42%; left: 25%; }
|
||||
&_m0600 { top: 45%; left: 19%; }
|
||||
&_m0700 { top: 42%; left: 16%; }
|
||||
&_m0800 { top: 39%; left: 12%; }
|
||||
&_m0900 { top: 22%; left: 5%; }
|
||||
&_m1000 { top: 74%; left: 4%; }
|
||||
&_m1100 { top: 67%; left: 0; }
|
||||
&_m1200 { top: 62%; left: 97%; }
|
||||
}
|
|
@ -2,8 +2,6 @@ import ICommit from 'ts/interfaces/Commit';
|
|||
import IHashMap, { HashMap } from 'ts/interfaces/HashMap';
|
||||
|
||||
import { ONE_DAY } from 'ts/helpers/formatter';
|
||||
import getCountryByTimeZone from 'ts/helpers/Parser/getCountryByTimeZone';
|
||||
import getCountryBySymbol from 'ts/helpers/Parser/getCountryBySymbol';
|
||||
import { createHashMap, createIncrement, increment } from 'ts/helpers/Math';
|
||||
|
||||
import userSettings from 'ts/store/UserSettings';
|
||||
|
@ -63,9 +61,10 @@ export default class DataGripByAuthor {
|
|||
statistic.lastCompany = commit.company;
|
||||
statistic.company.push({ title: commit.company, from: commit.timestamp });
|
||||
}
|
||||
if (commit.country && statistic.lastCountry !== commit.country) {
|
||||
if (commit.timezone && statistic.lastCountry !== commit.timezone) {
|
||||
statistic.lastTimezone = commit.timezone;
|
||||
statistic.lastCountry = commit.country;
|
||||
statistic.country.add(commit.country);
|
||||
statistic.country.push({ country: commit.country, timezone: commit.timezone, from: commit.milliseconds });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,11 +75,6 @@ export default class DataGripByAuthor {
|
|||
const commitsByHour = new Array(24).fill(0);
|
||||
commitsByHour[commit.hours] += 1;
|
||||
|
||||
const country = commit.country
|
||||
|| getCountryBySymbol(commit.author)
|
||||
|| getCountryBySymbol(commit.message)
|
||||
|| getCountryByTimeZone(commit.timezone, commit.author);
|
||||
|
||||
this.commits.set(commit.author, {
|
||||
author: commit.author,
|
||||
commits: 1,
|
||||
|
@ -95,8 +89,9 @@ export default class DataGripByAuthor {
|
|||
? [{ title: commit.company, from: commit.milliseconds }]
|
||||
: [],
|
||||
lastCompany: commit.company,
|
||||
country: new Set([country]),
|
||||
lastCountry: country,
|
||||
country: [{ country: commit.country, timezone: commit.timezone, from: commit.milliseconds }],
|
||||
lastTimezone: commit.timezone,
|
||||
lastCountry: commit.country,
|
||||
device: commit.device,
|
||||
commitsByDayAndHour,
|
||||
commitsByHour,
|
||||
|
|
49
src/ts/helpers/DataGrip/components/country.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import IHashMap, { HashMap } from 'ts/interfaces/HashMap';
|
||||
import { increment } from 'ts/helpers/Math';
|
||||
import { getVpnList, getTravels } from 'ts/helpers/Parser/getCountryDistance';
|
||||
|
||||
export default class DataGripByCountry {
|
||||
countries: HashMap<any> = new Map();
|
||||
|
||||
vpn: IHashMap<any> = {};
|
||||
|
||||
devices: IHashMap<any> = {};
|
||||
|
||||
statistic: any = [];
|
||||
|
||||
clear() {
|
||||
this.countries.clear();
|
||||
this.vpn = {};
|
||||
this.devices = {};
|
||||
this.statistic = [];
|
||||
}
|
||||
|
||||
#addAuthor(country: string, author: string) {
|
||||
const statistic = this.countries.get(country);
|
||||
if (statistic) {
|
||||
statistic.employments.push(author);
|
||||
} else {
|
||||
this.countries.set(country, {
|
||||
country,
|
||||
employments: [author],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateTotalInfo(dataGripByAuthor: any) {
|
||||
dataGripByAuthor.statistic.forEach((author: any) => {
|
||||
const vpnList = getVpnList(author.country);
|
||||
author.country = getTravels(author.country, vpnList);
|
||||
this.#addAuthor(author.lastCountry || 'unknown', author.author);
|
||||
increment(this.devices, author.device || 'unknown');
|
||||
|
||||
Array.from(vpnList.keys()).forEach((country: string) => {
|
||||
increment(this.vpn, country);
|
||||
});
|
||||
});
|
||||
|
||||
this.statistic = Array.from(this.countries.values())
|
||||
.sort((a: any, b: any) => b?.employments?.length - a?.employments?.length);
|
||||
this.countries.clear();
|
||||
}
|
||||
}
|
23
src/ts/helpers/FileGrip/components/FileBuilder/Tasks.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import ICommit from 'ts/interfaces/Commit';
|
||||
import { IDirtyFile } from 'ts/interfaces/FileInfo';
|
||||
|
||||
export default class FileBuilderTasks {
|
||||
static getProps(commit: ICommit) {
|
||||
return {
|
||||
tasks: new Set([commit.task]),
|
||||
timestamp: new Set([commit.timestamp]),
|
||||
totalTasks: 0,
|
||||
totalDays: 0,
|
||||
};
|
||||
}
|
||||
|
||||
static updateProps(file: IDirtyFile, commit: ICommit) {
|
||||
file.tasks.add(commit.task);
|
||||
file.timestamp.add(commit.timestamp);
|
||||
}
|
||||
|
||||
static updateTotal(file: IDirtyFile) {
|
||||
file.totalTasks = file.tasks.size;
|
||||
file.totalDays = file.timestamp.size;
|
||||
}
|
||||
}
|
51
src/ts/helpers/FileGrip/components/refactor.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { IDirtyFile } from 'ts/interfaces/FileInfo';
|
||||
|
||||
type ArrayIdFile = [string, IDirtyFile];
|
||||
|
||||
function isCorrectFile(file: IDirtyFile) {
|
||||
return !(
|
||||
file.action === 'D'
|
||||
|| file.path.length < 3
|
||||
|| !file.extension
|
||||
|| { json: true, xml: true, md: true, config: true }[file.extension]
|
||||
|| { test: true, config: true }[file.type]
|
||||
);
|
||||
}
|
||||
|
||||
function getFilesByProperty(
|
||||
list: IDirtyFile[],
|
||||
property: string,
|
||||
): ArrayIdFile[] {
|
||||
list.sort((a: IDirtyFile, b: IDirtyFile) => b[property] - a[property]);
|
||||
return list
|
||||
.slice(0, 20)
|
||||
.map((file: IDirtyFile) => [file.id, file]);
|
||||
}
|
||||
|
||||
|
||||
export default class FileGripByRefactor {
|
||||
files: IDirtyFile[] = [];
|
||||
|
||||
updateTotalInfo(files: IDirtyFile[]) {
|
||||
const list = files.filter(isCorrectFile);
|
||||
const filesWithProblems = [
|
||||
...getFilesByProperty(list, 'lines'),
|
||||
...getFilesByProperty(list, 'totalDays'),
|
||||
...getFilesByProperty(list, 'totalTasks'),
|
||||
];
|
||||
|
||||
const uniqueFilesWithProblems = Array.from(
|
||||
(new Map<string, IDirtyFile>(filesWithProblems)).values(),
|
||||
).filter((file: IDirtyFile) => (
|
||||
file.lines > 50 && file.totalDays > 10
|
||||
));
|
||||
|
||||
uniqueFilesWithProblems.sort((a: any, b: any) => b.totalDays - a.totalDays);
|
||||
|
||||
this.files = uniqueFilesWithProblems;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.files = [];
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import IHashMap from 'ts/interfaces/HashMap';
|
|||
import { getTypeAndScope, getTask, getTaskNumber } from './getTypeAndScope';
|
||||
import getInfoFromNameAndEmail from './getCompany';
|
||||
import { getGithubPrInfo } from './getMergeInfo';
|
||||
import getCountryByTimeZone from './getCountryByTimeZone';
|
||||
|
||||
const MASTER_BRANCH = {
|
||||
master: true,
|
||||
|
@ -47,13 +48,17 @@ export default function getCommitInfo(
|
|||
let email = parts[2] || '';
|
||||
if (email.indexOf('@') === -1) email = '';
|
||||
|
||||
const companyKey = `${author}>in>${email}`;
|
||||
if (!refEmailAuthor[companyKey]) {
|
||||
// @ts-ignore
|
||||
const companyKey = `${author}>mail>${email}`;
|
||||
if (!refEmailAuthor[companyKey]) { // @ts-ignore
|
||||
refEmailAuthor[companyKey] = getInfoFromNameAndEmail(author, email);
|
||||
}
|
||||
// @ts-ignore
|
||||
const { company, country, device } = refEmailAuthor[companyKey];
|
||||
} // @ts-ignore
|
||||
const { company, domain, device } = refEmailAuthor[companyKey];
|
||||
|
||||
const countryKey = `${author}>time>${timezone}`;
|
||||
if (!refEmailAuthor[countryKey]) {// @ts-ignore
|
||||
refEmailAuthor[countryKey] = getCountryByTimeZone(timezone, domain, author);
|
||||
} // @ts-ignore
|
||||
const country = refEmailAuthor[countryKey];
|
||||
|
||||
const authorID = author.replace(/\s|\t/gm, '');
|
||||
if (authorID && refEmailAuthor[authorID] && refEmailAuthor[authorID] !== author) {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import getCountryByDomain from './getCountryByDomain';
|
||||
import getDevice from './getDevice';
|
||||
|
||||
const PUBLIC_SERVICES = [
|
||||
|
@ -22,6 +21,7 @@ const PUBLIC_SERVICES = [
|
|||
'me',
|
||||
'qq',
|
||||
'dev',
|
||||
'list',
|
||||
'localhost',
|
||||
];
|
||||
|
||||
|
@ -73,7 +73,6 @@ function isUserName(author?: string, company?: string): boolean {
|
|||
export default function getInfoFromNameAndEmail(author?: string, email?: string) {
|
||||
const companyByAuthor = getCompanyByName(author);
|
||||
const [companyByEmail, domain] = getCompanyAndDomainByEmail(email);
|
||||
const country = getCountryByDomain(domain);
|
||||
const device = getDevice(companyByEmail);
|
||||
|
||||
const companyName = companyByAuthor || companyByEmail || '';
|
||||
|
@ -84,5 +83,5 @@ export default function getInfoFromNameAndEmail(author?: string, email?: string)
|
|||
|| isIP.test(companyName);
|
||||
const company = (!isInCorrect && !device) ? companyName : '';
|
||||
|
||||
return { company, country, device };
|
||||
return { company, domain, device };
|
||||
}
|
||||
|
|
253
src/ts/helpers/Parser/getCountryByDomain.ts
Normal file
|
@ -0,0 +1,253 @@
|
|||
export const REF_DOMAIN_COUNTRY = {
|
||||
ac: 'Ascension Island',
|
||||
ad: 'Andorra',
|
||||
ae: 'United Arab Emirates',
|
||||
af: 'Afghanistan',
|
||||
ag: 'Antigua and Barbuda',
|
||||
ai: 'Anguilla',
|
||||
al: 'Albania',
|
||||
am: 'Armenia',
|
||||
ao: 'Angola',
|
||||
aq: 'Antarctica',
|
||||
ar: 'Argentina',
|
||||
as: 'American Samoa',
|
||||
at: 'Austria',
|
||||
au: 'Australia',
|
||||
aw: 'Aruba (Kingdom of the Netherlands)',
|
||||
ax: 'Aland (Finland)',
|
||||
az: 'Azerbaijan',
|
||||
ba: 'Bosnia and Herzegovina',
|
||||
bb: 'Barbados',
|
||||
bd: 'Bangladesh',
|
||||
be: 'Belgium',
|
||||
bf: 'Burkina Faso',
|
||||
bg: 'Bulgaria',
|
||||
bh: 'Bahrain',
|
||||
bi: 'Burundi',
|
||||
bj: 'Benin',
|
||||
bm: 'Bermuda',
|
||||
bn: 'Brunei',
|
||||
bo: 'Bolivia',
|
||||
bq: 'Caribbean Netherlands',
|
||||
br: 'Brazil',
|
||||
bs: 'Bahamas',
|
||||
bt: 'Bhutan',
|
||||
bw: 'Botswana',
|
||||
by: 'Belarus',
|
||||
bz: 'Belize',
|
||||
ca: 'Canada',
|
||||
cc: 'Cocos (Keeling) Islands',
|
||||
cd: 'Democratic Republic of the Congo',
|
||||
cf: 'Central African Republic',
|
||||
cg: 'Republic of the Congo',
|
||||
ch: 'Switzerland',
|
||||
ci: 'Ivory Coast',
|
||||
ck: 'Cook Islands',
|
||||
cl: 'Chile',
|
||||
cm: 'Cameroon',
|
||||
cn: 'China',
|
||||
co: 'Colombia',
|
||||
cr: 'Costa Rica',
|
||||
cu: 'Cuba',
|
||||
cv: 'Cape Verde',
|
||||
cw: 'Curacao (Kingdom of the Netherlands)',
|
||||
cx: 'Christmas Island',
|
||||
cy: 'Cyprus',
|
||||
cz: 'Czech Republic',
|
||||
de: 'Germany',
|
||||
dj: 'Djibouti',
|
||||
dk: 'Denmark',
|
||||
dm: 'Dominica',
|
||||
do: 'Dominican Republic',
|
||||
dz: 'Algeria',
|
||||
ec: 'Ecuador',
|
||||
ee: 'Estonia',
|
||||
eg: 'Egypt',
|
||||
eh: 'Western Sahara',
|
||||
er: 'Eritrea',
|
||||
es: 'Spain',
|
||||
et: 'Ethiopia',
|
||||
eu: 'Europe',
|
||||
fi: 'Finland',
|
||||
fj: 'Fiji',
|
||||
fk: 'Falkland Islands',
|
||||
fo: 'Faroe Islands (Kingdom of Denmark)',
|
||||
fr: 'France',
|
||||
ga: 'Gabon',
|
||||
gd: 'Grenada',
|
||||
ge: 'Georgia',
|
||||
gf: 'French Guiana',
|
||||
gg: 'Guernsey',
|
||||
gh: 'Ghana',
|
||||
gi: 'Gibraltar',
|
||||
gl: 'Greenland (Kingdom of Denmark)',
|
||||
gm: 'The Gambia',
|
||||
gn: 'Guinea',
|
||||
gp: 'Guadeloupe',
|
||||
gq: 'Equatorial Guinea10 July 1997',
|
||||
gr: 'Greece',
|
||||
gs: 'United Kingdom',
|
||||
gt: 'Guatemala',
|
||||
gu: 'Guam',
|
||||
gw: 'Guinea-Bissau',
|
||||
gy: 'Guyana',
|
||||
hk: 'Hong Kong',
|
||||
hm: 'Heard Island and McDonald Islands',
|
||||
hn: 'Honduras',
|
||||
hr: 'Croatia',
|
||||
ht: 'Haiti',
|
||||
hu: 'Hungary',
|
||||
id: 'Indonesia',
|
||||
ie: 'Ireland',
|
||||
il: 'Israel',
|
||||
im: 'Isle of Man',
|
||||
in: 'India',
|
||||
iq: 'Iraq',
|
||||
ir: 'Iran',
|
||||
is: 'Iceland',
|
||||
it: 'Italy',
|
||||
je: 'Jersey',
|
||||
jm: 'Jamaica',
|
||||
jo: 'Jordan',
|
||||
jp: 'Japan',
|
||||
ke: 'Kenya',
|
||||
kg: 'Kyrgyzstan',
|
||||
kh: 'Cambodia',
|
||||
ki: 'Kiribati',
|
||||
km: 'Comoros',
|
||||
kn: 'Saint Kitts and Nevis',
|
||||
kp: 'North Korea',
|
||||
kr: 'South Korea',
|
||||
kw: 'Kuwait',
|
||||
ky: 'Cayman Islands',
|
||||
kz: 'Kazakhstan',
|
||||
la: 'Laos',
|
||||
lb: 'Lebanon',
|
||||
lc: 'Saint Lucia',
|
||||
li: 'Liechtenstein',
|
||||
lk: 'Sri Lanka',
|
||||
lr: 'Liberia',
|
||||
ls: 'Lesotho',
|
||||
lt: 'Lithuania',
|
||||
lu: 'Luxembourg',
|
||||
lv: 'Latvia',
|
||||
ly: 'Libya',
|
||||
ma: 'Morocco',
|
||||
mc: 'Monaco',
|
||||
md: 'Moldova',
|
||||
me: 'Montenegro',
|
||||
mg: 'Madagascar',
|
||||
mh: 'Marshall Islands',
|
||||
mk: 'North Macedonia',
|
||||
ml: 'Mali',
|
||||
mm: 'Myanmar',
|
||||
mn: 'Mongolia',
|
||||
mo: 'Macau',
|
||||
mp: 'Northern Mariana Islands',
|
||||
mq: 'Martinique',
|
||||
mr: 'Mauritania',
|
||||
ms: 'Montserrat',
|
||||
mt: 'Malta',
|
||||
mu: 'Mauritius',
|
||||
mv: 'Maldives',
|
||||
mw: 'Malawi',
|
||||
mx: 'Mexico',
|
||||
my: 'Malaysia',
|
||||
mz: 'Mozambique',
|
||||
na: 'Namibia',
|
||||
nc: 'New Caledonia',
|
||||
ne: 'Niger',
|
||||
nf: 'Norfolk Island',
|
||||
ng: 'Nigeria',
|
||||
ni: 'Nicaragua',
|
||||
nl: 'Netherlands',
|
||||
no: 'Norway',
|
||||
np: ' Nepal',
|
||||
nr: 'Nauru',
|
||||
nu: 'Niue',
|
||||
nz: 'New Zealand',
|
||||
om: 'Oman',
|
||||
pa: 'Panama',
|
||||
pe: 'Peru',
|
||||
pf: 'French Polynesia',
|
||||
pg: 'Papua New Guinea',
|
||||
ph: 'Philippines',
|
||||
pk: 'Pakistan',
|
||||
pl: 'Poland',
|
||||
pm: 'Saint-Pierre and Miquelon',
|
||||
pn: 'Pitcairn Islands',
|
||||
pr: 'Puerto Rico',
|
||||
ps: 'Palestine',
|
||||
pt: 'Portugal',
|
||||
pw: 'Palau',
|
||||
py: 'Paraguay',
|
||||
qa: 'Qatar',
|
||||
re: 'Réunion',
|
||||
ro: 'Romania',
|
||||
rs: 'Serbia',
|
||||
ru: 'Russia',
|
||||
rw: 'Rwanda',
|
||||
sa: 'Saudi Arabia',
|
||||
sb: 'Solomon Islands',
|
||||
sc: 'Seychelles',
|
||||
sd: 'Sudan',
|
||||
se: 'Sweden',
|
||||
sg: 'Singapore',
|
||||
sh: 'Saint Helena, Ascension and Tristan da Cunha',
|
||||
si: 'Slovenia',
|
||||
sk: 'Slovakia',
|
||||
sl: 'Sierra Leone',
|
||||
sm: 'San Marino',
|
||||
sn: 'Senegal',
|
||||
so: 'Somalia',
|
||||
sr: 'Suriname',
|
||||
ss: 'South Sudan',
|
||||
st: 'São Tomé and Príncipe',
|
||||
sv: 'El Salvador',
|
||||
sx: 'Sint Maarten',
|
||||
sy: 'Syria',
|
||||
sz: 'Eswatini',
|
||||
tc: 'Turks and Caicos Islands',
|
||||
td: 'Chad',
|
||||
tf: 'French Southern and Antarctic Lands',
|
||||
tg: 'Togo',
|
||||
th: 'Thailand',
|
||||
tj: 'Tajikistan',
|
||||
tl: 'East Timor',
|
||||
tm: 'Turkmenistan',
|
||||
tn: 'Tunisia',
|
||||
to: 'Tonga',
|
||||
tr: 'Turkey',
|
||||
tt: 'Trinidad and Tobago',
|
||||
tw: 'Taiwan',
|
||||
tz: 'Tanzania',
|
||||
ua: 'Ukraine',
|
||||
ug: 'Uganda',
|
||||
uk: 'United Kingdom',
|
||||
us: 'USA',
|
||||
uy: 'Uruguay',
|
||||
uz: 'Uzbekistan',
|
||||
va: 'Vatican City',
|
||||
vc: 'Saint Vincent and the Grenadines',
|
||||
ve: 'Venezuela',
|
||||
vg: 'British Virgin Islands',
|
||||
vi: 'United States Virgin Islands',
|
||||
vn: 'Vietnam',
|
||||
vu: 'Vanuatu',
|
||||
wf: 'Wallis and Futuna',
|
||||
ws: 'Samoa',
|
||||
ye: 'Yemen',
|
||||
yt: 'Mayotte',
|
||||
za: 'South Africa',
|
||||
zm: 'Zambia',
|
||||
zw: 'Zimbabwe',
|
||||
africa: 'African Union',
|
||||
};
|
||||
|
||||
export const REF_NEW_OLD_DOMAIN = {
|
||||
su: 'ru',
|
||||
gov: 'us',
|
||||
mil: 'us',
|
||||
amazon: 'us',
|
||||
aws: 'us',
|
||||
};
|
168
src/ts/helpers/Parser/getCountryByTimeZone.ts
Normal file
|
@ -0,0 +1,168 @@
|
|||
import {
|
||||
REF_DOMAIN_COUNTRY,
|
||||
REF_NEW_OLD_DOMAIN,
|
||||
} from './getCountryByDomain';
|
||||
|
||||
const FAMILY = {
|
||||
ru: ['а', 'е', 'и', 'ivan', 'alexan', 'alexe', 'petr', 'konstan', 'sergey', 'dmitr', 'roman', 'pavel', 'vlad', 'nikol', 'maks', 'andre', 'oleg', 'denis', 'victor', 'eugen', 'ikhail', 'italy', 'yura', 'igor'],
|
||||
tr: ['ilmaz', 'aya', 'demir', 'elik', 'ahin', 'ildiz', 'ildirım'],
|
||||
pt: ['silva', 'santos', 'ferreira', 'pereira', 'oliveira', 'rodrigues', 'pereira', 'soares'],
|
||||
kr: ['kim', 'won', 'khan'],
|
||||
jp: ['sudzuki', 'yashi', 'kami', 'yuki', 'yama', 'sato', 'mato', 'moto', 'sawa', 'hiro'],
|
||||
uk: ['watson', 'thomson', 'smith', 'johnson', 'williams', 'jones', 'brown', 'davis', 'miller', 'wilson', 'moore', 'taylor'],
|
||||
es: ['ñ', 'gonzales', 'rodriguez', 'fernandez', 'garcia', 'lopez'],
|
||||
fr: ['blanchet', 'boucher', 'deschamps', 'dupont', 'fournier', 'garnier', 'laurent', 'lavigne', 'martin', 'monet'],
|
||||
it: ['rossi', 'ferrari', 'conti', 'romano', 'bruni', 'esposito', 'russo', 'marino', 'de luca', 'mancini'],
|
||||
pl: ['ł'],
|
||||
ee: ['õ'],
|
||||
il: ['ה', 'י', 'ו'],
|
||||
};
|
||||
|
||||
const LINE = {
|
||||
'-12:00': { countries: ['us'] },
|
||||
'-11:00': { countries: ['us'] },
|
||||
'-10:00': { countries: ['us'] },
|
||||
'-09:30': { countries: ['pf'] },
|
||||
'-09:00': { countries: ['us'] },
|
||||
'-08:00': { countries: ['us'] },
|
||||
'-07:00': { countries: ['ca', 'us'] },
|
||||
'-06:00': { countries: ['ca', 'us', 'mx', 'hn', 'ni'] },
|
||||
'-05:00': {
|
||||
title: 'Canada or USA or Caribbean or Peru',
|
||||
countries: ['ca', 'us', 'jm', 'dm', 'pa', 'pe', 'cu', 'co'],
|
||||
},
|
||||
'-04:00': { countries: ['ca', 'us', 'South America'] },
|
||||
'-03:00': {
|
||||
countries: ['ar', 'br'],
|
||||
name: {
|
||||
ar: FAMILY.es,
|
||||
br: FAMILY.pt,
|
||||
},
|
||||
},
|
||||
'-02:00': { countries: ['br'] }, // not gl
|
||||
'-01:00': { countries: ['pt'] },
|
||||
'+00:00': {
|
||||
countries: ['pt', 'uk'],
|
||||
name: {
|
||||
pt: FAMILY.pt,
|
||||
uk: FAMILY.uk,
|
||||
ru: FAMILY.ru,
|
||||
},
|
||||
},
|
||||
'+01:00': {
|
||||
title: 'Europe',
|
||||
countries: [
|
||||
'uk', 'es', 'fr', 'de', 'it', 'ch', 'at', 'pl', 'be', 'li', 'rs', 'se', 'no', 'me', 'si', 'sk', 'dk', 'nl',
|
||||
'dz', 'ne', 'td', 'ao',
|
||||
],
|
||||
name: {
|
||||
es: FAMILY.es,
|
||||
fr: FAMILY.fr,
|
||||
it: FAMILY.it,
|
||||
pl: FAMILY.pl,
|
||||
ee: FAMILY.ee,
|
||||
},
|
||||
},
|
||||
'+02:00': {
|
||||
title: 'Finland or Ukraine or Balkans or Israel',
|
||||
countries: [
|
||||
'fi', 'lv', 'lt', 'ee', 'ua', 'bg', 'gr', 'cy', 'il',
|
||||
'eg', 'ly', 'za',
|
||||
],
|
||||
name: {
|
||||
es: FAMILY.es,
|
||||
pl: FAMILY.pl,
|
||||
ee: FAMILY.ee,
|
||||
il: FAMILY.il,
|
||||
},
|
||||
},
|
||||
'+03:00': {
|
||||
countries: ['ru', 'tr'],
|
||||
name: {
|
||||
ru: FAMILY.ru,
|
||||
tr: FAMILY.tr,
|
||||
},
|
||||
},
|
||||
'+03:30': { countries: ['ir'] },
|
||||
'+04:00': {
|
||||
countries: ['ru'],
|
||||
},
|
||||
'+04:30': { countries: ['af'] },
|
||||
'+05:00': {
|
||||
title: 'Yekaterinburg or Middle Asia or Pakistan',
|
||||
countries: ['ru', 'kz', 'uz', 'uz', 'kg', 'tm', 'tj', 'pk'],
|
||||
name: {
|
||||
ru: FAMILY.ru,
|
||||
},
|
||||
},
|
||||
'+05:30': { countries: ['in'] },
|
||||
'+05:45': { countries: ['np'] },
|
||||
'+06:00': { countries: ['ru'] },
|
||||
'+07:00': {
|
||||
countries: ['th', 'vn', 'id'],
|
||||
name: {
|
||||
ru: FAMILY.ru,
|
||||
},
|
||||
},
|
||||
'+08:00': {
|
||||
title: 'China or Philippines',
|
||||
countries: ['cn', 'ph', 'ml'],
|
||||
},
|
||||
'+09:00': {
|
||||
countries: ['kr', 'jp'],
|
||||
name: {
|
||||
kr: FAMILY.kr,
|
||||
jp: FAMILY.jp,
|
||||
},
|
||||
},
|
||||
'+09:30': { countries: ['au'] },
|
||||
'+10:00': { countries: ['au'] },
|
||||
'+10:30': { countries: ['au'] },
|
||||
'+11:00': { countries: ['au'] }, // not ru
|
||||
'+12:00': { countries: ['nz'] },
|
||||
'+13:00': { countries: ['ws'] },
|
||||
};
|
||||
|
||||
function updateLines() {
|
||||
let keys = Object.keys(LINE);
|
||||
keys.forEach((timezone: string, index: number) => {
|
||||
LINE[timezone].prev = LINE[(keys[index - 1] || '')]?.countries || [];
|
||||
LINE[timezone].next = LINE[(keys[index + 1] || '')]?.countries || [];
|
||||
if (!LINE[timezone].title) {
|
||||
LINE[timezone].title = LINE[timezone].countries.map((id: string) => REF_DOMAIN_COUNTRY[id] || id).join(' or ');
|
||||
}
|
||||
});
|
||||
}
|
||||
updateLines();
|
||||
|
||||
export default function getCountryByTimeZone(
|
||||
timeZone?: string,
|
||||
domain?: string,
|
||||
name?: string,
|
||||
) {
|
||||
const data = LINE[timeZone || ''];
|
||||
if (!data) return '';
|
||||
|
||||
if (data.countries.length === 1) {
|
||||
return REF_DOMAIN_COUNTRY[data.countries[0]];
|
||||
}
|
||||
|
||||
if (domain && [
|
||||
...data.prev,
|
||||
...data.countries,
|
||||
...data.next,
|
||||
].indexOf(domain) !== -1) {
|
||||
const newDomain = REF_NEW_OLD_DOMAIN[domain || ''] || domain;
|
||||
return REF_DOMAIN_COUNTRY[newDomain];
|
||||
}
|
||||
|
||||
if (name && data.name) {
|
||||
const family = name.toLowerCase();
|
||||
for (let key in data.name) {
|
||||
const hasSuffix = data.name[key].some((text: string) => family.indexOf(text) !== -1);
|
||||
if (hasSuffix) return REF_DOMAIN_COUNTRY[key];
|
||||
}
|
||||
}
|
||||
|
||||
return data.title;
|
||||
}
|
87
src/ts/helpers/Parser/getCountryDistance.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
import { HashMap } from 'ts/interfaces/HashMap';
|
||||
import { ONE_DAY } from 'ts/helpers/formatter';
|
||||
|
||||
const BY_X = {
|
||||
'-12:00': 1,
|
||||
'-11:00': 2,
|
||||
'-10:00': 3,
|
||||
'-09:30': 3.5,
|
||||
'-09:00': 4,
|
||||
'-08:00': 5,
|
||||
'-07:00': 6,
|
||||
'-06:00': 7,
|
||||
'-05:00': 8,
|
||||
'-04:00': 9,
|
||||
'-03:00': 10,
|
||||
'-02:00': 11,
|
||||
'-01:00': 12,
|
||||
'+00:00': 13,
|
||||
'+01:00': 14,
|
||||
'+02:00': 15,
|
||||
'+03:00': 16,
|
||||
'+03:30': 16.5,
|
||||
'+04:00': 17,
|
||||
'+04:30': 17.5,
|
||||
'+05:00': 18,
|
||||
'+05:30': 18.5,
|
||||
'+05:45': 18.75,
|
||||
'+06:00': 19,
|
||||
'+07:00': 20,
|
||||
'+08:00': 21,
|
||||
'+09:00': 22,
|
||||
'+09:30': 22.5,
|
||||
'+10:00': 23,
|
||||
'+10:30': 23.5,
|
||||
'+11:00': 24,
|
||||
'+12:00': 25,
|
||||
'+13:00': 26,
|
||||
};
|
||||
|
||||
function getDistance(timezoneA: string, timezoneB: string) {
|
||||
return Math.abs(BY_X[timezoneA] - BY_X[timezoneB]);
|
||||
}
|
||||
|
||||
export function getVpnList(countries: any[]) {
|
||||
const vpn = new Map();
|
||||
|
||||
if (countries.length < 2) return vpn;
|
||||
|
||||
for (let i = 0, l = countries.length; i < l; i++) {
|
||||
const from = countries[i];
|
||||
const to = countries[i + 1];
|
||||
const next = countries[i + 2];
|
||||
if (!to || !next) continue;
|
||||
const isFast = (next?.from - from?.from) < ONE_DAY;
|
||||
const isThisPlace = from.timezone === next.timezone;
|
||||
if (isFast && isThisPlace) {
|
||||
vpn.set(to.country, to.timezone);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return vpn;
|
||||
}
|
||||
|
||||
export function getTravels(countries: any[], vpnList: HashMap<string>) {
|
||||
if (countries.length === 1) return null;
|
||||
|
||||
let from = countries[0].timezone;
|
||||
const path = [countries[0]];
|
||||
|
||||
const formattedCountries = countries.length > 3
|
||||
? countries.filter((item: any) => !vpnList.has(item.country))
|
||||
: countries;
|
||||
|
||||
for (let i = 1, l = formattedCountries.length; i < l; i++) {
|
||||
const country = formattedCountries[i];
|
||||
const to = country.timezone;
|
||||
if (from === to) continue;
|
||||
if (getDistance(from, to) > 1) {
|
||||
from = to;
|
||||
path.push(country);
|
||||
}
|
||||
}
|
||||
|
||||
return (path.length === 1) ? null : path;
|
||||
}
|
||||
|
5
src/ts/helpers/Parser/getDevice.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default function getDevice(company?: string) {
|
||||
return company && (/(MACBOOK)|(-AIR)|(-IMAC)/gi).test(company)
|
||||
? 'MacBook'
|
||||
: '';
|
||||
}
|
14
src/ts/helpers/Parser/getMergeInfo.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
// "Merge pull request #3 in repository from TASK-123-add-profile to master"
|
||||
// "Merge pull request #3 from facebook/compiler into master"
|
||||
// "Merge pull request #3 from facebook/compiler"
|
||||
export function getGithubPrInfo(text: string) {
|
||||
const json = (text || '')
|
||||
.replace(/"/gim, '')
|
||||
.replace('#', '#": "')
|
||||
.replace(' in ', '", "in": "')
|
||||
.replace(' from ', '", "from": "')
|
||||
.replace(' to ', '", "to": "')
|
||||
.replace(' into ', '", "to": "');
|
||||
const data = JSON.parse(`{"${json}"}`);
|
||||
return [data['Merge pull request #'], data.in, data.from, data.to];
|
||||
}
|
65
src/ts/pages/Team/components/Author/components/PieCharts.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import dataGripStore from 'ts/store/DataGrip';
|
||||
|
||||
import PageWrapper from 'ts/components/Page/wrapper';
|
||||
import PageColumn from 'ts/components/Page/column';
|
||||
import getOptions from 'ts/components/LineChart/helpers/getOptions';
|
||||
|
||||
import PieChart from 'ts/components/PieChart';
|
||||
import { STATUS, WORK_DAYS } from '../contstants';
|
||||
import { increment } from 'ts/helpers/Math';
|
||||
|
||||
function getStatusChart(rows: any[]) {
|
||||
const order = Object.values(STATUS);
|
||||
const options = getOptions({ order, limit: 1 });
|
||||
const details = rows.reduce((acc: any, row: any) => {
|
||||
if (row.isStaff) increment(acc, STATUS.STAFF);
|
||||
else if (row.isDismissed) increment(acc, STATUS.DISMISSED);
|
||||
else increment(acc, STATUS.WORK);
|
||||
return acc;
|
||||
}, {});
|
||||
return [options, details];
|
||||
}
|
||||
|
||||
function getDaysChart(rows: any[]) {
|
||||
const order = Object.values(WORK_DAYS);
|
||||
const options = getOptions({ order, limit: 1, suffix: 'page.team.author.daysChart.item' });
|
||||
const details = rows.reduce((acc: any, row: any) => {
|
||||
if (row.daysAll < 183) increment(acc, WORK_DAYS.HALF);
|
||||
else if (row.daysAll < 365) increment(acc, WORK_DAYS.ONE);
|
||||
else if (row.daysAll < 547) increment(acc, WORK_DAYS.HALF_ONE);
|
||||
else if (row.daysAll < 730) increment(acc, WORK_DAYS.TWO);
|
||||
else increment(acc, WORK_DAYS.MORE);
|
||||
return acc;
|
||||
}, {});
|
||||
return [options, details];
|
||||
}
|
||||
|
||||
const PieCharts = observer((): React.ReactElement | null => {
|
||||
const rows = dataGripStore.dataGrip.author.statistic;
|
||||
const [statusOptions, statusDetails] = getStatusChart(rows);
|
||||
const [daysOptions, daysDetails] = getDaysChart(rows);
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<PageColumn>
|
||||
<PieChart
|
||||
title="page.team.author.statusChart.title"
|
||||
options={statusOptions}
|
||||
details={statusDetails}
|
||||
/>
|
||||
</PageColumn>
|
||||
<PageColumn>
|
||||
<PieChart
|
||||
title="page.team.author.daysChart.title"
|
||||
options={daysOptions}
|
||||
details={daysDetails}
|
||||
/>
|
||||
</PageColumn>
|
||||
</PageWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
export default PieCharts;
|
201
src/ts/pages/Team/components/Author/components/View.tsx
Normal file
|
@ -0,0 +1,201 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ICommit from 'ts/interfaces/Commit';
|
||||
import IHashMap from 'ts/interfaces/HashMap';
|
||||
import { IPagination } from 'ts/interfaces/Pagination';
|
||||
import { getDate, getMoney, getShortNumber } 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, getMaxByLength } from 'ts/pages/Common/helpers/getMax';
|
||||
|
||||
interface ViewProps {
|
||||
response?: IPagination<any>;
|
||||
updateSort?: Function;
|
||||
rowsForExcel?: any[];
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
export function View({ response, updateSort, rowsForExcel, mode }: ViewProps) {
|
||||
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 taskChart = getOptions({ max: getMaxByLength(response, 'tasks'), suffix: 'page.team.author.tasksSmall' });
|
||||
const commitsChart = getOptions({ max: getMax(response, 'commits') });
|
||||
const typeChart = getOptions({ 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={200}
|
||||
/>
|
||||
<Column
|
||||
title="page.team.author.status"
|
||||
formatter={(row: any) => {
|
||||
if (row.isStaff) return staff;
|
||||
if (row.isDismissed) return dismissed;
|
||||
return works;
|
||||
}}
|
||||
template={(value: string) => <UiKitTags value={value} />}
|
||||
width={100}
|
||||
/>
|
||||
<Column
|
||||
isSortable="company"
|
||||
title="page.team.author.company"
|
||||
properties="lastCompany"
|
||||
template={(value: string) => <UiKitTags value={value} />}
|
||||
width={150}
|
||||
/>
|
||||
<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}
|
||||
title="page.team.author.daysAll"
|
||||
properties="daysAll"
|
||||
formatter={(value: number) => value || 1}
|
||||
width={90}
|
||||
/>
|
||||
<Column
|
||||
isSortable="daysWorked"
|
||||
title="page.team.author.workedLosses"
|
||||
minWidth={300}
|
||||
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)}
|
||||
/>
|
||||
<Column
|
||||
isSortable
|
||||
properties="tasks"
|
||||
title="page.team.author.tasks"
|
||||
minWidth={200}
|
||||
template={(value: number) => (
|
||||
<LineChart
|
||||
options={taskChart}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
formatter={(tasks: any) => (tasks?.length || 0)}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.SHORT_NUMBER}
|
||||
title="page.team.author.daysForTask"
|
||||
properties="daysForTask"
|
||||
formatter={getShortNumber}
|
||||
width={120}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.SHORT_NUMBER}
|
||||
title="page.team.author.scopes"
|
||||
properties="scopes"
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.SHORT_NUMBER}
|
||||
properties="commits"
|
||||
/>
|
||||
<Column
|
||||
isSortable
|
||||
title="page.team.author.commits"
|
||||
properties="commits"
|
||||
minWidth={100}
|
||||
template={(value: number) => (
|
||||
<LineChart
|
||||
options={commitsChart}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Column
|
||||
title="page.team.author.types"
|
||||
properties="types"
|
||||
width={400}
|
||||
template={(details: IHashMap<number>) => (
|
||||
<LineChart
|
||||
options={typeChart}
|
||||
details={details}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
View.defaultProps = {
|
||||
response: undefined,
|
||||
};
|
||||
|
||||
export default View;
|
15
src/ts/pages/Team/components/Author/contstants.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { t } from 'ts/helpers/Localization';
|
||||
|
||||
export const STATUS = {
|
||||
WORK: t('page.team.author.type.work'),
|
||||
DISMISSED: t('page.team.author.type.dismissed'),
|
||||
STAFF: t('page.team.author.type.staff'),
|
||||
};
|
||||
|
||||
export const WORK_DAYS = {
|
||||
HALF: t('page.team.author.days.half'),
|
||||
ONE: t('page.team.author.days.one'),
|
||||
HALF_ONE: t('page.team.author.days.15'),
|
||||
TWO: t('page.team.author.days.two'),
|
||||
MORE: t('page.team.author.days.more'),
|
||||
};
|
78
src/ts/pages/Team/components/Author/index.tsx
Normal file
|
@ -0,0 +1,78 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
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 PageWrapper from 'ts/components/Page/wrapper';
|
||||
import PageColumn from 'ts/components/Page/column';
|
||||
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 Recommendations from 'ts/components/Recommendations';
|
||||
|
||||
import Description from 'ts/components/Description';
|
||||
import PieCharts from './components/PieCharts';
|
||||
import View from './components/View';
|
||||
|
||||
const Author = observer(({
|
||||
mode,
|
||||
}: ICommonPageProps): React.ReactElement | null => {
|
||||
const { t } = useTranslation();
|
||||
const rows = dataGripStore.dataGrip.author.statistic;
|
||||
|
||||
if (!rows?.length) {
|
||||
return mode !== 'print' ? (<NothingFound />) : null;
|
||||
}
|
||||
const recommendations = dataGripStore.dataGrip.recommendations.team?.byAuthor;
|
||||
|
||||
return (
|
||||
<>
|
||||
{mode !== 'fullscreen' && (
|
||||
<Recommendations
|
||||
mode={mode}
|
||||
recommendations={recommendations}
|
||||
/>
|
||||
)}
|
||||
|
||||
<br />
|
||||
<br />
|
||||
<PieCharts />
|
||||
|
||||
<Title title="page.team.author.title"/>
|
||||
<DataLoader
|
||||
to="response"
|
||||
loader={(pagination?: IPaginationRequest, sort?: ISort[]) => getFakeLoader({
|
||||
content: rows, pagination, sort, mode,
|
||||
})}
|
||||
watch={`${mode}${dataGripStore.hash}`}
|
||||
>
|
||||
<View
|
||||
mode={mode}
|
||||
rowsForExcel={rows}
|
||||
/>
|
||||
<Pagination />
|
||||
</DataLoader>
|
||||
|
||||
<PageWrapper>
|
||||
<PageColumn>
|
||||
<Description
|
||||
text={t('page.team.author.description1')}
|
||||
/>
|
||||
</PageColumn>
|
||||
<PageColumn>
|
||||
<Description
|
||||
text={t('page.team.author.description2')}
|
||||
/>
|
||||
</PageColumn>
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default Author;
|
63
src/ts/pages/Team/components/Company/components/Charts.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import dataGripStore from 'ts/store/DataGrip';
|
||||
|
||||
import PageWrapper from 'ts/components/Page/wrapper';
|
||||
import PageColumn from 'ts/components/Page/column';
|
||||
import getOptions from 'ts/components/LineChart/helpers/getOptions';
|
||||
|
||||
import PieChart from 'ts/components/PieChart';
|
||||
import { WORK_DAYS } from '../../Author/contstants';
|
||||
import { increment } from 'ts/helpers/Math';
|
||||
|
||||
function getStatusChart(rows: any[]) {
|
||||
const order = rows.map((data: any) => data.company);
|
||||
const limit = order.length > 10 ? 2 : 1;
|
||||
const options = getOptions({ order, limit, suffix: 'page.team.company.employments.item' });
|
||||
const details = Object.fromEntries(
|
||||
rows.map((row: any) => [row.company, row.employments.length]),
|
||||
);
|
||||
return [options, details];
|
||||
}
|
||||
|
||||
function getDaysChart(rows: any[]) {
|
||||
const order = Object.values(WORK_DAYS);
|
||||
const options = getOptions({ order, limit: 1, suffix: 'page.team.company.daysChart.item' });
|
||||
const details = rows.reduce((acc: any, row: any) => {
|
||||
if (row.totalDays < 183) increment(acc, WORK_DAYS.HALF);
|
||||
else if (row.totalDays < 365) increment(acc, WORK_DAYS.ONE);
|
||||
else if (row.totalDays < 547) increment(acc, WORK_DAYS.HALF_ONE);
|
||||
else if (row.totalDays < 730) increment(acc, WORK_DAYS.TWO);
|
||||
else increment(acc, WORK_DAYS.MORE);
|
||||
return acc;
|
||||
}, {});
|
||||
return [options, details];
|
||||
}
|
||||
|
||||
const PieCharts = observer((): React.ReactElement | null => {
|
||||
const rows = dataGripStore.dataGrip.company.statistic;
|
||||
const [statusOptions, statusDetails] = getStatusChart(rows);
|
||||
const [daysOptions, daysDetails] = getDaysChart(rows);
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<PageColumn>
|
||||
<PieChart
|
||||
title="page.team.company.employments.title"
|
||||
options={statusOptions}
|
||||
details={statusDetails}
|
||||
/>
|
||||
</PageColumn>
|
||||
<PageColumn>
|
||||
<PieChart
|
||||
title="page.team.company.daysChart.title"
|
||||
options={daysOptions}
|
||||
details={daysDetails}
|
||||
/>
|
||||
</PageColumn>
|
||||
</PageWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
export default PieCharts;
|
135
src/ts/pages/Team/components/Company/components/Companies.tsx
Normal file
|
@ -0,0 +1,135 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
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.company.active.yes'),
|
||||
t('page.team.company.active.no'),
|
||||
];
|
||||
|
||||
const taskChart = getOptions({ max: getMax(response, 'tasks'), 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"
|
||||
width={200}
|
||||
/>
|
||||
<Column
|
||||
title="page.team.author.status"
|
||||
formatter={(row: any) => (row.isActive ? works : dismissed)}
|
||||
template={(value: string) => <UiKitTags value={value} />}
|
||||
width={140}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.STRING}
|
||||
properties="from"
|
||||
title="page.team.author.firstCommit"
|
||||
width={130}
|
||||
formatter={getDate}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.STRING}
|
||||
properties="to"
|
||||
title="page.team.author.lastCommit"
|
||||
width={130}
|
||||
formatter={getDate}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.SHORT_NUMBER}
|
||||
properties="totalDays"
|
||||
width={90}
|
||||
/>
|
||||
<Column
|
||||
isSortable
|
||||
title="page.team.author.daysAll"
|
||||
properties="totalDays"
|
||||
width={150}
|
||||
template={(value: number) => (
|
||||
<LineChart
|
||||
options={daysChart}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.SHORT_NUMBER}
|
||||
properties="tasks"
|
||||
width={90}
|
||||
/>
|
||||
<Column
|
||||
isSortable
|
||||
title="page.team.author.tasks"
|
||||
properties="tasks"
|
||||
width={150}
|
||||
template={(value: number) => (
|
||||
<LineChart
|
||||
options={taskChart}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Column
|
||||
properties="emptyCell"
|
||||
minWidth={40}
|
||||
/>
|
||||
</DataView>
|
||||
);
|
||||
}
|
||||
|
||||
Companies.defaultProps = {
|
||||
response: undefined,
|
||||
};
|
||||
|
||||
export default Companies;
|
146
src/ts/pages/Team/components/Company/components/Employments.tsx
Normal 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={140}
|
||||
/>
|
||||
<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;
|
62
src/ts/pages/Team/components/Country/components/Charts.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import dataGripStore from 'ts/store/DataGrip';
|
||||
|
||||
import PageWrapper from 'ts/components/Page/wrapper';
|
||||
import getOptions from 'ts/components/LineChart/helpers/getOptions';
|
||||
|
||||
import PieChart from 'ts/components/PieChart';
|
||||
import PageColumn from 'ts/components/Page/column';
|
||||
import { increment } from 'ts/helpers/Math';
|
||||
|
||||
function getCountryChart(rows: any[]) {
|
||||
const order = rows.map((data: any) => data.country);
|
||||
const limit = order.length > 10 ? 2 : 1;
|
||||
const options = getOptions({ order, limit, suffix: 'page.team.country.chart.item' });
|
||||
const details = Object.fromEntries(
|
||||
rows.map((row: any) => [row.country, row.employments.length]),
|
||||
);
|
||||
return [options, details];
|
||||
}
|
||||
|
||||
function getTimeZoneChart(authors: any[]) {
|
||||
const details = authors.reduce((acc: any, author) => {
|
||||
increment(acc, author.lastCommit.timezone.replace(':', '.'));
|
||||
return acc;
|
||||
}, {});
|
||||
const options = getOptions({
|
||||
order: Object.keys(details).sort(),
|
||||
limit: 5,
|
||||
suffix: 'page.team.country.chart.item',
|
||||
});
|
||||
return [options, details];
|
||||
}
|
||||
|
||||
const PieCharts = observer((): React.ReactElement | null => {
|
||||
const authors = dataGripStore.dataGrip.author.statistic;
|
||||
const rows = dataGripStore.dataGrip.country.statistic;
|
||||
const [countryOptions, countryDetails] = getCountryChart(rows);
|
||||
const [timezoneOptions, timezoneDetails] = getTimeZoneChart(authors);
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<PageColumn>
|
||||
<PieChart
|
||||
title="page.team.country.pieByDomain.title"
|
||||
options={countryOptions}
|
||||
details={countryDetails}
|
||||
/>
|
||||
</PageColumn>
|
||||
<PageColumn>
|
||||
<PieChart
|
||||
title="page.team.country.pieByTimezone.title"
|
||||
options={timezoneOptions}
|
||||
details={timezoneDetails}
|
||||
/>
|
||||
</PageColumn>
|
||||
</PageWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
export default PieCharts;
|
|
@ -0,0 +1,90 @@
|
|||
import React from 'react';
|
||||
|
||||
import { IPagination } from 'ts/interfaces/Pagination';
|
||||
import dataGripStore from 'ts/store/DataGrip';
|
||||
|
||||
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 { getMaxByLength } from 'ts/pages/Common/helpers/getMax';
|
||||
|
||||
import Employments from '../../Company/components/Employments';
|
||||
|
||||
interface CompaniesProps {
|
||||
response?: IPagination<any>;
|
||||
updateSort?: Function;
|
||||
rowsForExcel?: any[];
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
function Countries({ response, updateSort, rowsForExcel, mode }: CompaniesProps) {
|
||||
if (!response) return null;
|
||||
|
||||
const employmentsChart = getOptions({ max: getMaxByLength(response, 'employments') });
|
||||
|
||||
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="country"
|
||||
title="page.team.country.table.country"
|
||||
width={200}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.SHORT_NUMBER}
|
||||
properties="employments"
|
||||
formatter={(employments: string[]) => employments.length}
|
||||
/>
|
||||
<Column
|
||||
isSortable
|
||||
properties="employments"
|
||||
width={200}
|
||||
template={(employments: any) => (
|
||||
<LineChart
|
||||
options={employmentsChart}
|
||||
value={employments.length}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.STRING}
|
||||
properties="employments"
|
||||
minWidth={300}
|
||||
title="page.team.country.table.employments"
|
||||
formatter={(employments: string[]) => employments.join(', ')}
|
||||
/>
|
||||
</DataView>
|
||||
);
|
||||
}
|
||||
|
||||
Countries.defaultProps = {
|
||||
response: undefined,
|
||||
};
|
||||
|
||||
export default Countries;
|
61
src/ts/pages/Team/components/Country/components/Fly.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
import React from 'react';
|
||||
|
||||
import { IPagination } from 'ts/interfaces/Pagination';
|
||||
|
||||
import DataView from 'ts/components/DataView';
|
||||
import Column from 'ts/components/Table/components/Column';
|
||||
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
|
||||
import { getDate } from 'ts/helpers/formatter';
|
||||
|
||||
interface TravelProps {
|
||||
response?: IPagination<any>;
|
||||
updateSort?: Function;
|
||||
rowsForExcel?: any[];
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
function Fly({ response, updateSort, rowsForExcel, mode }: TravelProps) {
|
||||
if (!response) return null;
|
||||
|
||||
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
|
||||
isSortable
|
||||
template={ColumnTypesEnum.STRING}
|
||||
title="page.team.country.travel.date"
|
||||
properties="from"
|
||||
formatter={getDate}
|
||||
width={142}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.STRING}
|
||||
width={40}
|
||||
formatter={() => '✈️'}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.STRING}
|
||||
width={72}
|
||||
properties="timezone"
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.STRING}
|
||||
properties="country"
|
||||
title="page.team.country.travel.country"
|
||||
/>
|
||||
</DataView>
|
||||
);
|
||||
}
|
||||
|
||||
Fly.defaultProps = {
|
||||
response: undefined,
|
||||
};
|
||||
|
||||
export default Fly;
|
87
src/ts/pages/Team/components/Country/components/Travel.tsx
Normal file
|
@ -0,0 +1,87 @@
|
|||
import React from 'react';
|
||||
|
||||
import { IPagination } from 'ts/interfaces/Pagination';
|
||||
|
||||
import DataView from 'ts/components/DataView';
|
||||
import Column from 'ts/components/Table/components/Column';
|
||||
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
|
||||
import getOptions from 'ts/components/LineChart/helpers/getOptions';
|
||||
import LineChart from 'ts/components/LineChart';
|
||||
|
||||
import { getMaxByLength } from 'ts/pages/Common/helpers/getMax';
|
||||
import Fly from './Fly';
|
||||
|
||||
interface TravelProps {
|
||||
response?: IPagination<any>;
|
||||
updateSort?: Function;
|
||||
rowsForExcel?: any[];
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
function Travel({ response, updateSort, rowsForExcel, mode }: TravelProps) {
|
||||
if (!response) return null;
|
||||
|
||||
const flyChart = getOptions({ max: getMaxByLength(response, 'country'), suffix: 'page.team.country.travel.flyItem' });
|
||||
|
||||
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?.country;
|
||||
return (
|
||||
<Fly // @ts-ignore
|
||||
response={{ content }}
|
||||
mode="details"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
isFixed
|
||||
template={ColumnTypesEnum.STRING}
|
||||
properties="author"
|
||||
title="page.team.country.travel.author"
|
||||
width={200}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.SHORT_NUMBER}
|
||||
properties="country"
|
||||
formatter={(country: any) => country.length - 1}
|
||||
/>
|
||||
<Column
|
||||
isSortable
|
||||
properties="country"
|
||||
title="page.team.country.travel.fly"
|
||||
width={200}
|
||||
template={(value: number) => (
|
||||
<LineChart
|
||||
options={flyChart}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
formatter={(tasks: any) => (tasks?.length || 0)}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.STRING}
|
||||
title="page.team.country.travel.path"
|
||||
formatter={(row: any) => row.country.map((c: any) => c.country).join(' ✈️ ')}
|
||||
/>
|
||||
</DataView>
|
||||
);
|
||||
}
|
||||
|
||||
Travel.defaultProps = {
|
||||
response: undefined,
|
||||
};
|
||||
|
||||
export default Travel;
|
39
src/ts/pages/Team/components/Country/components/VPN.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import dataGripStore from 'ts/store/DataGrip';
|
||||
|
||||
import PageWrapper from 'ts/components/Page/wrapper';
|
||||
import getOptions from 'ts/components/LineChart/helpers/getOptions';
|
||||
|
||||
import PieChart from 'ts/components/PieChart';
|
||||
import { HashMap } from 'ts/interfaces/HashMap';
|
||||
|
||||
function getVpnChart(details: HashMap<number>) {
|
||||
const order = Object.entries(details)
|
||||
.sort((a: any, b: any) => b[1] - a[1])
|
||||
.map((a: any) => a[0]);
|
||||
|
||||
return getOptions({
|
||||
order,
|
||||
limit: 1,
|
||||
suffix: 'page.team.country.vpn.item',
|
||||
});
|
||||
}
|
||||
|
||||
const PieCharts = observer((): React.ReactElement | null => {
|
||||
const vpnDetails = dataGripStore.dataGrip.country.vpn;
|
||||
const vpnOptions = getVpnChart(vpnDetails);
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<PieChart
|
||||
title="page.team.country.vpn.title"
|
||||
options={vpnOptions}
|
||||
details={vpnDetails}
|
||||
/>
|
||||
</PageWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
export default PieCharts;
|
76
src/ts/pages/Team/components/Country/index.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
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 Countries from './components/Countries';
|
||||
import CountryCharts from './components/Charts';
|
||||
import TimeZoneMap from 'ts/components/TimeZoneMap';
|
||||
import PageWrapper from 'ts/components/Page/Box';
|
||||
|
||||
import Travel from './components/Travel';
|
||||
|
||||
const Country = observer(({
|
||||
mode,
|
||||
}: ICommonPageProps): React.ReactElement | null => {
|
||||
const authors = dataGripStore.dataGrip.author.statistic;
|
||||
const countryRows = dataGripStore.dataGrip.country.statistic;
|
||||
const travel = authors.filter((dot: any) => dot?.country?.length)
|
||||
.sort((a: any, b: any) => b?.country?.length - a?.country?.length);
|
||||
|
||||
if (!countryRows?.length) {
|
||||
return mode !== 'print' ? (<NothingFound/>) : null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageWrapper>
|
||||
<Title title="page.team.country.byTimezone"/>
|
||||
<TimeZoneMap authors={authors}/>
|
||||
</PageWrapper>
|
||||
<CountryCharts/>
|
||||
<Title title="page.team.country.table.title"/>
|
||||
<DataLoader
|
||||
to="response"
|
||||
loader={(pagination?: IPaginationRequest, sort?: ISort[]) => getFakeLoader({
|
||||
content: countryRows, pagination, sort, mode,
|
||||
})}
|
||||
watch={`${mode}${dataGripStore.hash}`}
|
||||
>
|
||||
<Countries
|
||||
mode={mode}
|
||||
rowsForExcel={countryRows}
|
||||
/>
|
||||
<Pagination/>
|
||||
</DataLoader>
|
||||
{travel.length ? (
|
||||
<>
|
||||
<Title title="page.team.country.travel.title"/>
|
||||
<DataLoader
|
||||
to="response"
|
||||
loader={(pagination?: IPaginationRequest, sort?: ISort[]) => getFakeLoader({
|
||||
content: travel, pagination, sort, mode,
|
||||
})}
|
||||
watch={`${mode}${dataGripStore.hash}`}
|
||||
>
|
||||
<Travel
|
||||
mode={mode}
|
||||
rowsForExcel={countryRows}
|
||||
/>
|
||||
<Pagination/>
|
||||
</DataLoader>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default Country;
|
75
src/ts/pages/Team/components/Files/Tasks.tsx
Normal file
|
@ -0,0 +1,75 @@
|
|||
import React from 'react';
|
||||
|
||||
import { IPagination } from 'ts/interfaces/Pagination';
|
||||
|
||||
import UiKitTags from 'ts/components/UiKit/components/Tags';
|
||||
import DataView from 'ts/components/DataView';
|
||||
import Column from 'ts/components/Table/components/Column';
|
||||
import { PRLink, TaskLink } from 'ts/components/ExternalLink';
|
||||
|
||||
interface TasksProps {
|
||||
response?: IPagination<any>;
|
||||
updateSort?: Function;
|
||||
rowsForExcel?: any[];
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
function Tasks({ response, updateSort, rowsForExcel, mode }: TasksProps) {
|
||||
if (!response) return null;
|
||||
|
||||
return (
|
||||
<DataView
|
||||
rowsForExcel={rowsForExcel}
|
||||
rows={response.content}
|
||||
sort={response.sort}
|
||||
updateSort={updateSort}
|
||||
mode="details"
|
||||
type={mode === 'print' ? 'cards' : undefined}
|
||||
columnCount={mode === 'print' ? 3 : undefined}
|
||||
>
|
||||
<Column
|
||||
isFixed
|
||||
isSortable
|
||||
template={(value: string) => (
|
||||
<TaskLink task={value} />
|
||||
)}
|
||||
title="page.team.tasks.task"
|
||||
properties="task"
|
||||
width={120}
|
||||
/>
|
||||
<Column
|
||||
properties="types"
|
||||
width={100}
|
||||
template={(value: any) => (
|
||||
<UiKitTags value={Object.keys(value)} />
|
||||
)}
|
||||
/>
|
||||
<Column
|
||||
properties="scope"
|
||||
width={100}
|
||||
template={(value: any) => (
|
||||
<UiKitTags value={Object.keys(value)} />
|
||||
)}
|
||||
/>
|
||||
<Column
|
||||
isSortable
|
||||
minWidth={80}
|
||||
template={(row: any) => {
|
||||
const links = row.prIds.map((id: string) => (
|
||||
<PRLink
|
||||
key={id}
|
||||
prId={id}
|
||||
/>
|
||||
));
|
||||
return (<>{links}</>);
|
||||
}}
|
||||
/>
|
||||
</DataView>
|
||||
);
|
||||
}
|
||||
|
||||
Tasks.defaultProps = {
|
||||
response: undefined,
|
||||
};
|
||||
|
||||
export default Tasks;
|
84
src/ts/pages/Team/components/PR/Anonymous.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
import React from 'react';
|
||||
|
||||
import { IPagination } from 'ts/interfaces/Pagination';
|
||||
|
||||
import DataView from 'ts/components/DataView';
|
||||
import Column from 'ts/components/Table/components/Column';
|
||||
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
|
||||
import { PRLink } from 'ts/components/ExternalLink';
|
||||
import { getDate } from 'ts/helpers/formatter';
|
||||
|
||||
interface IPRViewProps {
|
||||
response?: IPagination<any>;
|
||||
updateSort?: Function;
|
||||
rowsForExcel?: any[];
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
function Anonymous({
|
||||
response,
|
||||
updateSort,
|
||||
rowsForExcel,
|
||||
mode,
|
||||
}: IPRViewProps) {
|
||||
if (!response) return null;
|
||||
|
||||
return (
|
||||
<DataView
|
||||
rowsForExcel={rowsForExcel}
|
||||
rows={response.content}
|
||||
sort={response.sort}
|
||||
updateSort={updateSort}
|
||||
mode={mode}
|
||||
type={mode === 'print' ? 'cards' : undefined}
|
||||
columnCount={mode === 'print' ? 2 : undefined}
|
||||
fullScreenMode="anonymous"
|
||||
>
|
||||
{mode === 'print' ? (
|
||||
<Column
|
||||
isSortable
|
||||
properties="prId"
|
||||
width={140}
|
||||
/>
|
||||
) : (
|
||||
<Column
|
||||
isSortable
|
||||
template={(value: string, row: any) => {
|
||||
return (
|
||||
<PRLink prId={row?.prId} />
|
||||
);
|
||||
}}
|
||||
properties="prId"
|
||||
width={120}
|
||||
/>
|
||||
)}
|
||||
<Column
|
||||
isSortable
|
||||
template={ColumnTypesEnum.STRING}
|
||||
title="page.team.pr.date"
|
||||
properties="dateMerge"
|
||||
formatter={getDate}
|
||||
width={130}
|
||||
/>
|
||||
<Column
|
||||
isSortable
|
||||
template={ColumnTypesEnum.STRING}
|
||||
title="page.team.pr.mergeAuthor"
|
||||
properties="author"
|
||||
/>
|
||||
<Column
|
||||
isSortable
|
||||
template={ColumnTypesEnum.STRING}
|
||||
title="page.team.pr.branch"
|
||||
properties="branch"
|
||||
/>
|
||||
</DataView>
|
||||
);
|
||||
}
|
||||
|
||||
Anonymous.defaultProps = {
|
||||
mode: undefined,
|
||||
response: undefined,
|
||||
};
|
||||
|
||||
export default Anonymous;
|
167
src/ts/pages/Team/components/Refactor.tsx
Normal file
|
@ -0,0 +1,167 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { IPaginationRequest } from 'ts/interfaces/Pagination';
|
||||
import ICommit from 'ts/interfaces/Commit';
|
||||
import ISort from 'ts/interfaces/Sort';
|
||||
import { IPagination } from 'ts/interfaces/Pagination';
|
||||
import { getDate } from 'ts/helpers/formatter';
|
||||
import dataGripStore from 'ts/store/DataGrip';
|
||||
|
||||
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 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 Title from 'ts/components/Title';
|
||||
|
||||
import ICommonPageProps from 'ts/components/Page/interfaces/CommonPageProps';
|
||||
import { getMax } from 'ts/pages/Common/helpers/getMax';
|
||||
|
||||
import Tasks from './Files/Tasks';
|
||||
|
||||
interface CompaniesProps {
|
||||
response?: IPagination<any>;
|
||||
updateSort?: Function;
|
||||
rowsForExcel?: any[];
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
function View({ response, updateSort, rowsForExcel, mode }: CompaniesProps) {
|
||||
if (!response) return null;
|
||||
|
||||
const linesChart = getOptions({ max: getMax(response, 'lines'), suffix: 'page.team.refactor.lines' });
|
||||
const taskChart = getOptions({ max: getMax(response, 'totalTasks'), suffix: 'page.team.refactor.tasks' });
|
||||
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}
|
||||
properties="tasks"
|
||||
formatter={(row: any) => {
|
||||
const content = Array.from(row?.tasks)
|
||||
.reverse()
|
||||
.map((taskId: any) => dataGripStore.dataGrip.tasks.statisticByName.get(taskId))
|
||||
.filter(v => v);
|
||||
return (
|
||||
<Tasks // @ts-ignore
|
||||
response={{ content }}
|
||||
mode="details"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
isFixed
|
||||
template={ColumnTypesEnum.STRING}
|
||||
properties="pathString"
|
||||
title="page.team.refactor.path"
|
||||
width={400}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.STRING}
|
||||
properties="firstCommit"
|
||||
title="page.team.refactor.firstCommit"
|
||||
width={130}
|
||||
formatter={(commit: ICommit) => getDate(commit.timestamp)}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.SHORT_NUMBER}
|
||||
properties="lines"
|
||||
width={90}
|
||||
/>
|
||||
<Column
|
||||
isSortable="lines"
|
||||
title="page.team.refactor.totalLines"
|
||||
properties="lines"
|
||||
minWidth={150}
|
||||
template={(value: number) => (
|
||||
<LineChart
|
||||
options={linesChart}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.SHORT_NUMBER}
|
||||
properties="totalDays"
|
||||
width={90}
|
||||
/>
|
||||
<Column
|
||||
isSortable="totalDays"
|
||||
title="page.team.refactor.totalDays"
|
||||
properties="totalDays"
|
||||
minWidth={150}
|
||||
template={(value: number) => (
|
||||
<LineChart
|
||||
options={daysChart}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.SHORT_NUMBER}
|
||||
properties="totalTasks"
|
||||
width={90}
|
||||
/>
|
||||
<Column
|
||||
isSortable="totalTasks"
|
||||
title="page.team.refactor.totalTasks"
|
||||
properties="totalTasks"
|
||||
minWidth={150}
|
||||
template={(value: number) => (
|
||||
<LineChart
|
||||
options={taskChart}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</DataView>
|
||||
);
|
||||
}
|
||||
|
||||
View.defaultProps = {
|
||||
response: undefined,
|
||||
};
|
||||
|
||||
const Refactor = observer(({
|
||||
mode,
|
||||
}: ICommonPageProps): React.ReactElement | null => {
|
||||
const content = dataGripStore.fileGrip.refactor.files;
|
||||
|
||||
if (!content?.length) {
|
||||
return <NothingFound />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title title="page.team.refactor.title"/>
|
||||
<DataLoader
|
||||
to="response"
|
||||
loader={(pagination?: IPaginationRequest, sort?: ISort[]) => getFakeLoader({
|
||||
content, pagination, sort, mode,
|
||||
})}
|
||||
>
|
||||
<View />
|
||||
<Pagination />
|
||||
</DataLoader>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default Refactor;
|
||||
|
124
src/ts/pages/Team/components/Release/View.tsx
Normal file
|
@ -0,0 +1,124 @@
|
|||
import React from 'react';
|
||||
|
||||
import { IPagination } from 'ts/interfaces/Pagination';
|
||||
import dataGripStore from 'ts/store/DataGrip';
|
||||
|
||||
import DataView from 'ts/components/DataView';
|
||||
import Column from 'ts/components/Table/components/Column';
|
||||
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
|
||||
import getOptions from 'ts/components/LineChart/helpers/getOptions';
|
||||
import LineChart from 'ts/components/LineChart';
|
||||
import { getMax } from 'ts/pages/Common/helpers/getMax';
|
||||
import { getDate } from 'ts/helpers/formatter';
|
||||
|
||||
import AllPR from '../PR/All';
|
||||
|
||||
interface ViewProps {
|
||||
response?: IPagination<any>;
|
||||
updateSort?: Function;
|
||||
rowsForExcel?: any[];
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
function View({ response, updateSort, rowsForExcel, mode }: ViewProps) {
|
||||
if (!response) return null;
|
||||
|
||||
const delay = getMax(response, 'delayInDays');
|
||||
const waiting = getMax(response, 'waitingInDays');
|
||||
const max = Math.max(delay, waiting);
|
||||
const delayChart = getOptions({ max, suffix: 'page.team.release.chart' });
|
||||
|
||||
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.prIds.map((prId: string) => (
|
||||
dataGripStore?.dataGrip?.pr?.pr?.get(prId)
|
||||
)).filter((v: any) => v);
|
||||
return (
|
||||
<AllPR // @ts-ignore
|
||||
response={{ content }}
|
||||
mode="details"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
isFixed
|
||||
template={ColumnTypesEnum.STRING}
|
||||
title="page.team.release.title"
|
||||
properties="title"
|
||||
width={200}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.STRING}
|
||||
title="page.team.release.from"
|
||||
width={150}
|
||||
properties="from"
|
||||
formatter={getDate}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.STRING}
|
||||
title="page.team.release.to"
|
||||
width={150}
|
||||
properties="to"
|
||||
formatter={getDate}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.SHORT_NUMBER}
|
||||
title="page.team.release.prLength"
|
||||
properties="prLength"
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.SHORT_NUMBER}
|
||||
properties="delayInDays"
|
||||
/>
|
||||
<Column
|
||||
isSortable
|
||||
title="page.team.release.delay"
|
||||
properties="delayInDays"
|
||||
width={170}
|
||||
minWidth={170}
|
||||
template={(value: number) => (
|
||||
<LineChart
|
||||
options={delayChart}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.SHORT_NUMBER}
|
||||
properties="waitingInDays"
|
||||
/>
|
||||
<Column
|
||||
isSortable
|
||||
title="page.team.release.waiting"
|
||||
properties="waitingInDays"
|
||||
width={170}
|
||||
minWidth={170}
|
||||
template={(value: number) => (
|
||||
<LineChart
|
||||
options={delayChart}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</DataView>
|
||||
);
|
||||
}
|
||||
|
||||
View.defaultProps = {
|
||||
response: undefined,
|
||||
};
|
||||
|
||||
export default View;
|
58
src/ts/pages/Team/components/Release/index.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
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 UiKitButton from 'ts/components/UiKit/components/Button';
|
||||
import NothingFound from 'ts/components/NothingFound';
|
||||
import Title from 'ts/components/Title';
|
||||
|
||||
import View from './View';
|
||||
import saveChangeLog from './saveChangeLog';
|
||||
|
||||
import style from '../../styles/release.module.scss';
|
||||
|
||||
const Release = observer(({
|
||||
mode,
|
||||
}: ICommonPageProps): React.ReactElement | null => {
|
||||
const { t } = useTranslation();
|
||||
const rows = dataGripStore.dataGrip.release.statistic;
|
||||
if (rows?.length < 2) return mode !== 'print' ? (<NothingFound />) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{mode === 'print' ? (
|
||||
<Title title="sidebar.team.extension"/>
|
||||
) : (
|
||||
<UiKitButton
|
||||
mode={['slim']}
|
||||
className={style.team_release_download}
|
||||
onClick={saveChangeLog}
|
||||
>
|
||||
{t('page.team.release.download')}
|
||||
</UiKitButton>
|
||||
)}
|
||||
<DataLoader
|
||||
to="response"
|
||||
loader={(pagination?: IPaginationRequest) => getFakeLoader({
|
||||
content: rows, pagination, mode,
|
||||
})}
|
||||
watch={`${mode}${dataGripStore.hash}`}
|
||||
>
|
||||
<View
|
||||
mode={mode}
|
||||
rowsForExcel={rows}
|
||||
/>
|
||||
<Pagination />
|
||||
</DataLoader>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default Release;
|
61
src/ts/pages/Team/components/Release/saveChangeLog.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { downloadFile } from 'ts/helpers/File';
|
||||
import { getDateForExcel } from 'ts/helpers/formatter';
|
||||
import dataGripStore from 'ts/store/DataGrip';
|
||||
import userSettings from 'ts/store/UserSettings';
|
||||
|
||||
function groupByType(prs: any[]) {
|
||||
return prs.reduce((acc: any, item: any) => {
|
||||
const type = item.type || '';
|
||||
if (!acc[type]) {
|
||||
acc[type] = [];
|
||||
}
|
||||
acc[type].push(item);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function getTaskDescription(pr: any) {
|
||||
const message = pr.message.substring(pr.message.lastIndexOf(':') + 2)
|
||||
.replace(pr.task, '')
|
||||
.trim();
|
||||
const prefix = userSettings?.settings?.linksPrefix?.task || '/';
|
||||
const formattedTask = pr.task?.[0] === '#'
|
||||
? pr.task.replace('#', '')
|
||||
: pr.task;
|
||||
return `- [${formattedTask}](${prefix}${formattedTask}) ${message}`;
|
||||
}
|
||||
|
||||
function getReleaseDescription(prs: any) {
|
||||
const types = groupByType(prs);
|
||||
return Object.keys(types)
|
||||
.sort()
|
||||
.map((type: string) => {
|
||||
const tasks = types[type].map(getTaskDescription).join('\n');
|
||||
if (!type) return `\n${tasks}`;
|
||||
return `\n### ${type}\n${tasks}`;
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
function getChangeLogString() {
|
||||
const rows = dataGripStore.dataGrip.release.statistic;
|
||||
const list = rows.map((release: any) => {
|
||||
const date = getDateForExcel(release.lastCommit.date);
|
||||
const prs = release.pr
|
||||
.map((prId: string) => dataGripStore.dataGrip.pr.pr.get(prId))
|
||||
.filter((v: any) => v);
|
||||
const description = getReleaseDescription(prs);
|
||||
return `
|
||||
## [${release.title}] - ${date}
|
||||
${description}`;
|
||||
}).join('\n');
|
||||
|
||||
return `# Change Log
|
||||
${list}`;
|
||||
}
|
||||
|
||||
export default function saveChangeLog() {
|
||||
const content = getChangeLogString();
|
||||
const type = 'text/csv;charset=windows-utf-8;'; // utf-8;';
|
||||
const file = new Blob([content], { type });
|
||||
downloadFile(file, 'CHANGELOG.md');
|
||||
}
|
60
src/ts/pages/Team/components/Tasks/Filters.tsx
Normal 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;
|
69
src/ts/pages/Team/components/Tasks/Release.tsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
import React from 'react';
|
||||
|
||||
import { IPagination } from 'ts/interfaces/Pagination';
|
||||
import IHashMap from 'ts/interfaces/HashMap';
|
||||
|
||||
import DataView from 'ts/components/DataView';
|
||||
import Column from 'ts/components/Table/components/Column';
|
||||
import { PRLink } from 'ts/components/ExternalLink';
|
||||
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
|
||||
import { getDate } from 'ts/helpers/formatter';
|
||||
|
||||
interface ReleaseProps {
|
||||
isCorrectPR?: IHashMap<boolean>;
|
||||
response?: IPagination<any>;
|
||||
updateSort?: Function;
|
||||
rowsForExcel?: any[];
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
function Release({ isCorrectPR, response, updateSort, rowsForExcel, mode }: ReleaseProps) {
|
||||
if (!response) return null;
|
||||
|
||||
return (
|
||||
<DataView
|
||||
rowsForExcel={rowsForExcel}
|
||||
rows={response.content}
|
||||
sort={response.sort}
|
||||
updateSort={updateSort}
|
||||
mode="details"
|
||||
type={mode === 'print' ? 'cards' : undefined}
|
||||
columnCount={mode === 'print' ? 3 : undefined}
|
||||
>
|
||||
<Column
|
||||
isFixed
|
||||
template={ColumnTypesEnum.STRING}
|
||||
title="page.team.release.title"
|
||||
properties="title"
|
||||
width={120}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.STRING}
|
||||
title="page.team.release.to"
|
||||
width={198}
|
||||
formatter={(row: any) => getDate(row.to || row.from)}
|
||||
/>
|
||||
<Column
|
||||
isSortable
|
||||
minWidth={80}
|
||||
template={(row: any) => {
|
||||
const links = row?.prIds
|
||||
?.filter((id: string) => isCorrectPR?.[id])
|
||||
?.map((id: string) => (
|
||||
<PRLink
|
||||
key={id}
|
||||
prId={id}
|
||||
/>
|
||||
));
|
||||
return (<>{links}</>);
|
||||
}}
|
||||
/>
|
||||
</DataView>
|
||||
);
|
||||
}
|
||||
|
||||
Release.defaultProps = {
|
||||
response: undefined,
|
||||
};
|
||||
|
||||
export default Release;
|
126
src/ts/pages/Team/components/Tasks/View.tsx
Normal file
|
@ -0,0 +1,126 @@
|
|||
import React from 'react';
|
||||
|
||||
import { IPagination } from 'ts/interfaces/Pagination';
|
||||
import dataGripStore from 'ts/store/DataGrip';
|
||||
|
||||
import DataView from 'ts/components/DataView';
|
||||
import Column from 'ts/components/Table/components/Column';
|
||||
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
|
||||
import UiKitTags from 'ts/components/UiKit/components/Tags';
|
||||
import { PRLink, TaskLink } from 'ts/components/ExternalLink';
|
||||
|
||||
import { getDate } from 'ts/helpers/formatter';
|
||||
|
||||
import Release from './Release';
|
||||
|
||||
interface ViewProps {
|
||||
response?: IPagination<any>;
|
||||
updateSort?: Function;
|
||||
rowsForExcel?: any[];
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
export function View({ response, updateSort, rowsForExcel, mode }: ViewProps) {
|
||||
if (!response) return null;
|
||||
|
||||
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}
|
||||
properties="releaseIds"
|
||||
formatter={(row: any) => {
|
||||
const content = Array.from(row?.releaseIds)
|
||||
.reverse()
|
||||
.map((id: any) => dataGripStore.dataGrip.release.release[id])
|
||||
.filter(v => v);
|
||||
const isCorrectPr = Object.fromEntries(
|
||||
row.prIds.map((id: string) => [id, true]),
|
||||
);
|
||||
return (
|
||||
<Release // @ts-ignore
|
||||
response={{ content }}
|
||||
isCorrectPR={isCorrectPr}
|
||||
mode="details"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
isFixed
|
||||
isSortable
|
||||
template={(value: string) => (
|
||||
<TaskLink task={value} />
|
||||
)}
|
||||
title="page.team.tasks.task"
|
||||
properties="task"
|
||||
width={120}
|
||||
/>
|
||||
<Column
|
||||
properties="types"
|
||||
width={100}
|
||||
template={(value: any) => (
|
||||
<UiKitTags value={Object.keys(value)} />
|
||||
)}
|
||||
/>
|
||||
<Column
|
||||
properties="scope"
|
||||
width={100}
|
||||
template={(value: any) => (
|
||||
<UiKitTags value={Object.keys(value)} />
|
||||
)}
|
||||
/>
|
||||
<Column
|
||||
isSortable
|
||||
width={80}
|
||||
template={(row: any) => {
|
||||
const links = row.prIds.map((id: string) => (
|
||||
<PRLink
|
||||
key={id}
|
||||
prId={id}
|
||||
/>
|
||||
));
|
||||
return (<>{links}</>);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.STRING}
|
||||
properties="comments"
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.STRING}
|
||||
title="page.team.tasks.author"
|
||||
properties="author"
|
||||
width={170}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.STRING}
|
||||
title="page.team.tasks.from"
|
||||
properties="from"
|
||||
width={150}
|
||||
formatter={getDate}
|
||||
/>
|
||||
<Column
|
||||
template={ColumnTypesEnum.STRING}
|
||||
title="page.team.tasks.to"
|
||||
properties="to"
|
||||
width={150}
|
||||
formatter={getDate}
|
||||
/>
|
||||
</DataView>
|
||||
);
|
||||
}
|
||||
|
||||
View.defaultProps = {
|
||||
response: undefined,
|
||||
};
|
||||
|
||||
export default View;
|
53
src/ts/pages/Team/components/Tasks/index.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
import React, { useState } 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 PageWrapper from 'ts/components/Page/wrapper';
|
||||
|
||||
import Filters from './Filters';
|
||||
import View from './View';
|
||||
|
||||
const Tasks = observer(({
|
||||
mode,
|
||||
}: ICommonPageProps): React.ReactElement | null => {
|
||||
const rows = dataGripStore.dataGrip.tasks.statistic;
|
||||
const [filters, setFilters] = useState<any>({ user: 0, company: 0 });
|
||||
|
||||
if (!rows?.length) return mode !== 'print' ? (<NothingFound />) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title title="common.filters" />
|
||||
<PageWrapper>
|
||||
<Filters
|
||||
filters={filters}
|
||||
onChange={setFilters}
|
||||
/>
|
||||
</PageWrapper>
|
||||
<DataLoader
|
||||
to="response"
|
||||
loader={(pagination?: IPaginationRequest, sort?: ISort[]) => getFakeLoader({
|
||||
content: rows, pagination, sort, mode,
|
||||
})}
|
||||
watch={`${mode}${dataGripStore.hash}`}
|
||||
>
|
||||
<View
|
||||
mode={mode}
|
||||
rowsForExcel={rows}
|
||||
/>
|
||||
<Pagination />
|
||||
</DataLoader>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default Tasks;
|
6
src/ts/pages/Team/styles/release.module.scss
Normal file
|
@ -0,0 +1,6 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.team_release_download {
|
||||
position: relative;
|
||||
top: -12px;
|
||||
}
|
|
@ -134,8 +134,14 @@ export default `
|
|||
§ page.team.country.pieByTimezone.title: По времени
|
||||
§ page.team.country.chart.item: сотрудников
|
||||
§ page.team.country.table.title: Список сотрудников
|
||||
§ page.team.country.table.country: Местоположение
|
||||
§ page.team.country.table.country: Локация
|
||||
§ page.team.country.table.employments: Сотрудники
|
||||
§ page.team.country.travel.title: Командировки (или VPN, или rebase)
|
||||
§ page.team.country.travel.author: Сотрудник
|
||||
§ page.team.country.travel.fly: Количество перелётов
|
||||
§ page.team.country.travel.path: Список локаций
|
||||
§ page.team.country.travel.date: Дата перлёта
|
||||
§ page.team.country.travel.country: Локация
|
||||
§ page.team.refactor.title: Кандидаты на рефакторинг
|
||||
§ page.team.refactor.lines: строк
|
||||
§ page.team.refactor.tasks: задач
|
||||
|
|