Загрузка данных


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>;
type FixingTableRender = NonNullable<ResizableColumnType<FixingTableRow>['render']>;

const adaptColumnRender = (
  render: VirtualTableColumnType<FixingData>['render'],
): ResizableColumnType<FixingTableRow>['render'] => {
  if (!render) {
    return undefined;
  }

  return (value, record, index) =>
    render(value, record, index ?? 0) as ReturnType<FixingTableRender>;
};

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((item) =>
        Object.assign<Record<string, unknown>, FixingData>({}, item),
      ),
    [fixingsData],
  );

  const tableColumns = useMemo<ResizableColumnType<FixingTableRow>[]>(
    () =>
      columns.map((column, index) => {
        const { render, width, position, ...columnProps } = column;

        return {
          ...columnProps,
          position: position ?? index,
          width: typeof width === 'number' ? width : undefined,
          render: adaptColumnRender(render),
        };
      }),
    [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>
            {selectedRow && (
              <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>
        )}

        {selectedRow && activeOption === 'table' && (
          <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)[];

const 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,
  );