Merge branch 'develop' into story/TEAMMSBMOB-19579-partner-check

# Conflicts:
#	package-lock.json
#	package.json
#	services/msb-host/public/app-settings.json
#	services/msb-host/src/pages/ServicesPage/assets/index.ts
#	services/msb-host/src/widgets/Sidebar/ui/Sidebar.tsx
This commit is contained in:
Anna Zemlyachenko
2025-10-23 10:24:13 +03:00
1206 changed files with 111047 additions and 29107 deletions
+1
View File
@@ -6,5 +6,6 @@ COPY msb-operations-history /opt/site/msb-operations-history
COPY msb-deposits /opt/site/msb-deposits
COPY msb-payments /opt/site/msb-payments
COPY msb-statements-and-inquiries /opt/site/msb-statements-and-inquiries
COPY msb-treasury-deals /opt/site/msb-treasury-deals
COPY nginx.conf /etc/nginx/nginx.conf
+5 -2
View File
@@ -14,7 +14,7 @@ http {
root /opt/site;
index index.html;
location / {
location / {
root /opt/site/msb-host;
try_files $uri $uri/ /index.html =404;
}
@@ -36,5 +36,8 @@ http {
location /msb-statements-and-inquiries/ {
try_files $uri $uri/ =404;
}
location /msb-treasury-deals/ {
try_files $uri $uri/ =404;
}
}
}
}
+4246 -28903
View File
File diff suppressed because it is too large Load Diff
+16 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@msb/ib-module",
"version": "1.3.0-beta.4",
"version": "1.3.0-beta.5",
"files": [
"msb-host",
"msb-main-page",
@@ -10,6 +10,7 @@
"msb-statements-and-inquiries",
"msb-accounts",
"msb-fea",
"msb-treasury-deals",
"msb-partner-check"
],
"workspaces": [
@@ -31,6 +32,7 @@
"start:sandbox:payments": "lerna run start:sandbox --scope=msb-host --stream & lerna run start --scope=msb-payments --stream",
"start:accounts": "lerna run start --scope=msb-host --scope=msb-accounts --stream",
"start:sandbox:accounts": "lerna run start:sandbox --stream & lerna run start --scope=msb-host --scope=msb-accounts --stream",
"start:sandbox:treasury-deals": "lerna run start:sandbox --scope=msb-host --stream & lerna run start --scope=msb-treasury-deals --stream",
"start:host": "lerna run start --scope=msb-host --stream",
"start:sandbox:host": "lerna run start:sandbox --scope=msb-host --stream",
"start:fea": "lerna run start --scope=msb-host --scope=msb-fea --stream",
@@ -91,7 +93,19 @@
"@fractal-ui/extended": "30.15.0",
"@fractal-ui/library": "31.4.0",
"@fractal-ui/overlays": "30.15.0",
"@fractal-ui/styling": "30.14.2"
"@fractal-ui/styling": "30.14.2",
"react-circular-progressbar": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"react-smooth-dnd": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"react-texty": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
},
"dependencies": {
"@babel/core": "^7.28.0",
@@ -4,6 +4,7 @@ import type { ActivityFeedPageRecord } from '../endpoints';
const useOperationHistory = (before: Date = new Date(), after: Date | null = null, limit: number = 4, delta: number = 0) => {
const afterDate = after || new Date(before);
const twoSeconds = 2000;
if (after === null) {
afterDate.setMonth(afterDate.getMonth() - 1);
@@ -11,6 +12,7 @@ const useOperationHistory = (before: Date = new Date(), after: Date | null = nul
}
return useInfiniteQuery<ActivityFeedPageRecord[], Error | undefined>({
cacheTime: twoSeconds,
queryKey: [QUERY_KEYS_OPERATIONS_HISTORY, before, afterDate, limit],
queryFn: ({
pageParam = {
@@ -1,5 +1,6 @@
interface RemoteRouteConfigBase {
id: number | string;
hidden: boolean;
moduleName: string;
path: string;
ymCode: string;
@@ -40,6 +40,7 @@ type Authority =
| 'RUBLE_PAYMENT_ORDER.VIEW'
| 'RUBLE_PAYMENT_ORDER.COPY'
| 'RUBLE_PAYMENT_ORDER.EDIT'
| 'RUBLE_PAYMENT_ORDER.DELETE'
| 'TREASURY_GENERAL_AGREEMENTS_COMMON'; // если возможны другие значения
type ClientAuthoritiesDto = Record<string, Authority[]>;
+1 -1
View File
@@ -95,7 +95,7 @@ const TOKEN_KEY = 'token';
const ACCESS_TOKEN_KEY = 'access_token';
const REFRESH_TOKEN_KEY = 'refresh_token';
const SETTINGS_CONFIG_ERROR = 'Что-то пошло не так';
const defaultTimeout = 10_000;
const defaultTimeout = 30_000;
const XSRF_HEADER_NAME = 'x-xsrf-token';
const MOCK_SERVER_API_PATH_KEY = 'mock_server_api_path';
const FROM_LOGIN = 'fromLogin';
@@ -1,5 +1,5 @@
import { GET_BEST_RATES_MSB_HANDLER } from './showcase';
import { GET_BEST_RATES_MSB_HANDLER, GET_BEST_RATES_MSB_WITH_ID_HANDLER } from './showcase';
const showcaseHandlers = [GET_BEST_RATES_MSB_HANDLER];
const showcaseHandlers = [GET_BEST_RATES_MSB_HANDLER, GET_BEST_RATES_MSB_WITH_ID_HANDLER];
export { showcaseHandlers };
@@ -1,7 +1,7 @@
import type { GetBestRatesMSBDto } from '@msb/http/treasury-deals-client/endpoints';
const amountFrom = '2000000.01';
const clientId = '64b3578f-eaa2-4367-90f2-98965b589262';
const clientId = '871610b3-35f8-4eea-9eeb-01b3693a3e75';
const GET_BEST_RATES_MOCK: GetBestRatesMSBDto = {
data: [
@@ -11,7 +11,7 @@ const GET_BEST_RATES_MOCK: GetBestRatesMSBDto = {
amountFrom,
periodFrom: 1,
periodTo: 1,
clientId: '1814d014-0bd1-40f1-9a2a-9653bda2ee74',
clientId,
},
{
dealType: 'DEPOSIT',
@@ -19,7 +19,7 @@ const GET_BEST_RATES_MOCK: GetBestRatesMSBDto = {
amountFrom,
periodFrom: 121,
periodTo: 150,
clientId: '173ed58e-c8ad-45d8-a9c1-2e16b380363b',
clientId,
},
{
dealType: 'MNO',
@@ -27,9 +27,39 @@ const GET_BEST_RATES_MOCK: GetBestRatesMSBDto = {
amountFrom,
periodFrom: 151,
periodTo: 180,
clientId: '64b3578f-eaa2-4367-90f2-98965b589262',
clientId,
},
],
};
export { GET_BEST_RATES_MOCK };
const GET_BEST_RATES_WITH_ID_MOCK: GetBestRatesMSBDto = {
data: [
{
dealType: 'OVERNIGHT',
value: '15.10',
amountFrom,
periodFrom: 1,
periodTo: 1,
// eslint-disable-next-line sonarjs/no-duplicate-string
clientId: '1814d014-0bd1-40f1-9a2a-9653bda2ee74',
},
{
dealType: 'DEPOSIT',
value: '29.90',
amountFrom,
periodFrom: 189,
periodTo: 210,
clientId: '1814d014-0bd1-40f1-9a2a-9653bda2ee74',
},
{
dealType: 'MNO',
value: '28.10',
amountFrom,
periodFrom: 121,
periodTo: 150,
clientId: '1814d014-0bd1-40f1-9a2a-9653bda2ee74',
},
],
};
export { GET_BEST_RATES_MOCK, GET_BEST_RATES_WITH_ID_MOCK };
@@ -1,9 +1,13 @@
import { GET_BEST_RATES_MSB, type GetBestRatesMSBDto } from '@msb/http/treasury-deals-client/endpoints';
import { rest } from 'msw';
import { GET_BEST_RATES_MOCK } from './mocks';
import { GET_BEST_RATES_MOCK, GET_BEST_RATES_WITH_ID_MOCK } from './mocks';
const GET_BEST_RATES_MSB_HANDLER = rest.get<never, never, GetBestRatesMSBDto>(GET_BEST_RATES_MSB, (_, res, ctx) =>
res(ctx.json(GET_BEST_RATES_MOCK))
);
export { GET_BEST_RATES_MSB_HANDLER };
const GET_BEST_RATES_MSB_WITH_ID_HANDLER = rest.get<never, never, GetBestRatesMSBDto>(`${GET_BEST_RATES_MSB}/:id`, (_, res, ctx) =>
res(ctx.json(GET_BEST_RATES_WITH_ID_MOCK))
);
export { GET_BEST_RATES_MSB_HANDLER, GET_BEST_RATES_MSB_WITH_ID_HANDLER };
@@ -225,6 +225,7 @@ const USER_AUTHORITIES_MOCK: AuthoritiesResponseDto = {
'RUBLE_PAYMENT_ORDER.VIEW',
'RUBLE_PAYMENT_ORDER.CREATE',
'RUBLE_PAYMENT_ORDER.EDIT',
'RUBLE_PAYMENT_ORDER.DELETE',
'RUBLE_PAYMENT_ORDER.SEND',
'RUBLE_PAYMENT_ORDER.SIGN',
'RUBLE_PAYMENT_ORDER.COPY',
@@ -352,7 +352,7 @@ const TREASURY_COMMON_CLIENT_USER_INFO_UNION_MOCK: TreasuryCommonClientUserInfoU
],
accountsExist: true,
accountsPrivilegeLevel: 'EDIT',
generalAgreementsExist: false,
generalAgreementsExist: true,
generalAgreementsDUExist: false,
generalAgreementsPrivilegeLevel: 'EDIT',
gsnoReportsPrivilegeLevel: 'NONE',
@@ -423,7 +423,7 @@ const TREASURY_COMMON_CLIENT_USER_INFO_UNION_MOCK: TreasuryCommonClientUserInfoU
],
accountsExist: true,
accountsPrivilegeLevel: 'EDIT',
generalAgreementsExist: false,
generalAgreementsExist: true,
generalAgreementsDUExist: false,
generalAgreementsPrivilegeLevel: 'EDIT',
gsnoReportsPrivilegeLevel: 'NONE',
@@ -0,0 +1,84 @@
// Реэкспорт для обратной совместимости с ECO кодом
// @eco/file-manager -> @msb/platform-compat/file-manager
import { showFile } from '@msb/shared/lib/files/showFile';
import { request } from '@msb/platform-compat/services/common';
/**
* Сохранить файл (алиас для showFile)
* @param content - содержимое файла (ArrayBuffer, Blob, Uint8Array или base64 string)
* @param fileName - имя файла
* @param mimeType - MIME тип файла
*/
export const saveFile = (content: ArrayBuffer | Blob | Uint8Array | string, fileName: string, mimeType: string) => {
showFile(content, fileName, mimeType);
};
/**
* Скачать файл в формате base64
* @param attachmentId - ID вложения
* @param accessToken - токен доступа (опционально)
* @returns Promise с данными файла
*/
const downloadBase64 = async (attachmentId: string, accessToken?: string): Promise<{ data: ArrayBuffer; fileName?: string; mimeType?: string }> => {
const url = accessToken
? `/filestorage/external/download/base64?attachmentId=${attachmentId}&accessToken=${accessToken}`
: `/filestorage/external/download/base64?attachmentId=${attachmentId}`;
const response = await request({
url,
method: 'POST',
data: { attachmentId },
});
const base64Data = response.data?.data?.content || response.data?.content;
if (!base64Data) {
throw new Error('Не удалось загрузить файл');
}
// Конвертируем base64 в ArrayBuffer
const binaryString = atob(base64Data);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return {
data: bytes.buffer,
fileName: response.data?.data?.fileName || response.data?.fileName,
mimeType: response.data?.data?.mimeType || response.data?.mimeType,
};
};
/**
* Скачать файл (алиас для downloadBase64 с совместимым форматом)
*/
const download = async (attachmentId: string, accessToken?: string): Promise<{ data: ArrayBuffer; fileName?: string; type?: string }> => {
const result = await downloadBase64(attachmentId, accessToken);
return {
data: result.data,
fileName: result.fileName,
type: result.mimeType,
};
};
// Экспорт для admin и client
export const attachmentServiceClient = {
downloadBase64,
download,
saveFile,
showFile,
};
export const attachmentServiceAdmin = {
downloadBase64,
download,
saveFile,
showFile,
};
export { showFile };
+9
View File
@@ -0,0 +1,9 @@
export * from './services/common';
export * from './services/client';
export * from './services/admin';
export * from './services/core';
export * from './tools/localization';
export * from './ui';
@@ -0,0 +1,94 @@
import { LOCALE_CODE } from './constants';
import type { ITranslations, IConfig, Loader } from './interfaces';
const config: IConfig = {
locale: '',
areas: {},
};
let localeChangeObservers: Array<(locale: string) => void> = [];
/**
* Возвращает текущую локаль.
*/
export const getLocale = () => config.locale;
/**
* Возвращает локализацию.
* @param area Наименование локализации.
*/
export const getTranslationsByArea = (area: string): ITranslations => (config.areas[area] || {}).localeValues;
/**
* Загружает данный в локализацию.
* @param area Наименование локализации.
*/
const loadTranslations = (area: string) => {
const translations = config.areas[area].loader(getLocale(), area);
if (translations.then && typeof translations.then === 'function') {
return (translations as Promise<ITranslations>).then(result => {
config.areas[area].localeValues = result;
return result;
});
}
config.areas[area].localeValues = translations as ITranslations;
return Promise.resolve(translations);
};
/**
* Устанавливает язык.
* @param locale Язык.
*/
export const setLocale = (locale: string) => {
if (!locale) {
throw new Error(`setLocale: locale have to be defined but got ${locale}`);
}
config.locale = locale;
localeChangeNotify(locale);
return Promise.all(Object.keys(config.areas).map(area => loadTranslations(area)));
};
/**
* Устанавливает локализацию из config`a.
* @param area Наименование локализации.
*/
export const registerArea = (area: string, loader: Loader) => {
config.areas[area] = {
loader,
localeValues: {},
};
return loadTranslations(area);
};
/**
* Удаляет локализацию из config`a.
* @param area Наименование локализации.
*/
export const unregisterArea = (area: string) => {
delete config.areas[area];
};
export const localeChangeSubscribe = (func: (locale: string) => void) => {
localeChangeObservers.push(func);
return () => localeChangeUnsubscribe(func);
};
const localeChangeUnsubscribe = (func: (locale: string) => void) => {
const initialLength = localeChangeObservers.length;
localeChangeObservers = localeChangeObservers.filter(subscriber => subscriber !== func);
return initialLength !== localeChangeObservers.length;
};
const localeChangeNotify = (locale: string) => {
localeChangeObservers.forEach(observer => observer(locale));
};
setLocale(LOCALE_CODE.RU);
@@ -0,0 +1,17 @@
export const LOCALE_CODE = {
RU: 'ru',
EN_US: 'en-us',
};
export const SIZE_DESIGNATIONS = {
[LOCALE_CODE.RU]: {
MB: 'Мб',
KB: 'Кб',
B: 'Б',
},
[LOCALE_CODE.EN_US]: {
MB: 'Mb',
KB: 'Kb',
B: 'B',
},
};
export const SIZE_DECIMAL = 2;
@@ -0,0 +1,147 @@
import { getLocale } from './config';
import { SIZE_DESIGNATIONS } from './constants';
import { truncated } from './utils';
/**
* Возвращает строку в формате счёта.
* @param code Строка для форматирования.
* @param locale Локализация форматирования (не используется).
* @example
* // return 40817.810.0.99910004312
* formatAccountCode('40817810099910004312', 'ru')
* @returns Строка в формате счёта.
*/
export const formatAccountCode = (code: string) => {
if (!code) {
return '';
}
if (typeof code !== 'string') {
throw new Error('code have to be a string');
}
let c = code;
if (code.includes('.')) {
return code;
}
if (code.length < 20) {
let addition = '';
for (let i = 0; i < 20 - code.length; i += 1) {
addition += '0';
}
c = addition + c;
}
return `${c.substr(0, 5)}.${c.substr(5, 3)}.${c.substr(8, 1)}.${c.substr(9)}`;
};
/**
* Возвращает число Мбайт.
* @param byte Число байт.
* @param locale Локаль для форматирования.
* @example
* formatSizeMb('1572864', 'ru') // return 1.5 Мб
* formatSizeMb('1572864', 'en-us') // return 1.5 Kb
* @returns Строка с числом Мбайт.
*/
export const formatSizeMb = (byte: number | string, locale: string = getLocale()) => {
if (!byte) {
return '0';
}
const byteSize = Number(byte);
if (!byteSize) {
return '';
}
return `${truncated(byteSize / Math.pow(1024, 2))} ${SIZE_DESIGNATIONS[locale].MB}`;
};
/**
* Возвращает число Кбайт.
* @param byte Число байт.
* @param locale Локаль для форматирования.
* @example
* formatSizeKb('2560', 'ru') // return 2.5 Кб
* formatSizeKb('2560', 'en-us') // return 2.5 Kb
* @returns Строка с числом Кбайт.
*/
export const formatSizeKb = (byte: number | string, locale: string = getLocale()) => {
if (!byte) {
return '0';
}
const byteSize = Number(byte);
if (!byteSize) {
return '';
}
return `${truncated(Number(byte) / 1024)} ${SIZE_DESIGNATIONS[locale].KB}`;
};
/**
* Возвращает число Кбайт или Мбайт в зависимостит от принимаемого кол-ва байт,
* если меньше 1 Мбайта то выводится число Кбайт.
* @param byte Число байт.
* @param locale Локаль для форматирования.
* @example
* formatSize('1572864', 'ru') // return 1.5 Мб
* formatSize('1572864', 'en-us') // return 1.5 Mb
* formatSize('1523', 'ru') // return 1,48 Кб
* formatSize('15', 'ru') // return 15 Б
* @returns Строка с числом Кбайт или Мбайт в зависимостит от принимаемого кол-ва байт.
*/
export const formatSize = (byte: number | string, locale: string = getLocale()) => {
if (!byte) {
return '0';
}
const MB = Math.pow(1024, 2);
const byteSize = Number(byte);
if (!byteSize) {
return '';
}
if (byteSize >= MB) {
return formatSizeMb(byte, locale);
}
if (byteSize >= 1024) {
return formatSizeKb(byte, locale);
}
return `${byteSize} ${SIZE_DESIGNATIONS[locale].B}`;
};
type Formatter = (value: any, locale: string) => string;
/**
* Зарегистрированные форматеры.
*/
const formatters: Record<string, Formatter> = {};
/**
* Регистрирует форматер.
* @param formatterName {string} наименование форматера.
* @param formater {any} - функция форматера.
*/
export const registerFormatter = (formatterName: string, formater: Formatter) => {
formatters[formatterName] = formater;
};
/**
* Поиск зарегистрированного форматера.
* @param formatterName {string} наименование форматера.
*/
export const getFormatterByName = (formatterName: string) => formatters[formatterName];
registerFormatter('account', formatAccountCode);
registerFormatter('sizemb', formatSizeMb);
registerFormatter('sizekb', formatSizeKb);
registerFormatter('size', formatSize);
@@ -0,0 +1,4 @@
export * from './translate';
export * from './formatters';
export * from './config';
export * from './interfaces';
@@ -0,0 +1,13 @@
export type ITranslations = Record<string, Record<string, string> | string>;
export type Loader = (locale: string, area: string) => ITranslations | Promise<ITranslations>;
export interface IAreas {
loader: Loader;
localeValues: ITranslations;
}
export interface IConfig {
areas: Record<string, IAreas>;
locale: string;
}
@@ -0,0 +1,96 @@
import { getTranslationsByArea, getLocale } from './config';
import { getFormatterByName } from './formatters';
import { findThrulyExpression } from './utils';
const formatRegexp = /{([\w ,.]*)}/g;
export const format = (template: string, data: any, throwIfDataNotFound: boolean = false) => {
if (typeof template !== 'string') {
throw new Error("format: input value isn't string!");
}
return template.replace(formatRegexp, (_, params) => {
// В params будет приходит строка содержащая текст между фигурными скобками типа { paramName, formatterName }
// paramName = название параметра
// formatterName = название форматера (не обязательный).
// получаем массив [paramName, formatterName] из строки.
const param = params.replace(/ /g, '').split(',');
// получаем paramName
const key = param.shift();
// получаем formatterName
const formatterName = param.shift();
const keys = key.split('.');
const paramKey = keys.shift();
let v = data[paramKey];
for (let i = 0, l = keys.length; i < l; i += 1) {
v = v[keys[i]];
}
if (typeof v !== 'undefined' && v !== null) {
if (formatterName) {
const f = getFormatterByName(formatterName);
if (f) {
return f(v, getLocale());
}
console.log(`Formatter '${formatterName}' not found.`);
}
return v;
}
if (throwIfDataNotFound) {
throw new Error(`Param '${paramKey}' not found in params obj '${JSON.stringify(data)}' for template '${template}'`);
}
return '';
});
};
/**
* Возвращает строку локализации.
* @param area Наименование области локализации.
* @param key Ключ для поиска локализации в области area.
* @param params Параметры для вставки значения в локализацию.
* @returns Строка локализации.
*/
export const translate = (area: string, key: string, params?: Record<string, number | string>): string => {
const areaValues = getTranslationsByArea(area);
if (areaValues === undefined) {
throw new Error(`Translate error: area ${area} not found! Key: ${key}`);
}
const valueOrConfig = areaValues[key];
if (valueOrConfig === undefined) {
throw new Error(`value by key ${key} in area ${area} not found!`);
}
if (typeof valueOrConfig === 'string') {
return format(valueOrConfig, params);
}
if (typeof valueOrConfig === 'object' && valueOrConfig !== null && !Array.isArray(valueOrConfig)) {
if (!params) {
throw new Error(`Translate error: params have to be defined`);
}
const trueKey = findThrulyExpression(params, Object.keys(valueOrConfig));
if (!trueKey) {
const p1 = 'Translate error: at least one case should return true but all cases returned false,';
const p2 = ` area ${area}, key ${key}, params: ${JSON.stringify(params)}`;
throw new Error(`${p1}${p2}`);
}
return format(valueOrConfig[trueKey], params);
}
throw new Error(`Translate error: unknown value by key ${key} in area ${area}: ${valueOrConfig}`);
};
@@ -0,0 +1,24 @@
import { SIZE_DECIMAL } from './constants';
export const truncated = (num: number, decimal: number = SIZE_DECIMAL) => {
const numConverter = Math.pow(10, decimal);
return Math.trunc(num * numConverter) / numConverter;
};
export const getParamsStr = (params: Record<string, any>) =>
Object.keys(params)
.map(key => `var ${key} = ${JSON.stringify(params[key])};`)
.join('');
export const runExpression = (paramsStr: string, body: string) => {
const expression = `${paramsStr} return ${body};`;
return new Function(expression)();
};
export const findThrulyExpression = (params: Record<string, any>, expressions: string[]) => {
const vars = getParamsStr(params);
return expressions.find((key: string) => runExpression(vars, key));
};
+15
View File
@@ -0,0 +1,15 @@
{
"name": "@msb/platform-compat",
"version": "0.0.1",
"main": "index.ts",
"types": "index.ts",
"files": ["index.ts", "services", "tools", "ui", "ui.tsx", "file-manager"],
"dependencies": {
"@msb/http": "^1.0.0",
"@emotion/react": "11.8.1",
"bignumber.js": "^9.0.0"
}
}
@@ -0,0 +1,14 @@
// Minimal stubs to satisfy migrated imports from @platform/services/admin
import React from 'react';
export const MainLayout: React.FC<any> = ({ children }) => React.createElement('div', null, children);
export const useUser = () => ({ user: { firstName: '', secondName: '', patronymic: '' } });
export const SORT_DIRECTION = { ASC: 'ASC', DESC: 'DESC' } as const;
export const getRowScrollerPage = (_: any) => ({ page: 0, size: 20 });
export const uaaService = {
clientUser: {
getByUaaUserId: async (_: any) => ({ fio: '', email: '' }),
},
} as const;
export const metadataToRequestParams = (meta?: any) => ({ params: meta?.params ?? undefined });
+168
View File
@@ -0,0 +1,168 @@
// Реэкспорт для обратной совместимости с ECO кодом
// @platform/services/client -> @msb/platform-compat/services/client
import React from 'react';
import { useAppContext } from '@msb/shared';
import { cloudService } from '@msb/crypto/sign/services/cloud';
import { olkCryptoModule } from '@msb/crypto';
import type { ICryptoModule as IMSBCryptoModule, IBaseSignedDocument } from '@msb/crypto';
// Crypto exports
export { CryptoInstallerModal, ERROR } from '@msb/crypto';
export type { IBaseSignedDocument } from '@msb/crypto';
/**
* ECO-совместимый интерфейс криптомодуля.
* MSB использует batchSign (массив), ECO ожидает sign (одна строка).
*/
export interface ICryptoModule extends IMSBCryptoModule {
sign(thumbprint: string, data: string, options?: any): Promise<string | undefined>;
}
/**
* Адаптер MSB olkCryptoModule для ECO API.
* Преобразует sign() -> batchSign([...])[0]
*/
export const getCryptoModule = (): ICryptoModule => ({
...olkCryptoModule,
sign: async (thumbprint: string, data: string, options?: any) => {
const signatures = await olkCryptoModule.batchSign(
thumbprint,
[data],
options?.sessId,
options?.detached,
options?.calculatedHash,
options?.signType
);
return signatures?.[0];
},
});
// Token parsing
export { parseToken } from '@msb/http';
// Verify sign exports
export { useVerifySign, VerifySignModal } from '@msb/crypto';
export type { DocNameGetter, AttachmentNameGetter } from '@msb/crypto';
// Scope
export { isClientScope, SCOPE, setCryptoScope } from '@msb/crypto/core/utils/scope';
// Sign kinds
export { SIGN_KIND } from '@msb/crypto/core/stream-constants/dst';
// User & App Config
export { useUser, FractalThemeContext } from './common';
export { useAppContext as useAppConfig } from '@msb/shared';
// Auth hook (совместимость с ECO)
export const useAuth = () => {
const context = useAppContext();
return React.useMemo(
() => ({
isAuthenticated: !!context.userProfile,
user: context.userProfile ?? null,
authorities: (context.userAuthorities as any)?.authorities ?? [],
tokenString: '', // TODO: получать из localStorage или другого источника
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
}),
[context.userProfile, context.userAuthorities]
);
};
// Cloud Crypto - облачная подпись из MSB
export const getCloudCrypto = () => {
return cloudService;
};
// Валидаторы для форм (совместимость с ECO)
export const validators = {};
export const err = {
notEmpty: (message: string) => (value: any) => {
if (!value || (typeof value === 'string' && !value.trim())) {
return message;
}
return undefined;
},
decimalMin: (min: number, inclusive: boolean, message: string) => (value: any) => {
const num = parseFloat(value);
if (isNaN(num)) {
return message;
}
if (inclusive ? num < min : num <= min) {
return message;
}
return undefined;
},
decimalMax: (max: number, inclusive: boolean, message: string) => (value: any) => {
const num = parseFloat(value);
if (isNaN(num)) {
return message;
}
if (inclusive ? num > max : num >= max) {
return message;
}
return undefined;
},
pattern: (regex: RegExp, message: string) => (value: any) => {
if (value && !regex.test(String(value))) {
return message;
}
return undefined;
},
required: (message: string) => (value: any) => {
if (!value && value !== 0) {
return message;
}
return undefined;
}
};
// Additional ECO compatibility exports
export interface IDstForSign {
id: string;
name?: string;
[key: string]: any;
}
export interface RenderDocument<T = any> {
(doc: T): React.ReactNode;
}
export interface ICollectionResponse<T = any> {
data: T[];
meta?: IMetaData;
}
export interface IMetaData {
total?: number;
page?: number;
size?: number;
[key: string]: any;
}
export const certificateService = {
getCertificates: () => Promise.resolve([]),
getActiveCertificate: () => Promise.resolve(null),
};
export interface IUserData {
userId?: string;
firstName?: string;
lastName?: string;
patronymic?: string;
email?: string;
[key: string]: any;
}
export interface ICloudSign {
enabled?: boolean;
[key: string]: any;
}
@@ -0,0 +1,50 @@
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
import React from 'react';
import { ThemeContext as EmotionThemeContext } from '@emotion/react';
import { network } from '@msb/http';
import { useAppContext } from '@msb/shared';
type RequestConfig = AxiosRequestConfig;
const request = (config: RequestConfig): Promise<AxiosResponse> => {
// Удаляем префикс /api из URL, так как network.client уже имеет baseURL='/api'
if (config.url && config.url.startsWith('/api')) {
config = { ...config, url: config.url.replace(/^\/api/, '') };
}
return network.client.request(config);
};
const COMMON_STREAM_URL = {
CERT_ENROLL: '/cert-enroll',
USER: '/user',
} as const;
// Stubs/compat for eco platform API used in migrated code
const metadataToRequestParams = (meta?: any) => ({ params: meta?.params ?? undefined });
const action = (..._args: any[]) => undefined;
const getAppConfigItem = (_key: string) => undefined;
const FractalThemeContext = EmotionThemeContext as unknown as React.Context<any>;
// User hooks
const useUser = () => {
const context = useAppContext();
return React.useMemo(() => context.userProfile, [context.userProfile]);
};
// Bank client interface (stub)
export interface IBankClient {
id?: string;
name?: string;
[key: string]: any;
}
// Create context utility (stub for ECO compatibility)
export const createContext = (config?: any) => ({
sign: config?.sign ?? {},
certificateUserService: null,
});
export { request, COMMON_STREAM_URL, metadataToRequestParams, action, getAppConfigItem, FractalThemeContext, useUser };
+38
View File
@@ -0,0 +1,38 @@
// Minimal core
export const to = async <T>(p: Promise<T>): Promise<[any, T | undefined]> => {
try {
const result = await p;
return [null, result];
} catch (e) {
return [e, undefined];
}
};
export type FieldRule = { field: string; validators: Array<(value: any, values?: any) => string | undefined | null | boolean> };
export type Validator = (values: any) => Record<string, string>;
export const check = (field: string) => ({
on: (...validators: FieldRule['validators']): FieldRule => ({ field, validators }),
});
export const composeValidation = (...rules: Array<FieldRule | undefined | null>): ((values: any) => Record<string, string>) => {
const normalized = rules.filter(Boolean) as FieldRule[];
return (values: any) => {
const errors: Record<string, string> = {};
for (const rule of normalized) {
const value = values?.[rule.field];
for (const v of rule.validators) {
const res = v(value, values);
if (typeof res === 'string' && res) {
errors[rule.field] = res;
break;
}
}
}
return errors;
};
};
export const createValidator = (composed: ((values: any) => Record<string, string>) | any, _validatorsLib?: any) => {
const validate = typeof composed === 'function' ? composed : () => ({}) as any;
return { validate };
};
@@ -0,0 +1,5 @@
// Реэкспорт для обратной совместимости с ECO кодом
// @platform/tools/big-number -> @msb/platform-compat/tools/big-number
export { BigNumber } from 'bignumber.js';
+58
View File
@@ -0,0 +1,58 @@
// Реэкспорт для обратной совместимости с ECO кодом
// @platform/core -> @msb/platform-compat/tools/core
// Утилита для безопасного выполнения промисов
export const to = async <T,>(promise: Promise<T>): Promise<[Error | null, T | null]> => {
try {
const data = await promise;
return [null, data];
} catch (error) {
return [error as Error, null];
}
};
// Валидация форм
export type FieldRule = {
field: string;
validators: Array<(value: any, values?: any) => string | undefined | null | boolean>
};
export type Validator = (values: any) => Record<string, string>;
export const check = (field: string) => ({
on: (...validators: FieldRule['validators']): FieldRule => ({ field, validators }),
});
export const composeValidation = (...rules: Array<FieldRule | undefined | null>): ((values: any) => Record<string, string>) => {
const normalized = rules.filter(Boolean) as FieldRule[];
return (values: any) => {
const errors: Record<string, string> = {};
for (const rule of normalized) {
const value = values?.[rule.field];
for (const v of rule.validators) {
const res = v(value, values);
if (typeof res === 'string' && res) {
errors[rule.field] = res;
break;
}
}
}
return errors;
};
};
export const createValidator = (composed: ((values: any) => Record<string, string>) | any, _validatorsLib?: any) => {
const validate = typeof composed === 'function' ? composed : () => ({}) as any;
return { validate };
};
// Create executer utility (stub for ECO compatibility)
export const createExecuter = (_context?: any) => {
return {
execute: async (_params?: any) => {
// Заглушка для execute
return Promise.resolve();
},
};
};
+431
View File
@@ -0,0 +1,431 @@
import React from 'react';
import type { ReactNode, FC, ReactElement } from 'react';
import { BaseDialog } from '@msb/shared/ui/modals/BaseDialog';
// Local minimal icon set to avoid circular deps with @eco icons
const DummyIcon: FC<any> = () => null;
const WdIcons: Record<string, FC<any>> = {
Add: DummyIcon,
Print24: DummyIcon,
Export24: DummyIcon,
RollBack24: DummyIcon,
CheckMark24: DummyIcon,
Delete24: DummyIcon,
ChevronLeft24: DummyIcon,
ChevronDown24: DummyIcon,
update: DummyIcon,
Info24: DummyIcon,
View24: DummyIcon,
Cancel24: DummyIcon,
};
export type GapCompound = FC<any> & {
XS: FC<any>;
X2S: FC<any>;
X3L?: FC<any>;
SM: FC<any>;
LG: FC<any>;
X2L: FC<any>;
XL: FC<any>;
};
const GapBase: FC<any> = props => <div style={{ height: 12 }} {...props} />;
const GapXS: FC<any> = props => <div style={{ height: 8 }} {...props} />;
const GapX2S: FC<any> = props => <div style={{ height: 6 }} {...props} />;
const GapSM: FC<any> = props => <div style={{ height: 12 }} {...props} />;
const GapLG: FC<any> = props => <div style={{ height: 20 }} {...props} />;
const GapX2L: FC<any> = props => <div style={{ height: 28 }} {...props} />;
const GapXL: FC<any> = props => <div style={{ height: 24 }} {...props} />;
const GapX3L: FC<any> = props => <div style={{ height: 36 }} {...props} />;
export const Gap = Object.assign(GapBase, { XS: GapXS, X2S: GapX2S, SM: GapSM, LG: GapLG, X2L: GapX2L, XL: GapXL, X3L: GapX3L }) as GapCompound;
export interface TypographyCompound { P: FC<any>; PBold: FC<any>; H2: FC<any>; SmallText: FC<any>; }
type TextComponents = TypographyCompound & { Text: FC<any>; TextBold: FC<any>; H3: FC<any>; Caption: FC<any>; P2: FC<any>; H2Regular: FC<any> };
export const Typography = {
P: (props: any) => <p {...props} />,
P2: (props: any) => <p {...props} />,
PBold: (props: any) => <p style={{ fontWeight: 600 }} {...props} />,
H2: (props: any) => <h2 {...props} />,
H2Regular: (props: any) => <h2 {...props} />,
H3: (props: any) => <h3 {...props} />,
SmallText: (props: any) => <small {...props} />,
Caption: (props: any) => <small {...props} />,
Text: (props: any) => <span {...props} />,
TextBold: (props: any) => <strong {...props} />,
} as TextComponents;
type AdjustCompound = FC<any> & { getPadClass: (pads?: any[]) => string };
const AdjustBase: FC<any> = props => <div {...props} />;
export const Adjust = Object.assign(AdjustBase, {
getPadClass: (_pads?: any[]) => '',
}) as AdjustCompound;
export const Loader: FC<any> = () => <div role="status" aria-label="loading" />;
export const Box: FC<any> = props => <div {...props} />;
type HorizonCompound = FC<any> & { Spacer: FC<any> };
const HorizonBase: FC<any> = props => <div style={{ display: 'flex', gap: 8, alignItems: 'center' }} {...props} />;
const Spacer: FC<any> = () => <div style={{ flex: 1 }} />;
export const Horizon = Object.assign(HorizonBase, { Spacer }) as HorizonCompound;
export const Label: FC<any> = props => <label {...props} />;
type PatternCompound = FC<any> & { Span: FC<any> };
const PatternBase: FC<any> = props => <div {...props} />;
export const PatternSpan: FC<any> = props => <div {...props} />;
export const Pattern = Object.assign(PatternBase, { Span: PatternSpan }) as PatternCompound;
export const BUTTON = { PRIMARY: 'primary', REGULAR: 'regular', SECONDARY: 'secondary', CANCEL: 'cancel', SUBMIT: 'submit' } as const;
// HOC asModal - оборачивает компонент для работы с модалками (для обратной совместимости с ECO)
export const asModal = <P extends any>(Component: React.ComponentType<P>): React.FC<P> => {
return Component as React.FC<P>;
};
export const Fields: any = {
INN: (props: any) => <input {...props} />,
Select: (props: any) => <select {...props} />,
Checkbox: (props: any) => <input type="checkbox" {...props} />,
OKPO: (props: any) => <input maxLength={10} {...props} />,
OKATO: (props: any) => <input {...props} />,
Date: (props: any) => <input type="date" {...props} />,
SwitchBar: (props: any) => <input type="checkbox" {...props} />,
Account: (props: any) => <input {...props} />,
Money: (props: any) => <input type="number" {...props} />,
Phone: (props: any) => <input type="tel" {...props} />,
KPP: (props: any) => <input {...props} />,
OGRN: (props: any) => <input {...props} />,
SNILS: (props: any) => <input {...props} />,
PassportSerial: (props: any) => <input {...props} />,
PassportNumber: (props: any) => <input {...props} />,
PassportBranchCode: (props: any) => <input {...props} />,
Text: (props: any) => <input type="text" {...props} />,
TextArea: (props: any) => <textarea {...props} />,
MultiSelect: (props: any) => <select multiple {...props} />,
RadioGroup: (props: any) => <div {...props} />,
};
// Input is an alias for Fields (ECO compatibility)
export const Input = Fields;
export const LABEL_POSITION = { LEFT: 'left', RIGHT: 'right', TOP: 'top' } as const;
type LimitWidthCompound = FC<{ size?: any }> & { SIZE: { AUTO: 'AUTO' } };
const LimitWidthBase: FC<{ size?: any }> = ({ children }) => <div style={{ maxWidth: 980, width: '100%' }}>{children}</div>;
export const LimitWidth = Object.assign(LimitWidthBase, { SIZE: { AUTO: 'AUTO' as const } }) as LimitWidthCompound;
export const MiniTable: FC<{ columns: any[]; rows: any[]; selectable?: boolean }> = ({ columns, rows }) => (
<table>
<thead>
<tr>
{columns.map((c: any) => (
<th key={c.title || c.header}>{c.title || c.header}</th>
))}
</tr>
</thead>
<tbody>
{rows?.map((r: any, idx: number) => (
<tr key={idx}>
{columns.map((c: any, ci: number) => (
<td key={ci}>{c.selector ? c.selector(r) : r[c.dataKey]}</td>
))}
</tr>
))}
</tbody>
</table>
);
export const FilterLine: FC<{ text?: ReactNode }> = ({ text, children }) => (
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
{text ? <label style={{ minWidth: 200 }}>{text}</label> : null}
<div>{children}</div>
</div>
);
export const FormLookup = {
Text: (_props: any) => <input />,
};
export const CardSelect: FC<any> = ({ options = [], value, onChange, className }) => (
<select className={className} value={value} onChange={e => onChange?.(e.target.value)}>
{options.map((o, i) => (
<option key={i} value={o?.value ?? o}>
{o?.label ?? String(o)}
</option>
))}
</select>
);
export const WizardTabs: FC<any> = ({ children }) => <div>{children}</div>;
export const Line: FC<any> = props => <hr {...props} />;
export const WithInfoTooltip: FC<any> = ({ children }) => <>{children}</>;
export type ModalActionCompat = {
name?: string;
label: string;
onClick?: () => void;
disabled?: boolean;
extraSmall?: boolean;
buttonType?: 'primary' | 'regular';
};
export interface DialogTemplateProps {
header?: ReactNode;
content?: ReactNode;
additionalContent?: ReactNode;
actions?: ModalActionCompat[];
onClose?: () => void;
isOpen?: boolean;
preventSimpleClose?: boolean;
extraSmall?: boolean;
dataType?: any;
footerAddon?: ReactNode;
icon?: () => ReactElement;
opened?: boolean;
message?: ReactNode;
children?: ReactNode;
}
export const DialogTemplate: FC<DialogTemplateProps> = ({ header, content, additionalContent, actions, onClose, isOpen = true, preventSimpleClose }) => {
const primary = actions?.[0];
const secondary = actions?.[1];
const messageContent = additionalContent ?? content;
return (
<BaseDialog
header={typeof header === 'string' ? header : undefined}
isOpen={isOpen}
message={typeof messageContent === 'string' || typeof messageContent === 'undefined' ? messageContent : String(messageContent)}
okButtonText={primary?.label}
onOk={primary?.onClick}
onCancel={secondary?.onClick ?? onClose}
cancelButtonText={secondary?.label}
hideCancelButton={!secondary}
onClose={onClose}
preventSimpleClose={!!preventSimpleClose}
/>
);
};
// Links and buttons
export const Link: FC<any> = ({ children, icon: IconComp, ...rest }) => (
<button
style={{ background: 'none', border: 'none', color: 'inherit', textDecoration: 'underline', cursor: 'pointer', padding: 0 }}
{...rest}
>
{IconComp ? <IconComp /> : null}
{children}
</button>
);
export const LinkButton: FC<any> = ({ children, icon: IconComp, ...rest }) => (
<button style={{ background: 'none', border: 'none', color: 'inherit', cursor: 'pointer' }} {...rest}>
{IconComp ? <IconComp /> : null}
{children}
</button>
);
// Informer
export const Informer: FC<any> = ({ text, children }) => <div role="note">{text ?? children}</div>;
// already attached above via Object.assign
// Icons compat maps
export const Icons = Object.assign({}, WdIcons, {
Plus: (WdIcons as any).Add,
PrintFile: (WdIcons as any).Print24,
SendDoc: (WdIcons as any).Export24,
Download: (WdIcons as any).Export24,
DocRecall: (WdIcons as any).RollBack24,
Approved: (WdIcons as any).CheckMark24,
Danger: (WdIcons as any).Delete24,
ArrowLeft: (WdIcons as any).ChevronLeft24 ?? (WdIcons as any).ChevronDown24,
Delete: (WdIcons as any).Delete24,
Repeat: (WdIcons as any).update,
InfoDetailed: (WdIcons as any).Info24,
});
// SpecialIcons minimal
export const SpecialIcons = {
Success: (WdIcons as any).CheckMark24,
Error: (WdIcons as any).Cancel24 ?? (WdIcons as any).Delete24,
CurrentStep: (WdIcons as any).View24 ?? (WdIcons as any).Info24,
};
export const ServiceIcons = {
Question: (WdIcons as any).Info24,
Close: (WdIcons as any).Close24 ?? (WdIcons as any).Cancel24 ?? DummyIcon,
Home: DummyIcon,
ArrowUp: DummyIcon,
};
export type IDialogTemplateProps = DialogTemplateProps;
export const AUTO_COMPLETE = {
OFF: 'off',
ON: 'on',
NEW_PASSWORD: 'new-password',
} as const;
export type AUTO_COMPLETE_TYPE = typeof AUTO_COMPLETE;
export const DIALOG_TYPE = { WARNING: 'WARNING', SUCCESS: 'SUCCESS', ERROR: 'ERROR', CONFIRMATION: 'CONFIRMATION', ALERT: 'ALERT' } as const;
// Icon helpers compat
export type WebIcon = FC<any>;
export const createIcon = (svg: any): WebIcon => {
// Case 1: SVGR returns a React component function
if (typeof svg === 'function') {
const Comp: any = svg;
return (props: any) => <Comp {...props} />;
}
// Case 2: CommonJS module with default export as React component
if (typeof svg?.default === 'function') {
const Comp: any = svg.default;
return (props: any) => <Comp {...props} />;
}
// Case 3: URL string exported
const url: string | undefined = typeof svg === 'string' ? svg : typeof svg?.default === 'string' ? svg.default : undefined;
if (url) {
return ({ alt, ...props }: any) => <img src={url} alt={alt ?? ''} {...props} />;
}
// Fallback: no-op icon
return DummyIcon;
};
// Portal helpers compat
// ВАЖНО: Эти функции реализованы в ECO коде (services/msb-treasury-deals/src/widgets/modules/eco/common/components/portal-container)
// чтобы обеспечить синхронную инициализацию и избежать ошибки "add fn not implemented"
// Фактический импорт будет через alias @treasury-deals/common/components/portal-container
export { appendToPortal, deleteFromPortal } from '@treasury-deals/common/components/portal-container';
// Button types and components
export interface IPrimatyButtonProps {
dimension?: 'SM' | 'MD' | 'LG';
disabled?: boolean;
onClick?: (e: React.MouseEvent) => void;
children?: ReactNode;
className?: string;
type?: 'button' | 'submit' | 'reset';
extraSmall?: boolean;
dataAction?: string;
color?: string;
}
export const PrimaryButton = React.forwardRef<HTMLButtonElement, IPrimatyButtonProps>((props, ref) => {
const { extraSmall, dataAction, color, ...rest } = props;
return <button ref={ref} type={props.type ?? 'button'} data-action={dataAction} {...rest} style={{ color, ...props as any }} />;
});
PrimaryButton.displayName = 'PrimaryButton';
export interface IRegularButtonProps {
dimension?: 'SM' | 'MD' | 'LG';
disabled?: boolean;
onClick?: (e: React.MouseEvent) => void;
children?: ReactNode;
className?: string;
type?: 'button' | 'submit' | 'reset';
extraSmall?: boolean;
}
export const RegularButton = React.forwardRef<HTMLButtonElement, IRegularButtonProps>((props, ref) => {
const { extraSmall, ...rest } = props;
return <button ref={ref} type={props.type ?? 'button'} {...rest} style={{ ...props as any }} />;
});
RegularButton.displayName = 'RegularButton';
// Additional missing exports for ECO compatibility
export interface ICheckboxProps {
dimension?: string;
label?: ReactNode;
name?: string;
value?: any;
onChange?: (value: any) => any;
extraSmall?: boolean;
className?: string;
disabled?: boolean;
ref?: any;
}
export const Checkbox: FC<ICheckboxProps> = ({ label, value, onChange, className, disabled, ...props }) => (
<label className={className}>
<input type="checkbox" checked={value} disabled={disabled} onChange={e => onChange?.(e.target.checked)} {...props} />
{label}
</label>
);
export interface IOption<T = any> { label: string; value: T }
export interface ILookupRequestMetadata {
params?: any;
offset?: number;
pageSize?: number;
search?: any;
}
export interface ILookupResponse<T = any> {
data: T[];
meta?: any;
total?: number;
}
export const useDebounce = (value: any, delay: number) => value; // Stub
export interface IRadioGroupProps { options?: any[]; value?: any; onChange?: (v: any) => void; addon?: ReactNode }
export interface IButtonAction { label: string; onClick?: () => void }
export interface IBreadcrumb { title: string; path?: string }
export interface IConfirmationDialogProps { isOpen?: boolean; onClose?: () => void; onConfirm?: () => void }
export interface ICardSelectOption { label: string; value: any }
export interface IConfirmationExtraParams {
title?: string;
message?: string;
header?: string;
dialogType?: string;
okButtonText?: string;
cancelButtonText?: string;
onClose?: () => void;
}
export interface IAlertExtraParams {
title?: string;
message?: string;
header?: string;
dialogType?: string;
okButtonText?: string;
onClose?: () => void;
}
export interface IModalProps { isOpen?: boolean; onClose?: () => void }
export const Font: FC<any> = props => <div {...props} />;
export const Tooltip: FC<any> = ({ children }) => <>{children}</>;
export const ActionButton: FC<any> = props => <button {...props} />;
export const WithTooltip: FC<any> = ({ children }) => <>{children}</>;
export const Toggle: FC<any> = props => <input type="checkbox" {...props} />;
export const ROLE = {} as const;
export const RadioGroup: FC<IRadioGroupProps> = ({ options = [], value, onChange }) => (
<div>
{options.map((opt, i) => (
<label key={i}>
<input type="radio" checked={value === opt.value} onChange={() => onChange?.(opt.value)} />
{opt.label}
</label>
))}
</div>
);
export const Confirmation: FC<any> = DummyIcon;
export const Tabs: FC<any> = ({ children }) => <div>{children}</div>;
export const DocumentStatus: FC<any> = ({ children }) => <span>{children}</span>;
export const OutlineIcons = WdIcons;
export const Skeleton: FC<any> = () => <div>Loading...</div>;
export const Action: FC<any> = props => <button {...props} />;
export type ActionType = typeof Action;
export const DATA_TYPE = { WARNING: 'WARNING', SUCCESS: 'SUCCESS', ERROR: 'ERROR', CONFIRMATION: 'CONFIRMATION', ALERT: 'ALERT' } as const;
// Dialog utility
export const dialog = {
open: (_props: any) => {},
close: () => {},
show: (_name?: any, _component?: any, _props?: any, _callback?: any) => {},
showConfirmation: (_content?: any, _onOk?: any, _params?: any) => {},
showSuccessAlert: (_content?: any, _params?: any) => {},
showAlert: (_content?: any, _params?: any) => {},
};
export const ConfirmationDialog: FC<any> = DummyIcon;
// Icon set type
export type IconSet<T = any> = typeof WdIcons;
// Form validation utility
export const transformFormValidation = (rules: any, _validators?: any) => rules;
@@ -0,0 +1,2 @@
export { LinkButton } from '../ui';
+2
View File
@@ -0,0 +1,2 @@
export { Link } from '../ui';
+1
View File
@@ -9,6 +9,7 @@ const AUTHORITIES = {
EDIT: 'RUBLE_PAYMENT_ORDER.EDIT',
SEND: 'RUBLE_PAYMENT_ORDER.SEND',
SIGN: 'RUBLE_PAYMENT_ORDER.SIGN',
DELETE: 'RUBLE_PAYMENT_ORDER.DELETE',
},
TURNOVER_SUMMARY_VIEW: STATEMENTS_PERMISSIONS.VIEW,
STATEMENT_REQUEST: STATEMENTS_PERMISSIONS.REQUEST,
+9 -1
View File
@@ -1,17 +1,19 @@
enum PATHS {
HOME = '/',
OPERATIONS_HISTORY = '/operations-history',
OPERATIONS_HISTORY = '/activity-feed',
PAYMENTS = '/payments',
STATEMENTS_AND_INQUIRIES = '/statements-and-inquiries',
ACCOUNTS = '/accounts',
DEPOSITS = '/deposits',
FEA = '/fea',
SALARY_PROJECT = '/salary-project',
CREDIT_ACCOUNT = '/credit-account',
ACQUIRING = '/acquiring',
CONTACT = '/communication-with-the-bank',
SERVICES = '/services',
PARTNER_CHECK = '/partner-check',
BUSINESS_CARDS = '/business-cards',
WEB_DEALING = '/web-dealing',
}
enum PATHS_PROFILE {
@@ -60,6 +62,7 @@ const EXTERNAL_PATHS = {
BUSINESS_CARDS: `${ECO_CLIENT_ENDPOINT}/business-cards/cards`,
ACQUIRING: `${ECO_CLIENT_ENDPOINT}/paymenthub/lk`,
LETTERS: `${ECO_CLIENT_ENDPOINT}/letter`,
SALARY_PROJECT: `${ECO_CLIENT_ENDPOINT}/zp`,
CREDIT_CABINET: `${ECO_CLIENT_ENDPOINT}/credit-cabinet/msb`,
OPEN_NEW_ACCOUNT: `${ECO_CLIENT_ENDPOINT}/open-account/new`,
CLOSE_ACCOUNT: `${ECO_CLIENT_ENDPOINT}/closing-account/new`,
@@ -102,6 +105,10 @@ const ECO_SERVICES_PATHS = {
OFFER_LIST: `${ECO_CLIENT_ENDPOINT}/offer/list`,
} as const;
const CROSS_SALE_PATHS = {
FUEL_CARD: 'https://www.gazprombank.ru/special/a/toplivnaya-karta/',
};
const CROSS_SELLING_JSON = {
PRODUCT_CAROUSEL: '/product-carousel/product-carousel-data.json',
};
@@ -122,4 +129,5 @@ export {
CROSS_SELLING_JSON,
CROSS_BORDER_AB_PAYMENTS,
ACCOUNTS_PATHS,
CROSS_SALE_PATHS,
};
@@ -19,6 +19,8 @@ interface AppContextValue {
};
refetchSystemConfigs(): void;
config?: Map<string, unknown>;
}
const AppContext = createContext<AppContextValue | null>(null);
@@ -0,0 +1,13 @@
import type { AdvertisingBadgeSize } from '../model';
const TEXT_SIZES: Record<AdvertisingBadgeSize, string> = {
S: '10px',
M: '14px',
};
const ICON_SIZES: Record<AdvertisingBadgeSize, string> = {
S: '10px',
M: '12px',
};
export { TEXT_SIZES, ICON_SIZES };
@@ -0,0 +1,2 @@
export * from './localization';
export * from './constants';
@@ -0,0 +1,7 @@
const LOCALIZATION = {
ADVERTISEMENT: 'Реклама',
INN: 'ИНН',
ERID: 'ERID',
};
export { LOCALIZATION };
@@ -0,0 +1 @@
export * from './ui';
@@ -0,0 +1 @@
export * from './types';
@@ -0,0 +1,3 @@
type AdvertisingBadgeSize = 'M' | 'S';
export type { AdvertisingBadgeSize };
@@ -0,0 +1,9 @@
import styled from '@emotion/styled';
const Badge = styled.div`
display: flex;
gap: 2px;
align-items: center;
`;
export { Badge };
@@ -0,0 +1,44 @@
import { useRef } from 'react';
import { ContextMenuIcon } from '@fractal-ui/library';
import { Tooltip } from '@fractal-ui/overlays';
import { Text } from '@fractal-ui/styling';
import { ICON_SIZES, LOCALIZATION, TEXT_SIZES } from '../constants';
import type { AdvertisingBadgeSize } from '../model';
import * as S from './AdvertisingBadge.styles';
interface Props {
organizationName: string;
inn: string;
erid: string;
size?: AdvertisingBadgeSize;
}
const AdvertisingBadge = ({ organizationName, inn, erid, size = 'M' }: Props) => {
const tooltipRef = useRef(null);
return (
<>
<S.Badge ref={tooltipRef}>
<Text.P3 color="bg.primary" fontSize={TEXT_SIZES[size]} opacity="0.72">
{LOCALIZATION.ADVERTISEMENT}
</Text.P3>
<ContextMenuIcon color="bg.primary" opacity={0.72} size="XS" width={ICON_SIZES[size]} />
</S.Badge>
<Tooltip isInteracted anchorEl={tooltipRef} maxWidth={328}>
{organizationName && <Text.P3>{organizationName}</Text.P3>}
{inn && (
<Text.P3>
{LOCALIZATION.INN} {inn}
</Text.P3>
)}
{erid && (
<Text.P3>
{LOCALIZATION.ERID} {erid}
</Text.P3>
)}
</Tooltip>
</>
);
};
export { AdvertisingBadge };
@@ -0,0 +1 @@
export * from './AdvertisingBadge';
@@ -0,0 +1,15 @@
import styled from '@emotion/styled';
const CloseIconBox = styled.div(() => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '88px',
padding: '8px',
zIndex: 3,
backgroundColor: 'rgba(255, 255, 255, 0.16)',
backdropFilter: 'blur(5px)',
cursor: 'pointer',
}));
export { CloseIconBox };
@@ -0,0 +1,15 @@
import { type IconSize, CrossIcon } from '@fractal-ui/library';
import * as S from './CloseBannerButton.styles';
interface Props {
onClick(): void;
size?: IconSize;
}
const CloseBannerButton = ({ onClick, size = 'XS' }: Props) => (
<S.CloseIconBox onClick={onClick}>
<CrossIcon color="control.bg" size={size} />
</S.CloseIconBox>
);
export { CloseBannerButton };
@@ -0,0 +1 @@
export * from './CloseBannerButton';
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 28 KiB

@@ -3,13 +3,13 @@ import icons from '../assets';
const GHOST_BANNERS: BannerItemDto[] = [
{
id: '6856e7d9-d490-4fcb-aa2e-0b9a8d0f7aa3',
text: 'Счёт для бизнеса',
description: 'Открытие бесплатно',
codeName: 'main_page_accounts_banner',
id: '6856e7d9-d490-4fcb-aa2e-0b9a8d0f7aa5',
text: 'Зарплатный\nпроект',
description: 'Преимущества\nдля всех',
codeName: 'main_page_salary_banner',
imagePath: icons.salaryProject,
gradient: 'radial-gradient(100% 152.25% at 77.26% 100%, #634AE5 13.65%, #242629 100%)',
href: EXTERNAL_PATHS.OPEN_NEW_ACCOUNT,
href: EXTERNAL_PATHS.SALARY_PROJECT,
isExternal: true,
},
{
@@ -31,6 +31,7 @@ const StaticBannerBox = styled.div(
({ theme, shouldExpandTitleWidth, $gradient }) => ({
overflow: 'hidden',
display: shouldExpandTitleWidth ? 'block' : 'flex',
alignItems: 'start',
textDecoration: 'none',
position: 'relative',
padding: '12px 16px',
@@ -1,13 +1,9 @@
import { type FC } from 'react';
import {
// ContextMenuIcon,
CrossIcon,
} from '@fractal-ui/library';
import { CrossIcon } from '@fractal-ui/library';
import { Text, Title } from '@fractal-ui/styling';
import { Flex } from '@msb/shared';
import { Link } from 'react-router-dom';
import { useYaMetrika, YM_GOALS } from '../../../../../yandex-metrika';
// import { LOCALIZATION } from '../../../constants';
import * as S from './StaticBanner.styles';
interface Props {
@@ -47,17 +43,10 @@ const StaticBanner: FC<Props> = ({ text, description, imagePath, gradient, shoul
<Title.H4 color="control.bg" lineHeight="22px">
{text}
</Title.H4>
<Text.P3 color="control.bg" lineHeight="16px">
<Text.P3 color="control.bg" lineHeight="16px" opacity={0.72}>
{description}
</Text.P3>
</Flex>
{/* WIP: разработка тега реклама в следующей итерации */}
{/* <Flex row gap="2px">
<Text.P4 color="control.bg" opacity={0.72}>
{LOCALIZATION.AD}
</Text.P4>
<ContextMenuIcon color="control.bg" opacity={0.72} size="XS" />
</Flex> */}
<S.CloseIconBox onClick={handleCloseClick}>
<CrossIcon color="control.bg" size="XS" />
</S.CloseIconBox>
@@ -1,5 +1,4 @@
import styled from '@emotion/styled';
import { darkTheme } from '@fractal-ui/styling';
const BannerContainer = styled.div<{ $background: string }>(({ $background }) => ({
overflow: 'hidden',
@@ -14,7 +13,7 @@ const Image = styled.img({
position: 'absolute',
right: 0,
bottom: 0,
maxWidth: 166,
height: '174px',
});
const Content = styled.div({
@@ -36,17 +35,9 @@ const Title = styled.div({
});
const CloseIconBox = styled.div({
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
top: 16,
right: 16,
width: 32,
height: 32,
borderRadius: 88,
backgroundColor: darkTheme.colors.bg.ghost16,
});
export { BannerContainer, Image, Content, CloseIconBox, Title };
@@ -1,7 +1,7 @@
import type { FC } from 'react';
import { Button } from '@fractal-ui/core';
import { CrossIcon } from '@fractal-ui/library';
import { Text, Title } from '@fractal-ui/styling';
import { CloseBannerButton } from '../CloseBannerButton';
import * as S from './PromoBanner.styles';
import { usePromoBanner } from './usePromoBanner';
@@ -46,8 +46,8 @@ const PromoBanner: FC<PromoBannerProps> = ({
{buttonText}
</Button>
</S.Content>
<S.CloseIconBox onClick={onClose}>
<CrossIcon color="control.primary.typo" size="S" />
<S.CloseIconBox>
<CloseBannerButton size="S" onClick={onClose} />
</S.CloseIconBox>
</S.BannerContainer>
);
+2
View File
@@ -22,3 +22,5 @@ export { ConditionalWrapper } from './ConditionalWrapper';
export * from './HorizontalScroll';
export * from './GradientButton';
export * from './PromoBanner';
export * from './CloseBannerButton';
export * from './AdvertisingBadge';
@@ -44,19 +44,14 @@ class WebpackConfigBuilder {
};
}
applyResolve() {
// У стримов могут быть свои альясы в отдельной секции
const customAliases = this.appConfig.customAliases || {};
this.config.resolve = {
alias: {
alias: Object.assign({
// FSD alias
'@/app': node_path_1.default.resolve(this.srcPath, 'app'),
'@/shared': node_path_1.default.resolve(this.srcPath, 'shared'),
'@/pages': node_path_1.default.resolve(this.srcPath, 'pages'),
'@/widgets': node_path_1.default.resolve(this.srcPath, 'widgets'),
'@/features': node_path_1.default.resolve(this.srcPath, 'features'),
'@/entities': node_path_1.default.resolve(this.srcPath, 'entities'),
'@/app': node_path_1.default.resolve(this.srcPath, 'app'), '@/shared': node_path_1.default.resolve(this.srcPath, 'shared'), '@/pages': node_path_1.default.resolve(this.srcPath, 'pages'), '@/widgets': node_path_1.default.resolve(this.srcPath, 'widgets'), '@/features': node_path_1.default.resolve(this.srcPath, 'features'), '@/entities': node_path_1.default.resolve(this.srcPath, 'entities'),
// jsx-runtime
'react/jsx-dev-runtime': 'react/jsx-dev-runtime.js',
'react/jsx-runtime': 'react/jsx-runtime.js',
},
'react/jsx-dev-runtime': 'react/jsx-dev-runtime.js', 'react/jsx-runtime': 'react/jsx-runtime.js' }, customAliases),
extensions: ['.js', '.jsx', '.ts', '.tsx'],
};
}
@@ -35,6 +35,7 @@ const setModuleFederationPlugin = ({ moduleName, moduleFederationOptions, }) =>
},
'@fractal-ui/styling': {
singleton: true,
eager: true,
},
'@fractal-ui/library': {
singleton: true,
@@ -50,15 +51,19 @@ const setModuleFederationPlugin = ({ moduleName, moduleFederationOptions, }) =>
},
'styled-system': {
singleton: true,
eager: true,
},
'@styled-system/css': {
singleton: true,
eager: true,
},
'@emotion/react': {
singleton: true,
eager: true,
},
'@emotion/styled': {
singleton: true,
eager: true,
},
'react-animate-height': {
singleton: true,
@@ -21,6 +21,27 @@ const setModuleOptions = ({ isDevelopmentMode, isProductionMode }) => {
}
return {
rules: [
{
test: /\.s[ac]ss$/i,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
sourceMap: isDevelopmentMode,
modules: {
auto: /\.s[ac]ss$/i,
localIdentName: isDevelopmentMode ? '[name]__[local]--[hash:base64:5]' : '[hash:base64]',
},
},
},
{
loader: 'sass-loader',
options: { sourceMap: isDevelopmentMode },
},
],
},
{
test: /\.(png|svg|jpg|jpeg|gif|webp|woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
@@ -52,6 +52,9 @@ export class WebpackConfigBuilder {
}
public applyResolve() {
// У стримов могут быть свои альясы в отдельной секции
const customAliases = this.appConfig.customAliases || {};
this.config.resolve = {
alias: {
// FSD alias
@@ -65,6 +68,9 @@ export class WebpackConfigBuilder {
// jsx-runtime
'react/jsx-dev-runtime': 'react/jsx-dev-runtime.js',
'react/jsx-runtime': 'react/jsx-runtime.js',
// Добавим альясы от стрима, если они есть
...customAliases,
},
extensions: ['.js', '.jsx', '.ts', '.tsx'],
};
@@ -41,6 +41,7 @@ export const setModuleFederationPlugin = ({
},
'@fractal-ui/styling': {
singleton: true,
eager: true,
},
'@fractal-ui/library': {
singleton: true,
@@ -56,15 +57,19 @@ export const setModuleFederationPlugin = ({
},
'styled-system': {
singleton: true,
eager: true,
},
'@styled-system/css': {
singleton: true,
eager: true,
},
'@emotion/react': {
singleton: true,
eager: true,
},
'@emotion/styled': {
singleton: true,
eager: true,
},
'react-animate-height': {
singleton: true,
@@ -25,6 +25,27 @@ export const setModuleOptions = ({ isDevelopmentMode, isProductionMode }: IModeO
return {
rules: [
{
test: /\.s[ac]ss$/i,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
sourceMap: isDevelopmentMode,
modules: {
auto: /\.s[ac]ss$/i,
localIdentName: isDevelopmentMode ? '[name]__[local]--[hash:base64:5]' : '[hash:base64]',
},
},
},
{
loader: 'sass-loader',
options: { sourceMap: isDevelopmentMode },
},
],
},
{
test: /\.(png|svg|jpg|jpeg|gif|webp|woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
@@ -48,4 +48,6 @@ export interface IWebpackAppConfig {
moduleFederationOptions?: IModuleFederationPluginOptions;
/** Плагины */
plugins?: Configuration['plugins'];
/** Кастомные алиасы для резолва */
customAliases?: Record<string, string | string[]>;
}
+2
View File
@@ -42,4 +42,6 @@ export interface IWebpackAppConfig {
moduleFederationOptions?: IModuleFederationPluginOptions;
/** Плагины */
plugins?: Configuration['plugins'];
/** Кастомные алиасы для резолва */
customAliases?: Record<string, string | string[]>;
}
@@ -0,0 +1,102 @@
const ORGANIZATIONS_STUB = [
{
label: 'Организация 1',
value: 'org1',
},
{
label: 'Организация 2',
value: 'org2',
},
];
const ACCOUNTS_STUB = [
{
label: 'Расчётный в рублях',
value: 'type1',
},
{
label: 'Расчётный в валюте',
value: 'type2',
},
{
label: 'Участник закупок',
value: 'type3',
},
];
const CURRENCY_STUB = [
{
label: 'Валюта 1',
value: 'cur1',
},
{
label: 'Валюта 2',
value: 'cur2',
},
];
const OFFICE_STUB = [
{
label: 'Офис 1',
value: 'office1',
},
{
label: 'Офис 2',
value: 'office2',
},
];
const CONTRACTS_STUB = [
{
label: 'Договор 1',
value: 'contract1',
},
{
label: 'Договор 2',
value: 'contract2',
},
];
const RESERVED_ACCOUNTS_STUB = [
{
label: 'Зарезерв. счет 1',
value: 'reserved1',
},
{
label: 'Зарезерв счет 2',
value: 'reserved2',
},
];
const COMMISSION_ACCOUNTS_STUB = [
{
label: 'Комм. счет 1',
value: 'comm1',
},
{
label: 'Комм. счет 2',
value: 'comm2',
},
];
const SIGNATORIES_STUB = [
{
label: 'Подписант 1',
value: 'signatory1',
},
{
label: 'Подписант 2',
value: 'signatory2',
},
];
export {
SIGNATORIES_STUB,
COMMISSION_ACCOUNTS_STUB,
RESERVED_ACCOUNTS_STUB,
CONTRACTS_STUB,
OFFICE_STUB,
CURRENCY_STUB,
ACCOUNTS_STUB,
ORGANIZATIONS_STUB,
};
@@ -1 +1,3 @@
export * from './localization';
export * from './constants';
export * from './types';
@@ -1,7 +1,55 @@
const LOCALIZATION = {
TITLE: 'Заявка на открытие счета',
TITLE: 'Заявка на открытие счёта',
BACK: 'Назад',
SIGN_AND_SEND: 'Подписать и отправить',
SAVE: 'Сохранить',
FIELD_LABELS: {
ORGANIZATION: 'Организация',
ACCOUNT_TYPE: 'Тип счёта',
CURRENCY: 'Валюта',
OFFICE: 'Офис обслуживания',
CONTRACT: 'Договор банковского счёта',
RESERVED_ACCOUNT: 'Зарезервированный счёт',
NOT_FROM_RESERVED: 'Открыть новый счет, не из списка зарезервированных',
COMMISSION_ACCOUNT: 'Счёт списания комиссии',
SIGNATORY: 'Подписант по счету',
NO_CHANGES_CONFIRM:
'Подтверждаем, что на дату отправки заявления изменения в документах и сведениях, предоставленных ранее при открытии предыдущих счетов, отсутствуют',
SEND_SMS: 'Уведомить меня об исполнении по СМС и электронной почте',
AGREEMENT: {
TITLE: 'Подтверждаем согласие с тем, что:',
BULLET_1:
'в случае отказа клиента от открытия счёта в банке по каким-либо причинам, документы, предоставленные для открытия счёта, могут быть истребованы в письменной форме и подлежат возврату клиенту или его представителю (на основании доверенности) под роспись на указанном письме;',
BULLET_2:
'в случае, если по каким-либо причинам счёт не открыт, и клиентом в течение одного года с даты подачи документов в банк документы не истребованы, банк оставляет за собой право уничтожения указанных документов в установленном в банке порядке;',
BULLET_3:
'в случае, если по каким-либо причинам счёт не открыт, комиссия, уплаченная банку при предоставлении документов на открытие счёта, возврату не подлежит.',
},
},
FIELD_PLACEHOLDERS: {
ORGANIZATION: 'Выберите организацию',
CURRENCY: 'Выберите валюту',
OFFICE: 'Выберите офис',
CONTRACT: 'Выберите договор',
RESERVED_ACCOUNT: 'Выберите счёт',
COMMISSION_ACCOUNT: 'Выберите счёт',
SIGNATORY: 'Выберите подписанта',
},
INFORMERS: {
NO_DIGITAL_DOCUMENT_EXCHANGE: {
TITLE: 'Организация не подключена к системе электронного документооборота',
TEXT: 'Подпишите согласие об использовании системы в ГПБ Бизнес-Онлайн',
},
OPEN_ACCOUNT_REASONING: {
TITLE: 'Для ведения текущих расходов:',
BULLET_1: 'Открытие счёта без посещения офиса банка',
BULLET_2: 'После открытия по счёту можно принимать и отправлять платежи',
BULLET_3: 'Счёт открывается в течение 15 минут ',
BULLET_4: 'Счёт будет подключён к вашему тарифу «Стандартный Базовый»',
BULLET_5: 'Ознакомиться с тарифами и условиями обслуживания можно по ссылке',
},
NO_CONTRACT_IN_OFFICE: 'В выбранном офисе нет подходящих договоров банковского обслуживания. Будет заключён новый договор',
},
};
export { LOCALIZATION };
@@ -0,0 +1,3 @@
type FormValues = Record<string, any>;
export type { FormValues };
@@ -1,11 +1,25 @@
import { Button, Skeleton } from '@fractal-ui/core';
import { Button, List } from '@fractal-ui/core';
import { InfoBlock, Informer } from '@fractal-ui/extended';
import { Fields } from '@fractal-ui/form';
import { Container } from '@fractal-ui/layout';
import { Title } from '@fractal-ui/styling';
import { Text, Title } from '@fractal-ui/styling';
import { Breadcrumbs } from '@msb/fractal-ui-composites';
import { FEATURE_TOGGLE_NAMES } from '@msb/http';
import { ACCOUNTS_PATHS, checkHavePermission, Flex, NotAuthorizedError, useAppContext, useRedirectByToggle } from '@msb/shared';
import { ACCOUNTS_PATHS, Box, checkHavePermission, Flex, NotAuthorizedError, useAppContext, useRedirectByToggle } from '@msb/shared';
import { Form } from 'react-final-form';
import { useHistory } from 'react-router-dom';
import { LOCALIZATION } from '../lib';
import type { FormValues } from '../lib';
import {
ACCOUNTS_STUB,
COMMISSION_ACCOUNTS_STUB,
CONTRACTS_STUB,
CURRENCY_STUB,
LOCALIZATION,
OFFICE_STUB,
ORGANIZATIONS_STUB,
RESERVED_ACCOUNTS_STUB,
SIGNATORIES_STUB,
} from '../lib';
import { AUTHORITIES } from '@/shared/constants';
const AccountsOrderForm = () => {
@@ -20,8 +34,16 @@ const AccountsOrderForm = () => {
return null;
}
const handleFormSubmit = (values: FormValues) => {
console.log('form submitted :>> ', values);
history.push(ACCOUNTS_PATHS.OPEN_ACCOUNT_ORDERS_LIST);
};
return (
<Container>
/** TODO [m.kudryashov 21.10.2025 - 17:49]:
* обсудить с дизайнером возможность использования фракталовского контейнера.
*/
<Container padding="24px 24px 40px 24px">
<Breadcrumbs
items={[
{
@@ -32,16 +54,127 @@ const AccountsOrderForm = () => {
/>
<Title.H1>{LOCALIZATION.TITLE}</Title.H1>
{canRequest ? (
<Flex column gap={5} py={'4'}>
<Skeleton dataName="form" height={100} />
<Skeleton dataName="form" height={100} />
<Skeleton dataName="form" height={100} />
<Flex row justifyContent="flex-end">
<Button dataAction="sign-and-send" onClick={() => history.push(ACCOUNTS_PATHS.OPEN_ACCOUNT_ORDERS_LIST)}>
{LOCALIZATION.SIGN_AND_SEND}
</Button>
</Flex>
</Flex>
<Form<FormValues> onSubmit={handleFormSubmit}>
{({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<Flex column py="4">
<Flex column gap={7}>
<Fields.Select
label={LOCALIZATION.FIELD_LABELS.ORGANIZATION}
labelPosition="top"
name="organization"
options={ORGANIZATIONS_STUB}
placeholder={LOCALIZATION.FIELD_PLACEHOLDERS.ORGANIZATION}
/>
<Informer
statusIcon
secondaryButton="Перейти"
title={LOCALIZATION.INFORMERS.NO_DIGITAL_DOCUMENT_EXCHANGE.TITLE}
variant="error"
>
{LOCALIZATION.INFORMERS.NO_DIGITAL_DOCUMENT_EXCHANGE.TEXT}
</Informer>
<Fields.ChipsGroup label={LOCALIZATION.FIELD_LABELS.ACCOUNT_TYPE} name="accountType" options={ACCOUNTS_STUB} />
<InfoBlock variant="secondary">
<Title.H6 color="text.primary">{LOCALIZATION.INFORMERS.OPEN_ACCOUNT_REASONING.TITLE}</Title.H6>
<List size="M">
<li>{LOCALIZATION.INFORMERS.OPEN_ACCOUNT_REASONING.BULLET_1}</li>
<li>{LOCALIZATION.INFORMERS.OPEN_ACCOUNT_REASONING.BULLET_2}</li>
<li>{LOCALIZATION.INFORMERS.OPEN_ACCOUNT_REASONING.BULLET_3}</li>
</List>
<Text.P2 color="text.primary">{LOCALIZATION.INFORMERS.OPEN_ACCOUNT_REASONING.BULLET_4}</Text.P2>
<Text.P2 color="text.primary">{LOCALIZATION.INFORMERS.OPEN_ACCOUNT_REASONING.BULLET_5}</Text.P2>
</InfoBlock>
<Fields.Select
label={LOCALIZATION.FIELD_LABELS.CURRENCY}
labelPosition="top"
name="currency"
options={CURRENCY_STUB}
placeholder={LOCALIZATION.FIELD_PLACEHOLDERS.CURRENCY}
/>
<Fields.Select
label={LOCALIZATION.FIELD_LABELS.OFFICE}
labelPosition="top"
name="office"
options={OFFICE_STUB}
placeholder={LOCALIZATION.FIELD_PLACEHOLDERS.OFFICE}
/>
<Informer statusIcon variant="info">
{LOCALIZATION.INFORMERS.NO_CONTRACT_IN_OFFICE}
</Informer>
<Fields.Select
label={LOCALIZATION.FIELD_LABELS.CONTRACT}
labelPosition="top"
name="contract"
options={CONTRACTS_STUB}
placeholder={LOCALIZATION.FIELD_PLACEHOLDERS.CONTRACT}
/>
<Fields.Select
label={LOCALIZATION.FIELD_LABELS.RESERVED_ACCOUNT}
labelPosition="top"
name="reservedAccount"
options={RESERVED_ACCOUNTS_STUB}
placeholder={LOCALIZATION.FIELD_PLACEHOLDERS.RESERVED_ACCOUNT}
/>
<Fields.Switch name="notFromReserved">{LOCALIZATION.FIELD_LABELS.NOT_FROM_RESERVED}</Fields.Switch>
<Flex row gap={5}>
<Box flexGrow={1}>
<Fields.Select
label={LOCALIZATION.FIELD_LABELS.COMMISSION_ACCOUNT}
labelPosition="top"
name="commissionAccount"
options={COMMISSION_ACCOUNTS_STUB}
placeholder={LOCALIZATION.FIELD_PLACEHOLDERS.COMMISSION_ACCOUNT}
/>
</Box>
<Box flexGrow={1}>
<Fields.Select
label={LOCALIZATION.FIELD_LABELS.SIGNATORY}
labelPosition="top"
name="signatory"
options={SIGNATORIES_STUB}
placeholder={LOCALIZATION.FIELD_PLACEHOLDERS.SIGNATORY}
/>
</Box>
</Flex>
<Flex column backgroundColor="bg.secondary" borderRadius="L" gap={5} padding="7">
<Fields.Checkbox label={LOCALIZATION.FIELD_LABELS.NO_CHANGES_CONFIRM} name="noChangesConfirm" />
<Fields.Checkbox
label={
<div>
<Text.P2>{LOCALIZATION.FIELD_LABELS.AGREEMENT.TITLE}</Text.P2>
<List size="S" variant="secondary">
<li>{LOCALIZATION.FIELD_LABELS.AGREEMENT.BULLET_1}</li>
<li>{LOCALIZATION.FIELD_LABELS.AGREEMENT.BULLET_2}</li>
<li>{LOCALIZATION.FIELD_LABELS.AGREEMENT.BULLET_3}</li>
</List>
</div>
}
name="cancelPolicyConfirm"
/>
<Fields.Checkbox label={LOCALIZATION.FIELD_LABELS.SEND_SMS} name="sendSms" />
</Flex>
</Flex>
<Flex
row
backgroundColor="bg.primary"
bottom="0"
gap={3}
justifyContent="flex-end"
padding="24px 0 40px 0"
position="sticky"
>
<Button dataAction="save" variant="blue" onClick={() => history.push(ACCOUNTS_PATHS.OPEN_ACCOUNT_ORDERS_LIST)}>
{LOCALIZATION.SAVE}
</Button>
<Button dataAction="sign-and-send" type="submit">
{LOCALIZATION.SIGN_AND_SEND}
</Button>
</Flex>
</Flex>
</form>
)}
</Form>
) : (
<NotAuthorizedError />
)}
@@ -9,7 +9,7 @@ const MultiSelectField = createField(MultiSelect);
const TableFilters = () => {
const handleChange = (values: any) => {
console.log('values :>>', values);
console.log('table filters changed :>>', values);
};
return (
@@ -1,3 +1,4 @@
const QUERY_KEY_GET_BEST_RATES = 'GET_BEST_RATES' as const;
const QUERY_KEY_GET_BEST_RATES_WITH_ID = 'GET_BEST_RATES_WITH_ID' as const;
export { QUERY_KEY_GET_BEST_RATES };
export { QUERY_KEY_GET_BEST_RATES, QUERY_KEY_GET_BEST_RATES_WITH_ID };
@@ -7,4 +7,10 @@ const getBestRates = async (): Promise<GetBestRatesMSBDto> => {
return response.data;
};
export { getBestRates };
const getBestRatesWithId = async (id: string): Promise<GetBestRatesMSBDto> => {
const response = await network.client.get<GetBestRatesMSBDto>(`${GET_BEST_RATES_MSB}/${id}`);
return response.data;
};
export { getBestRates, getBestRatesWithId };
@@ -1,5 +1,5 @@
import { type GetBestRatesMSBDto, useQuery } from '@msb/http';
import { getBestRates, QUERY_KEY_GET_BEST_RATES } from '@/entities/showcase';
import { getBestRates, getBestRatesWithId, QUERY_KEY_GET_BEST_RATES, QUERY_KEY_GET_BEST_RATES_WITH_ID } from '@/entities/showcase';
const useBestRates = () => {
const { data: bestRatesData, isLoading: bestRatesIsLoading } = useQuery<GetBestRatesMSBDto, Error | undefined>({
@@ -7,7 +7,19 @@ const useBestRates = () => {
queryFn: getBestRates,
});
return { bestRatesData, bestRatesIsLoading };
const defaultClientId = bestRatesData?.data?.[0].clientId;
return { bestRatesData, bestRatesIsLoading, defaultClientId };
};
export { useBestRates };
const useBestRatesWithId = (id: string, defaultId: string) => {
const { data: bestRatesWithIdData, isLoading: bestRatesWithIdIsLoading } = useQuery<GetBestRatesMSBDto, Error | undefined>({
queryKey: [QUERY_KEY_GET_BEST_RATES_WITH_ID, id],
queryFn: () => getBestRatesWithId(id),
enabled: id !== defaultId,
});
return { bestRatesWithIdData, bestRatesWithIdIsLoading };
};
export { useBestRates, useBestRatesWithId };
@@ -0,0 +1,5 @@
const FIELDS = {
ORGANIZATIONS: 'organizations',
};
export { FIELDS };
@@ -1 +1,2 @@
export * from './localization';
export * from './fields';
@@ -1,9 +1,11 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Select } from '@fractal-ui/composites';
import { Title } from '@fractal-ui/styling';
import { useUserInfoUnion } from '@msb/http';
import { DOC_TYPES } from '@msb/shared';
import { LOCALIZATION } from '../constants';
import { FIELDS, LOCALIZATION } from '../constants';
import * as S from './Showcase.styles';
import { useBestRates } from '@/entities/showcase';
import { useBestRates, useBestRatesWithId } from '@/entities/showcase';
import { useOpenTreasuryDeals } from '@/features/OpenTreasuryDealsButton';
import { ACTIONS_LEADING_TO_TREASURY_DEALS_FORM } from '@/shared/constants';
import type { HistoryState } from '@/shared/model';
@@ -13,11 +15,26 @@ import { ShowcaseCard } from '@/widgets/Showcase/ui/Card';
const Showcase = () => {
const { userInfoUnionData } = useUserInfoUnion();
const { bestRatesData } = useBestRates();
const showcaseItems = bestRatesData && userInfoUnionData ? parseBestRatesToShowcase(bestRatesData, userInfoUnionData) : [];
const { bestRatesData, defaultClientId } = useBestRates();
const { openDeposit, openMNO, openGSNO } = useOpenTreasuryDeals();
const [chosenOrg, setChosenOrg] = useState<string | undefined>(undefined);
const organizations = useMemo(
() => userInfoUnionData?.data.userInformationByClient.filter(organization => organization.generalAgreementsExist),
[userInfoUnionData]
);
const organizationOptions = organizations?.map(org => ({ label: org.clientShortName, value: org.clientId })) || [];
const { bestRatesWithIdData } = useBestRatesWithId(chosenOrg || '', defaultClientId || '');
const currentBestRatesData = chosenOrg === defaultClientId ? bestRatesData : bestRatesWithIdData;
const showcaseItems = useMemo(() => {
if (!currentBestRatesData || !userInfoUnionData) return [];
return parseBestRatesToShowcase(currentBestRatesData, userInfoUnionData);
}, [currentBestRatesData, userInfoUnionData]);
const showcaseItemsWithAction = showcaseItems.map(item => {
const amountInfo = item.additionalInfo.find(info => info.id === 'amount')?.title;
const periodInfo = item.additionalInfo.find(info => info.id === 'period')?.title;
@@ -54,10 +71,21 @@ const Showcase = () => {
};
});
const handleChange = useCallback((value: number | string | undefined) => setChosenOrg(value as string), []);
useEffect(() => {
if (defaultClientId) {
setChosenOrg(defaultClientId);
}
}, [defaultClientId]);
return (
<PageLayout>
<S.ShowcaseContent>
<Title.H4>{LOCALIZATION.SHOWCASE_TITLE}</Title.H4>
{defaultClientId && organizations && organizations.length > 1 && (
<Select name={FIELDS.ORGANIZATIONS} options={organizationOptions} value={chosenOrg} onChange={handleChange} />
)}
{showcaseItemsWithAction.map(showcase => (
<ShowcaseCard key={showcase.id} showcaseItem={showcase} />
))}
+22 -2
View File
@@ -16,9 +16,9 @@
"id": 2,
"remoteEntryUrl": "/msb-operations-history/remoteEntry.js",
"remoteName": "msb-operations-history",
"ymCode": "operation",
"ymCode": "activity_feed",
"moduleName": "App",
"path": "/operations-history",
"path": "/activity-feed",
"title": "История операций",
"showOnMobile": true,
"shortTitle": "История",
@@ -76,6 +76,16 @@
"title": "ВЭД",
"exact": false
},
{
"id": 11,
"remoteEntryUrl": "/msb-salary-project/remoteEntry.js",
"remoteName": "msb-salary-project",
"moduleName": "App",
"ymCode": "salary_project",
"path": "/salary-project",
"title": "Зарплатный проект",
"exact": false
},
{
"id": 8,
"remoteEntryUrl": "/msb-credit-account/remoteEntry.js",
@@ -108,6 +118,16 @@
},
{
"id": 11,
"hidden": true,
"remoteEntryUrl": "/msb-treasury-deals/remoteEntry.js",
"remoteName": "msb-treasury-deals",
"moduleName": "App",
"path": "/web-dealing",
"title": "Казначейские продукты",
"exact": false
},
{
"id": 12,
"remoteEntryUrl": "/msb-partner-check/remoteEntry.js",
"remoteName": "msb-partner-check",
"moduleName": "App",
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -1,22 +1,29 @@
[
{
"id": 1,
"title": "Счёт\nбесплатно",
"title": "Топливная\nкарта",
"saleGradient": "sale2",
"imagePath": "/product-carousel/assets/accounts.webp",
"navigatePath": "/open-account/new",
"isEcoPath": true,
"ymCode": "your_products_accounts"
"imagePath": "/product-carousel/assets/fuel_card.webp",
"navigatePath": "https://www.gazprombank.ru/special/a/toplivnaya-karta/",
"ymCode": "your_products_gpn-trade_fuel_card",
"advertisingInfo": {
"organization": "ООО «Газпромнефть Региональные Продажи»",
"inn": "4703105075",
"erid": "2VtzquZy7tr"
}
},
{
"id": 2,
"title": "Бизнес-карта",
"description": "Онлайн",
"title": "Ренессанс\nстрахование",
"saleGradient": "sale5",
"imagePath": "/product-carousel/assets/business_cards.webp",
"navigatePath": "/business-cards/card-issue",
"isEcoPath": true,
"ymCode": "your_products_business_cards"
"imagePath": "/product-carousel/assets/insurance.webp",
"navigatePath": "https://www.renins.ru/gazprombank/",
"ymCode": "your_products_renins_insurance",
"advertisingInfo": {
"organization": "ПАО \"Группа Ренессанс Страхование\"",
"inn": "7725497022",
"erid": "2Vtzqw2UkSS"
}
},
{
"id": 3,
@@ -25,37 +32,33 @@
"saleGradient": "",
"isGift": true,
"imagePath": "/product-carousel/assets/gift.webp",
"navigatePath": "https://app.ab-payments.ru/auth",
"navigatePath": "https://sb-service.softbalance.ru/form/pages/gpb/",
"ymCode": ""
},
{
"id": 4,
"title": "Депозит\nдля бизнеса",
"description": "Онлайн",
"title": "Онлайн-\nбухгалтерия",
"saleGradient": "sale1",
"imagePath": "/product-carousel/assets/deposit.webp",
"navigatePath": "/deposits/treasury-deals",
"imagePath": "/product-carousel/assets/online_accounting.webp",
"navigatePath": "https://sb-service.softbalance.ru/form/pages/gpb/",
"navigateDocType": "DEPOSIT",
"ymCode": "your_products_deposits"
"ymCode": "your_products_1с_online_accounting"
},
{
"id": 5,
"title": "МНО",
"description": "Онлайн",
"saleGradient": "sale4",
"imagePath": "/product-carousel/assets/mno.webp",
"navigatePath": "/deposits/treasury-deals",
"title": "Юридическая\nподдержка",
"saleGradient": "sale3",
"imagePath": "/product-carousel/assets/legal_support.webp",
"navigatePath": "https://urist24.pro/gpb_bol/",
"navigateDocType": "MNO",
"ymCode": "your_products_mno"
"ymCode": "your_products_els_legal_support"
},
{
"id": 6,
"title": "Эквайринг",
"description": "Выгодно",
"saleGradient": "sale3",
"imagePath": "/product-carousel/assets/acquiring.webp",
"navigatePath": "/paymenthub/lk",
"isEcoPath": true,
"ymCode": "your_products_acquiring"
"title": "Валютные\nоперации",
"saleGradient": "sale4",
"imagePath": "/product-carousel/assets/currency_transaction.webp",
"navigatePath": "https://passport.gbo.gazprombank.ru/?redirect_uri=https%3A%2F%2Fgbo.gazprombank.ru%2Fvrko-currency-conversion",
"ymCode": "your_products_currency_operations"
}
]
@@ -1,11 +1,13 @@
import { type ReactElement } from 'react';
import { FEATURE_TOGGLE_NAMES } from '@msb/http';
import { DataIsFailedPageError, MEDIA, useAppContext, useFeatureToggles, useMediaQuery } from '@msb/shared';
import { DataIsFailedPageError, MEDIA, PATHS, useAppContext, useFeatureToggles, useMediaQuery } from '@msb/shared';
import { useLocation } from 'react-router-dom';
import { filterModulesByFeatureToggles } from '../model';
import * as S from './Layout.styles';
import Routes from './Routes';
import type { LayoutContentProps } from './types';
import { useScrollRestoration } from '@/shared/lib';
import { AcrossBanner } from '@/widgets/AcrossBanner';
import { HeaderMenu } from '@/widgets/HeaderMenu';
import { Organizations } from '@/widgets/Organizations';
import { Sidebar, MobileMenu } from '@/widgets/Sidebar';
@@ -19,8 +21,12 @@ const LayoutContent = ({ modules }: LayoutContentProps): ReactElement => {
useScrollRestoration();
const { pathname } = useLocation();
const isHomePage = pathname === PATHS.HOME;
return (
<S.Wrapper>
{isHomePage && <AcrossBanner />}
{!isMobile && <HeaderMenu organizationsSlot={<Organizations />} />}
<S.LayoutBox>
{!isMobile && <Sidebar modules={menuModules} />}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@@ -25,6 +25,7 @@ import payments from './Payments.webp';
import purchaseGazprom from './Purchase_gazprom.webp';
import purchaseSaleCurrency from './Purchase_sale_currency.webp';
import qr from './Qr.webp';
import salaryProject from './Salary_project.webp';
import selfEmployed from './Self_employed.webp';
import sendCurrency from './Send_currency.webp';
import treasuryProducts from './Treasury_products.webp';
@@ -59,5 +60,6 @@ export default {
orderCurrencyTransit,
purchaseSaleCurrency,
sendCurrency,
salaryProject,
partnerChecks,
};
@@ -3,9 +3,9 @@ import { type BannerItemDto, GHOST_BANNERS } from '@msb/shared';
const banners: BannerItemDto[] = [
{
...GHOST_BANNERS[0],
id: 'services_accounts_banner_id',
id: 'services_salary_banner_id',
codeName: 'services_accounts_banner',
ymCode: { services: { element_name: 'ghost_banners_accounts' } },
ymCode: { services: { element_name: 'ghost_banners_salary_project' } },
},
{
...GHOST_BANNERS[1],
@@ -29,6 +29,8 @@ export default {
TOOL_FOR_PAYING_BUSINESS_EXPENSES: 'Удобный инструмент для оплаты бизнес-расходов',
ACQUIRING: 'Эквайринг',
FLEXIBLE_TARIFFS_FOR_TRADE_ACQUIRING: 'Гибкие тарифы для торгового и интернет-эквайринга',
SALARY_PROJECT: 'Зарплатный проект',
FREE_FOR_SERVICE: '0 ₽ за обслуживание',
// Мои сервисы
SETTLEMENT_SERVICES: 'Расчётные сервисы',
@@ -1,5 +1,12 @@
import { lightTheme } from '@fractal-ui/styling';
import { ACCOUNTS_PATHS, CROSS_BORDER_AB_PAYMENTS, ECO_SERVICES_PATHS, PATHS, STATEMENTS_AND_INQUIRIES_PATHS } from '@msb/shared';
import {
ACCOUNTS_PATHS,
CROSS_BORDER_AB_PAYMENTS,
ECO_SERVICES_PATHS,
EXTERNAL_PATHS,
PATHS,
STATEMENTS_AND_INQUIRIES_PATHS,
} from '@msb/shared';
import icons from '../assets';
import type { ServicesGroup } from '../model';
import LOCALIZATION from './localization';
@@ -101,6 +108,13 @@ const SERVICES_FOR_YOU: ServicesGroup[] = [
icon: icons.acquiring,
navigateUrl: ECO_SERVICES_PATHS.ACQUIRING,
},
{
title: LOCALIZATION.SALARY_PROJECT,
description: LOCALIZATION.FREE_FOR_SERVICE,
iconBackground: lightTheme.gradients.glacier,
icon: icons.salaryProject,
navigateUrl: EXTERNAL_PATHS.SALARY_PROJECT,
},
],
},
];
@@ -11,6 +11,7 @@ import {
RubleOperationsIcon,
ProductsIcon as ServicesIcon,
CardIcon,
CardSalaryIcon,
} from '@fractal-ui/library';
import { PATHS } from '@msb/shared';
@@ -22,11 +23,13 @@ const SIDEBAR_ICONS: Omit<Record<PATHS, React.ReactNode>, PATHS.PARTNER_CHECK> =
[PATHS.ACCOUNTS]: <RubleOperationsIcon />,
[PATHS.DEPOSITS]: <DepositIcon />,
[PATHS.FEA]: <EarthIcon />,
[PATHS.SALARY_PROJECT]: <CardSalaryIcon />,
[PATHS.CREDIT_ACCOUNT]: <CreditIcon />,
[PATHS.BUSINESS_CARDS]: <CardIcon />,
[PATHS.ACQUIRING]: <GetATicketIcon />,
[PATHS.CONTACT]: <CommentIcon />,
[PATHS.SERVICES]: <ServicesIcon />,
[PATHS.WEB_DEALING]: <DepositIcon />,
};
export { PATHS, SIDEBAR_ICONS };
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,3 @@
import carImagePath from './Car.webp';
export { carImagePath };
@@ -0,0 +1,7 @@
const ADVERTISING_MAIN_PAGE_DATA = {
organization: 'ООО «Газпромнефть Региональные Продажи»',
inn: '4703105075',
erid: '2VtzqwqugVc',
};
export { ADVERTISING_MAIN_PAGE_DATA };
@@ -0,0 +1,3 @@
const ACROSS_BANNER_KEY = 'isAcrossBannerClosed';
export { ACROSS_BANNER_KEY };
@@ -0,0 +1,3 @@
export * from './localization';
export * from './advertisingBadgeData';
export * from './constants';
@@ -0,0 +1,7 @@
const LOCALIZATION = {
FUEL_DISCOUNT_CARDS: 'Скидка на топливо и ассортимент по топливным картам',
FUEL_DISCOUNT_CARDS_SHORT: 'Скидка по топливным картам',
MORE_DETAILS: 'Подробнее',
};
export { LOCALIZATION };
@@ -0,0 +1 @@
export { AcrossBanner } from './ui';
@@ -0,0 +1,92 @@
import styled from '@emotion/styled';
import { Button } from '@fractal-ui/core';
import { Title as TitleBase } from '@fractal-ui/styling';
import { MEDIA, SALE_GRADIENTS } from '@msb/shared';
const Banner = styled.div<{ $saleGradient: keyof typeof SALE_GRADIENTS }>`
position: relative;
width: 100%;
height: 64px;
padding: 8px;
display: flex;
align-items: center;
justify-content: center;
background: ${({ $saleGradient }) => SALE_GRADIENTS[$saleGradient]};
gap: 24px;
@media ${MEDIA.tablet} {
padding: 10px 24px;
}
@media ${MEDIA.mobileAndTablet} {
flex-direction: row-reverse;
justify-content: space-between;
gap: 8px;
height: 80px;
}
@media ${MEDIA.mobile} {
justify-content: start;
}
`;
const Image = styled.img`
height: 64px;
@media ${MEDIA.mobileAndTablet} {
margin-right: 52px;
}
@media ${MEDIA.mobile} {
height: 60px;
position: absolute;
right: 0;
}
@media (max-width: 375px) {
display: none;
}
`;
const MoreButton = styled(Button)`
// цвет из кросс сейла, нет в палитре фрактала
background-color: #282a2f;
height: 32px;
min-height: 32px;
&:hover {
background-color: #323236;
}
`;
const CloseButtonWrapper = styled.div`
position: absolute;
top: 0;
right: 0;
height: 100%;
display: flex;
align-items: center;
margin-right: 20px;
`;
const Content = styled.div`
z-index: 1;
display: flex;
flex-direction: row;
gap: 24px;
align-items: center;
@media ${MEDIA.mobileAndTablet} {
flex-direction: column;
gap: 8px;
align-items: start;
}
`;
const Title = styled(TitleBase.H5)`
@media ${MEDIA.mobileAndTablet} {
font-size: 16px;
}
`;
export { Banner, Image, MoreButton, CloseButtonWrapper, Content, Title };
@@ -0,0 +1,50 @@
import { useRef, useState } from 'react';
import { AdvertisingBadge, CloseBannerButton, CROSS_SALE_PATHS, Flex, MEDIA, SmoothAutoHeight, useMediaQuery } from '@msb/shared';
import { carImagePath } from '../assets';
import { ACROSS_BANNER_KEY, ADVERTISING_MAIN_PAGE_DATA, LOCALIZATION } from '../constants';
import * as S from './AcrossBanner.styles';
const AcrossBanner = () => {
const [isOpened, setIsOpened] = useState(!sessionStorage[ACROSS_BANNER_KEY]);
const handleCloseBanner = () => {
setIsOpened(false);
sessionStorage[ACROSS_BANNER_KEY] = true;
};
const isMobileOrTablet = useMediaQuery(MEDIA.mobileAndTablet);
const isMobile = useMediaQuery(MEDIA.mobile);
const bannerRef = useRef(null);
const handleClickMoreDetails = () => {
window.open(CROSS_SALE_PATHS.FUEL_CARD, '_blank', 'noopener,noreferrer');
};
return (
<SmoothAutoHeight elementRef={bannerRef} isCollapsed={!isOpened}>
<S.Banner ref={bannerRef} $saleGradient="sale4">
<S.Image src={carImagePath} />
<S.Content>
{/* // цвет из кросс сейла, нет в палитре фрактала */}
<S.Title color="#F4F4F4">{isMobile ? LOCALIZATION.FUEL_DISCOUNT_CARDS_SHORT : LOCALIZATION.FUEL_DISCOUNT_CARDS}</S.Title>
<Flex row flexDirection={isMobileOrTablet ? 'row-reverse' : 'row'} gap={isMobileOrTablet ? '16px' : '24px'}>
<AdvertisingBadge
erid={ADVERTISING_MAIN_PAGE_DATA.erid}
inn={ADVERTISING_MAIN_PAGE_DATA.inn}
organizationName={ADVERTISING_MAIN_PAGE_DATA.organization}
/>
<S.MoreButton dataAction="more" size="S" onClick={handleClickMoreDetails}>
{LOCALIZATION.MORE_DETAILS}
</S.MoreButton>
</Flex>
</S.Content>
<S.CloseButtonWrapper>
<CloseBannerButton onClick={handleCloseBanner} />
</S.CloseButtonWrapper>
</S.Banner>
</SmoothAutoHeight>
);
};
export { AcrossBanner };
@@ -0,0 +1 @@
export * from './AcrossBanner';
@@ -1,6 +1,7 @@
import { EXTERNAL_PATHS, PATHS } from '@msb/shared';
const LIST_OF_REDIRECTS: Partial<Record<PATHS, typeof EXTERNAL_PATHS[keyof typeof EXTERNAL_PATHS]>> = {
[PATHS.SALARY_PROJECT]: EXTERNAL_PATHS.SALARY_PROJECT,
[PATHS.CREDIT_ACCOUNT]: EXTERNAL_PATHS.CREDIT_ACCOUNT,
[PATHS.BUSINESS_CARDS]: EXTERNAL_PATHS.BUSINESS_CARDS,
[PATHS.ACQUIRING]: EXTERNAL_PATHS.ACQUIRING,
@@ -41,6 +41,7 @@ const Sidebar: FC<Props> = ({ modules = [] }) => {
};
const renderLink = (item: RemoteRouteConfigDto) => {
if (item.hidden) return null;
const icon = item.path in SIDEBAR_ICONS ? SIDEBAR_ICONS[item.path as keyof typeof SIDEBAR_ICONS] : null;
if (item.path in LIST_OF_REDIRECTS) {

Some files were not shown because too many files have changed in this diff Show More