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
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[]>;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { Link } from '../ui';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
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>
|
||||
);
|
||||
|
||||
@@ -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[]>;
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
))}
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
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} />}
|
||||
|
||||
|
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 };
|
||||
|
||||
|
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) {
|
||||
|
||||