Загрузка данных
import { Dropdown, Form, MenuProps } from 'antd';
import locale from 'antd/es/date-picker/locale/ru_RU';
import dayjs from 'dayjs';
import 'dayjs/locale/ru';
import React, { FC, useMemo, useState } from 'react';
import { DatePickerLight } from '@components/DatePicker';
import { RoundedClose } from '@components/Icons/RoundedClose';
import { Table, useTableController } from '@components/Table';
import { ResizableColumnType } from '@components/Table/types/columns';
import { TextRegular } from '@components/TextMad';
import { Footnote } from '@components/TextUI';
import WidgetContentWrapper from '@components/WidgetContentWrapper';
import WidgetHeader from '@components/WidgetHeader';
import features from '@features';
import { useSettingsComponent } from '@hooks/table/useSettingsComponent';
import { Button } from '@uikit/Button';
import { Icon } from '@uikit/Icon';
import { FixingsValuesChart } from '@widgets/Fixings/FixingsValuesChart';
import { FixingsValuesTable } from '@widgets/Fixings/FixingsValuesTable';
import {
FixingData,
useFixingsFacade,
} from '@widgets/Fixings/useFixingsFacade';
import { VirtualTableColumnType } from 'types/TableColumns';
import { WidgetContentBasicProps } from 'types/Widgets';
import './fixings.scss';
dayjs.locale('ru');
export interface FixingsProps {
state: {
firstColumns: VirtualTableColumnType<FixingData>[];
};
}
type WidgetFixingsProps = WidgetContentBasicProps<FixingsProps>;
type FixingTableRow = FixingData & Record<string, unknown>;
function toFixingTableRow(
row: FixingData,
): FixingTableRow {
return Object.assign<
Record<string, unknown>,
FixingData
>({}, row);
}
export const Fixings: FC<WidgetFixingsProps> = function (
props: WidgetFixingsProps,
): React.JSX.Element {
const {
selectedFixing,
filteredSelectedFixing,
selectedRow,
fixingsData,
fromDate,
toDate,
onCloseButtonClick,
onChangeDate,
columns,
} = useFixingsFacade();
const [controller, requester] =
useTableController<FixingTableRow>();
const [activeOption, setActiveOption] =
useState('table');
const {
settingsComponent,
setSettingsComponent,
} = useSettingsComponent();
const tableData = useMemo<FixingTableRow[]>(
() =>
(fixingsData ?? []).map(
toFixingTableRow,
),
[fixingsData],
);
const tableColumns = useMemo<
ResizableColumnType<FixingTableRow>[]
>(
() =>
columns.map((column, index) => ({
...column,
position:
column.position ?? index,
width:
typeof column.width === 'number'
? column.width
: undefined,
})),
[columns],
);
const dropdownItems = [
{
label: (
<div className="fixing-button">
<div className="icon">
<Icon variant="file-up" />
</div>
Экспортировать в Excel
</div>
),
key: '1',
},
] as MenuProps;
const settingsItems = [
{
key: '1',
label: settingsComponent,
},
];
const { widgetId } = props;
return (
<>
<WidgetHeader
showWidgetSetting
items={settingsItems}
titleExtra={
selectedRow && (
<Footnote
style={{
display: 'flex',
}}
>
{selectedRow.ticker}
</Footnote>
)
}
handlerSaveAsExcel={
features.hideSaveAsExcel
? undefined
: controller?.saveAsExcel
}
{...props}
/>
<WidgetContentWrapper
{...props}
scrollable={false}
>
{selectedRow && (
<div className="header">
<div className="header-btn-row">
<Button
variant={
activeOption === 'table'
? 'filled-primary'
: 'outlined-secondary'
}
onClick={() =>
setActiveOption('table')
}
text="Таблица"
/>
<Button
variant={
activeOption === 'chart'
? 'filled-primary'
: 'outlined-secondary'
}
onClick={() =>
setActiveOption('chart')
}
text="График"
/>
</div>
<div className="header-daterange">
<Form.Item label="Значения с">
<DatePickerLight
className="header-daterange-datepicker"
locale={locale}
format="DD-MM-YYYY"
onChange={(date) =>
onChangeDate(
date,
true,
)
}
defaultValue={dayjs(
fromDate,
)}
/>
</Form.Item>
<Form.Item label="до">
<DatePickerLight
className="header-daterange-datepicker"
locale={locale}
format="DD-MM-YYYY"
onChange={(date) =>
onChangeDate(
date,
false,
)
}
defaultValue={dayjs(
toDate,
)}
/>
</Form.Item>
</div>
<div
onClick={
onCloseButtonClick
}
className="icon"
>
<RoundedClose />
</div>
</div>
)}
{!selectedRow && (
<div className="fixing">
<Table<FixingTableRow>
widgetId={widgetId}
columns={tableColumns}
dataSource={tableData}
setSettingsComponent={
setSettingsComponent
}
setController={
requester
}
/>
</div>
)}
<Dropdown
menu={dropdownItems}
trigger={['contextMenu']}
>
<div>
<FixingsValuesTable
fixingSelectedValue={
filteredSelectedFixing ||
selectedFixing
}
widgetId={widgetId}
/>
</div>
</Dropdown>
{selectedRow &&
activeOption === 'chart' && (
<>
<TextRegular className="fixing-title">
{selectedRow.caption}
</TextRegular>
<FixingsValuesChart
fixingSelectedValue={
filteredSelectedFixing ||
selectedFixing
}
/>
</>
)}
</WidgetContentWrapper>
</>
);
};
import { Dropdown } from 'antd';
import React, {
FC,
useCallback,
useMemo,
useState,
} from 'react';
import { CountIcon } from '@components/Table/components/CountIcon';
import { Icon } from '@uikit/Icon';
import { decimalNumbersFormatter } from '@utils/decimalNumberFormatter';
import { onFilterHandlerT } from '../../../types/group';
import { CustomGroupCellMenu } from './customGroupCellMenu';
import styles from './styles.module.scss';
import './styles-global.module.scss';
type CustomGroupCellType = {
isShowChildrenCount: boolean;
childrenCount: number;
value: string;
valueVariants?: string[];
index?: number;
onFilterHandler?: onFilterHandlerT;
groupKey: string;
groupValue: string;
};
export const CustomGroupCell: FC<
CustomGroupCellType
> = ({
valueVariants,
onFilterHandler,
value,
isShowChildrenCount,
childrenCount,
groupKey,
groupValue,
}) => {
const [menuOpen, setMenuOpen] =
useState(false);
const withDropdown = Boolean(
valueVariants &&
onFilterHandler,
);
const onSelectHandler =
useCallback<onFilterHandlerT>(
(...params) => {
onFilterHandler?.(...params);
setMenuOpen(false);
},
[onFilterHandler],
);
const dropdownContent = useMemo(
() => (
<CustomGroupCellMenu
variants={valueVariants}
onSelectHandler={
onSelectHandler
}
selectedItemKey={value}
groupKey={groupKey}
groupValue={groupValue}
/>
),
[
valueVariants,
onSelectHandler,
value,
groupKey,
groupValue,
],
);
if (withDropdown) {
return (
<Dropdown
open={menuOpen}
onOpenChange={setMenuOpen}
dropdownRender={() =>
dropdownContent
}
trigger={['click']}
>
<div
className={
styles.filteredGroupCellWrapper
}
>
{decimalNumbersFormatter(
value,
)}
{isShowChildrenCount && (
<CountIcon
count={childrenCount}
/>
)}
<Icon
variant="chevron-down"
style={{
cursor: 'pointer',
}}
/>
</div>
</Dropdown>
);
}
return (
<div className="groupCellWrapper">
{decimalNumbersFormatter(
value,
)}
{isShowChildrenCount && (
<CountIcon
count={childrenCount}
/>
)}
</div>
);
};
import React from 'react';
import { Icon } from '@uikit/Icon';
import { BUTTON_MORE_ROW_KEY } from '@widgets/TradingResult/components/Repo/constants';
import { LoadMoreButton } from '@widgets/TradingResult/components/Repo/repo-table/component/loadMoreButton';
import { DataInfoByGroup } from '@widgets/TradingResult/components/Repo/repo-table/logic/table/useLoadGroupSubRowsData';
import { GroupFiltersStruct } from '@widgets/TradingResult/components/Repo/types/group';
import { Group } from 'types/MonitorRepo';
import repoTableStyles from '../styles/repoTable.module.scss';
interface InitialGroupFilterRecord {
key: string;
rowData: Record<
string,
unknown
>;
}
interface GroupFilterValue {
value: string;
}
type GroupRow = Group & {
key: string;
children?: unknown[];
};
interface ExpandIconProps {
onExpand: (
record: GroupRow,
event: React.MouseEvent<HTMLElement>,
) => void;
record: GroupRow;
}
const VALUES_TO_REFRESH = [
'aggPrice',
'aggQuantity',
'aggAccint',
'aggValue',
'aggRepoRate',
'aggRepoValue',
] as const satisfies readonly (keyof Group)[];
function isGroupFilterValue(
value: unknown,
): value is GroupFilterValue {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value)
) {
return false;
}
return (
'value' in value &&
typeof value.value === 'string'
);
}
export const createInitialGroupFilters = (
record: InitialGroupFilterRecord,
): GroupFiltersStruct =>
Object.entries(
record.rowData,
).reduce<GroupFiltersStruct>(
(
acc,
[filterName, dataValue],
) => {
if (
!isGroupFilterValue(
dataValue,
)
) {
return acc;
}
const groupValue =
record.key;
return {
...acc,
[groupValue]: {
...acc[groupValue],
[filterName]:
dataValue.value,
},
};
},
{},
);
export const ExpandIcon = ({
onExpand,
record,
}: ExpandIconProps) =>
record.children ? (
<div
className={
repoTableStyles.expandIconWrapper
}
onClick={(
event: React.MouseEvent<HTMLElement>,
) =>
onExpand(
record,
event,
)
}
>
<Icon variant="chevron-down" />
</div>
) : null;
export const unifyGroupAndChildrenRows = (
groupSubRowsData: Record<
string,
DataInfoByGroup
>,
groupsData: GroupRow[],
buttonMargin: number,
loadMore: (
groupValue: string,
) => Promise<void>,
): GroupRow[] => {
// ищем значения групп по которым существуют данные подстрок
const groupValues = Object.keys(
groupSubRowsData,
);
if (
groupValues.length === 0
) {
return groupsData;
}
groupValues.forEach(
(groupValue) => {
const groupData =
groupSubRowsData[
groupValue
];
if (!groupData) {
return;
}
const searchedGroup =
groupsData.find(
(group) =>
group.key ===
groupValue,
);
if (!searchedGroup) {
return;
}
const data =
groupData.data ?? [];
const page =
groupData.page ?? 0;
const size =
groupData.size ?? 0;
const totalValues =
groupData.total ?? 0;
const itemsCount =
data.length;
const isEmptyChildrenRows =
itemsCount === 0;
const loadedRowsNumber =
(page + 1) * size;
// подгруженные данные репо по группировке заносим в общую структуру групп как children
searchedGroup.children = [
...data,
{
key: `${BUTTON_MORE_ROW_KEY}${groupValue}`,
forceToSort: true,
secId: (
<LoadMoreButton
styles={{
marginLeft:
buttonMargin,
}}
onClick={() =>
loadMore(
groupValue,
)
}
loaded={
!isEmptyChildrenRows
? Math.min(
loadedRowsNumber,
totalValues,
itemsCount,
)
: 0
}
shouldDisplayButton={
itemsCount ===
loadedRowsNumber
}
total={
!isEmptyChildrenRows
? totalValues ||
loadedRowsNumber
: 0
}
/>
),
},
];
},
);
return [...groupsData];
};
/** Обновляем только пересчитываемые значения */
export const refreshRowAggValues = (
oldGroup: Group,
newGroup: Group,
): Group =>
VALUES_TO_REFRESH.reduce(
(group, key) => ({
...group,
[key]: newGroup[key],
}),
oldGroup,
);