diff --git a/src/ts/components/CardWithIcon/Banner.tsx b/src/ts/components/CardWithIcon/components/Banner.tsx similarity index 92% rename from src/ts/components/CardWithIcon/Banner.tsx rename to src/ts/components/CardWithIcon/components/Banner.tsx index d125375..6646c88 100644 --- a/src/ts/components/CardWithIcon/Banner.tsx +++ b/src/ts/components/CardWithIcon/components/Banner.tsx @@ -2,7 +2,7 @@ import React from 'react'; import Banner from 'ts/components/Banner'; -import style from './index.module.scss'; +import style from '../index.module.scss'; interface ICardWithBannerProps { long?: boolean; diff --git a/src/ts/components/CardWithIcon/components/Scoring.tsx b/src/ts/components/CardWithIcon/components/Scoring.tsx new file mode 100644 index 0000000..bb0fecd --- /dev/null +++ b/src/ts/components/CardWithIcon/components/Scoring.tsx @@ -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 ( +
+ {`${value} / ${total || value}`} +
+ ); +} + +Scoring.defaultProps = { + title: undefined, + value: undefined, + total: undefined, +}; + +export default Scoring; diff --git a/src/ts/components/CardWithIcon/index.module.scss b/src/ts/components/CardWithIcon/index.module.scss index 77a4b8c..a77a6ab 100644 --- a/src/ts/components/CardWithIcon/index.module.scss +++ b/src/ts/components/CardWithIcon/index.module.scss @@ -2,6 +2,8 @@ .card_with_icon, .card_with_icon_long { + position: relative; + display: inline-block; width: calc(50% - 12px); min-height: 270px; @@ -11,8 +13,9 @@ vertical-align: top; text-decoration: none; box-sizing: border-box; + text-align: center; - border-radius: 8px; + border-radius: var(--border-radius-m); border: 1px solid var(--color-border); background-color: var(--color-white); @@ -40,7 +43,8 @@ } &_title, - &_description { + &_description, + &_scoring { font-weight: 100; display: block; margin: 0 auto; @@ -57,7 +61,8 @@ margin: 0 0 4px 0; } - &_description { + &_description, + &_scoring { font-size: var(--font-xs); line-height: 16px; color: var(--color-grey); @@ -68,6 +73,22 @@ padding: 0; 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 { diff --git a/src/ts/components/CardWithIcon/index.tsx b/src/ts/components/CardWithIcon/index.tsx index 877a73e..ee8a529 100644 --- a/src/ts/components/CardWithIcon/index.tsx +++ b/src/ts/components/CardWithIcon/index.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; +import type { IScoringProps } from './components/Scoring'; +import Scoring from './components/Scoring'; import style from './index.module.scss'; interface ICardWithIconProps { @@ -11,6 +13,7 @@ interface ICardWithIconProps { color?: string; icon?: string; long?: boolean; + scoring?: IScoringProps; } function CardWithIcon({ @@ -21,8 +24,10 @@ function CardWithIcon({ color, icon, long = false, + scoring, }: ICardWithIconProps): React.ReactElement | null { const { t } = useTranslation(); + if (!value && value !== 0) return null; return ( @@ -48,6 +53,11 @@ function CardWithIcon({
{t(description || '')}
+ ); } @@ -58,6 +68,7 @@ CardWithIcon.defaultProps = { color: undefined, icon: undefined, long: false, + scoring: undefined, }; export default CardWithIcon; diff --git a/src/ts/components/CustomSelect/components/List.tsx b/src/ts/components/CustomSelect/components/List.tsx index c0dfa56..292c0d4 100644 --- a/src/ts/components/CustomSelect/components/List.tsx +++ b/src/ts/components/CustomSelect/components/List.tsx @@ -1,12 +1,13 @@ import React, { useEffect, useState } from 'react'; import UiKitSelectOption from './Option'; +import IOption from '../interfaces/Option'; import style from '../styles/index.module.scss'; interface UiKitSelectListProps { value: any; - options: any; + options: IOption[]; search?: string; keyCode?: string; setKeyCode: Function; @@ -26,8 +27,10 @@ function UiKitSelectList({ const [selectedIndex, setSelectedIndex] = useState(-1); console.log(value); - const searchResult = options - ?.filter((option: any) => option.title.indexOf(search) !== -1); + const searchText = search ? search.toLowerCase() : ''; + const searchResult = searchText + ? options?.filter((option: any) => option?._textForSearch?.indexOf(searchText) !== -1) + : options; useEffect(() => { if (!keyCode) return; diff --git a/src/ts/components/CustomSelect/components/Value.tsx b/src/ts/components/CustomSelect/components/Value.tsx index 0705535..d11d98b 100644 --- a/src/ts/components/CustomSelect/components/Value.tsx +++ b/src/ts/components/CustomSelect/components/Value.tsx @@ -1,10 +1,11 @@ import React from 'react'; +import IOption from '../interfaces/Option'; import style from '../styles/index.module.scss'; interface UiKitSelectValueProps { value: any; - options: any; + options: IOption[]; className?: string; onClick: (event: React.MouseEvent) => void; } diff --git a/src/ts/components/CustomSelect/helpers/index.ts b/src/ts/components/CustomSelect/helpers/index.ts index 0b2d853..8daf3a9 100644 --- a/src/ts/components/CustomSelect/helpers/index.ts +++ b/src/ts/components/CustomSelect/helpers/index.ts @@ -1,3 +1,5 @@ +import IOption from '../interfaces/Option'; + function getStringFromObject(value: any) { return value?.title || value?.name @@ -23,8 +25,9 @@ function getValue( formatter: (a: any, i?: number) => string, ) { const type = typeof value; - if (type === 'boolean') return value ? 'yes' : 'no'; - if (type === 'number' || type === 'string') return value; + if (type === 'boolean') return value ? 'true' : 'false'; + if (type === 'number') return `${value}`; + if (type === 'string') return value; if (!value) return ''; return Array.isArray(value) @@ -40,10 +43,12 @@ export function getId(value: any, index: number) { 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 { id: getId(value, index), - title: getTitle(value), + title, + _textForSearch: title.toLowerCase(), source: value, }; } diff --git a/src/ts/components/CustomSelect/index.tsx b/src/ts/components/CustomSelect/index.tsx index 2c502ab..eb5f89d 100644 --- a/src/ts/components/CustomSelect/index.tsx +++ b/src/ts/components/CustomSelect/index.tsx @@ -1,5 +1,6 @@ import React, { useMemo, useState } from 'react'; +import IOption from './interfaces/Option'; import UiKitSelectValue from './components/Value'; import UiKitSelectSearch from './components/Search'; import UiKitSelectList from './components/List'; @@ -25,7 +26,7 @@ function UiKitSelect({ const [search, setSearch] = useState(''); const [keyCode, setKeyCode] = useState(''); - const formattedOptions = useMemo(() => options?.map(getOption) || [], [options]); + const formattedOptions: IOption[] = useMemo(() => options?.map(getOption) || [], [options]); const formattedValue = useMemo(() => { const selectedOption = options.find((option: any) => option.id === value); return getTitle(selectedOption) || getTitle(value); diff --git a/src/ts/components/CustomSelect/interfaces/Option.ts b/src/ts/components/CustomSelect/interfaces/Option.ts new file mode 100644 index 0000000..8a8fe9c --- /dev/null +++ b/src/ts/components/CustomSelect/interfaces/Option.ts @@ -0,0 +1,6 @@ +export default interface IOption { + id: string | number | boolean; + title: string; + _textForSearch: string; + source: any; +} diff --git a/src/ts/helpers/DataGrip/components/scoring.ts b/src/ts/helpers/DataGrip/components/scoring.ts new file mode 100644 index 0000000..24c5fff --- /dev/null +++ b/src/ts/helpers/DataGrip/components/scoring.ts @@ -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 = {}; + + statisticByName: IHashMap = {}; + + 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; + }); + } +} diff --git a/src/ts/helpers/DataGrip/index.ts b/src/ts/helpers/DataGrip/index.ts index cd0ed05..49f7de4 100644 --- a/src/ts/helpers/DataGrip/index.ts +++ b/src/ts/helpers/DataGrip/index.ts @@ -13,6 +13,7 @@ import DataGripByGet from './components/get'; import DataGripByPR from './components/pr'; import DataGripByTasks from './components/tasks'; import DataGripByRelease from './components/release'; +import DataGripByScoring from './components/scoring'; class DataGrip { firstLastCommit: any = new MinMaxCounter(); @@ -39,6 +40,8 @@ class DataGrip { release: any = new DataGripByRelease(); + scoring: any = new DataGripByScoring(); + clear() { this.firstLastCommit.clear(); this.author.clear(); @@ -52,6 +55,7 @@ class DataGrip { this.pr.clear(); this.tasks.clear(); this.release.clear(); + this.scoring.clear(); } addCommit(commit: ICommit | ISystemCommit) { @@ -81,6 +85,7 @@ class DataGrip { this.pr.updateTotalInfo(this.author); this.tasks.updateTotalInfo(this.pr); this.release.updateTotalInfo(); + this.scoring.updateTotalInfo(this.author); } } diff --git a/src/ts/pages/Person/components/Money.tsx b/src/ts/pages/Person/components/Money.tsx index 1a70a95..070b582 100644 --- a/src/ts/pages/Person/components/Money.tsx +++ b/src/ts/pages/Person/components/Money.tsx @@ -15,6 +15,8 @@ import IPersonCommonProps from '../interfaces/CommonProps'; const Money = observer(({ user }: IPersonCommonProps): React.ReactElement => { 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 taskNumber = statistic.tasks.length; @@ -36,24 +38,40 @@ const Money = observer(({ user }: IPersonCommonProps): React.ReactElement => { icon="./assets/cards/money_total.png" title="page.person.money.moneyAll.title" description="page.person.money.moneyAll.description" + scoring={{ + value: scoring.moneyAll, + total: scoringTotal.moneyAll, + }} /> diff --git a/src/ts/pages/Person/components/Speed.tsx b/src/ts/pages/Person/components/Speed.tsx index e88c178..b8bd1f6 100644 --- a/src/ts/pages/Person/components/Speed.tsx +++ b/src/ts/pages/Person/components/Speed.tsx @@ -4,7 +4,7 @@ import { observer } from 'mobx-react-lite'; import { getShortNumber } from 'ts/helpers/formatter'; 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 IsStaff from 'ts/components/NothingFound/components/IsStaff'; import PageWrapper from 'ts/components/Page/wrapper'; diff --git a/src/ts/pages/Person/components/Total.tsx b/src/ts/pages/Person/components/Total.tsx index 300e65a..06ad586 100644 --- a/src/ts/pages/Person/components/Total.tsx +++ b/src/ts/pages/Person/components/Total.tsx @@ -6,7 +6,7 @@ import achievementByAuthor from 'ts/helpers/achievement/byCompetition'; import ACHIEVEMENT_TYPE from 'ts/helpers/achievement/constants/type'; 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 Description from 'ts/components/Description'; import PageWrapper from 'ts/components/Page/wrapper'; @@ -37,6 +37,8 @@ function AchievementBlock({ title, achievements }: IAchievementBlockProps) { const Total = observer(({ user }: IPersonCommonProps): React.ReactElement => { const { t } = useTranslation(); 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 taskNumber = statistic.tasks.length; const achievements = achievementByAuthor.authors[statistic.author]; @@ -51,24 +53,40 @@ const Total = observer(({ user }: IPersonCommonProps): React.ReactElement => { icon="./assets/cards/work_days.png" title="page.person.total.daysWorked.title" description="page.person.total.daysWorked.description" + scoring={{ + value: scoring.daysWorked, + total: scoringTotal.daysWorked, + }} /> diff --git a/src/ts/translations/ru/pages.ts b/src/ts/translations/ru/pages.ts index 7ee5320..2fa7b8a 100644 --- a/src/ts/translations/ru/pages.ts +++ b/src/ts/translations/ru/pages.ts @@ -181,6 +181,7 @@ export default ` § page.person.total.daysWorked.description: Учтены только дни, в которые делались коммиты § page.person.total.tasks.title: задач § page.person.total.tasks.description: Если коммиты правильно подписаны +§ page.person.scoring.toolbar: Позиция по этой метрике, относительно других сотрудников § page.person.character.title: Персонаж § page.person.achievement.title: Достижения § page.person.achievement.positive: Позитивные