This commit is contained in:
bakhirev 2024-08-13 00:01:58 +03:00
parent 2e29d4ede2
commit 5d890b4848
15 changed files with 202 additions and 15 deletions

View file

@ -2,7 +2,7 @@ import React from 'react';
import Banner from 'ts/components/Banner'; import Banner from 'ts/components/Banner';
import style from './index.module.scss'; import style from '../index.module.scss';
interface ICardWithBannerProps { interface ICardWithBannerProps {
long?: boolean; long?: boolean;

View file

@ -0,0 +1,36 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import style from '../index.module.scss';
export interface IScoringProps {
title?: string;
value?: number;
total?: number;
}
function Scoring({
title,
value,
total,
}: IScoringProps): React.ReactElement | null {
const { t } = useTranslation();
if (!value) return null;
return (
<div
title={t(title || 'page.person.scoring.toolbar')}
className={style.card_with_icon_scoring}
>
{`${value} / ${total || value}`}
</div>
);
}
Scoring.defaultProps = {
title: undefined,
value: undefined,
total: undefined,
};
export default Scoring;

View file

@ -2,6 +2,8 @@
.card_with_icon, .card_with_icon,
.card_with_icon_long { .card_with_icon_long {
position: relative;
display: inline-block; display: inline-block;
width: calc(50% - 12px); width: calc(50% - 12px);
min-height: 270px; min-height: 270px;
@ -11,8 +13,9 @@
vertical-align: top; vertical-align: top;
text-decoration: none; text-decoration: none;
box-sizing: border-box; box-sizing: border-box;
text-align: center;
border-radius: 8px; border-radius: var(--border-radius-m);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
background-color: var(--color-white); background-color: var(--color-white);
@ -40,7 +43,8 @@
} }
&_title, &_title,
&_description { &_description,
&_scoring {
font-weight: 100; font-weight: 100;
display: block; display: block;
margin: 0 auto; margin: 0 auto;
@ -57,7 +61,8 @@
margin: 0 0 4px 0; margin: 0 0 4px 0;
} }
&_description { &_description,
&_scoring {
font-size: var(--font-xs); font-size: var(--font-xs);
line-height: 16px; line-height: 16px;
color: var(--color-grey); color: var(--color-grey);
@ -68,6 +73,22 @@
padding: 0; padding: 0;
line-height: 270px; line-height: 270px;
} }
&_scoring {
position: absolute;
bottom: -11px;
left: 30%;
right: 30%;
display: inline-block;
padding: var(--space-xxxs) var(--space-xs);
white-space: nowrap;
text-align: center;
border-radius: var(--border-radius-s);
border: 1px solid var(--color-border);
background-color: var(--color-white);
}
} }
.card_with_icon_long { .card_with_icon_long {

View file

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { IScoringProps } from './components/Scoring';
import Scoring from './components/Scoring';
import style from './index.module.scss'; import style from './index.module.scss';
interface ICardWithIconProps { interface ICardWithIconProps {
@ -11,6 +13,7 @@ interface ICardWithIconProps {
color?: string; color?: string;
icon?: string; icon?: string;
long?: boolean; long?: boolean;
scoring?: IScoringProps;
} }
function CardWithIcon({ function CardWithIcon({
@ -21,8 +24,10 @@ function CardWithIcon({
color, color,
icon, icon,
long = false, long = false,
scoring,
}: ICardWithIconProps): React.ReactElement | null { }: ICardWithIconProps): React.ReactElement | null {
const { t } = useTranslation(); const { t } = useTranslation();
if (!value && value !== 0) return null; if (!value && value !== 0) return null;
return ( return (
@ -48,6 +53,11 @@ function CardWithIcon({
<figcaption className={style.card_with_icon_description}> <figcaption className={style.card_with_icon_description}>
{t(description || '')} {t(description || '')}
</figcaption> </figcaption>
<Scoring
title={scoring?.title}
value={scoring?.value}
total={scoring?.total}
/>
</figure> </figure>
); );
} }
@ -58,6 +68,7 @@ CardWithIcon.defaultProps = {
color: undefined, color: undefined,
icon: undefined, icon: undefined,
long: false, long: false,
scoring: undefined,
}; };
export default CardWithIcon; export default CardWithIcon;

View file

@ -1,12 +1,13 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import UiKitSelectOption from './Option'; import UiKitSelectOption from './Option';
import IOption from '../interfaces/Option';
import style from '../styles/index.module.scss'; import style from '../styles/index.module.scss';
interface UiKitSelectListProps { interface UiKitSelectListProps {
value: any; value: any;
options: any; options: IOption[];
search?: string; search?: string;
keyCode?: string; keyCode?: string;
setKeyCode: Function; setKeyCode: Function;
@ -26,8 +27,10 @@ function UiKitSelectList({
const [selectedIndex, setSelectedIndex] = useState<number>(-1); const [selectedIndex, setSelectedIndex] = useState<number>(-1);
console.log(value); console.log(value);
const searchResult = options const searchText = search ? search.toLowerCase() : '';
?.filter((option: any) => option.title.indexOf(search) !== -1); const searchResult = searchText
? options?.filter((option: any) => option?._textForSearch?.indexOf(searchText) !== -1)
: options;
useEffect(() => { useEffect(() => {
if (!keyCode) return; if (!keyCode) return;

View file

@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import IOption from '../interfaces/Option';
import style from '../styles/index.module.scss'; import style from '../styles/index.module.scss';
interface UiKitSelectValueProps { interface UiKitSelectValueProps {
value: any; value: any;
options: any; options: IOption[];
className?: string; className?: string;
onClick: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void; onClick: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
} }

View file

@ -1,3 +1,5 @@
import IOption from '../interfaces/Option';
function getStringFromObject(value: any) { function getStringFromObject(value: any) {
return value?.title return value?.title
|| value?.name || value?.name
@ -23,8 +25,9 @@ function getValue(
formatter: (a: any, i?: number) => string, formatter: (a: any, i?: number) => string,
) { ) {
const type = typeof value; const type = typeof value;
if (type === 'boolean') return value ? 'yes' : 'no'; if (type === 'boolean') return value ? 'true' : 'false';
if (type === 'number' || type === 'string') return value; if (type === 'number') return `${value}`;
if (type === 'string') return value;
if (!value) return ''; if (!value) return '';
return Array.isArray(value) return Array.isArray(value)
@ -40,10 +43,12 @@ export function getId(value: any, index: number) {
return getValue(value, (v: any) => getIdFromObject(v, index)); return getValue(value, (v: any) => getIdFromObject(v, index));
} }
export function getOption(value: any, index: number) { export function getOption(value: any, index: number): IOption {
const title = getTitle(value);
return { return {
id: getId(value, index), id: getId(value, index),
title: getTitle(value), title,
_textForSearch: title.toLowerCase(),
source: value, source: value,
}; };
} }

View file

@ -1,5 +1,6 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import IOption from './interfaces/Option';
import UiKitSelectValue from './components/Value'; import UiKitSelectValue from './components/Value';
import UiKitSelectSearch from './components/Search'; import UiKitSelectSearch from './components/Search';
import UiKitSelectList from './components/List'; import UiKitSelectList from './components/List';
@ -25,7 +26,7 @@ function UiKitSelect({
const [search, setSearch] = useState<string>(''); const [search, setSearch] = useState<string>('');
const [keyCode, setKeyCode] = useState<string>(''); const [keyCode, setKeyCode] = useState<string>('');
const formattedOptions = useMemo(() => options?.map(getOption) || [], [options]); const formattedOptions: IOption[] = useMemo(() => options?.map(getOption) || [], [options]);
const formattedValue = useMemo(() => { const formattedValue = useMemo(() => {
const selectedOption = options.find((option: any) => option.id === value); const selectedOption = options.find((option: any) => option.id === value);
return getTitle(selectedOption) || getTitle(value); return getTitle(selectedOption) || getTitle(value);

View file

@ -0,0 +1,6 @@
export default interface IOption {
id: string | number | boolean;
title: string;
_textForSearch: string;
source: any;
}

View file

@ -0,0 +1,61 @@
import IHashMap from 'ts/interfaces/HashMap';
const PROPERTIES = [
{ property: 'daysWorked', sort: 1 },
{ property: 'daysLosses', sort: -1 },
{ property: 'commits', sort: 1 },
{ property: 'daysForTask', sort: -1 },
{ property: 'tasks', sort: 1 },
{ property: 'moneyAll', sort: 1 },
{ property: 'moneyWorked', sort: 1 },
{ property: 'moneyLosses', sort: -1 },
{ property: 'weekendPayment', sort: -1 },
];
export default class DataGripByScoring {
total: IHashMap<number> = {};
statisticByName: IHashMap<any> = {};
constructor() {
this.clear();
}
clear() {
this.total = {};
this.statisticByName = {};
}
updateTotalInfo(dataGripByAuthor: any) {
const list = [...dataGripByAuthor.statistic];
list.forEach((user: any) => {
this.statisticByName[user.author] = {};
});
PROPERTIES.forEach((config: any) => {
const values = list.map((user: any) => {
const value = user[config.property] || 0;
return Array.isArray(value)
? value?.length
: value;
});
const uniqValues = Array.from(new Set(values));
const places = uniqValues
.sort((a:number, b:number) => (b - a) * config.sort)
.map((v, i) => [v, i + 1]);
const refValuePlace = Object.fromEntries(places);
list.forEach((user: any) => {
const userValue = user[config.property];
const userFormattedValue = Array.isArray(userValue)
? userValue?.length
: userValue;
this.statisticByName[user.author][config.property] = refValuePlace[userFormattedValue];
});
this.total[config.property] = uniqValues.length;
});
}
}

View file

@ -13,6 +13,7 @@ import DataGripByGet from './components/get';
import DataGripByPR from './components/pr'; import DataGripByPR from './components/pr';
import DataGripByTasks from './components/tasks'; import DataGripByTasks from './components/tasks';
import DataGripByRelease from './components/release'; import DataGripByRelease from './components/release';
import DataGripByScoring from './components/scoring';
class DataGrip { class DataGrip {
firstLastCommit: any = new MinMaxCounter(); firstLastCommit: any = new MinMaxCounter();
@ -39,6 +40,8 @@ class DataGrip {
release: any = new DataGripByRelease(); release: any = new DataGripByRelease();
scoring: any = new DataGripByScoring();
clear() { clear() {
this.firstLastCommit.clear(); this.firstLastCommit.clear();
this.author.clear(); this.author.clear();
@ -52,6 +55,7 @@ class DataGrip {
this.pr.clear(); this.pr.clear();
this.tasks.clear(); this.tasks.clear();
this.release.clear(); this.release.clear();
this.scoring.clear();
} }
addCommit(commit: ICommit | ISystemCommit) { addCommit(commit: ICommit | ISystemCommit) {
@ -81,6 +85,7 @@ class DataGrip {
this.pr.updateTotalInfo(this.author); this.pr.updateTotalInfo(this.author);
this.tasks.updateTotalInfo(this.pr); this.tasks.updateTotalInfo(this.pr);
this.release.updateTotalInfo(); this.release.updateTotalInfo();
this.scoring.updateTotalInfo(this.author);
} }
} }

View file

@ -15,6 +15,8 @@ import IPersonCommonProps from '../interfaces/CommonProps';
const Money = observer(({ user }: IPersonCommonProps): React.ReactElement => { const Money = observer(({ user }: IPersonCommonProps): React.ReactElement => {
const statistic = user; const statistic = user;
const scoringTotal = dataGripStore.dataGrip.scoring.total;
const scoring = dataGripStore.dataGrip.scoring.statisticByName[user.author];
const byTimestamp = dataGripStore.dataGrip.timestamp.statisticByAuthor[statistic.author]; const byTimestamp = dataGripStore.dataGrip.timestamp.statisticByAuthor[statistic.author];
const taskNumber = statistic.tasks.length; const taskNumber = statistic.tasks.length;
@ -36,24 +38,40 @@ const Money = observer(({ user }: IPersonCommonProps): React.ReactElement => {
icon="./assets/cards/money_total.png" icon="./assets/cards/money_total.png"
title="page.person.money.moneyAll.title" title="page.person.money.moneyAll.title"
description="page.person.money.moneyAll.description" description="page.person.money.moneyAll.description"
scoring={{
value: scoring.moneyAll,
total: scoringTotal.moneyAll,
}}
/> />
<CardWithIcon <CardWithIcon
value={getShortMoney(statistic.moneyWorked)} value={getShortMoney(statistic.moneyWorked)}
icon="./assets/cards/money_work.png" icon="./assets/cards/money_work.png"
title="page.person.money.moneyWorked.title" title="page.person.money.moneyWorked.title"
description="page.person.money.moneyWorked.description" description="page.person.money.moneyWorked.description"
scoring={{
value: scoring.moneyWorked,
total: scoringTotal.moneyWorked,
}}
/> />
<CardWithIcon <CardWithIcon
value={getShortMoney(statistic.moneyLosses)} value={getShortMoney(statistic.moneyLosses)}
icon="./assets/cards/money_lazy.png" icon="./assets/cards/money_lazy.png"
title="page.person.money.moneyLosses.title" title="page.person.money.moneyLosses.title"
description="page.person.money.moneyLosses.description" description="page.person.money.moneyLosses.description"
scoring={{
value: scoring.moneyLosses,
total: scoringTotal.moneyLosses,
}}
/> />
<CardWithIcon <CardWithIcon
value={getShortMoney(byTimestamp.weekendPayment)} value={getShortMoney(byTimestamp.weekendPayment)}
icon="./assets/cards/money_holidays.png" icon="./assets/cards/money_holidays.png"
title="page.team.total.weekendPayment.title" title="page.team.total.weekendPayment.title"
description="page.team.total.weekendPayment.description" description="page.team.total.weekendPayment.description"
scoring={{
value: scoring.weekendPayment,
total: scoringTotal.weekendPayment,
}}
/> />
</div> </div>
</PageColumn> </PageColumn>

View file

@ -4,7 +4,7 @@ import { observer } from 'mobx-react-lite';
import { getShortNumber } from 'ts/helpers/formatter'; import { getShortNumber } from 'ts/helpers/formatter';
import CardWithIcon from 'ts/components/CardWithIcon'; import CardWithIcon from 'ts/components/CardWithIcon';
import CardWithBanner from 'ts/components/CardWithIcon/Banner'; import CardWithBanner from 'ts/components/CardWithIcon/components/Banner';
import NothingFound from 'ts/components/NothingFound'; import NothingFound from 'ts/components/NothingFound';
import IsStaff from 'ts/components/NothingFound/components/IsStaff'; import IsStaff from 'ts/components/NothingFound/components/IsStaff';
import PageWrapper from 'ts/components/Page/wrapper'; import PageWrapper from 'ts/components/Page/wrapper';

View file

@ -6,7 +6,7 @@ import achievementByAuthor from 'ts/helpers/achievement/byCompetition';
import ACHIEVEMENT_TYPE from 'ts/helpers/achievement/constants/type'; import ACHIEVEMENT_TYPE from 'ts/helpers/achievement/constants/type';
import CardWithIcon from 'ts/components/CardWithIcon'; import CardWithIcon from 'ts/components/CardWithIcon';
import CardWithBanner from 'ts/components/CardWithIcon/Banner'; import CardWithBanner from 'ts/components/CardWithIcon/components/Banner';
import Achievements from 'ts/components/Achievement'; import Achievements from 'ts/components/Achievement';
import Description from 'ts/components/Description'; import Description from 'ts/components/Description';
import PageWrapper from 'ts/components/Page/wrapper'; import PageWrapper from 'ts/components/Page/wrapper';
@ -37,6 +37,8 @@ function AchievementBlock({ title, achievements }: IAchievementBlockProps) {
const Total = observer(({ user }: IPersonCommonProps): React.ReactElement => { const Total = observer(({ user }: IPersonCommonProps): React.ReactElement => {
const { t } = useTranslation(); const { t } = useTranslation();
const statistic = user; const statistic = user;
const scoringTotal = dataGripStore.dataGrip.scoring.total;
const scoring = dataGripStore.dataGrip.scoring.statisticByName[user.author];
const commitsWithGet = dataGripStore.dataGrip.get.getsByAuthor[user.author]; const commitsWithGet = dataGripStore.dataGrip.get.getsByAuthor[user.author];
const taskNumber = statistic.tasks.length; const taskNumber = statistic.tasks.length;
const achievements = achievementByAuthor.authors[statistic.author]; const achievements = achievementByAuthor.authors[statistic.author];
@ -51,24 +53,40 @@ const Total = observer(({ user }: IPersonCommonProps): React.ReactElement => {
icon="./assets/cards/work_days.png" icon="./assets/cards/work_days.png"
title="page.person.total.daysWorked.title" title="page.person.total.daysWorked.title"
description="page.person.total.daysWorked.description" description="page.person.total.daysWorked.description"
scoring={{
value: scoring.daysWorked,
total: scoringTotal.daysWorked,
}}
/> />
<CardWithIcon <CardWithIcon
value={taskNumber ? taskNumber : null} value={taskNumber ? taskNumber : null}
icon="./assets/cards/tasks.png" icon="./assets/cards/tasks.png"
title="page.person.total.tasks.title" title="page.person.total.tasks.title"
description="page.person.total.tasks.description" description="page.person.total.tasks.description"
scoring={{
value: scoring.tasks,
total: scoringTotal.tasks,
}}
/> />
<CardWithIcon <CardWithIcon
value={statistic.daysLosses} value={statistic.daysLosses}
icon="./assets/cards/lazy.png" icon="./assets/cards/lazy.png"
title="page.team.total.daysLosses.title" title="page.team.total.daysLosses.title"
description="page.team.total.daysLosses.description" description="page.team.total.daysLosses.description"
scoring={{
value: scoring.daysLosses,
total: scoringTotal.daysLosses,
}}
/> />
<CardWithIcon <CardWithIcon
value={statistic.commits} value={statistic.commits}
icon="./assets/cards/commits.png" icon="./assets/cards/commits.png"
title="page.team.total.commits.title" title="page.team.total.commits.title"
description="page.team.total.commits.description" description="page.team.total.commits.description"
scoring={{
value: scoring.commits,
total: scoringTotal.commits,
}}
/> />
<CardWithBanner long /> <CardWithBanner long />
</div> </div>

View file

@ -181,6 +181,7 @@ export default `
§ page.person.total.daysWorked.description: Учтены только дни, в которые делались коммиты § page.person.total.daysWorked.description: Учтены только дни, в которые делались коммиты
§ page.person.total.tasks.title: задач § page.person.total.tasks.title: задач
§ page.person.total.tasks.description: Если коммиты правильно подписаны § page.person.total.tasks.description: Если коммиты правильно подписаны
§ page.person.scoring.toolbar: Позиция по этой метрике, относительно других сотрудников
§ page.person.character.title: Персонаж § page.person.character.title: Персонаж
§ page.person.achievement.title: Достижения § page.person.achievement.title: Достижения
§ page.person.achievement.positive: Позитивные § page.person.achievement.positive: Позитивные