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


import dayjs, { type Dayjs } from 'dayjs';

import { feedbackController } from '@api/controllers/feedbackController';

import { commonDateFormat } from '@configs/standartDateFormat';
import { AboutInstrumentTradingResultType } from 'types/AboutInstrument';

import { Candle } from 'types/Candles';

import axios, {
  axiosCurves,
  axiosIndicative,
  axiosInstanceFormalization,
  axiosInstanceNews,
  axiosInstanceNTBAnalytics,
  axiosInstanceNTBTrading,
  axiosInstanceNTPro,
  axiosInstanceSPFI,
  axiosInstanceTD,
  axiosInstanceTMD,
  axiosInstanceTus,
  uninterceptedAxiosInstance,
} from './axios';

import { abbrevationsController } from './controllers/abbrevationsController';
import { candlesController } from './controllers/candles';
import { frontExceptionController } from './controllers/frontExceptionController';
import { newsSettingsController } from './controllers/newsController';
import { noTradeChatController } from './controllers/noTradeChatController';
import { tradeChatController } from './controllers/tradeChatController';
import { widgetPropertiesController } from './controllers/widgetProperties';
import { widgetsController } from './controllers/widgets';
import { workspaceController } from './controllers/workspace';

import type { ResponseSearchSmartType } from '@components/InputSearchCommand/type/type';
import type { Contract } from '@modules/contracts';
import type { BoardType, Counterparty } from '@widgets/AboutInstrument/types';
import type { QuotesNTProTypeServer } from '@widgets/QuotesNTPro/type';

import type { AxiosRequestConfig, AxiosResponse } from 'axios';
import type { IChatFolder, IChatMessages, ICreateChatFolder, IUpdateChatFolder } from 'types/Chats';
import type {
  CurveValue,
  IndicativeCurveFilters,
  IndicativeCurveType,
  IssCurveFilters,
  IssCurveGroup,
  IssCurveType,
  ZeroCouponCurveLiveRequestParams,
  ZeroCouponCurveLiveResponse,
} from 'types/Curves';
import type { CustomerDataType } from 'types/Customers';
import type { Firm } from 'types/Firm';
import type { FixingData } from 'types/FixingData';
import type { Instrument } from 'types/Instruments';
import type { INewsFeed, INewsItem } from 'types/News';
// import type { NSDCostType } from 'types/NSDCost';
import type { Payment } from 'types/Payment';
import type { QuotesType } from 'types/Quotes';
import type { GetBarsParams } from 'types/Requests/BarsTypes';
import type { SittingCB } from 'types/SittingCB';
import type { User, UserSettings } from 'types/User';

const api = {
  getBars({ currencyPair, interval, date, count }: GetBarsParams) {
    return axiosInstanceTD.get<Candle[]>(
      `/api/v1/candles/list/offset?secId=${currencyPair}&date=${date}&interval=${interval}&count=${count}`,
    );
  },
  getQuotes() {
    return axios.post('api/v1/blotters/quotes', {
      timestamp: -1,
      keys: [
        'AIX:MAIN:BAST',
        'AIX:MAIN:Bond',
        'AIX:MAIN:0000000000000',
        'AIX:MAIN:aix123',
        'AIX:AOTC:EQOTCa',
        'AIX:MAIN:TickEQ1',
        'AIX:MAIN:COVID19',
        'AIX:MAIN:HSBK',
        'AIX:MAIN:Forte_AIX',
        'AIX:MAIN:Tatn_AIX',
        'AIX:MAIN:170920201635',
        'INT:IUSA:123987',
        'INT:IUSA:HSBK_priv_IUSA',
        'INT:IUSA:AAPL',
        'INT:IUSA:aaa8888811122',
        'INT:IUSA:aaa1',
        'INT:IUSA:171107122020',
        'KSE:TQBR:RAHT',
        'KSE:EQBR:HSBKp',
        'KSE:TQBR:KZTK',
        'KSE:TQBR:KZAP',
        'KSE:TQBR:HSBK',
        'KSE:TQBR:hydro10',
        'INT:ILSE:KAZ',
        'INT:IOTC:EQOTCI',
        'INT:ILSE:tick_test1201',
        'INT:IOTC:test_IOTS1201',
        'INT:ILSE:tick_009',
        'INT:ILSE:13360812',
        'INT:ILSE:Forte_LSE',
        'INT:IUSA:Test666',
      ],
    });
  },
  getMarketDepth(key: Contract['key']) {
    return axiosInstanceTMD.post('/api/v1/blotters/marketdepths', {
      key,
      timestamp: -1,
    });
  },
  workspaceController,
  widgetsController,
  widgetPropertiesController,
  frontExceptionController,
  candlesController,
  newsSettingsController,
  getChatCustomers(market?: string, spfiTradingPermission?: boolean) {
    return axios.get<CustomerDataType[]>('/api/v1/data/cache/customers', { params: { market, spfiTradingPermission } });
  },
  getCounterparties() {
    return axios.get<CustomerDataType[]>('/api/v1/data/customers');
  },
  getCounterpartiesById(id: number) {
    return axios.get<Counterparty>(`/api/v1/data/customers/${id}`, {
      full: true,
    } as AxiosRequestConfig<{ full: boolean }>);
  },
  getInstrumentById(id: number): Promise<AxiosResponse<Instrument>> {
    return axios.get(`/api/v1/data/instruments/${id}`);
  },
  getInstrumentList(): Promise<AxiosResponse<Instrument[]>> {
    return axios.get('/api/v1/data/instruments?');
  },
  tradeChatController,
  noTradeChatController,
  getNewsList(dateRange: string[]) {
    return axiosInstanceTD.get(`news/getList?from=${dateRange[0]}&till=${dateRange[1]}`);
  },
  getQuotesList() {
    return axiosInstanceTD.get('quotes/');
  },
  getBoardByInstrumentId(id: number, floorCode: string) {
    return axios.get<BoardType[]>(`/api/v1/data/boards?floorCode=${floorCode}&instrumentId=${id}`);
  },
  getDealsList(isTradeChat = false) {
    return axios.get(`/api/v1/blotters/deals${isTradeChat ? '?isTradeChat=true' : ''}`);
  },
  hideTrade(talkId: number) {
    return axios.put(`api/v1/blotters/deals/hide/${talkId}`);
  },
  getTermsDict() {
    return axiosInstanceTD.get<{ dictionary: Record<string, string> }>('/api/v1/contracts/dictionary/term');
  },
  getBoardsDict() {
    return axiosInstanceTD.get<{ dictionary: Record<string, string> }>('/api/v1/contracts/dictionary/board');
  },
  getInstrGroupDict() {
    return axiosInstanceTD.get<{ dictionary: Record<string, string> }>('/api/v1/contracts/dictionary/instrGroupType');
  },
  getIssContracts() {
    return axiosInstanceTD.get<Contract[]>('/api/v1/contracts');
  },
  getIndicativeContracts() {
    return axiosIndicative.get<Contract[]>('/api/v1/contracts');
  },
  getNtProContracts() {
    return axiosInstanceNTPro.get<Contract[]>('/api/v1/contracts');
  },
  getSapfirContracts() {
    return axiosInstanceSPFI.get<Contract[]>('/api/v1/contracts');
  },
  getNtbAnalyticsContracts() {
    return axiosInstanceNTBAnalytics.get<Contract[]>('/api/v1/contracts');
  },
  getNtbContracts() {
    return axiosInstanceNTBTrading.get<Contract[]>('/api/v1/contracts');
  },
  getFixingsList(): Promise<AxiosResponse<FixingData[]>> {
    return axiosInstanceTD.get('/api/fixings/');
  },
  getFixingById(key: string): Promise<AxiosResponse<FixingData[]>> {
    return axiosInstanceTD.get(`/api/fixings?code=${key}`);
  },
  getQuotesByKeys(keys: string[], signal?: AbortSignal) {
    return axiosInstanceTD.put<QuotesType[]>('/api/v1/quotes', keys, { signal });
  },
  getQuotesByIndicativeKeys(keys: string[], signal?: AbortSignal) {
    return axiosIndicative.put<QuotesType[]>('/api/v1/quotes', keys, { signal });
  },
  getQuotesListWithParams({ skip, limit }: { skip: number; limit: number }) {
    return axiosInstanceTD.get<QuotesType[]>('/api/v1/quotes/list', {
      params: {
        skip,
        limit,
      },
    });
  },
  getTurnoversByDate(date: Date) {
    const year = date.getFullYear();
    const month = date.getMonth() + 1 < 10 ? `0${date.getMonth() + 1}` : date.getMonth() + 1;
    const day = date.getDate() < 10 ? `0${date.getDate()}` : date.getDate();

    const newDate = `${year}-${month}-${day}`;

    return axiosInstanceTD.get(`/api/v1/turnovers?date=${newDate}`);
  },

  /** Кривые */
  getCurveGroups() {
    return axiosCurves.get<IssCurveGroup[]>('/api/v1/data/curves/groups');
  },
  getCurves() {
    return axiosCurves.get<IssCurveType[]>('/api/v1/data/curves');
  },
  getCurvesFilter: (params: { groupCurveId?: number; id?: number; currencyCode?: string } = {}) =>
    axiosCurves.get<IssCurveFilters>('/api/v1/data/curvesfilter', { params }),
  getCurvePoints(groupId: number, curveId: number, date: Date | string) {
    const newDate = date instanceof Date ? dayjs(date).format(commonDateFormat.backendDateFormat) : date;
    return axiosCurves.get<CurveValue[]>(`/api/v1/data/curvepoints/${groupId}/${curveId}/${newDate}`);
  },

  getZeroCouponCurveLive: (params?: ZeroCouponCurveLiveRequestParams) =>
    axiosInstanceTD.post<ZeroCouponCurveLiveResponse>('/api/v1/kbd/live', params),

  /** Кривые индикативные котировок */
  getIndicativeCurves: () => axiosIndicative.get<IndicativeCurveType[]>('/api/v1/data/curves'),
  getIndicativeCurveFilter: (params: { groupCurveId?: number; curveId?: string; currencyCode?: string } = {}) =>
    axiosIndicative.get<IndicativeCurveFilters>('/api/v1/data/curvesfilter', { params }),
  getIndicativeCurvePoints(key: string, date: Dayjs) {
    const formattedDate = date.format(commonDateFormat.backendDateFormat);
    return axiosIndicative.get<CurveValue[]>(`/api/v1/data/curvepoints/${key}/${formattedDate}`);
  },

  getQuoteByIssKey(key: string) {
    const issKeyParts = key.split(':');
    return axiosInstanceTD.get<QuotesType>(
      `/api/v1/quotes?secId=${issKeyParts[2]}&boardId=${issKeyParts[1]}&tradingSystem=${issKeyParts[0]}`,
    );
  },
  getPaymentsByIsin(isin: string) {
    return axiosInstanceTD.get<Payment[]>(`/api/v1/payments?isin=${isin}`);
  },
  getTradingResults(security: string, date: string | Dayjs, board: string) {
    return axiosInstanceTD.get<AboutInstrumentTradingResultType[]>(
      `api/trading-result?mode=instrument&security=${security}&till=${date}&from=${date}&board=${board}`,
    );
  },
  getQuotesNTPro() {
    return axiosInstanceNTPro.get<QuotesNTProTypeServer[]>('/quotes');
  },
  getIndexesBySecId(key: string) {
    const issKeyParts = key.split(':');
    return axios.get(`/api/v1/data/indexes/${issKeyParts[2]}`);
  },
  getUserPermissions(): Promise<AxiosResponse<string[]>> {
    return axios.get('/api/v1/permission/user');
  },
  /** Получение основной информации по пользователю */
  getCurrentUserInfo(): Promise<AxiosResponse<User>> {
    return axios.get('api/v1/users/current');
  },
  updateMenuLocked(id: number, locked: boolean): Promise<AxiosResponse<User>> {
    return axios.patch('api/v1/users/settings', {
      user_id: id,
      is_menu_locked: locked,
    });
  },
  updateUser(user: User): Promise<AxiosResponse<User>> {
    return axios.put('api/v1/users', user);
  },
  getCurrentUserSettings(id: number) {
    return axios.get<UserSettings>(`/api/v1/notification-users/${id}`);
  },
  updateCurrentUserSettings(settigs: UserSettings) {
    return axios.put('api/v1/notification-users', settigs);
  },
  getSearchSmart(search: string, signal: AbortSignal) {
    return axiosInstanceTus.get<ResponseSearchSmartType>(`/api/v1/search-smart?search=${encodeURIComponent(search)}`, {
      signal,
    });
  },
  healthcheckTd(): Promise<AxiosResponse<{ status: string }>> {
    return axiosInstanceTD.get('/actuator/health-td');
  },
  healthcheckTb(): Promise<AxiosResponse<{ status: string }>> {
    return axios.get('/actuator/health-tb');
  },
  healthcheckSPFI(): Promise<AxiosResponse<{ status: string }>> {
    return axiosInstanceSPFI.get('/actuator/health-spfi');
  },
  healthcheckForm(): Promise<AxiosResponse<{ status: string }>> {
    return axiosInstanceFormalization.get('/actuator/health-form');
  },
  connectionCheckRequest(): Promise<AxiosResponse<{ status: string }>> {
    return uninterceptedAxiosInstance.get('https://iss.moex.com/iss/sitenews/');
  },
  getFirms(): Promise<AxiosResponse<Firm[]>> {
    return axios.get('/api/v1/data/cache/firms');
  },
  abbrevationsController,
  feedbackController,
  // Отключено в рамках TRADERADAR-12666 Отключение источника НРД
  // getNSDCost() {
  //   return axiosInstanceTD.get<NSDCostType[]>('/api/v1/data/nsdCost');
  // },
  getChatFolders(): Promise<AxiosResponse<IChatFolder[]>> {
    return axiosInstanceFormalization.get('/api/v1/folders');
  },
  createChatFolder(folderData: ICreateChatFolder): Promise<AxiosResponse<IChatFolder>> {
    return axiosInstanceFormalization.post('/api/v1/folders', folderData);
  },
  deleteChatFolder(folderId: number): Promise<AxiosResponse> {
    return axiosInstanceFormalization.delete(`/api/v1/folders/${folderId}`);
  },
  updateChatFolder(folderData: IUpdateChatFolder): Promise<AxiosResponse<IChatFolder>> {
    return axiosInstanceFormalization.patch(`/api/v1/folders/${folderData.folderId}`, folderData);
  },
  updateFolderPosition(folderPosition: Record<string, number>): void {
    axiosInstanceFormalization.post('/api/v1/folders/positions', folderPosition);
  },
  sendChatMessages(data: IChatMessages): Promise<AxiosResponse> {
    return axiosInstanceFormalization.post('/api/v1/chat-messages', data);
  },
  getNewsItemById(newsId: string): Promise<AxiosResponse<INewsItem>> {
    return axiosInstanceNews.get(`/api/v1/news/${newsId}`);
  },
  findNewsByText(text?: string): Promise<AxiosResponse<INewsItem[]>> {
    const requestUrl = `/api/v1/news/search${text ? `?search=${encodeURIComponent(text)}` : ''}`;
    return axiosInstanceNews.get(requestUrl);
  },
  getCombinedNews(searchParams?: string): Promise<AxiosResponse<INewsItem[]>> {
    const requestUrl = searchParams ? `/api/v1/news/search?${searchParams}` : '/api/v1/news/search';
    return axiosInstanceNews.get(requestUrl);
  },
  getNewsFeeds(): Promise<AxiosResponse<INewsFeed[]>> {
    return axiosInstanceNews.get('/api/v1/newsfeeds');
  },

  getContractsIssInfo(): Promise<AxiosResponse<{ key: string }>> {
    return axiosInstanceTD.get('/api/v1/contracts/info');
  },
  getSittingCB() {
    return axiosInstanceTD.get<SittingCB>('/api/v1/data/sittingCB');
  },
};

export default api;

export type Api = typeof api;


import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';

import { indicativeQuotesController } from '@api/controllers/indicativeQuotesController';
import api from '@api/index';

import { candleToBar } from '@utils/candleToBar';

import { ChartIndicativeData } from './types';
import { isIndicativeTicker } from './utils/isIndicativeTicker';
import { transformKeyToLowerCase } from './utils/transformKeyToLowerCase';

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

dayjs.extend(utc);

export interface PeriodParams {
  from: number;
  to: number;
  countBack: number;
  firstDataRequest: boolean;
}
export interface HistoryMetadata {
  noData: boolean;
}
export type HistoryCallback = (candles: Candle[], metadata: HistoryMetadata) => void;
export type SubscribeBarsCallback = (candle: Candle) => void;

interface RequestBarsArgs {
  currencyPair: string;
  interval: string;
  periodParams: PeriodParams;
  onHistoryCallback?: HistoryCallback;
  ticker?: string;
  indicativeData?: ChartIndicativeData;
}

interface RequestRealtimeBarsArgs {
  currencyPair: string;
  interval: string;
  ticker?: string;
  indicativeData?: ChartIndicativeData;
  onRealtimeCallback?: SubscribeBarsCallback;
}

export type CustomBarsResolver = (args: {
  ticker?: string;
  currencyPair: string;
  periodParams: PeriodParams;
  interval: string;
}) => Promise<Candle[]>;

const barsResolvers = new Map<string, CustomBarsResolver>();

export const registerBarsResolver = (boardKey: string, resolver: CustomBarsResolver) => {
  barsResolvers.set(boardKey, resolver);
};

const getResolverKey = (ticker?: string) => {
  const [source, board] = ticker?.split(/[:.]/) ?? [];

  if (!source || !board) {
    return '';
  }

  return `${source}:${board}`;
};

const getBarsResolver = (ticker?: string) => barsResolvers.get(getResolverKey(ticker));

export async function requestBars({
  currencyPair,
  interval,
  periodParams,
  onHistoryCallback,
  ticker,
  indicativeData,
}: RequestBarsArgs): Promise<Candle[]> {
  const customResolver = getBarsResolver(ticker);
  if (customResolver) {
    try {
      const customBars = await customResolver({ ticker, currencyPair, periodParams, interval });
      const toMs = periodParams.to * 1000;
      const olderBars = customBars.filter((bar) => bar.time < toMs);
      onHistoryCallback?.(olderBars, { noData: olderBars.length === 0 });
      return olderBars;
    } catch (e) {
      onHistoryCallback?.([], { noData: true });
      return [];
    }
  }

  const date = new Date(periodParams.to * 1000);
  const year = date.getUTCFullYear();
  const month = `0${date.getUTCMonth() + 1}`.slice(-2);
  const day = `0${date.getUTCDate()}`.slice(-2);
  const hours = `0${date.getUTCHours()}`.slice(-2);
  const minutes = `0${date.getUTCMinutes()}`.slice(-2);
  const seconds = `0${date.getUTCSeconds()}`.slice(-2);

  const hasIndicativeBoardInTicker = isIndicativeTicker(ticker);

  // если при инициализации графика были данные indicativeData и текущий инструмент совпадает
  // то отправляем запрос на индикатив
  // иначе на инструменты
  const isIndicativeInstrument = (indicativeData && indicativeData.key === ticker) || hasIndicativeBoardInTicker;

  if (isIndicativeInstrument) {
    const dateStr = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;

    const lowerCaseKey = transformKeyToLowerCase(currencyPair);

    if (!lowerCaseKey) {
      onHistoryCallback?.([], { noData: true });
      return [];
    }

    try {
      const { data } = await indicativeQuotesController.getCandles({
        count: periodParams.countBack,
        key: lowerCaseKey,
        date: dateStr,
        interval,
      });

      const candles = data.indicativeCandles.reverse().map(candleToBar);

      onHistoryCallback?.(candles, {
        noData: candles.length === 0,
      });

      return candles;
    } catch {
      onHistoryCallback?.([], {
        noData: true,
      });

      return [];
    }
  }

  const dateStr = `${year}-${month}-${day}%20${hours}:${minutes}:${seconds}`;

  try {
    const { data } = await api.getBars({
      currencyPair,
      date: dateStr,
      interval,
      count: periodParams.countBack,
      ticker,
    });

    const bars = data.reverse().map(candleToBar);

    onHistoryCallback?.(bars, {
      noData: bars.length === 0,
    });

    return bars;
  } catch {
    onHistoryCallback?.([], {
      noData: true,
    });

    return [];
  }
}

export async function requestRealtimeBars({
  currencyPair,
  interval,
  ticker,
  onRealtimeCallback,
  indicativeData,
}: RequestRealtimeBarsArgs): Promise<Candle | undefined> {
  if (getBarsResolver(ticker)) {
    return;
  }

  const hasIndicativeBoardInTicker = isIndicativeTicker(ticker);

  // если при инициализации графика были данные indicativeData и текущий инструмент совпадает
  // то отправляем запрос на индикатив
  // иначе на инструменты
  const isIndicativeInstrument = (indicativeData && indicativeData.key === ticker) || hasIndicativeBoardInTicker;

  if (isIndicativeInstrument) {
    const lowerCaseKey = transformKeyToLowerCase(currencyPair);

    if (!lowerCaseKey) {
      return undefined;
    }

    try {
      const { data } = await indicativeQuotesController.getCandles({
        count: 1,
        key: lowerCaseKey,
        date: dayjs().utc().add(1, 'minute').format('YYYY-MM-DDTHH:mm:ss'),
        interval,
      });

      if (data.indicativeCandles.length === 0) {
        return undefined;
      }

      const sortedData = [...data.indicativeCandles].sort(
        (first, second) => new Date(first.end).valueOf() - new Date(second.end).valueOf(),
      );

      const firstCandle = sortedData[0];

      if (!firstCandle) {
        return undefined;
      }

      const bar = candleToBar(firstCandle);

      onRealtimeCallback?.(bar);

      return bar;
    } catch (error) {
      console.error('error from requestRealTimeBars indicativeQuotesController: ', error);

      return undefined;
    }
  }

  try {
    const { data } = await api.getBars({
      currencyPair,
      date: dayjs().utc().add(1, 'minute').format('YYYY-MM-DD%20HH:mm:ss'),
      interval,
      count: 1,
      ticker,
    });

    if (data.length === 0) {
      return undefined;
    }

    const sortedData = [...data].sort(
      (first, second) => new Date(first.end).valueOf() - new Date(second.end).valueOf(),
    );

    const firstCandle = sortedData[0];

    if (!firstCandle) {
      return undefined;
    }

    const bar = candleToBar(firstCandle);

    onRealtimeCallback?.(bar);

    return bar;
  } catch (error) {
    console.error('error from requestRealTimeBars: ', error);

    return undefined;
  }
}



import { IndicativeOptionsResponse } from '@widgets/IndicativeQuotes/types/indicativeQuotesFiltersTypes';
import {
  CreateQuote,
  GetCandlesRequestParams,
  GetCandlesResponse,
  GroupingDataResponse,
  HistoryDataParams,
  HistoryDataResponse,
  IndicativeQuote,
  IndicativeQuotesGroupParams,
  IndicativeQuotesRequestParams,
  UpdateQuote,
} from 'types/IndicativeQuotes/indicativeQuotes';

import { QuotesType } from 'types/Quotes';

import { axiosIndicative, axiosInstanceNTPro, axiosInstanceTD } from '../axios';

import type { AxiosResponse } from 'axios';

const createNewQuote = {
  bid: 0,
  bidYield: 0,
  bidQuantity: 0,
  ask: 0,
  askYield: 0,
  askQuantity: 0,
  secId: '',
  secTypeId: 0,
  settlement: '',
};

const getAxiosInstance = (quotesType: string) => {
  switch (quotesType) {
    case 'MOEX ISS': {
      return axiosInstanceTD;
    }
    case 'NTPRO': {
      return axiosInstanceNTPro;
    }
    case 'IndicativeQuotes': {
      return axiosIndicative;
    }
    default:
      return axiosInstanceTD;
  }
};

export const indicativeQuotesController = {
  /** Получение списка текущих индикативных котировок фирмы пользователя */
  async getUserData(params: IndicativeQuotesRequestParams): Promise<AxiosResponse<{ quotes: IndicativeQuote[] }>> {
    return axiosIndicative.get('/api/v1/quotes/indicative/user', { params });
  },

  async createNewQuotes(params: CreateQuote): Promise<AxiosResponse<{ quotes: IndicativeQuote[] }>> {
    return axiosIndicative.post('/api/v1/quotes/indicative', { ...createNewQuote, ...params });
  },

  /** Получение значений для фильтров */
  async getFilterOptions(): Promise<AxiosResponse<IndicativeOptionsResponse>> {
    return axiosIndicative.get('/api/v1/quotes/indicative/options');
  },

  /** Получение значений для множественных фильтров */
  async getFilterMultipleOptions(): Promise<AxiosResponse<IndicativeOptionsResponse>> {
    return axiosIndicative.post('/api/v1/quotes/indicative/options');
  },

  /** Получение файла шаблона */
  async getFileTemplate(): Promise<AxiosResponse<Blob>> {
    return axiosIndicative.get('/api/v1/quotes/indicative/template', { responseType: 'blob' });
  },

  /** Получение списка текущих индикативных котировок */
  async getGroupedData(params: IndicativeQuotesGroupParams): Promise<AxiosResponse<GroupingDataResponse>> {
    return axiosIndicative.post('/api/v1/quotes/indicative/groups', { ...params });
  },

  /** Обновление */
  async updateQuotes(params: UpdateQuote): Promise<AxiosResponse<{ quotes: IndicativeQuote[] }>> {
    return axiosIndicative.put('/api/v1/quotes/indicative', params);
  },

  /** Исторические данные */
  async getHistoryData(params: HistoryDataParams): Promise<AxiosResponse<HistoryDataResponse>> {
    return axiosIndicative.get('/api/v1/quotes/indicative/history', { params });
  },

  async getCandles(params: GetCandlesRequestParams): Promise<AxiosResponse<GetCandlesResponse>> {
    return axiosIndicative.get('/api/v1/candles/list/offset', { params });
  },

  async removeQuotesData(params: { ids: number[] }) {
    return axiosIndicative.post('/api/v1/quotes/indicative/delete', params);
  },

  async getQuotes(type: string, keys: string[]): Promise<AxiosResponse<QuotesType[]>> {
    const keysWithDots = keys.map((key) => key.replaceAll(':', '.'));

    const axiosInstance = getAxiosInstance(type);
    return axiosInstance.put('/api/v1/quotes', keysWithDots);
  },

  async getQuotesHistory(type: string, keys: string[], date: string): Promise<AxiosResponse<QuotesType[]>> {
    const keysWithDots = keys.map((key) => key.replaceAll(':', '.'));

    const axiosInstance = getAxiosInstance(type);
    return axiosInstance.post('/api/v1/quotes/history', { keys: keysWithDots, date });
  },
};


import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';

import { getStartTime, parseTimeframe } from 'moex-chart';

import {
  getISSOddTimeframePart,
  moexChartTimeConverter,
  moexChartToIssTimeframe,
} from '@utils/chartToReqTimeConverter';
import { DEFAULT_SYMBOL } from '@widgets/Chart/const';

import { requestBars, requestRealtimeBars } from '../../requestBars';
import { ChartIndicativeData } from '../../types';

import type { ManipulateType } from 'dayjs';
import type { Candle, Timeframes } from 'moex-chart';

dayjs.extend(duration);

interface HistoryRequestState {
  untilTime?: number;
  request: Promise<Candle[] | null> | null;
}

function getRequestSymbol(symbolRaw?: string): string | undefined {
  const symbol = String(symbolRaw ?? '').trim();

  if (!symbol || symbol === DEFAULT_SYMBOL) {
    return undefined;
  }

  return symbol;
}

const collectCandleGroup = ({
  data,
  startIndex,
  iterationCounter,
  startTime,
}: {
  data: Candle[];
  startIndex: number;
  iterationCounter: number;
  startTime: number;
}): {
  aggregatedCandle: Candle | undefined;
  consumedCount: number;
} => {
  let aggregatedCandle: Candle | undefined;
  let offset = 0;
  let consumedCount = 0;

  while (offset < iterationCounter) {
    const currentCandle = data[startIndex + offset];

    if (!currentCandle) {
      break;
    }

    if (!aggregatedCandle) {
      aggregatedCandle = currentCandle;
    } else {
      aggregatedCandle = {
        ...aggregatedCandle,
        high: Math.max(currentCandle.high, aggregatedCandle.high),
        low: Math.min(currentCandle.low, aggregatedCandle.low),
        close: currentCandle.close,
        volume: (aggregatedCandle.volume ?? 0) + (currentCandle.volume ?? 0),
      };
    }

    consumedCount += 1;

    if (startIndex + offset + 1 === data.length || currentCandle.time > startTime * 1000) {
      break;
    }

    offset += 1;
  }

  return {
    aggregatedCandle,
    consumedCount,
  };
};

const proceedConvolution = ({
  data,
  moexCandle,
  requestedTimeframe,
  dayjsUnit,
  iterationCounter,
}: {
  data: Candle[];
  moexCandle: number;
  requestedTimeframe: Timeframes;
  dayjsUnit: ManipulateType;
  iterationCounter: number;
}): Candle[] => {
  const result: Candle[] = [];

  let index = 0;

  while (index < data.length - 1) {
    const firstCandle = data[index];

    if (!firstCandle) {
      break;
    }

    const startTime = getStartTime(
      requestedTimeframe,
      dayjs(firstCandle.time).add(moexCandle, dayjsUnit).unix() * 1000,
    );

    const { aggregatedCandle, consumedCount } = collectCandleGroup({
      data,
      startIndex: index,
      iterationCounter,
      startTime,
    });

    if (!aggregatedCandle || consumedCount === 0) {
      break;
    }

    result.push(aggregatedCandle);
    index += consumedCount;
  }

  return result;
};

const timeframeConvolution = (data: Candle[], requestedTimeframe: Timeframes): Candle[] => {
  const issTimeframe = moexChartToIssTimeframe(requestedTimeframe);

  if (issTimeframe === requestedTimeframe) {
    return data;
  }

  const { candleWidth: moexCandle, dayjsUnit } = parseTimeframe(requestedTimeframe);

  const firstCandle = data[0];

  if (!firstCandle) {
    return [];
  }

  const dataStartTime = getStartTime(
    requestedTimeframe,
    dayjs(firstCandle.time).add(moexCandle, dayjsUnit).unix() * 1000,
  );

  const { value, unit } = getISSOddTimeframePart(issTimeframe);

  let startIndex = 0;

  while (
    startIndex < data.length &&
    startIndex < 60 &&
    dataStartTime !== dayjs(data[startIndex].time).subtract(value, unit).unix()
  ) {
    startIndex += 1;
  }

  if (startIndex >= data.length || startIndex >= 60) {
    return [];
  }

  const { candleWidth: issCandle } = parseTimeframe(issTimeframe);

  return proceedConvolution({
    data: data.slice(startIndex),
    moexCandle,
    requestedTimeframe,
    dayjsUnit,
    iterationCounter: moexCandle / issCandle,
  });
};

// По хорошему - класс должен быть синглтоном, чтобы кормить MoexChart одинаковой датой,
// и не плодить несколько подключений на одни символа
class DataSourceProvider {
  private prevRealtimeDataArr: Candle[] = [];

  private prevRealtimeData: Candle | undefined;

  private realtimeShouldBeConvoluted = false;

  private realtimeTimer: ReturnType<typeof setInterval> | null = null;

  private historyRequests = new Map<string, HistoryRequestState>();

  public getDataSource =
    (indicativeData?: ChartIndicativeData, cb?: (timeframe: Timeframes) => void) =>
    async (timeframe: Timeframes, symbolId: string, until?: Candle): Promise<Candle[] | null> => {
      const symbol = getRequestSymbol(symbolId);

      if (!symbol) {
        return null;
      }

      const historyRequestKey = `${symbol}:${timeframe}`;
      const historyRequestState = this.historyRequests.get(historyRequestKey);

      if (historyRequestState && historyRequestState.untilTime === until?.time) {
        if (historyRequestState.request) {
          return historyRequestState.request;
        }

        if (until) {
          return null;
        }
      }

      cb?.(timeframe);

      const historyRequest = this.requestHistoryData({
        timeframe,
        symbol,
        until,
        indicativeData,
      });

      this.historyRequests.set(historyRequestKey, {
        untilTime: until?.time,
        request: historyRequest,
      });

      try {
        const data = await historyRequest;

        return data;
      } finally {
        if (this.historyRequests.get(historyRequestKey)?.request === historyRequest) {
          if (until) {
            this.historyRequests.set(historyRequestKey, {
              untilTime: until.time,
              request: null,
            });
          } else {
            this.historyRequests.delete(historyRequestKey);
          }
        }
      }
    };

  public startRealtime({
    getSymbols,
    getTimeframe,
    update,
    periodMs = 5000,
    indicativeData,
  }: {
    getSymbols: () => string[];
    getTimeframe: () => Timeframes;
    update: (symbolId: string, candle: Candle) => void;
    periodMs?: number;
    indicativeData?: ChartIndicativeData;
  }): () => void {
    if (this.realtimeTimer) {
      clearInterval(this.realtimeTimer);
    }

    this.realtimeTimer = setInterval(() => {
      const timeframe = getTimeframe();
      const symbolIds = getSymbols();

      Promise.all(
        symbolIds.map(async (symbolId) => {
          const symbol = getRequestSymbol(symbolId);

          if (!symbol) {
            return;
          }

          const data = await requestRealtimeBars({
            currencyPair: symbol.replaceAll(':', '.'),
            interval: moexChartTimeConverter(timeframe),
            ticker: symbol,
            indicativeData,
          });

          if (!data) {
            return;
          }

          if (!this.prevRealtimeData) {
            this.prevRealtimeData = data;
            this.prevRealtimeDataArr = [data];
            update(symbol, data);

            return;
          }

          if (JSON.stringify(data) !== JSON.stringify(this.prevRealtimeData)) {
            this.realtimeConvolution(timeframe, data, (candle) => {
              update(symbol, candle);
            });
          }
        }),
      );
    }, periodMs);

    return () => {
      if (this.realtimeTimer) {
        clearInterval(this.realtimeTimer);
      }

      this.realtimeTimer = null;
    };
  }

  private async requestHistoryData({
    timeframe,
    symbol,
    until,
    indicativeData,
  }: {
    timeframe: Timeframes;
    symbol: string;
    until?: Candle;
    indicativeData?: ChartIndicativeData;
  }): Promise<Candle[] | null> {
    const interval = moexChartTimeConverter(timeframe);
    const date = until?.time || Math.round(Date.now() / 1000);

    const data = await requestBars({
      currencyPair: symbol.replaceAll(':', '.'),
      interval,
      periodParams: {
        firstDataRequest: true,
        to: date,
        from: Date.now(),
        countBack: 2000,
      },
      ticker: symbol,
      indicativeData,
    });

    if (data.length === 0) {
      return null;
    }

    const issTimeframe = moexChartToIssTimeframe(timeframe);

    if (issTimeframe === timeframe) {
      this.realtimeShouldBeConvoluted = false;

      return data;
    }

    this.realtimeShouldBeConvoluted = true;

    return timeframeConvolution(data, timeframe);
  }

  private realtimeConvolution(timeframe: Timeframes, data: Candle, update: (candle: Candle) => void): void {
    let candle = data;

    if (this.realtimeShouldBeConvoluted && this.prevRealtimeData) {
      const { candleWidth: moexCandle, dayjsUnit } = parseTimeframe(timeframe);

      const dataStartTimeFromPrevData = getStartTime(
        timeframe,
        dayjs(this.prevRealtimeData.time).add(moexCandle, dayjsUnit).unix() * 1000,
      );

      const dataStartTime = getStartTime(timeframe, dayjs(data.time).add(moexCandle, dayjsUnit).unix() * 1000);

      const isNextTimeframeStep = dataStartTime !== dataStartTimeFromPrevData;

      if (isNextTimeframeStep) {
        this.prevRealtimeDataArr = [data];
      } else {
        const isNewBarInsideTimeframe = this.prevRealtimeData.time !== data.time;

        if (isNewBarInsideTimeframe) {
          this.prevRealtimeDataArr.push(data);
        } else {
          this.prevRealtimeDataArr[this.prevRealtimeDataArr.length - 1] = data;
        }

        const firstCandle = this.prevRealtimeDataArr[0];
        const lastCandle = this.prevRealtimeDataArr[this.prevRealtimeDataArr.length - 1];

        if (firstCandle && lastCandle) {
          candle = {
            time: firstCandle.time,
            open: firstCandle.open,
            high: Math.max(...this.prevRealtimeDataArr.map(({ high }) => high)),
            low: Math.min(...this.prevRealtimeDataArr.map(({ low }) => low)),
            close: lastCandle.close,
            volume: this.prevRealtimeDataArr.reduce((total, currentCandle) => total + (currentCandle.volume ?? 0), 0),
          };
        }
      }
    }

    this.prevRealtimeData = data;

    update(candle);
  }
}

export { DataSourceProvider };


import { Intervals, MoexChart, Timeframes } from 'moex-chart';
import { useCallback, useEffect, useRef, useState } from 'react';

import { useChangeProperties, useSelectProperties } from '@modules/widgetProperties';
import { WidgetProperties } from '@widgets/Chart/properties/types';
import { ChartIndicativeData } from '@widgets/Chart/types';

import { MOEX_CHART_CONFIG } from '../constants';
import { DataSourceProvider } from '../dataSourceProvide';

import type { __CompareManager__, IMoexChart, SymbolInfoInput } from 'moex-chart';

interface TUseMoexChartProps {
  symbolInfo: SymbolInfoInput;
  indicativeData?: ChartIndicativeData;
}

export const useMoexChart = ({ symbolInfo, indicativeData }: TUseMoexChartProps) => {
  const moexChartState = useSelectProperties(
    (widgetProperties: Partial<WidgetProperties>) => widgetProperties.moexChartState,
  );

  const { updateProperties } = useChangeProperties<WidgetProperties>();

  const [isCompareOpen, setIsCompareOpen] = useState(false);
  const [isSymbolSearchOpen, setIsSymbolSearchOpen] = useState(false);

  const containerRef = useRef<HTMLDivElement | null>(null);
  const chartRef = useRef<MoexChart | null>(null);
  const compareManagerRef = useRef<__CompareManager__ | null>(null);

  const currentSymbolInfoRef = useRef<SymbolInfoInput>(symbolInfo);
  const timeframeRef = useRef<Timeframes | undefined>(moexChartState?.timeframe);
  const savedDataRef = useRef<string | undefined>(moexChartState?.savedData);
  const initialIntervalRef = useRef<Intervals | undefined>(moexChartState?.initialInterval);
  const updateTimeframeRef = useRef<((timeframe: Timeframes) => void) | null>(null);

  useEffect(() => {
    const currentSymbolInfo = currentSymbolInfoRef.current;

    if (
      currentSymbolInfo.symbolId === symbolInfo.symbolId &&
      currentSymbolInfo.symbol === symbolInfo.symbol &&
      currentSymbolInfo.symbolName === symbolInfo.symbolName
    ) {
      return;
    }

    currentSymbolInfoRef.current = symbolInfo;
    chartRef.current?.setSymbol(symbolInfo);
  }, [symbolInfo]);

  useEffect(() => {
    timeframeRef.current = moexChartState?.timeframe;
    savedDataRef.current = moexChartState?.savedData;
  }, [moexChartState?.savedData, moexChartState?.timeframe]);

  updateTimeframeRef.current = (timeframe: Timeframes): void => {
    if (timeframeRef.current === timeframe) {
      return;
    }

    timeframeRef.current = timeframe;

    updateProperties((state) => {
      state.moexChartState = {
        ...state.moexChartState,
        timeframe,
      };
    });
  };

  const saveSnapshot = useCallback((): void => {
    const snapshot = chartRef.current?.getSnapshot();

    if (!snapshot) {
      return;
    }

    const savedData = JSON.stringify(snapshot);

    if (savedDataRef.current === savedData) {
      return;
    }

    savedDataRef.current = savedData;

    updateProperties((state) => {
      state.moexChartState = {
        ...state.moexChartState,
        timeframe: timeframeRef.current ?? Timeframes['1m'],
        savedData,
      };
    });
  }, [updateProperties]);

  const getSnapshotWithCurrentSettings = (
    snapshot: typeof MOEX_CHART_CONFIG.snapshot | IMoexChart['snapshot'],
    timeframe: Timeframes,
    initialInterval?: Intervals,
  ): IMoexChart['snapshot'] => ({
    ...snapshot,
    charts: snapshot.charts.map((chartSnapshot) => ({
      ...chartSnapshot,
      ...currentSymbolInfoRef.current,
      timeframe,
      ...(initialInterval ? { interval: initialInterval } : {}),
    })),
  });

  const applySnapshot = (): void => {
    if (!savedDataRef.current || !chartRef.current) {
      return;
    }

    const savedSnapshot = JSON.parse(savedDataRef.current) as IMoexChart['snapshot'];
    const timeframe = timeframeRef.current ?? Timeframes['1m'];

    chartRef.current.setSnapshot(getSnapshotWithCurrentSettings(savedSnapshot, timeframe));
    compareManagerRef.current = chartRef.current.getCompareManager();
  };

  useEffect(() => {
    const container = containerRef.current;

    if (!container) {
      return undefined;
    }

    const timeframe = timeframeRef.current ?? Timeframes['1m'];
    const initialInterval = initialIntervalRef.current;

    const savedSnapshot = savedDataRef.current
      ? (JSON.parse(savedDataRef.current) as IMoexChart['snapshot'])
      : MOEX_CHART_CONFIG.snapshot;

    const dataProvider = new DataSourceProvider();

    const chart = new MoexChart({
      ...MOEX_CHART_CONFIG,
      container,
      snapshot: getSnapshotWithCurrentSettings(savedSnapshot, timeframe, initialInterval),
      chartCollectionPreset: {
        ...MOEX_CHART_CONFIG.chartCollectionPreset,
        openCompareModal: () => setIsCompareOpen(true),
        openSymbolSearchModal: () => setIsSymbolSearchOpen(true),
        getDataSource: dataProvider.getDataSource(indicativeData, (nextTimeframe) => {
          updateTimeframeRef.current?.(nextTimeframe);
        }),
        startRealtime: (getSymbols, getTimeframe, update) =>
          dataProvider.startRealtime({
            getSymbols,
            getTimeframe,
            update,
            indicativeData,
          }),
      },
    });

    chartRef.current = chart;
    compareManagerRef.current = chart.getCompareManager();

    if (initialInterval) {
      initialIntervalRef.current = undefined;

      updateProperties((state) => {
        state.moexChartState = {
          ...state.moexChartState,
          initialInterval: undefined,
        };
      });
    }

    const intervalId = setInterval(() => {
      saveSnapshot();
    }, 1000);

    return () => {
      clearInterval(intervalId);
      saveSnapshot();

      chartRef.current = null;
      compareManagerRef.current = null;
      chart.destroy();
    };

    // eslint-disable-next-line react-hooks/exhaustive-deps -- график пересоздаётся только при изменении источника индикативных данных
  }, [indicativeData?.key]);

  return {
    containerRef,
    isCompareOpen,
    isSymbolSearchOpen,
    compareManagerRef,
    setIsCompareOpen,
    setIsSymbolSearchOpen,
    saveSnapshot,
    applySnapshot,
    hasSavedSnapshot: Boolean(moexChartState?.savedData),
  };
};

import React from 'react';

import { SymbolSearchModal } from '@widgets/Chart/components/MoexChart/components/SymbolSearchModal';

import { CompareModal } from './components/CompareModal';
import { useMoexChart } from './hooks';

import type { ChartIndicativeData, SelectedInstrument } from '../../types';

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

import 'moex-chart/dist/styles.css';

interface TRProps {
  symbolInfo: SymbolInfoInput;
  indicativeData?: ChartIndicativeData;
  widgetId: number;
  addInstrumentFromModal: (instruments: SelectedInstrument[]) => void;
}

function MoexChart({ symbolInfo, indicativeData, widgetId, addInstrumentFromModal }: TRProps) {
  const {
    containerRef,
    isCompareOpen,
    isSymbolSearchOpen,
    compareManagerRef,
    setIsCompareOpen,
    setIsSymbolSearchOpen,
  } = useMoexChart({
    symbolInfo,
    indicativeData,
  });

  const handleSymbolChange = (instrument: SelectedInstrument): void => {
    addInstrumentFromModal([instrument]);
  };

  return (
    <div
      style={{
        flex: '1 1 0',
        minHeight: 0,
        minWidth: 0,
      }}
    >
      <div ref={containerRef} />

      {isCompareOpen && (
        <CompareModal
          compareManager={compareManagerRef}
          widgetId={widgetId}
          isOpen={isCompareOpen}
          setOpen={setIsCompareOpen}
        />
      )}

      {isSymbolSearchOpen && (
        <SymbolSearchModal
          widgetId={widgetId}
          isOpen={isSymbolSearchOpen}
          setOpen={setIsSymbolSearchOpen}
          onSymbolChange={handleSymbolChange}
        />
      )}
    </div>
  );
}

export default React.memo(MoexChart);



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,
    currentSymbolInfo,
    setIsWidgetHeaderContextMenuOpen,
    isWidgetHeaderContextMenuOpen,
    onDropInstrument,
    addInstrumentFromModal,
    isOver,
  } = useChartComponentFacade(props);

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

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

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

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

  const symbolInfo = indicativeData
    ? {
        symbolId: indicativeData.key,
        symbol: indicativeData.secId,
        symbolName: `${indicativeData.instrumentName} ${indicativeData.settlement} - ${indicativeData.firmName}`,
      }
    : currentSymbolInfo;

  const instrumentName = symbolInfo.symbolName || contractsInstrumentName || symbolInfo.symbol || symbolInfo.symbolId;

  return (
    <DNDWrapper
      ref={dropRef}
      isOver={isOver}
      canDrop
    >
      <WidgetHeader
        {...props}
        dropdownOpen={dropDownOpen}
        setDropdownOpen={setDropdownOpen}
        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
            symbolInfo={symbolInfo}
            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>
  );
};


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, SelectedInstrument } from '../types';

import type { SymbolInfoInput } from 'moex-chart';
import type { Dispatch, SetStateAction } from 'react';
import type { Widget } from 'types/Widgets';

interface UseChartComponentFacadeReturn {
  dropDownOpen: boolean;
  setDropdownOpen: Dispatch<SetStateAction<boolean>>;
  currentSymbolInfo: SymbolInfoInput;
  isWidgetHeaderContextMenuOpen: boolean;
  setIsWidgetHeaderContextMenuOpen: Dispatch<SetStateAction<boolean>>;
  onDropInstrument: (symbolId: string, withUpdate?: boolean) => void;
  addInstrumentFromModal: (instruments: SelectedInstrument[]) => 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;

  // TODO временно реф из-за непредсказуемого изменения если используется useState,
  // нужен рефакторинг и вернуть обратно useState
  const currentSymbolInfoRef = useRef<SymbolInfoInput>({
    symbolId: widgetProperties?.chartState?.savedInstrument ?? DEFAULT_SYMBOL,
    symbol: widgetProperties?.chartState?.savedSymbol,
    symbolName: widgetProperties?.chartState?.savedSymbolName,
  });

  const [currentSymbolInfo, setCurrentSymbolInfo] = useState<SymbolInfoInput>(currentSymbolInfoRef.current);

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

  const triggerRelatedWidgetsToUpdateRef = useRef(triggerRelatedWidgetsToUpdate);

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

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

      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,
              ...chartStateValue,
            },
            // при смене инструмента очищаем индикативные данные, переданные из виджета Индикативные котировки
            indicativeData: cleanIndicativeData ? undefined : oldProps.indicativeData,
          },
        }),
      );
    },
    [dispatch, widgetId],
  );

  const onInstrumentChange = useCallback(
    (symbolInfo: SymbolInfoInput, withUpdate?: boolean, unbind = true): void => {
      const symbolId = symbolInfo.symbolId.trim();

      if (!symbolId) {
        return;
      }

      setIsWidgetHeaderContextMenuOpen(false);

      const currentValue = currentSymbolInfoRef.current;
      const symbolIdChanged = currentValue.symbolId !== symbolId;

      const symbol = symbolInfo.symbol?.trim() || (symbolIdChanged ? undefined : currentValue.symbol);
      const symbolName = symbolInfo.symbolName?.trim() || (symbolIdChanged ? undefined : currentValue.symbolName);

      const nextSymbolInfo: SymbolInfoInput = {
        symbolId,
        symbol,
        symbolName,
      };

      const symbolChanged = currentValue.symbol !== symbol;
      const symbolNameChanged = currentValue.symbolName !== symbolName;

      if (!symbolIdChanged && !symbolChanged && !symbolNameChanged) {
        return;
      }

      currentSymbolInfoRef.current = nextSymbolInfo;
      setCurrentSymbolInfo(nextSymbolInfo);

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

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

      if (symbolIdChanged) {
        triggerRelatedWidgetsToUpdateRef.current(symbolId);
      }
    },
    [dispatch, saveContentProps, widgetId],
  );

  const addInstrumentFromModal = useCallback(
    (instruments: SelectedInstrument[]): void => {
      const instrument = instruments[0];

      if (!instrument?.issKey) {
        return;
      }

      onInstrumentChange({
        symbolId: instrument.issKey,
        symbol: instrument.symbol,
        symbolName: instrument.displayName,
      });
    },
    [onInstrumentChange],
  );

  const onInstrumentChangeFromBind = useCallback(
    (symbolInfo: SymbolInfoInput): void => {
      onInstrumentChange(symbolInfo, true, false);
    },
    [onInstrumentChange],
  );

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

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

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

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

        if (symbolId) {
          onInstrumentChange({ symbolId });
        }
      },
    );

    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();
    };
  }, [onInstrumentChange, widgetId]);

  return {
    dropDownOpen,
    setDropdownOpen,
    currentSymbolInfo,
    isWidgetHeaderContextMenuOpen,
    setIsWidgetHeaderContextMenuOpen,
    onDropInstrument,
    addInstrumentFromModal,
    isOver,
  };
}


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

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

import type { WidgetProperties as BaseWidgetProperties } from '@modules/widgetProperties/types';
import type { WidgetContentBasicProps } from 'types/Widgets';

export interface ChartIndicativeData extends BaseWidgetProperties {
  secId: string;
  instrumentName: string;
  settlement: string;
  firmName: string;
  key: string;
}

export interface SelectedInstrument {
  issKey: Contract['issKey'];
  displayName: Contract['displayName'];
  symbol: Contract['symbol'];
}

export type ChartContainerProps = WidgetContentBasicProps<WidgetProperties>;