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


import { Contract } from '@modules/contracts/types';

import { ChartIndicativeData } from '../types';

import type { Timeframes } from 'moex-chart';

export type WidgetProperties = {
  chartState: {
    savedInstrument: Contract['issKey'] | null;
    interval: string;
    savedData?: string;
  };
  indicativeData?: ChartIndicativeData;
  moexChartState?: {
    timeframe: Timeframes;
    savedData?: string;
  };
};



import type { WidgetContentBasicProps } from 'types/Widgets';
import type { WidgetProperties } from './properties/types';

export type ChartIndicativeData = {
  secId: string;
  instrumentName: string;
  settlement: string;
  firmName: string;
  key: string;
};

export type ChartContainerProps = WidgetContentBasicProps<WidgetProperties>;



import type { Workspace } from './Workspace';
import type { Position } from 'react-rnd';

export interface WidgetPosition {
  x: number;
  y: number;
  widgetNumber?: number;
  maxX?: number;
  maxY?: number;
}

export enum WidgetContentType {
  testIFrame = 'testIFrame',
  graphic = 'graphic',
  news = 'news',
  newsIss = 'newsIss',
  corporateActions = 'corporateActions',
  glass = 'glass',
  instruments = 'instruments',
  noTradeChat = 'noTradeChat',
  HHI = 'HHI',
  contacts = 'contacts',
  aboutInstruments = 'aboutInstruments',
  fixings = 'fixings',
  FUTOI = 'FUTOI',
  curves = 'curves',
  // Виджет Карта рынка облигаций
  // для совестимости с предыдущей версией названия из  /tb/api/v1/users/current -> widgetTypes
  profitabilityAndDuration = 'profitabilityAndDuration',
  widgetTurnovers = 'widgetTurnovers',
  tradingResult = 'tradingResult',
  quotesNTPro = 'quotesNTPro',
  instruction = 'instruction',
  macroData = 'macroData',
  otcTurnover = 'otcTurnover',
  techSupport = 'techSupport',
  screener = 'screener',
  review = 'review',
  helpCenter = 'helpCenter',
  ordersJournal = 'ordersJournal',
  indicativeQuotes = 'indicativeQuotes',
  swapCalculator = 'swapCalculator',
  spfiPrices = 'spfiPrices',
  issuerCard = 'issuerCard',
  ntbOrdersAndDeals = 'ntbOrdersAndDeals',
  ntbPositions = 'ntbPositions',
  ntbLogisticAuto = 'ntbLogisticAuto',
  ntbLogisticFreight = 'ntbLogisticFreight',

  tradeJournal = 'tradeJournal',
  draftBrokerSpfi = 'draftBrokerSpfi',
  tradeJournalDetails = 'tradeJournalDetails',
  ntbIndexes = 'ntbIndexes',
  depositCcpOrderTables = 'depositCcpOrderTables',
  depositCcpTradeTables = 'depositCcpTradeTables',
  depositCcpReferenceTables = 'depositCcpReferenceTables',
  depositCcpRiskTables = 'depositCcpRiskTables',
  userAdministration = 'userAdministration',
}

export interface WidgetSizes {
  /** 'px' */
  minHeight?: string;
  /** '%' */
  height: string;
  /** 'px' */
  minWidth?: string;
  /** '%' */
  width: string;
  /** '%' */
  restWidth?: number;
}

export interface WidgetSizeLimits {
  minHeight: number; // px
  minWidth: number; // px
  maxWidth?: number; // px
  maxHeight?: number; // px
}

export interface BeforeWidgetExpandParams {
  position: Position;
  sizes: WidgetSizes;
}

export interface ICreateWidgetByDragProps {
  sizes?: WidgetSizes;
  position?: WidgetPosition;
}

export interface Widget {
  name: string;
  sizes: WidgetSizes;
  sizeLimits?: WidgetSizeLimits;
  zIndex: number;
  id: number;
  workspaceId: Workspace['id'];
  position: WidgetPosition;
  type: WidgetContentType;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: типипзировать
  widgetContentProps: Record<string, any>;
  isExpand: boolean;
  beforeExpandParams: BeforeWidgetExpandParams | null;
  /** @deprecated Для множественной связки используется более универсальное поле masters */
  master?: number | null;
  /** Для определения мастеров при связке (binding) виджетов. */
  masters?: number[] | null;
  externalProperties?: { [key: string]: string | number }[] | null;
  withoutSend?: boolean;
  moved?: boolean;
  inFocus?: boolean;
  /**
   *
   */
  isStacked?: boolean;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: типипзировать
export interface WidgetContentBasicProps<T = any> {
  widgetId: Widget['id'];
  widgetContentProps?: T;
  onWidgetContentClick?: () => void;
  refWrapper?: React.MutableRefObject<HTMLDivElement | null>;
  /** undefined - не отображаем, null - дизейблим */
  handlerSaveAsExcel?: (() => void) | null;
  isMobileView?: boolean;
  style?: Record<string, string>;
  className?: string;
}

export interface PackedPropsProperties {
  key: string;
  value: string | number | boolean | undefined | null;
}

export interface WidgetPackedProps {
  widgetId: number;
  properties: PackedPropsProperties[];
  isRemoval?: boolean;
}

export interface GlassWidgetConfig {
  view: 'four-col-1' | 'four-col-2' | 'four-col-3';
  showYield: boolean;
  showSpread: boolean;
  choosenInstrument: string;
}



import { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';

import { communicator } from '@core/comm';
import { useAppSelect } from '@hooks/useAppSelector';
import { CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT, HIGHLIGHT_WIDGET_EVENT } from '@modules/widgets/shared';
import { addContentPropsToWidget, unbindWidgets } from '@store/slices/widgets';
import { getState } from '@store/store';
import { useWidgetsBind } from '@utils/hooks/useWidgetsBind';

import { DEFAULT_SYMBOL } from '../const';

import { useChartPublicContext } from './useChartPublicContext';

import type { WidgetProperties } from '../properties/types';
import type { ChartContainerProps } from '../types';

import type { Contract } from '@modules/contracts';
import type { Dispatch, SetStateAction } from 'react';
import type { Widget } from 'types/Widgets';

interface UseChartComponentFacadeReturn {
  dropDownOpen: boolean;
  setDropdownOpen: Dispatch<SetStateAction<boolean>>;
  currentInstrument: string;
  isWidgetHeaderContextMenuOpen: boolean;
  setIsWidgetHeaderContextMenuOpen: Dispatch<SetStateAction<boolean>>;
  onDropInstruments: (val: string, withUpdate?: boolean) => void;
  addInstrumentFromModal: (instruments: Pick<Contract, 'issKey'>[]) => void;
  isOver: boolean;
}

type SaveContentPropsOptions = Partial<WidgetProperties['chartState']> & {
  withUpdate?: boolean;
  cleanIndicativeData?: boolean;
};

export default function useChartComponentFacade(props: ChartContainerProps): UseChartComponentFacadeReturn {
  const { widgetId } = props;

  const dispatch = useDispatch();

  const [isOver, setIsOver] = useState(false);
  const [dropDownOpen, setDropdownOpen] = useState(false);
  const [isWidgetHeaderContextMenuOpen, setIsWidgetHeaderContextMenuOpen] = useState(false);

  const widgetProperties = useAppSelect(
    (state) => state.widgets.widgets.find(({ id }) => id === widgetId)?.widgetContentProps,
  ) as WidgetProperties | undefined;

  const initialInstrument = widgetProperties?.chartState?.savedInstrument ?? DEFAULT_SYMBOL;

  // TODO временно реф из-за непредсказуемого изменения если используется useState,
  // нужен рефакторинг и вернуть обратно useState
  const currentInstrumentRef = useRef(initialInstrument);
  const [currentInstrument, setCurrentInstrument] = useState(initialInstrument);

  const { triggerRelatedWidgetsToUpdate, getMasterInstrumentFromPublicContext } = useWidgetsBind({
    widgetId,
  });

  const triggerRelatedWidgetsToUpdateRef = useRef(triggerRelatedWidgetsToUpdate);

  useEffect(() => {
    triggerRelatedWidgetsToUpdateRef.current = triggerRelatedWidgetsToUpdate;
  }, [triggerRelatedWidgetsToUpdate]);

  const saveContentProps = useCallback(
    (val: SaveContentPropsOptions): void => {
      const { withUpdate, cleanIndicativeData, ...chartStateVal } = val;

      const oldProps = (getState().widgets.widgets.find(({ id }) => id === widgetId) as Widget | undefined)
        ?.widgetContentProps;

      if (!oldProps) {
        return;
      }

      dispatch(
        addContentPropsToWidget({
          id: widgetId,
          withoutSend: !withUpdate,
          widgetContentProps: {
            chartState: {
              ...oldProps.chartState,
              ...chartStateVal,
            },
            // при смене инструмента очищаем индикативные данные, переданные из виджета Индикативные котировки
            indicativeData: cleanIndicativeData ? undefined : oldProps.indicativeData,
          },
        }),
      );
    },
    [dispatch, widgetId],
  );

  const onInstrumentChange = useCallback(
    (newVal: string, withUpdate?: boolean, unbind = true): void => {
      if (!newVal) {
        return;
      }

      setIsWidgetHeaderContextMenuOpen(false);

      if (currentInstrumentRef.current === newVal) {
        return;
      }

      currentInstrumentRef.current = newVal;
      setCurrentInstrument(newVal);

      if (unbind) {
        dispatch(unbindWidgets({ widgetId }));
      }

      // при смене инструмента очищаем индикативные данные виджета график
      // т.к. логика для графика индикатива построена на наличии в widgetContentProps данных indicativeData
      saveContentProps({
        savedInstrument: newVal,
        withUpdate,
        cleanIndicativeData: true,
      });

      triggerRelatedWidgetsToUpdateRef.current(newVal);
    },
    [dispatch, saveContentProps, widgetId],
  );

  const addInstrumentFromModal = useCallback(
    (instruments: Pick<Contract, 'issKey'>[]) => {
      const issKey = instruments[0]?.issKey;

      if (!issKey) {
        return;
      }

      onInstrumentChange(issKey);
    },
    [onInstrumentChange],
  );

  const onInstrumentChangeFromBind = useCallback(
    (instrumentId: string) => {
      onInstrumentChange(instrumentId, true, false);
    },
    [onInstrumentChange],
  );

  const onDropInstruments = useCallback(
    (val: string, withUpdate?: boolean): void => {
      onInstrumentChange(val, withUpdate);
    },
    [onInstrumentChange],
  );

  useChartPublicContext({
    setCurrInstrument: onInstrumentChangeFromBind,
    widgetId,
    getMasterInstrumentFromPublicContext,
  });

  useEffect(() => {
    triggerRelatedWidgetsToUpdateRef.current(currentInstrumentRef.current);
  }, []);

  useEffect(() => {
    const unsubscribe = communicator.listen(
      {
        messageType: CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT,
      },
      (message) => {
        const issKey = (message as Record<number, string>)[widgetId];

        if (issKey) {
          addInstrumentFromModal([{ issKey }]);
        }
      },
    );

    const unsubscribeHighlighter = communicator.listen(
      {
        messageType: HIGHLIGHT_WIDGET_EVENT,
      },
      (message) => {
        const typedMessage = message as Record<number, boolean>;

        if (Object.keys(typedMessage).includes(String(widgetId))) {
          setIsOver(typedMessage[widgetId]);
        }
      },
    );

    return () => {
      unsubscribe();
      unsubscribeHighlighter();
    };
  }, [addInstrumentFromModal, widgetId]);

  return {
    dropDownOpen,
    setDropdownOpen,
    currentInstrument,
    isWidgetHeaderContextMenuOpen,
    setIsWidgetHeaderContextMenuOpen,
    onDropInstruments,
    addInstrumentFromModal,
    isOver,
  };
}



import React, { FC, lazy, useContext, useState } from 'react';

import { DNDWrapper } from '@components/DNDWrapper';
import { InstrumentSearch } from '@components/InstrumentSearch';
import WidgetContentWrapper from '@components/WidgetContentWrapper';
import WidgetHeader from '@components/WidgetHeader';
import { useDropInstrument } from '@hooks/dnd';

import { WidgetEnvContext } from '@terminal/desktop/workspaces/default/components/Widget/context';
import { useWidgetHeaderName } from '@utils/useWidgetName';

import useChartComponentFacade from './hooks/useChartComponentFacade';

import type { ChartContainerProps } from './types';

const MoexChart = lazy(() => import('./components/MoexChart/MoexChart'));

export const Chart: FC<ChartContainerProps> = function (props): JSX.Element {
  const indicativeData = props.widgetContentProps?.indicativeData;

  const {
    dropDownOpen,
    setDropdownOpen,
    currentInstrument,
    setIsWidgetHeaderContextMenuOpen,
    isWidgetHeaderContextMenuOpen,
    onDropInstruments,
    addInstrumentFromModal,
    isOver,
  } = useChartComponentFacade(props);

  /* на дроп обновляем название бумаги в сторе
    и делаем апдейт на бэк, чтобы сохранить изменения
    при обновлении страницы */
  const { dropRef } = useDropInstrument((dragData) => {
    onDropInstruments(dragData.properties.issKey, true);
  });

  const [isOpenEmptyAction, setIsOpenEmptyAction] = useState<boolean>(false);

  const openInstrumentModal = () => {
    setIsOpenEmptyAction(true);
    setIsWidgetHeaderContextMenuOpen(false);
  };

  // TODO: Зависит от сеток
  const { isDraggedOver } = useContext(WidgetEnvContext);
  const contractsInstrumentName = useWidgetHeaderName(currentInstrument);

  const instrumentName = indicativeData
    ? `${indicativeData.instrumentName} ${indicativeData.settlement} - ${indicativeData.firmName}`
    : contractsInstrumentName;

  return (
    <DNDWrapper
      ref={dropRef}
      isOver={isOver}
      canDrop
    >
      <WidgetHeader
        {...props}
        dropdownOpen={dropDownOpen}
        setDropdownOpen={setDropdownOpen}
        itemsSearchIcon={[true]}
        handlerSaveAsExcel={null}
        addToWidgetNamePrefix={instrumentName}
        setIsOpenContextMenuFromWidget={setIsWidgetHeaderContextMenuOpen}
        isOpenContextMenuFromWidget={isWidgetHeaderContextMenuOpen}
        openInstrumentsModal={openInstrumentModal}
      />
      <WidgetContentWrapper {...props}>
        <div
          style={{
            display: 'flex',
            flexDirection: 'column',
            height: '100%',
            pointerEvents: isDraggedOver ? 'none' : 'inherit',
          }}
        >
          <MoexChart
            symbol={currentInstrument}
            symbolName={instrumentName || currentInstrument}
            indicativeData={indicativeData}
            widgetId={props.widgetId}
            addInstrumentFromModal={addInstrumentFromModal}
          />

          {isOpenEmptyAction && (
            <InstrumentSearch
              setOpen={setIsOpenEmptyAction}
              isOpen={isOpenEmptyAction}
              variant="single"
              widgetId={props.widgetId}
              // withNRD={false}
              addInstruments={addInstrumentFromModal}
            />
          )}
        </div>
      </WidgetContentWrapper>
    </DNDWrapper>
  );
};