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


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