feat(короткая заявка): TEAMMSBDAY-2309 [ОНБ_МСБ] добавил скроллер на фрактале
This commit is contained in:
@@ -1797,5 +1797,6 @@
|
||||
"action.fine": "Хорошо",
|
||||
"action.documentsRequested.header": "Документы запрошены",
|
||||
"documentsRequested.afterUploadDocs.description": "После загрузки документов клиентом, вы сможете завершить формирование заявки и отправить ее в ЕСК",
|
||||
"documentsRequested.result.success": "Заявка отправлена в ЕСК"
|
||||
"documentsRequested.result.success": "Заявка отправлена в ЕСК",
|
||||
"fields.venue.label": "Место встречи"
|
||||
}
|
||||
@@ -3,6 +3,7 @@ export * from './print';
|
||||
export * from './manage';
|
||||
export * from './suv-message-history';
|
||||
export * from './view';
|
||||
export * from './short-view';
|
||||
export * from './reform-doc-list';
|
||||
export * from './repeat-unload';
|
||||
export * from './close-tessa-task';
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { openShortView } from 'actions';
|
||||
import type { OnboardingShortRequestBankScrollerDto } from 'interfaces/admin';
|
||||
import type { IActionConfig } from '@platform/core';
|
||||
import { singleAction } from '@platform/core';
|
||||
import type { IBaseContext } from '@platform/services';
|
||||
|
||||
/**
|
||||
* Банк: Функция просмотра короткой заявки.
|
||||
* Вызывается на скроллере и управлении короткой заявкой.
|
||||
*/
|
||||
export const shortView: IActionConfig<IBaseContext, OnboardingShortRequestBankScrollerDto> = {
|
||||
...openShortView,
|
||||
guardians: [singleAction],
|
||||
};
|
||||
@@ -9,6 +9,7 @@ export * from './fix';
|
||||
export * from './skip-attachment';
|
||||
export * from './open';
|
||||
export * from './open-view';
|
||||
export * from './open-short-view';
|
||||
export * from './check-acting-request-before-create';
|
||||
export * from './check-acting-request-before-create-fractal';
|
||||
export * from './check-active-request-before-create';
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { OnboardingShortRequestBankScrollerDto } from 'interfaces/admin';
|
||||
import type { ShortOnboardingRequestDto } from 'pages/onboarding-short-new/interfaces';
|
||||
import { open as openAction } from '@platform/services';
|
||||
|
||||
/**
|
||||
* Функция открытия короткой заявки в режиме просмотра. (Базовая функция).
|
||||
*/
|
||||
export const openShortView = openAction(
|
||||
({ id }: OnboardingShortRequestBankScrollerDto | ShortOnboardingRequestDto) => `/onboarding-short/full/view/${id}`
|
||||
);
|
||||
+1
-1
@@ -16,6 +16,6 @@ export const beforeUsage = async () =>
|
||||
loadConfig(ONBOARDING_CONFIG),
|
||||
]);
|
||||
|
||||
export { AdminOnboardingShortScroller } from 'pages/scroller/admin/short-requests';
|
||||
// export { AdminOnboardingShortScroller } from 'pages/scroller/admin/short-requests';
|
||||
|
||||
export const routes = [<Route key="onboarding-admin-routes" component={AdminRoutes} path={'/onboarding-short'} />];
|
||||
|
||||
@@ -6,3 +6,4 @@ export * from './bank-client-info-bank-scroller-dto';
|
||||
export * from './onboarding-request-bank-scroller-dto';
|
||||
export * from './need-client-interaction-result-dto';
|
||||
export * from './onboarding-request-archive-bank-scroller-dto';
|
||||
export * from './onboarding-short-request-bank-scroller-dto';
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { OnboardingRequestBankScrollerDto } from './onboarding-request-bank-scroller-dto';
|
||||
|
||||
/** Короткая заявка на открытие первого счета на банковском скроллере. */
|
||||
export type OnboardingShortRequestBankScrollerDto = Omit<
|
||||
OnboardingRequestBankScrollerDto,
|
||||
'clientBankConnectionInfo' | 'gpbBoConnectionInfo'
|
||||
>;
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CallPurposeDto } from 'pages/call-purpose/interfaces';
|
||||
import type { ScheduledCallInfoDto } from 'pages/scheduled-call-info/interfaces';
|
||||
import type { STATUS, REQUEST_STEP, SIGN_PLACE, LIMIT_SCHEME_CODE } from 'stream-constants';
|
||||
import type { UserType, DOCUMENT_TYPE_CODE, ACCOUNT_TYPE } from '@platform/services';
|
||||
import type { AccountInfoDto } from './account-info-dto';
|
||||
@@ -308,5 +308,5 @@ export interface OnboardingRequestDto<Step = REQUEST_STEP> {
|
||||
/**
|
||||
* Дата и время звонка.
|
||||
*/
|
||||
callPurpose?: CallPurposeDto;
|
||||
scheduledCallInfo?: ScheduledCallInfoDto;
|
||||
}
|
||||
|
||||
@@ -5469,5 +5469,8 @@
|
||||
},
|
||||
"documentsRequested.result.success": {
|
||||
"ru": "Заявка отправлена в ЕСК"
|
||||
},
|
||||
"fields.venue.label": {
|
||||
"ru": "Место встречи"
|
||||
}
|
||||
}
|
||||
}
|
||||
+2161
-2121
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,7 @@ import {
|
||||
resendInvites,
|
||||
createDepartureRequestFractal,
|
||||
assignResponsible,
|
||||
shortView
|
||||
} from 'actions/admin';
|
||||
import { changeStatusActionExtended } from 'actions/admin/change-status-extended';
|
||||
import type { ShowModalFn } from 'components';
|
||||
@@ -43,7 +44,7 @@ import { ServiceIcons, Icons, SpecialIcons } from '@platform/ui';
|
||||
export const VIEW: IActionWithAuth = {
|
||||
icon: ServiceIcons.EyeOpened,
|
||||
label: locale.manageRequest.action.view,
|
||||
action: view,
|
||||
action: shortView,
|
||||
name: 'VIEW',
|
||||
authorities: [AUTHORITIES_ONBOARDING_BANK.VIEW],
|
||||
};
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface CallPurposeDto {
|
||||
date: string;
|
||||
time: string;
|
||||
}
|
||||
@@ -20,9 +20,9 @@ const getSteps = (): Record<string, IStep> => ({
|
||||
value: REQUEST_STEP.TARIFF,
|
||||
label: locale.steps.tariff,
|
||||
},
|
||||
[REQUEST_STEP.CALL_PURPOSE]: {
|
||||
[REQUEST_STEP.CALL_SCHEDULE]: {
|
||||
/** Шаг 3: Назначение звонка. */
|
||||
value: REQUEST_STEP.CALL_PURPOSE,
|
||||
value: REQUEST_STEP.CALL_SCHEDULE,
|
||||
label: locale.steps.callPurpose,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { AnyObject } from 'final-form';
|
||||
import type { ISelectOrganizationForm } from 'interfaces';
|
||||
import { locale } from 'localization';
|
||||
import { getCallPurposeValidation } from 'pages/call-purpose/validation';
|
||||
import { validateOrganizationForm } from 'pages/organization-ip-fractal/validation';
|
||||
import { getOrganizationUlFormValidation } from 'pages/organization-ul-fractal/validation';
|
||||
import { getProductsFormValidation } from 'pages/products/validation';
|
||||
import { getScheduledCallInfoValidation } from 'pages/scheduled-call-info/validation';
|
||||
|
||||
/**
|
||||
* Информация о шаге заявки.
|
||||
@@ -21,7 +21,7 @@ export enum REQUEST_STEP {
|
||||
/**
|
||||
* Назначение звонка.
|
||||
*/
|
||||
CALL_PURPOSE = 'CALL_PURPOSE',
|
||||
CALL_SCHEDULE = 'CALL_SCHEDULE',
|
||||
/**
|
||||
* Завершение.
|
||||
*/
|
||||
@@ -31,20 +31,20 @@ export enum REQUEST_STEP {
|
||||
export const getFormViewHeader = (clientShortName: string): Record<string, string> => ({
|
||||
[REQUEST_STEP.DATACLIENT]: clientShortName,
|
||||
[REQUEST_STEP.TARIFF]: locale.steps.tariff,
|
||||
[REQUEST_STEP.CALL_PURPOSE]: locale.steps.callPurpose,
|
||||
[REQUEST_STEP.CALL_SCHEDULE]: locale.steps.callPurpose,
|
||||
});
|
||||
|
||||
/** Валидаторы формы ИП. */
|
||||
export const getFormValidatorIp = (currentUserId: string): Record<string, (values: AnyObject) => Record<string, unknown>> => ({
|
||||
[REQUEST_STEP.DATACLIENT]: validateOrganizationForm,
|
||||
[REQUEST_STEP.TARIFF]: getProductsFormValidation(currentUserId),
|
||||
[REQUEST_STEP.CALL_PURPOSE]: getCallPurposeValidation(),
|
||||
[REQUEST_STEP.CALL_SCHEDULE]: getScheduledCallInfoValidation(),
|
||||
});
|
||||
|
||||
/** Валидаторы формы ЮЛ. */
|
||||
export const getFormValidatorUl = (currentUserId: string): Record<string, (values: AnyObject) => Record<string, unknown>> => ({
|
||||
[REQUEST_STEP.DATACLIENT]: getOrganizationUlFormValidation(),
|
||||
[REQUEST_STEP.TARIFF]: getProductsFormValidation(currentUserId),
|
||||
[REQUEST_STEP.CALL_PURPOSE]: getCallPurposeValidation(),
|
||||
[REQUEST_STEP.CALL_SCHEDULE]: getScheduledCallInfoValidation(),
|
||||
});
|
||||
export const getSelectOrgFieldName = (fieldName: keyof ISelectOrganizationForm) => fieldName;
|
||||
|
||||
@@ -93,7 +93,7 @@ export const ContentOld: React.FC<ContentProps & FormRenderProps<IShortOnboardin
|
||||
// return viewMode ? locale.reservedAccounts.agreementTextBankView : locale.reservedAccounts.agreementTextBank;
|
||||
// }, [viewMode]);
|
||||
|
||||
const showSkipButton = !viewMode && [REQUEST_STEP.CALL_PURPOSE, REQUEST_STEP.TARIFF].includes(step);
|
||||
const showSkipButton = !viewMode && [REQUEST_STEP.CALL_SCHEDULE, REQUEST_STEP.TARIFF].includes(step);
|
||||
|
||||
// const showCreateTechReqButton = step === REQUEST_STEP.SERVICES_FORMS && onboardingCreateTechReqEnabled && !doc?.lastFoRequest;
|
||||
const showCreateTechReqButton = false;
|
||||
@@ -123,7 +123,7 @@ export const ContentOld: React.FC<ContentProps & FormRenderProps<IShortOnboardin
|
||||
}, []);
|
||||
|
||||
const handleSkip = useCallback(() => {
|
||||
const submitTypeNew = step === REQUEST_STEP.CALL_PURPOSE ? FORM_SUBMIT_TYPE.SKIP_CALL_PURPOSE : FORM_SUBMIT_TYPE.SKIP;
|
||||
const submitTypeNew = step === REQUEST_STEP.CALL_SCHEDULE ? FORM_SUBMIT_TYPE.SKIP_CALL_PURPOSE : FORM_SUBMIT_TYPE.SKIP;
|
||||
|
||||
change(FORM_FIELDS.SUBMIT_TYPE, submitTypeNew);
|
||||
void submit();
|
||||
@@ -144,7 +144,7 @@ export const ContentOld: React.FC<ContentProps & FormRenderProps<IShortOnboardin
|
||||
scrollToFirstError(errors);
|
||||
}
|
||||
|
||||
formStep === REQUEST_STEP.CALL_PURPOSE
|
||||
formStep === REQUEST_STEP.CALL_SCHEDULE
|
||||
? change(FORM_FIELDS.SUBMIT_TYPE, FORM_SUBMIT_TYPE.SEND)
|
||||
: change(FORM_FIELDS.SUBMIT_TYPE, FORM_SUBMIT_TYPE.NEXT);
|
||||
}, [getFormState, change]);
|
||||
@@ -202,7 +202,7 @@ export const ContentOld: React.FC<ContentProps & FormRenderProps<IShortOnboardin
|
||||
|
||||
if (viewMode) {
|
||||
submitButtonText = locale.action.next;
|
||||
} else if (step === REQUEST_STEP.CALL_PURPOSE) {
|
||||
} else if (step === REQUEST_STEP.CALL_SCHEDULE) {
|
||||
submitButtonText = locale.action.scheduleCall;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CallPurpose } from '../call-purpose';
|
||||
import { ScheduledCallInfo } from '../scheduled-call-info';
|
||||
import OrganizationFormIp from './components/organization-ip-fractal';
|
||||
import OrganizationFormUl from './components/organization-ul-fractal';
|
||||
import { REQUEST_STEP } from './constants';
|
||||
@@ -10,12 +10,12 @@ import { Tariff } from './views/tariff';
|
||||
export const FORM_VIEW_IP: Record<string, React.FC> = {
|
||||
[REQUEST_STEP.DATACLIENT]: OrganizationFormIp,
|
||||
[REQUEST_STEP.TARIFF]: Tariff,
|
||||
[REQUEST_STEP.CALL_PURPOSE]: CallPurpose,
|
||||
[REQUEST_STEP.CALL_SCHEDULE]: ScheduledCallInfo,
|
||||
};
|
||||
|
||||
/** Шаги формы ЮЛ. */
|
||||
export const FORM_VIEW_UL: Record<string, React.FC> = {
|
||||
[REQUEST_STEP.DATACLIENT]: OrganizationFormUl,
|
||||
[REQUEST_STEP.TARIFF]: Tariff,
|
||||
[REQUEST_STEP.CALL_PURPOSE]: CallPurpose,
|
||||
[REQUEST_STEP.CALL_SCHEDULE]: ScheduledCallInfo,
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ export const getBankFormSteps = (accountType: ACCOUNT_TYPE) => [REQUEST_STEP.DAT
|
||||
|
||||
// TODO: если не понадобиться, то удалить accountType
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const getClientFormSteps = (accountType: ACCOUNT_TYPE) => [REQUEST_STEP.DATACLIENT, REQUEST_STEP.TARIFF, REQUEST_STEP.CALL_PURPOSE];
|
||||
export const getClientFormSteps = (accountType: ACCOUNT_TYPE) => [REQUEST_STEP.DATACLIENT, REQUEST_STEP.TARIFF, REQUEST_STEP.CALL_SCHEDULE];
|
||||
/**
|
||||
* Получение списка шагов по порядку.
|
||||
*
|
||||
|
||||
@@ -18,9 +18,9 @@ import { WrapperDocumentActions } from './styled';
|
||||
/** ЭФ Клиента "Ожидание звонка". */
|
||||
export const ResultClientViewCallWaiting: React.FC = () => {
|
||||
const { data: doc } = useDoc();
|
||||
const { number: docNumber, date: docDate, callPurpose, bankClient } = doc!;
|
||||
const callPurposeDate = callPurpose && dateTime(callPurpose.date).format('DD MMMM');
|
||||
const times = callPurpose ? callPurpose.time.split('–') : [];
|
||||
const { number: docNumber, date: docDate, scheduledCallInfo, bankClient } = doc!;
|
||||
const scheduledCallInfoDate = scheduledCallInfo && dateTime(scheduledCallInfo.date).format('DD MMMM');
|
||||
const times = scheduledCallInfo ? scheduledCallInfo.time.split('–') : [];
|
||||
const [timeStart, timeEnd] = times;
|
||||
const isIp = bankClient.clientType === BANK_CLIENT_TYPE.IP;
|
||||
|
||||
@@ -99,9 +99,9 @@ export const ResultClientViewCallWaiting: React.FC = () => {
|
||||
badgeTitle={locale.resultView.callPurpose.status}
|
||||
badgeType="warning"
|
||||
title={
|
||||
callPurposeDate
|
||||
scheduledCallInfoDate
|
||||
? locale.resultView.callPurpose.header({
|
||||
date: callPurposeDate,
|
||||
date: scheduledCallInfoDate,
|
||||
timeStart,
|
||||
timeEnd,
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Поля формы "Назначить звонок". */
|
||||
export enum CALL_PURPOSE_FIELDS {
|
||||
// Дата звонка
|
||||
DATE = 'callPurpose.date',
|
||||
DATE = 'scheduledCallInfo.date',
|
||||
// Временной слот звонка
|
||||
TIME = 'callPurpose.time',
|
||||
TIME = 'scheduledCallInfo.time',
|
||||
}
|
||||
@@ -6,18 +6,18 @@ import { locale } from 'localization';
|
||||
import { useField, useForm } from 'react-final-form';
|
||||
import { getRequestFieldName } from 'utils';
|
||||
import { CALL_PURPOSE_FIELDS } from './constants';
|
||||
import type { CallPurposeDto } from './interfaces';
|
||||
import type { ScheduledCallInfoDto } from './interfaces';
|
||||
import { generateDateSlots, generateTimeSlots } from './utils';
|
||||
|
||||
/** Блок "Назначение звонка". */
|
||||
export const CallPurpose: React.FC = () => {
|
||||
export const ScheduledCallInfo: React.FC = () => {
|
||||
const formApi = useForm();
|
||||
const {
|
||||
input: { value: callPurpose },
|
||||
} = useField<CallPurposeDto>(getRequestFieldName('callPurpose'), { subscription: { value: true } });
|
||||
input: { value: scheduledCallInfo },
|
||||
} = useField<ScheduledCallInfoDto>(getRequestFieldName('scheduledCallInfo'), { subscription: { value: true } });
|
||||
|
||||
const dateChips = useMemo(() => generateDateSlots(), []);
|
||||
const timeChips = useMemo(() => generateTimeSlots(callPurpose?.date ?? dateChips[0].value), [callPurpose?.date, dateChips]);
|
||||
const timeChips = useMemo(() => generateTimeSlots(scheduledCallInfo?.date ?? dateChips[0].value), [scheduledCallInfo?.date, dateChips]);
|
||||
|
||||
const isLoading = false;
|
||||
|
||||
@@ -41,4 +41,4 @@ export const CallPurpose: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
CallPurpose.displayName = 'CallPurpose';
|
||||
ScheduledCallInfo.displayName = 'ScheduledCallInfo';
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface ScheduledCallInfoDto {
|
||||
date: string;
|
||||
time: string;
|
||||
}
|
||||
@@ -4,12 +4,13 @@ import type { AnySchema } from 'yup';
|
||||
import * as yup from 'yup';
|
||||
import { validateSync } from '@platform/validation';
|
||||
|
||||
const callPurposeSchema = (): AnySchema =>
|
||||
const scheduledCallInfoSchema = (): AnySchema =>
|
||||
yup.object().shape({
|
||||
callPurpose: yup.object({
|
||||
scheduledCallInfo: yup.object({
|
||||
date: yup.string().required(locale.callPurpose.date.errors.empty),
|
||||
time: yup.string().required(locale.callPurpose.date.errors.empty),
|
||||
}),
|
||||
});
|
||||
|
||||
export const getCallPurposeValidation = () => validateSync(callPurposeSchema()) as (values: AnyObject) => Record<string, unknown>;
|
||||
export const getScheduledCallInfoValidation = () =>
|
||||
validateSync(scheduledCallInfoSchema()) as (values: AnyObject) => Record<string, unknown>;
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
} from 'pages/scroller/admin/constants';
|
||||
import { NewFilter, validateFilterFields, TechnicalFilter } from 'pages/scroller/admin/filter';
|
||||
import { AdminOnboardingScroller } from 'pages/scroller/admin/new-requests';
|
||||
import { AdminOnboardingShortScroller } from 'pages/scroller/admin/short-requests';
|
||||
import { AdminTechnicalScroller } from 'pages/scroller/admin/technical';
|
||||
import { TAB, TABS_OPTIONS } from 'pages/scroller/constants';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
@@ -36,6 +35,7 @@ import {
|
||||
import { MainLayout } from '@platform/services/admin';
|
||||
import type { IOption } from '@platform/ui';
|
||||
import { Box, FilterButton, Horizon, ROLE, ScrollerManagerHeader, Separator, Tabs } from '@platform/ui';
|
||||
import AdminOnboardingShortScroller from '../short-admin/scroller/scroller';
|
||||
import css from './style.scss';
|
||||
|
||||
const ScrollerPage = () => {
|
||||
@@ -72,24 +72,28 @@ const ScrollerPage = () => {
|
||||
filterResult: newIpFilterResult,
|
||||
filter: NewFilter,
|
||||
filterValidation: validateFilterFields,
|
||||
hideFilter: false,
|
||||
},
|
||||
[TAB.SHORT]: {
|
||||
scroller: AdminOnboardingShortScroller,
|
||||
filterResult: shortFilterResult,
|
||||
filter: NewFilter,
|
||||
filterValidation: validateFilterFields,
|
||||
hideFilter: true,
|
||||
},
|
||||
[TAB.ARCHIVE]: {
|
||||
scroller: ArchiveRequestsScroller,
|
||||
filterResult: archiveFilterResult,
|
||||
filter: ArchiveRequestsFilters,
|
||||
filterValidation: validateArchiveRequestFilterFields,
|
||||
hideFilter: false,
|
||||
},
|
||||
[TAB.TECHNICAL]: {
|
||||
scroller: AdminTechnicalScroller,
|
||||
filterResult: technicalFilterResult,
|
||||
filter: TechnicalFilter,
|
||||
filterValidation: validateFilterFields,
|
||||
hideFilter: false,
|
||||
},
|
||||
}),
|
||||
[newIpFilterResult, archiveFilterResult, technicalFilterResult, shortFilterResult]
|
||||
@@ -174,7 +178,7 @@ const ScrollerPage = () => {
|
||||
<Box className={enableFractalAdapter ? css.container : undefined} fill="BASE">
|
||||
<ScrollerManagerHeader
|
||||
filterNode={
|
||||
currentScroller.filterResult ? (
|
||||
currentScroller.filterResult && !currentScroller.hideFilter ? (
|
||||
<FilterButton
|
||||
data-name={'filter'}
|
||||
data-role={ROLE.BUTTON}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import { Gap, Typography } from '@platform/ui';
|
||||
import css from './styles.scss';
|
||||
|
||||
/** Интерфейс компонента для первой строки столбца. */
|
||||
interface IFirstRowProps {
|
||||
/** Название поля для data-field. */
|
||||
dataField?: string;
|
||||
}
|
||||
|
||||
/** Первая строка в столбце. */
|
||||
export const FirstRow: React.FC<IFirstRowProps> = ({ children, dataField }) => (
|
||||
<Typography.Text data-field={dataField} line="COLLAPSE" title={children?.toString()}>
|
||||
{children}
|
||||
</Typography.Text>
|
||||
);
|
||||
|
||||
FirstRow.displayName = 'FirstRow';
|
||||
|
||||
/** Интерфейс компонента плашка с признаком заявки. */
|
||||
interface IBannerBlockProps {
|
||||
/** Название поля для data-field. */
|
||||
dataField?: string;
|
||||
}
|
||||
|
||||
/** Плашка с признаком заявки. */
|
||||
export const BannerBlock: React.FC<IBannerBlockProps> = ({ children, dataField }) => (
|
||||
<div data-field={dataField}>
|
||||
<span className={css.banner}>{children}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
BannerBlock.displayName = 'BannerBlock';
|
||||
|
||||
/** Тег заявки. */
|
||||
export const TagBlock: React.FC = ({ children }) => (
|
||||
<div>
|
||||
<span className={css.tag}>{children}</span>
|
||||
</div>
|
||||
);
|
||||
TagBlock.displayName = 'TagBlock';
|
||||
|
||||
/** Интерфейс компонента столбца. */
|
||||
interface IColumnProps {
|
||||
/** Первая строка в столбце. */
|
||||
firstRow: ReactNode;
|
||||
/** Вторая строка в столбце. */
|
||||
secondRow?: ReactNode;
|
||||
}
|
||||
|
||||
/** Компонент столбца таблицы. */
|
||||
export const Column: React.FC<IColumnProps> = ({ firstRow, secondRow }) => (
|
||||
<>
|
||||
{typeof firstRow === 'string' ? (
|
||||
<Typography.Text line="COLLAPSE" title={firstRow?.toString()}>
|
||||
{firstRow}
|
||||
</Typography.Text>
|
||||
) : (
|
||||
firstRow
|
||||
)}
|
||||
{secondRow && (
|
||||
<>
|
||||
<Gap.XS />
|
||||
{secondRow}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
Column.displayName = 'Column';
|
||||
|
||||
/** Интерфейс компонента для второй строки столбца. */
|
||||
interface ISecondRowProps {
|
||||
/** Лейбл контента. */
|
||||
label?: string;
|
||||
/** Сворачивание контента. */
|
||||
collapsed?: boolean;
|
||||
/** Название поля для data-field. */
|
||||
dataField: string;
|
||||
/** Дочерные элементы. */
|
||||
children: number | string;
|
||||
/** Тег. */
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
/** Дополнительный компонент для второй строки столбца таблицы. */
|
||||
export const SecondRow: React.FC<ISecondRowProps> = ({ label, dataField, children, collapsed, tag }) => (
|
||||
<>
|
||||
{label && (
|
||||
<>
|
||||
<Typography.SmallText inline fill="FAINT">
|
||||
{label}
|
||||
</Typography.SmallText>
|
||||
<Gap.X2S inline />
|
||||
</>
|
||||
)}
|
||||
<Typography.SmallTextBold
|
||||
inline
|
||||
data-field={dataField}
|
||||
fill="FAINT"
|
||||
line={collapsed ? 'COLLAPSE' : undefined}
|
||||
style={collapsed ? { minWidth: 0, overflow: 'hidden', whiteSpace: 'nowrap' } : undefined}
|
||||
title={children.toString()}
|
||||
>
|
||||
{children || '-'}
|
||||
</Typography.SmallTextBold>
|
||||
{tag && (
|
||||
<>
|
||||
<Gap.X2S />
|
||||
<TagBlock>{tag}</TagBlock>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
SecondRow.displayName = 'SecondRow';
|
||||
@@ -1,278 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { OnboardingIcons } from 'components';
|
||||
import type { IOnboardingAppConfigAdmin } from 'interfaces';
|
||||
import type { OnboardingRequestBankScrollerDto } from 'interfaces/admin';
|
||||
import { locale } from 'localization';
|
||||
import { ADMIN_STATUS_BY_COLOR } from 'stream-constants/admin';
|
||||
import { CALL_RESULT_LABELS } from 'stream-constants/admin/mini-crm';
|
||||
import { MARKETING_CAMPAIGN_CODE_LABELS } from 'stream-constants/admin/promo';
|
||||
import { SIGN_PLACE_ICONS, SIGN_PLACE_LABELS } from 'stream-constants/admin/signer-info';
|
||||
import { getBankRequestStatusText, getOnbConfig, getSurnameWithInitials } from 'utils';
|
||||
import { formatDate, formatDateTime } from '@platform/tools/date-time';
|
||||
import type { IOmniTableColumn } from '@platform/ui';
|
||||
import { Box, Gap, Horizon, Icons, Status, Typography, WithInfoTooltip } from '@platform/ui';
|
||||
import { BannerBlock, Column, FirstRow, SecondRow } from './column';
|
||||
import { StatusColumnInviteError } from './status-column-invite-error';
|
||||
import css from './styles.scss';
|
||||
|
||||
/** Колонка "Дата и номер". */
|
||||
const DateAndNumberColumn: React.FC<OnboardingRequestBankScrollerDto> = ({ date, number, lastFoRequest }) => (
|
||||
<Column
|
||||
firstRow={<FirstRow dataField="date">{formatDate(date)}</FirstRow>}
|
||||
secondRow={
|
||||
<SecondRow dataField="number" tag={lastFoRequest ? locale.scroller.tag.techreq : undefined}>
|
||||
{locale.scroller.card.number({ value: number })}
|
||||
</SecondRow>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
DateAndNumberColumn.displayName = 'DateAndNumberColumn';
|
||||
|
||||
/** Колонка клиента банка (Заявитель). */
|
||||
const BankClientColumn: React.FC<OnboardingRequestBankScrollerDto> = row => {
|
||||
const {
|
||||
bankClient,
|
||||
accountInfo: { isBsc },
|
||||
} = row;
|
||||
const { inn, shortName, name } = bankClient || { inn: '', name: '' };
|
||||
|
||||
const bscCode = isBsc ? locale.bankSupport.BSC : undefined;
|
||||
const marketingCampaignCode: string | undefined = bankClient.marketingCampaignCode
|
||||
? MARKETING_CAMPAIGN_CODE_LABELS[bankClient.marketingCampaignCode]
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Column
|
||||
firstRow={shortName || name}
|
||||
secondRow={
|
||||
<Horizon>
|
||||
{inn && (
|
||||
<SecondRow dataField="inn" label={locale.scroller.card.inn}>
|
||||
{inn}
|
||||
</SecondRow>
|
||||
)}
|
||||
{/* очень важный див, без него не работает text-overflow: ellispis во flex-контейнере */}
|
||||
<div />
|
||||
</Horizon>
|
||||
}
|
||||
/>
|
||||
{bscCode && (
|
||||
<>
|
||||
<Gap.XS />
|
||||
<BannerBlock dataField="bsc">{bscCode}</BannerBlock>
|
||||
</>
|
||||
)}
|
||||
{marketingCampaignCode && (
|
||||
<>
|
||||
<Gap.XS />
|
||||
<BannerBlock dataField="marketingCampaign">{marketingCampaignCode}</BannerBlock>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
BankClientColumn.displayName = 'BankClientColumn';
|
||||
|
||||
/** Колонка "Подразделение". */
|
||||
export const BranchColumn: React.FC<OnboardingRequestBankScrollerDto> = ({ branch = { name: '' } }) => {
|
||||
const { name: branchName } = branch;
|
||||
|
||||
return (
|
||||
<Column
|
||||
firstRow={
|
||||
<Typography.Text data-field="branchName" line="BREAK">
|
||||
{branchName}
|
||||
</Typography.Text>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
BranchColumn.displayName = 'BranchColumn';
|
||||
|
||||
/** Колонка "Подписание договора". */
|
||||
export const ContractSignColumn: React.FC<OnboardingRequestBankScrollerDto> = ({ signPlace, lastManagerVisit }) => {
|
||||
const Icon = useMemo(() => {
|
||||
if (lastManagerVisit) {
|
||||
return OnboardingIcons.DeliveryComplete;
|
||||
}
|
||||
|
||||
return SIGN_PLACE_ICONS[signPlace];
|
||||
}, [lastManagerVisit, signPlace]);
|
||||
|
||||
if (lastManagerVisit || signPlace) {
|
||||
return (
|
||||
<Column
|
||||
firstRow={
|
||||
<Horizon align="TOP">
|
||||
<Icon fill="FAINT" />
|
||||
<Gap.SM />
|
||||
<Typography.Text>{lastManagerVisit ? locale.signPlace.departureComplete : SIGN_PLACE_LABELS[signPlace]}</Typography.Text>
|
||||
</Horizon>
|
||||
}
|
||||
secondRow={
|
||||
lastManagerVisit ? (
|
||||
<Typography.SmallText fill="FAINT" line="COLLAPSE" title={lastManagerVisit.comment}>
|
||||
{lastManagerVisit.comment}
|
||||
</Typography.SmallText>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
ContractSignColumn.displayName = 'ContractSignColumn';
|
||||
|
||||
/** Колонка "Звонки клиенту". */
|
||||
export const ClientCallColumn: React.FC<OnboardingRequestBankScrollerDto> = ({ lastClientCall, responsibleBankUserFio }) => {
|
||||
const { assignResponsibleEnabled } = getOnbConfig<IOnboardingAppConfigAdmin>();
|
||||
|
||||
const responsibleUser = responsibleBankUserFio ? `${locale.common.employee} ${getSurnameWithInitials(responsibleBankUserFio)}` : '';
|
||||
|
||||
if (lastClientCall) {
|
||||
return (
|
||||
<Column
|
||||
firstRow={
|
||||
<>
|
||||
<Typography.Text>
|
||||
{formatDateTime(lastClientCall.callDateTime, { format: 'DD.MM.YYYY, HH:mm', keepLocalTime: true })}
|
||||
</Typography.Text>
|
||||
<Typography.Text>{CALL_RESULT_LABELS[lastClientCall.callResult]}</Typography.Text>
|
||||
</>
|
||||
}
|
||||
secondRow={
|
||||
<>
|
||||
{assignResponsibleEnabled && responsibleBankUserFio && (
|
||||
<Typography.SmallText fill="FAINT" line="COLLAPSE" title={responsibleUser}>
|
||||
{responsibleUser}
|
||||
</Typography.SmallText>
|
||||
)}
|
||||
<Typography.SmallText fill="FAINT" line="COLLAPSE" title={lastClientCall.operationistComment}>
|
||||
{lastClientCall.operationistComment}
|
||||
</Typography.SmallText>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
ClientCallColumn.displayName = 'ClientCallColumn';
|
||||
|
||||
/** Колонка "Статус заявки". */
|
||||
export const StatusColumn: React.FC<OnboardingRequestBankScrollerDto> = props => {
|
||||
const { status, commentForBank, lastFoRequest, onRework } = props;
|
||||
|
||||
const statusText = getBankRequestStatusText({ status, lastFoRequest });
|
||||
|
||||
return (
|
||||
<Column
|
||||
firstRow={
|
||||
<Status data-field="status" type={ADMIN_STATUS_BY_COLOR[status]}>
|
||||
<Typography.Text>
|
||||
{statusText}
|
||||
{onRework && ` (${locale.status.bank.onRework.toLowerCase()})`}
|
||||
</Typography.Text>
|
||||
</Status>
|
||||
}
|
||||
secondRow={
|
||||
<Box>
|
||||
<StatusColumnInviteError {...props} />
|
||||
<WithInfoTooltip text={commentForBank}>
|
||||
{ref => (
|
||||
<Typography.SmallText fill={'FAINT'} innerRef={ref} line="COLLAPSE">
|
||||
{commentForBank}
|
||||
</Typography.SmallText>
|
||||
)}
|
||||
</WithInfoTooltip>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
StatusColumn.displayName = 'StatusColumn';
|
||||
|
||||
/** Колонка "Информация о признаках "Группа Газпром" и "Особый приоритет обработки"". */
|
||||
export const SignsColumn: React.FC<OnboardingRequestBankScrollerDto> = ({
|
||||
accountInfo: { isGazpromContractor },
|
||||
externalSystemsRequisites,
|
||||
}) => (
|
||||
<Horizon align="TOP">
|
||||
{externalSystemsRequisites?.specialPriority && (
|
||||
<WithInfoTooltip text={locale.scroller.specialPriority}>
|
||||
{ref => (
|
||||
<Box ref={ref}>
|
||||
<OnboardingIcons.Warning className={css.icon} fill="WARNING" scale="MD" />
|
||||
</Box>
|
||||
)}
|
||||
</WithInfoTooltip>
|
||||
)}
|
||||
<Horizon.Spacer />
|
||||
{isGazpromContractor && <Icons.Bookmark className={css.icon} fill="FAINT" scale="MD" />}
|
||||
</Horizon>
|
||||
);
|
||||
|
||||
/** Колонки таблицы ЭФ "Журнал заявок на открытие первого счета". */
|
||||
export const getColumns = () => {
|
||||
const specialPriorityEnabled = getOnbConfig().onboarding?.specialPriorityInScrollerEnabled;
|
||||
|
||||
const columns: Array<IOmniTableColumn<OnboardingRequestBankScrollerDto>> = [
|
||||
{
|
||||
title: locale.columns.bank.dateAndNumber,
|
||||
width: 140,
|
||||
selector: DateAndNumberColumn,
|
||||
},
|
||||
{
|
||||
title: locale.columns.bank.bankClient,
|
||||
width: 210,
|
||||
selector: BankClientColumn,
|
||||
},
|
||||
{
|
||||
title: locale.columns.bank.branch,
|
||||
width: 220,
|
||||
selector: BranchColumn,
|
||||
},
|
||||
{
|
||||
title: locale.columns.bank.contractSign,
|
||||
width: 220,
|
||||
selector: ContractSignColumn,
|
||||
},
|
||||
{
|
||||
title: getOnbConfig<IOnboardingAppConfigAdmin>()?.clientCallsScrollerSortEnabled
|
||||
? locale.columns.bank.calls
|
||||
: locale.columns.bank.callsOld,
|
||||
width: 220,
|
||||
selector: ClientCallColumn,
|
||||
},
|
||||
{
|
||||
title: locale.columns.bank.status,
|
||||
width: 220,
|
||||
selector: StatusColumn,
|
||||
},
|
||||
];
|
||||
|
||||
if (specialPriorityEnabled) {
|
||||
columns.push({
|
||||
width: 64,
|
||||
selector: SignsColumn,
|
||||
});
|
||||
} else {
|
||||
columns.unshift({
|
||||
width: 16,
|
||||
selector({ accountInfo: { isGazpromContractor } }) {
|
||||
return isGazpromContractor && <Icons.Bookmark className={css.icon} fill={'CRITIC'} scale={'SM'} />;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return columns;
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
import React from 'react';
|
||||
import type { IOnboardingAppConfigAdmin } from 'interfaces';
|
||||
import type { OnboardingRequestBankScrollerDto } from 'interfaces/admin';
|
||||
import { locale } from 'localization';
|
||||
import { STATUS } from 'stream-constants';
|
||||
import { getOnbConfig } from 'utils/configs';
|
||||
import { Typography } from '@platform/ui';
|
||||
|
||||
/** Компонент ошибки приглашений для колонки статуса. */
|
||||
export const StatusColumnInviteError: React.FC<OnboardingRequestBankScrollerDto> = ({
|
||||
hasAccessGrantedInviteSentError,
|
||||
hasAccessRevokeError,
|
||||
hasSimpleInviteSentError,
|
||||
status,
|
||||
}) => {
|
||||
const { showAccessRightsAndInviteSuccessInfo, revokeOrgDuplicateInvitesEnbaled } = getOnbConfig<IOnboardingAppConfigAdmin>();
|
||||
|
||||
const isApprovedOrWaitingForEskAccounts = [STATUS.APPROVED, STATUS.WAITING_FOR_ESK_ACCOUNTS].includes(status);
|
||||
const isRefusedOrRejected = [STATUS.REFUSED, STATUS.REJECTED].includes(status);
|
||||
|
||||
// Условие флагов с учетом проверки на статус если у нас включена настройка доработки отзывов.
|
||||
const hasAccessGrantedInviteSentErrorExtended =
|
||||
hasAccessGrantedInviteSentError && (!revokeOrgDuplicateInvitesEnbaled || isApprovedOrWaitingForEskAccounts);
|
||||
const hasAccessRevokeErrorExtended = hasAccessRevokeError && (!revokeOrgDuplicateInvitesEnbaled || isRefusedOrRejected);
|
||||
|
||||
if (showAccessRightsAndInviteSuccessInfo && (hasAccessGrantedInviteSentErrorExtended || hasAccessRevokeErrorExtended)) {
|
||||
return (
|
||||
<Typography.SmallText fill="CRITIC">
|
||||
{hasAccessGrantedInviteSentErrorExtended ? locale.accessGrantedInviteSentError : locale.accessRevokeError}
|
||||
</Typography.SmallText>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasSimpleInviteSentError) {
|
||||
return <Typography.SmallText fill="CRITIC">{locale.errorInviteSent}</Typography.SmallText>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
StatusColumnInviteError.displayName = 'StatusColumnInviteError';
|
||||
@@ -1,21 +0,0 @@
|
||||
.icon {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.banner {
|
||||
padding: 0 4px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
background-color: var(--box-fill-faint-strong);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 0 4px;
|
||||
color: var(--font-fill-accent-strong);
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
display: inline-block;
|
||||
background-color: #cedff4;
|
||||
border-radius: 4px;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// prettier-ignore
|
||||
// This file is automatically generated by typings-for-css-modules.
|
||||
// Don't change it directly!
|
||||
|
||||
declare namespace StylesScssNamespace {
|
||||
export interface IStylesScss {
|
||||
'banner': string;
|
||||
'icon': string;
|
||||
'tag': string;
|
||||
}
|
||||
}
|
||||
|
||||
declare const StylesScssModule: StylesScssNamespace.IStylesScss;
|
||||
|
||||
export = StylesScssModule;
|
||||
@@ -1,157 +0,0 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import type { IOnboardingAdminContext } from 'action-executers/admin';
|
||||
import { onboardingAdminExecutor } from 'action-executers/admin';
|
||||
import type { History, Location } from 'history';
|
||||
import type { OnboardingRequestDto } from 'interfaces';
|
||||
import type { OnboardingRequestBankScrollerDto } from 'interfaces/admin';
|
||||
import { locale } from 'localization';
|
||||
import { getRowActions, getToolbarActions } from 'pages/scroller/admin/action-config';
|
||||
import {
|
||||
CATEGORY_BANK,
|
||||
DBO_CONNECTION_FILTER,
|
||||
FILTER_FIELD,
|
||||
getSortFields,
|
||||
ON_REWORK_FILTER_ALL,
|
||||
SORT_FIELD,
|
||||
} from 'pages/scroller/admin/constants';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import type { IOnboardingAdminService } from 'services';
|
||||
import { onboardingAdminService } from 'services';
|
||||
import { AUTHORITIES_ONBOARDING_BANK, CATEGORY, LOCALIZATION_RESOURCE, SCROLLER_QUERY_PARAMS, STATUS } from 'stream-constants';
|
||||
import type { IFilters } from '@platform/core';
|
||||
import { applyMiddlewares, getTranslator, onSuccessMiddleware } from '@platform/core';
|
||||
import type { IActionServiceContext, IMetaData } from '@platform/services';
|
||||
import { getRowScrollerPage, parseUrlSearch, SORT_DIRECTION, useAuth, useScrollerData } from '@platform/services';
|
||||
import type { IPlaceholderProps } from '@platform/ui';
|
||||
import { Placeholder } from '@platform/ui';
|
||||
import { getColumns } from './columns';
|
||||
import style from './style.scss';
|
||||
|
||||
const DummyLayout: React.FC = ({ children }) => <div className={style.layout}>{children}</div>;
|
||||
|
||||
/** Админский скроллер заявок на открытие первого счёта. */
|
||||
export const AdminOnboardingShortScroller = React.memo(() => {
|
||||
const { hasAuthority } = useAuth();
|
||||
const { search }: Location = useLocation();
|
||||
const { filters } = useScrollerData() as { filters: IFilters };
|
||||
|
||||
const defaultCategory = useMemo(
|
||||
() => (hasAuthority(AUTHORITIES_ONBOARDING_BANK.UPDATE) ? CATEGORY_BANK.MANUAL_PROCESSING : CATEGORY.ALL),
|
||||
[hasAuthority]
|
||||
);
|
||||
|
||||
const sortFields = useMemo(() => getSortFields(), []);
|
||||
|
||||
const requestIds: string[] = useMemo(() => {
|
||||
const queryParams = parseUrlSearch(search);
|
||||
|
||||
if (queryParams[SCROLLER_QUERY_PARAMS.REQUEST_IDS]) {
|
||||
return String(decodeURIComponent(queryParams[SCROLLER_QUERY_PARAMS.REQUEST_IDS])).split(',');
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [search]);
|
||||
|
||||
const filteredFilters = useMemo(() => {
|
||||
const result: IFilters = Object.keys(filters)
|
||||
.filter(fieldName => !(fieldName === FILTER_FIELD.ON_REWORK && filters[FILTER_FIELD.ON_REWORK].value === ON_REWORK_FILTER_ALL))
|
||||
.filter(
|
||||
fieldName =>
|
||||
!(fieldName === FILTER_FIELD.DBO_CONNECTION && filters[FILTER_FIELD.DBO_CONNECTION].value === DBO_CONNECTION_FILTER.ALL)
|
||||
)
|
||||
.reduce((allFilters, fieldName) => {
|
||||
allFilters[fieldName] = filters[fieldName];
|
||||
|
||||
return allFilters;
|
||||
}, {});
|
||||
|
||||
// Отображение заявок через url сделано не через фильтры напрямую, т.к. ui не поддерживает множественный ввод
|
||||
if (requestIds.length > 0 && !result[FILTER_FIELD.ID]) {
|
||||
result[FILTER_FIELD.ID] = {
|
||||
condition: 'in',
|
||||
fieldName: FILTER_FIELD.ID,
|
||||
value: requestIds,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [filters, requestIds]);
|
||||
|
||||
const isFilter = Object.keys(filteredFilters).length > 0;
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [placeholderProps, setPlaceholderProps] = useState<IPlaceholderProps>(
|
||||
isFilter ? locale.scroller.placeholderWithFilter : locale.scroller.placeholder
|
||||
);
|
||||
|
||||
const onRowDoubleClick = useCallback(({ row }, router: History) => router.push(`/onboarding/manage/${row.id}`), []);
|
||||
|
||||
const fetcher = useCallback(
|
||||
(meta: IMetaData) => onboardingAdminService.getList({ ...meta, filters: filteredFilters }),
|
||||
[filteredFilters]
|
||||
);
|
||||
|
||||
const categoryFetcher = useCallback(
|
||||
(meta: IMetaData) =>
|
||||
onboardingAdminService.getCounter({ ...meta, filters: filteredFilters }).then(categories => {
|
||||
if (!categories.reduce((count, currentCategory) => count + currentCategory.count, 0) && !isFilter) {
|
||||
// заявок нет ни в одной из категорий и фильтр не применяли
|
||||
setPlaceholderProps(locale.scroller.placeholderEmpty);
|
||||
} else {
|
||||
// применили фильтр или заявок нет в данной категории
|
||||
setPlaceholderProps(isFilter ? locale.scroller.placeholderWithFilter : locale.scroller.placeholder);
|
||||
}
|
||||
|
||||
return categories;
|
||||
}),
|
||||
[filteredFilters, isFilter]
|
||||
);
|
||||
|
||||
const scrollerExecutor = applyMiddlewares<IOnboardingAdminContext>(
|
||||
// FIXME: в onSuccessMiddleware ошибка в возвращаемом в callback типе
|
||||
// @ts-expect-error Бага в платформе.
|
||||
onSuccessMiddleware(({ succeeded }) => {
|
||||
if (succeeded.length > 0) {
|
||||
const [result]: [OnboardingRequestDto] = succeeded;
|
||||
|
||||
// Если админ намерен редактировать заявку, заранее добавляем данные в запрос заявки.
|
||||
if ([STATUS.EDITING_DRAFT, STATUS.EDITING_REPLACING_DOCS].includes(result.status)) {
|
||||
queryClient.setQueryData<OnboardingRequestDto>(result.id, result);
|
||||
}
|
||||
}
|
||||
})
|
||||
)(onboardingAdminExecutor);
|
||||
|
||||
const ScrollerPage = getRowScrollerPage<OnboardingRequestBankScrollerDto, IActionServiceContext<IOnboardingAdminService>>({
|
||||
fetcher,
|
||||
categories: {
|
||||
fetcher: categoryFetcher,
|
||||
locale: getTranslator(LOCALIZATION_RESOURCE),
|
||||
defaultCategory,
|
||||
},
|
||||
hideHeader: true,
|
||||
executer: scrollerExecutor,
|
||||
selectable: true,
|
||||
storageKey: 'onboarding-scroller/admin',
|
||||
sorting: {
|
||||
byDefault: {
|
||||
direction: SORT_DIRECTION.DESC,
|
||||
fieldName: SORT_FIELD.CREATED_AT,
|
||||
},
|
||||
sortFields,
|
||||
},
|
||||
onRowDoubleClick,
|
||||
toolbarActions: getToolbarActions,
|
||||
rowActions: getRowActions,
|
||||
columns: getColumns(),
|
||||
mainLayout: DummyLayout,
|
||||
hideContainer: true,
|
||||
placeholder: <Placeholder {...placeholderProps} fullHeight className={style.placeholder} />,
|
||||
});
|
||||
|
||||
return <ScrollerPage />;
|
||||
});
|
||||
|
||||
AdminOnboardingShortScroller.displayName = 'AdminOnboardingScroller';
|
||||
@@ -1,8 +0,0 @@
|
||||
.placeholder {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.layout {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// prettier-ignore
|
||||
// This file is automatically generated by typings-for-css-modules.
|
||||
// Don't change it directly!
|
||||
|
||||
declare namespace StyleScssNamespace {
|
||||
export interface IStyleScss {
|
||||
'layout': string;
|
||||
'placeholder': string;
|
||||
}
|
||||
}
|
||||
|
||||
declare const StyleScssModule: StyleScssNamespace.IStyleScss;
|
||||
|
||||
export = StyleScssModule;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './no-requests-banner';
|
||||
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { useScrollerContext } from '@base-components/scroller';
|
||||
import styled from '@emotion/styled';
|
||||
import type { SystemResponseProps } from '@fractal-ui/extended';
|
||||
import { SystemResponse } from '@fractal-ui/extended';
|
||||
import { useBreakpoints } from '@fractal-ui/styling';
|
||||
import styledCss from '@styled-system/css';
|
||||
import { locale } from 'localization';
|
||||
import { COMMON_STREAM_URL, useRedirect } from '@platform/services';
|
||||
import { SCROLLER_VIEW_TYPE } from '../scroller/interfaces';
|
||||
import { useScrollerViewType } from '../use-scroller-view-type';
|
||||
|
||||
/** Контейнер. */
|
||||
const Container = styled.div<{ isMobile: boolean }>(
|
||||
styledCss({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
}),
|
||||
({ isMobile }) =>
|
||||
isMobile
|
||||
? styledCss({
|
||||
px: 5,
|
||||
pt: 9,
|
||||
pb: 5,
|
||||
textAlign: 'center',
|
||||
backgroundColor: 'bg.primary',
|
||||
borderRadius: '16px',
|
||||
boxShadow: '0 0 16px 0 rgba(78, 88, 134, 0.04)',
|
||||
})
|
||||
: undefined
|
||||
);
|
||||
|
||||
/** Баннер отображения отсутствия заявок. */
|
||||
export const NoRequestsBanner: React.FC = () => {
|
||||
const createRequest = useRedirect(`${COMMON_STREAM_URL.ONBOARDING}/new`);
|
||||
const { current, XS } = useBreakpoints();
|
||||
const bannerType = useScrollerViewType();
|
||||
const { resetFilters } = useScrollerContext();
|
||||
|
||||
const getSize = () => {
|
||||
switch (current) {
|
||||
case 'XS':
|
||||
return 'S';
|
||||
case 'S':
|
||||
return 'M';
|
||||
default:
|
||||
return 'L';
|
||||
}
|
||||
};
|
||||
|
||||
const systemResponsePropsByBannerType: Record<string, SystemResponseProps> = {
|
||||
[SCROLLER_VIEW_TYPE.NO_REQUESTS]: {
|
||||
text: locale.scroller.fractal.noRequestsBanner.noRequests.text,
|
||||
description: locale.scroller.fractal.noRequestsBanner.noRequests.description,
|
||||
mainButtonText: locale.scroller.fractal.noRequestsBanner.noRequests.actionText,
|
||||
statusIcon: 'addNew',
|
||||
mainButtonVariant: 'primary',
|
||||
mainButtonDataAction: 'createRequest',
|
||||
onMainButtonClick: createRequest,
|
||||
size: getSize(),
|
||||
},
|
||||
[SCROLLER_VIEW_TYPE.NO_REQUESTS_IN_CURRENT_CATEGORY]: {
|
||||
textWidth: 285,
|
||||
descriptionWidth: 285,
|
||||
text: locale.scroller.fractal.noRequestsBanner.noRequestsInCurrentCategory.text,
|
||||
description: locale.scroller.fractal.noRequestsBanner.noRequestsInCurrentCategory.description,
|
||||
mainButtonText: locale.scroller.fractal.noRequestsBanner.noRequestsInCurrentCategory.actionText,
|
||||
statusIcon: 'addNew',
|
||||
mainButtonDataAction: 'createRequest',
|
||||
onMainButtonClick: createRequest,
|
||||
size: getSize(),
|
||||
},
|
||||
[SCROLLER_VIEW_TYPE.NO_REQUESTS_WITH_FILTERS]: {
|
||||
text: locale.scroller.fractal.noRequestsBanner.noRequestsWithFilters.text,
|
||||
description: locale.scroller.fractal.noRequestsBanner.noRequestsWithFilters.description,
|
||||
mainButtonText: locale.scroller.fractal.noRequestsBanner.noRequestsWithFilters.actionText,
|
||||
statusIcon: 'empty',
|
||||
mainButtonDataAction: 'resetFilters',
|
||||
onMainButtonClick: resetFilters,
|
||||
size: getSize(),
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Container isMobile={XS}>
|
||||
<SystemResponse {...systemResponsePropsByBannerType[bannerType]} />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
NoRequestsBanner.displayName = 'NoRequestsBanner';
|
||||
@@ -0,0 +1,101 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { locale } from 'localization';
|
||||
import { APPROVED_STATUSES, IN_PROGRESS_STATUSES, STATUS as REG_DOC_STATUS, STATUS_LABEL } from 'stream-constants';
|
||||
import { activeFilterFields, getStatusListLables } from 'utils';
|
||||
import { DATE_FORMAT, filterFields } from '@platform/services';
|
||||
|
||||
/** Поля фильтра. */
|
||||
export enum FILTER_FIELD {
|
||||
/** Дата. */
|
||||
DATE = 'date',
|
||||
/** Дата от. */
|
||||
DATE_FROM = 'dateFrom',
|
||||
/** Дата до. */
|
||||
DATE_TO = 'dateTo',
|
||||
/** ИНН организации. */
|
||||
CLIENT_INN = 'clientInn',
|
||||
/** Наименование организации. */
|
||||
CLIENT_NAME = 'clientName',
|
||||
/** Подразделения. */
|
||||
BRANCHES = 'branches',
|
||||
/** Номер заявки. */
|
||||
NUMBER = 'number',
|
||||
/** Статус. */
|
||||
STATUS = 'status',
|
||||
}
|
||||
|
||||
/** Наименование полей фильтра. */
|
||||
export const FILTER_LABELS: Record<keyof typeof FILTER_FIELD, string> = {
|
||||
DATE: locale.scroller.filter.date,
|
||||
DATE_FROM: locale.scroller.filter.dateFrom,
|
||||
DATE_TO: locale.scroller.filter.dateTo,
|
||||
CLIENT_INN: locale.scroller.filter.clientInn,
|
||||
CLIENT_NAME: locale.scroller.filter.organization,
|
||||
BRANCHES: locale.scroller.filter.service,
|
||||
NUMBER: locale.scroller.filter.number,
|
||||
STATUS: locale.scroller.filter.statusFractal,
|
||||
};
|
||||
|
||||
/** Наименование полей фильтра по ключам. */
|
||||
export const FILTER_LABELS_BY_KEY = Object.entries(FILTER_LABELS).reduce(
|
||||
(acc, [key, value]) => ({ ...acc, [FILTER_FIELD[key]]: value }),
|
||||
{}
|
||||
);
|
||||
|
||||
export const FILTER_FIELDS = {
|
||||
[FILTER_FIELD.DATE_FROM]: filterFields.ge(undefined, FILTER_FIELD.DATE),
|
||||
[FILTER_FIELD.DATE_TO]: filterFields.le(undefined, FILTER_FIELD.DATE),
|
||||
[FILTER_FIELD.CLIENT_INN]: filterFields.eq(undefined, FILTER_FIELD.CLIENT_INN),
|
||||
[FILTER_FIELD.CLIENT_NAME]: filterFields.contains(undefined, FILTER_FIELD.CLIENT_NAME),
|
||||
[FILTER_FIELD.BRANCHES]: filterFields.in(undefined, FILTER_FIELD.BRANCHES),
|
||||
[FILTER_FIELD.STATUS]: filterFields.in(undefined, FILTER_FIELD.STATUS, value => {
|
||||
if (IN_PROGRESS_STATUSES.includes(value as REG_DOC_STATUS)) {
|
||||
return IN_PROGRESS_STATUSES;
|
||||
}
|
||||
|
||||
if (APPROVED_STATUSES.includes(value as REG_DOC_STATUS)) {
|
||||
return APPROVED_STATUSES;
|
||||
}
|
||||
|
||||
return [value];
|
||||
}),
|
||||
[FILTER_FIELD.NUMBER]: filterFields.eq(undefined, FILTER_FIELD.NUMBER, value => Number(value)),
|
||||
};
|
||||
|
||||
/** Описание активных фильтров. */
|
||||
export const ACTIVE_FILTER_FIELDS = {
|
||||
[FILTER_FIELD.DATE_FROM]: activeFilterFields.simple({
|
||||
formatter: value => dayjs(value).format(DATE_FORMAT),
|
||||
}),
|
||||
[FILTER_FIELD.DATE_TO]: activeFilterFields.simple({
|
||||
formatter: value => dayjs(value).format(DATE_FORMAT),
|
||||
}),
|
||||
[FILTER_FIELD.CLIENT_INN]: activeFilterFields.simple(),
|
||||
[FILTER_FIELD.CLIENT_NAME]: activeFilterFields.simple(),
|
||||
[FILTER_FIELD.BRANCHES]: activeFilterFields.list(),
|
||||
[FILTER_FIELD.STATUS]: activeFilterFields.simple({ formatter: value => STATUS_LABEL[value] }),
|
||||
[FILTER_FIELD.NUMBER]: activeFilterFields.simple(),
|
||||
};
|
||||
|
||||
/** Поле для сортировки. */
|
||||
export enum SORT_FIELD {
|
||||
/** Дата создания. */
|
||||
CREATED_AT = 'createdAt',
|
||||
/** Номер заявки. */
|
||||
REQUEST_NUMBER = 'number',
|
||||
/** Статус. */
|
||||
STATUS = 'status',
|
||||
}
|
||||
|
||||
/** Список статусов. */
|
||||
export const FILTER_STATUS_OPTIONS = getStatusListLables(
|
||||
Object.values(REG_DOC_STATUS).filter(status => !['DELETED', 'REFUSED'].includes(status)),
|
||||
STATUS_LABEL,
|
||||
true
|
||||
);
|
||||
|
||||
/** Минимальная для выбора дата. */
|
||||
export const MIN_POSSIBLE_DATE = dayjs('1990-07-31').startOf('day').format();
|
||||
|
||||
/** Максимальная для выбора дата. */
|
||||
export const MAX_POSSIBLE_DATE = dayjs('3000-12-31').endOf('day').format();
|
||||
@@ -0,0 +1 @@
|
||||
export * from './scroller';
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { UseQueryOptions } from 'react-query';
|
||||
import { useQuery } from 'react-query';
|
||||
import { QUERY_KEYS } from 'stream-constants';
|
||||
import { getPrefixedQueryKey } from 'utils';
|
||||
import type { IBranchV2, ICollectionResponse } from '@platform/services';
|
||||
import { dictionaryService } from '@platform/services';
|
||||
|
||||
/** Получение списка филиалов. */
|
||||
export const useBranches = (options?: UseQueryOptions<ICollectionResponse<IBranchV2>, unknown, ICollectionResponse<IBranchV2>>) =>
|
||||
useQuery(
|
||||
getPrefixedQueryKey(QUERY_KEYS.SCROLLER_BRANCHES),
|
||||
() =>
|
||||
dictionaryService.branchV2.getList({
|
||||
offset: 0,
|
||||
pageSize: 10_000,
|
||||
filters: {
|
||||
isDeleted: {
|
||||
condition: 'eq',
|
||||
fieldName: 'isDeleted',
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
refetchOnMount: false,
|
||||
...options,
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
/** Ключ хранения фильтров в localStorage. */
|
||||
export const STORAGE_KEY_FILTERS = 'onboarding-short-scroller-filters';
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { FC } from 'react';
|
||||
import React from 'react';
|
||||
import { useScrollerContext } from '@base-components/scroller';
|
||||
import { createMaskedInput } from '@fractal-ui/composites';
|
||||
import { createField, Fields } from '@fractal-ui/form';
|
||||
import { useBreakpoints } from '@fractal-ui/styling';
|
||||
import { Wrapper } from 'components';
|
||||
import { locale } from 'localization';
|
||||
import { NUMBER_FILTER_MAX_LENGTH } from 'stream-constants';
|
||||
import { FILTER_FIELD, FILTER_LABELS, FILTER_STATUS_OPTIONS, MAX_POSSIBLE_DATE, MIN_POSSIBLE_DATE } from '../../constants';
|
||||
import { getDateFromMaxDate, getDateToMinDate } from '../utils';
|
||||
import { InnField } from './components/inn-field';
|
||||
import { OrganizationField } from './components/organization-field';
|
||||
|
||||
const NumberInput = createField(createMaskedInput({ mask: /^\d+$/ }));
|
||||
|
||||
/** Ширина лейблов у полей. */
|
||||
const LABEL_WIDTH = 180;
|
||||
|
||||
/** Общие фильтры. */
|
||||
export const CommonFilters: FC = () => {
|
||||
const { XS } = useBreakpoints();
|
||||
|
||||
const labelPosition = XS ? 'top' : 'left';
|
||||
const inputSize = XS ? 'M' : 'S';
|
||||
|
||||
const { filterFormValues } = useScrollerContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
{XS && (
|
||||
<>
|
||||
<Wrapper alignItems="end" display="grid" gap="2" gridTemplateColumns={'1fr 9px 1fr'} width="100%">
|
||||
<Fields.DatePicker
|
||||
showPlaceholder
|
||||
label={locale.scroller.filter.createDate}
|
||||
labelPosition="top"
|
||||
maxDate={getDateFromMaxDate(filterFormValues.dateTo)}
|
||||
minDate={MIN_POSSIBLE_DATE}
|
||||
name={FILTER_FIELD.DATE_FROM}
|
||||
size={inputSize}
|
||||
/>
|
||||
<Wrapper alignItems="center" display="flex" height="32px" justifyContent="center" width="9px">
|
||||
–
|
||||
</Wrapper>
|
||||
<Fields.DatePicker
|
||||
showPlaceholder
|
||||
labelPosition="top"
|
||||
maxDate={MAX_POSSIBLE_DATE}
|
||||
minDate={getDateToMinDate(filterFormValues.dateFrom)}
|
||||
name={FILTER_FIELD.DATE_TO}
|
||||
size={inputSize}
|
||||
/>
|
||||
</Wrapper>
|
||||
<OrganizationField
|
||||
alwaysMaxHeight
|
||||
label={locale.scroller.filter.organization}
|
||||
labelPosition="top"
|
||||
name={FILTER_FIELD.CLIENT_NAME}
|
||||
placeholder={XS ? undefined : FILTER_LABELS.CLIENT_NAME}
|
||||
size={inputSize}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<NumberInput
|
||||
label={FILTER_LABELS.NUMBER}
|
||||
labelPosition={labelPosition}
|
||||
labelWidth={LABEL_WIDTH}
|
||||
maxLength={NUMBER_FILTER_MAX_LENGTH}
|
||||
name={FILTER_FIELD.NUMBER}
|
||||
size={inputSize}
|
||||
/>
|
||||
{/* TODO: починить, пока не работает */}
|
||||
{/* <BranchField */}
|
||||
{/* alwaysMaxHeight */}
|
||||
{/* label={FILTER_LABELS.BRANCHES} */}
|
||||
{/* labelPosition={labelPosition} */}
|
||||
{/* labelWidth={LABEL_WIDTH} */}
|
||||
{/* name={FILTER_FIELD.BRANCHES} */}
|
||||
{/* size={inputSize} */}
|
||||
{/* /> */}
|
||||
<InnField
|
||||
alwaysMaxHeight
|
||||
label={FILTER_LABELS.CLIENT_INN}
|
||||
labelPosition={labelPosition}
|
||||
labelWidth={LABEL_WIDTH}
|
||||
name={FILTER_FIELD.CLIENT_INN}
|
||||
size={inputSize}
|
||||
/>
|
||||
<Fields.Select
|
||||
alwaysMaxHeight
|
||||
label={FILTER_LABELS.STATUS}
|
||||
labelPosition={labelPosition}
|
||||
labelWidth={LABEL_WIDTH}
|
||||
name={FILTER_FIELD.STATUS}
|
||||
options={FILTER_STATUS_OPTIONS}
|
||||
size={inputSize}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
CommonFilters.displayName = 'CommonFilters';
|
||||
@@ -0,0 +1,140 @@
|
||||
import type { ComponentProps, FC } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useScrollerContext } from '@base-components/scroller';
|
||||
import type { OptionProps } from '@fractal-ui/composites';
|
||||
import { Fields } from '@fractal-ui/form';
|
||||
import { useBranchesPagination } from 'hooks';
|
||||
import type { OnChangeTypeFractal } from 'interfaces';
|
||||
import { locale } from 'localization';
|
||||
import { FILTER_FIELD } from 'pages/scroller/client-fractal/constants';
|
||||
import { useBranches } from 'queries';
|
||||
import { useField, useForm } from 'react-final-form';
|
||||
import type { IBranchV2, IMetaData } from '@platform/services';
|
||||
import { conditions } from '@platform/services';
|
||||
|
||||
type FieldProps = Omit<ComponentProps<typeof Fields.MultiCombobox>, 'options' | 'type'>;
|
||||
|
||||
/** Фильтры для запроса подразделений. */
|
||||
const filters: IMetaData['filters'] = {
|
||||
isDeleted: {
|
||||
value: false,
|
||||
condition: conditions.eq,
|
||||
fieldName: 'isDeleted',
|
||||
},
|
||||
};
|
||||
|
||||
/** Маппер для создания опции для компонента. */
|
||||
const formatToOption = (branch: IBranchV2) => ({
|
||||
value: branch.id,
|
||||
label: (branch.name === locale.scroller.filter.subsidiary ? branch.filialName : branch.name) ?? '',
|
||||
});
|
||||
|
||||
type InputType = 'default' | 'error' | 'loading' | 'notFound';
|
||||
|
||||
/** Поле с выбором отделения банка. */
|
||||
export const BranchField: FC<FieldProps> = props => {
|
||||
const [selectedOptions, setSelectedOptions] = useState<OptionProps[]>([]);
|
||||
|
||||
const {
|
||||
input: { value: branchIds },
|
||||
} = useField<string[]>('branches', { subscription: { value: true } });
|
||||
|
||||
const { filterFormValues } = useScrollerContext();
|
||||
const { change } = useForm();
|
||||
|
||||
const initialBranchIds = filterFormValues.branches;
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialBranchIds) {
|
||||
// Мультиселект не может определить свой стейт корректно, если четко не задать значение по-умолчанию
|
||||
change(FILTER_FIELD.BRANCHES, []);
|
||||
}
|
||||
}, [initialBranchIds, change]);
|
||||
|
||||
useBranches(
|
||||
{
|
||||
offset: 0,
|
||||
pageSize: initialBranchIds?.length || 0,
|
||||
filters: {
|
||||
...filters,
|
||||
id: {
|
||||
value: initialBranchIds,
|
||||
condition: conditions.in,
|
||||
fieldName: 'id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
enabled: !!(initialBranchIds && initialBranchIds.length > 0),
|
||||
onSuccess: branches => {
|
||||
setSelectedOptions(branches.map(formatToOption));
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
const { data, hasNextPage, isFetchingNextPage, fetchNextPage } = useBranchesPagination({ search: searchText, filters });
|
||||
|
||||
const { options, total } = useMemo(
|
||||
() =>
|
||||
data?.pages?.reduce<{ options: OptionProps[]; total: number }>(
|
||||
(acc, dat) => {
|
||||
const pageOptions = dat?.data?.reduce<OptionProps[]>((dataAcc, branch) => {
|
||||
// Не показываем опции которые уже выбраны.
|
||||
if (selectedOptions.some(({ value }) => value === branch.id)) {
|
||||
return dataAcc;
|
||||
}
|
||||
|
||||
return dataAcc.concat(formatToOption(branch));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
options: acc.options.concat(pageOptions),
|
||||
total: dat.total,
|
||||
};
|
||||
},
|
||||
{ options: selectedOptions.filter(({ value }) => branchIds.includes(String(value))), total: 0 }
|
||||
) || { options: [], total: 0 },
|
||||
[branchIds, data?.pages, selectedOptions]
|
||||
);
|
||||
|
||||
const handleChange: OnChangeTypeFractal<string[]> = useCallback(
|
||||
({ value: values }) => {
|
||||
const branch = options.filter(({ value: branchId }) => values.includes(branchId as string));
|
||||
|
||||
setSelectedOptions(branch);
|
||||
},
|
||||
[options]
|
||||
);
|
||||
|
||||
const handleScrollEnd = useCallback(async () => {
|
||||
await fetchNextPage();
|
||||
}, [fetchNextPage]);
|
||||
|
||||
const searchFn = useCallback(() => options, [options]);
|
||||
const onSearch = useCallback((value?: string) => setSearchText(value || ''), []);
|
||||
|
||||
const inputType: InputType = useMemo(() => {
|
||||
if (!data) return 'loading';
|
||||
|
||||
if (total === 0) return 'notFound';
|
||||
|
||||
return 'default';
|
||||
}, [data, total]);
|
||||
|
||||
return (
|
||||
<Fields.MultiCombobox
|
||||
{...props}
|
||||
options={options}
|
||||
searchFn={searchFn}
|
||||
showLoaderAfterOptions={hasNextPage && !isFetchingNextPage}
|
||||
type={inputType}
|
||||
onChange={handleChange as any}
|
||||
onListScrollEnd={handleScrollEnd}
|
||||
onSearch={onSearch}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
BranchField.displayName = 'BranchField';
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { ComponentProps, FC } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Fields } from '@fractal-ui/form';
|
||||
import { useQuery } from 'react-query';
|
||||
import { QUERY_KEYS } from 'stream-constants';
|
||||
import { getPrefixedQueryKey } from 'utils';
|
||||
import { dictionaryService } from '@platform/services';
|
||||
import { useDebounce } from '@platform/ui';
|
||||
|
||||
type FieldProps = Omit<ComponentProps<typeof Fields.Select>, 'onSearch' | 'options' | 'type'>;
|
||||
|
||||
/** Поле поиска по ИНН. */
|
||||
export const InnField: FC<FieldProps> = props => {
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const debouncedSearchValue = useDebounce(searchValue, 300);
|
||||
|
||||
const { data = [], isFetching } = useQuery(
|
||||
getPrefixedQueryKey(QUERY_KEYS.CLIENT_INN, debouncedSearchValue),
|
||||
() => dictionaryService.bankClient.searchByName({ offset: 0, pageSize: 10, search: debouncedSearchValue }, 'innKio'),
|
||||
{
|
||||
refetchOnMount: false,
|
||||
select: ({ data: bankClients }) => bankClients.map(row => ({ value: row.innKio, label: row.innKio })),
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Fields.Select
|
||||
{...props}
|
||||
options={data}
|
||||
type={isFetching ? 'loading' : undefined}
|
||||
onSearch={value => setSearchValue(value as string)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { ComponentProps, FC } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Fields } from '@fractal-ui/form';
|
||||
import { useQuery } from 'react-query';
|
||||
import { QUERY_KEYS } from 'stream-constants';
|
||||
import { getPrefixedQueryKey } from 'utils';
|
||||
import { dictionaryService } from '@platform/services';
|
||||
import { useDebounce } from '@platform/ui';
|
||||
|
||||
type FieldProps = Omit<ComponentProps<typeof Fields.InputSearch>, 'onSearch' | 'options' | 'type'> & { onChange?(value: string): void };
|
||||
|
||||
/** Поле поиска по названию организации. */
|
||||
export const OrganizationField: FC<FieldProps> = ({ onFocus, onBlur, onChange, ...props }) => {
|
||||
const [changedValue, setChangedValue] = useState('');
|
||||
const debouncedSearchValue = useDebounce(changedValue, 300);
|
||||
|
||||
const { data = [], isFetching } = useQuery(
|
||||
getPrefixedQueryKey(QUERY_KEYS.CLIENT_NAME, debouncedSearchValue),
|
||||
() => dictionaryService.bankClient.searchByName({ offset: 0, pageSize: 10, search: debouncedSearchValue }, 'shortName'),
|
||||
{
|
||||
refetchOnMount: false,
|
||||
select: ({ data: bankClients }) => bankClients.map(row => ({ value: row.shortName, label: row.shortName })),
|
||||
}
|
||||
);
|
||||
|
||||
const onBlurHandler: typeof onBlur = e => {
|
||||
onBlur?.(e);
|
||||
|
||||
setChangedValue('');
|
||||
|
||||
onChange?.(changedValue);
|
||||
};
|
||||
|
||||
const onChangeHandler = e => {
|
||||
setChangedValue(e.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fields.InputSearch
|
||||
{...props}
|
||||
options={data}
|
||||
type={isFetching ? 'loading' : undefined}
|
||||
onBlur={onBlurHandler}
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { FC } from 'react';
|
||||
import React from 'react';
|
||||
import { useScrollerContext } from '@base-components/scroller';
|
||||
import { Fields } from '@fractal-ui/form';
|
||||
import { useBreakpoints } from '@fractal-ui/styling';
|
||||
import { Wrapper } from 'components';
|
||||
import { useForm } from 'react-final-form';
|
||||
import { FILTER_FIELD, FILTER_LABELS, MAX_POSSIBLE_DATE, MIN_POSSIBLE_DATE } from '../../constants';
|
||||
import { getDateFromMaxDate, getDateToMinDate } from '../utils';
|
||||
import { OrganizationField } from './components/organization-field';
|
||||
|
||||
/** Компонент быстрых фильтров. */
|
||||
export const QuickFilters: FC = () => {
|
||||
const { submit } = useForm();
|
||||
const { XS, S } = useBreakpoints();
|
||||
const { filterFormValues } = useScrollerContext();
|
||||
|
||||
const inputSize = S ? 'XS' : 'S';
|
||||
|
||||
const onChange = () => {
|
||||
if (!XS) {
|
||||
void submit();
|
||||
}
|
||||
};
|
||||
|
||||
const datePickerWidth = S ? 120 : 140;
|
||||
const inputSearchWidth = S ? 190 : 330;
|
||||
|
||||
return (
|
||||
<Wrapper alignItems="center" display="flex" gap="5">
|
||||
<Wrapper alignItems="center" display="flex" gap="3">
|
||||
<Fields.DatePicker
|
||||
showPlaceholder
|
||||
maxDate={getDateFromMaxDate(filterFormValues.dateTo)}
|
||||
minDate={MIN_POSSIBLE_DATE}
|
||||
name={FILTER_FIELD.DATE_FROM}
|
||||
size={inputSize}
|
||||
width={datePickerWidth}
|
||||
onChange={onChange}
|
||||
/>
|
||||
—
|
||||
<Fields.DatePicker
|
||||
showPlaceholder
|
||||
maxDate={MAX_POSSIBLE_DATE}
|
||||
minDate={getDateToMinDate(filterFormValues.dateFrom)}
|
||||
name={FILTER_FIELD.DATE_TO}
|
||||
size={inputSize}
|
||||
width={datePickerWidth}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Wrapper>
|
||||
<OrganizationField
|
||||
name={FILTER_FIELD.CLIENT_NAME}
|
||||
placeholder={FILTER_LABELS.CLIENT_NAME}
|
||||
size={inputSize}
|
||||
width={inputSearchWidth}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
QuickFilters.displayName = 'QuickFilters';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './scroller';
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { OnboardingRequestArchiveDto } from 'interfaces';
|
||||
|
||||
/** Тип отображения скроллера. */
|
||||
export enum SCROLLER_VIEW_TYPE {
|
||||
/** Заявки не найдены в категории. */
|
||||
NO_REQUESTS_IN_CURRENT_CATEGORY = 'NO_REQUESTS_IN_CURRENT_CATEGORY',
|
||||
/** Заявки не найдены с примененными фильтрами. */
|
||||
NO_REQUESTS_WITH_FILTERS = 'NO_REQUESTS_WITH_FILTERS',
|
||||
/** Заявок нет вообще. */
|
||||
NO_REQUESTS = 'NO_REQUESTS',
|
||||
/** Дефолтное отображение формы скроллера. */
|
||||
DEFAULT = 'DEFAULT',
|
||||
}
|
||||
|
||||
/** Проверяет, является ли объект архивной заявкой. */
|
||||
export const isArchiveDto = (dto: any): dto is OnboardingRequestArchiveDto => !('bankClient' in dto);
|
||||
@@ -0,0 +1,231 @@
|
||||
import type { FC } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import type { IMetaData, ScrollerProviderProps } from '@base-components/scroller';
|
||||
import { ScrollerContent, ScrollerProvider, useScrollerContext } from '@base-components/scroller';
|
||||
import type { MenuItemProps } from '@fractal-ui/composites';
|
||||
import { DeleteIcon, DownloadIcon, EditIcon, EyeOpenIcon } from '@fractal-ui/library';
|
||||
import { useBreakpoints } from '@fractal-ui/styling';
|
||||
import { useActionUtils } from 'actions-fractal';
|
||||
import type { OnboardingShortRequestBankScrollerDto } from 'interfaces/admin';
|
||||
import { locale } from 'localization';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { onboardingShortAdminService } from 'services/onboarding-short-admin';
|
||||
import { AUTHORITIES_ONBOARDING_BANK, CATEGORY, SCROLLER_QUERY_PARAMS } from 'stream-constants';
|
||||
import type { Guardian, ICollectionResponse } from '@platform/core';
|
||||
import { SORT_DIRECTION } from '@platform/services';
|
||||
import { NoRequestsBanner } from '../components';
|
||||
import { FILTER_FIELDS, SORT_FIELD } from '../constants';
|
||||
import { isArchiveDto } from '../scroller/interfaces';
|
||||
import { Card } from '../scroller/table/card';
|
||||
import { columns } from '../scroller/table/columns';
|
||||
import { STORAGE_KEY_FILTERS } from './constants';
|
||||
import { CommonFilters } from './filters/common-filters';
|
||||
import { QuickFilters } from './filters/quick-filters';
|
||||
import { useGetActiveFilterTags } from './use-get-active-filter-tags';
|
||||
|
||||
/** Скроллер. */
|
||||
const Scroller: FC = () => {
|
||||
const { pathname, search } = useLocation();
|
||||
const history = useHistory();
|
||||
const { setCurrentCategory, categories } = useScrollerContext();
|
||||
|
||||
useEffect(() => {
|
||||
const queryParams = new URLSearchParams(search);
|
||||
const tabParam = queryParams.get(SCROLLER_QUERY_PARAMS.CATEGORY) as CATEGORY;
|
||||
const targetTab = tabParam && Object.values(CATEGORY).includes(tabParam) ? tabParam : undefined;
|
||||
|
||||
if (targetTab) {
|
||||
if (categories.some(cat => cat.value === targetTab)) {
|
||||
setCurrentCategory(targetTab);
|
||||
}
|
||||
|
||||
history.replace(pathname);
|
||||
}
|
||||
}, [categories, history, pathname, search, setCurrentCategory]);
|
||||
|
||||
return <ScrollerContent />;
|
||||
};
|
||||
|
||||
Scroller.displayName = 'Scroller';
|
||||
|
||||
/** Страница скроллера. */
|
||||
const ScrollerPage: FC = () => {
|
||||
const { XS } = useBreakpoints();
|
||||
const router = useHistory();
|
||||
// const { archiveRequestsInScrollerEnabled } = getOnbConfig<IOnboardingAppConfigAdmin>();
|
||||
|
||||
const { getActiveFilterTags } = useGetActiveFilterTags();
|
||||
|
||||
const { getAvailableActions } = useActionUtils();
|
||||
|
||||
const getRowActions = useCallback(
|
||||
(actions: Array<MenuItemProps & { authorities: string[]; guardians?: Guardian[] }>, doc: OnboardingShortRequestBankScrollerDto) =>
|
||||
getAvailableActions(actions, doc).map(({ authorities, guardians, ...item }) => item),
|
||||
[getAvailableActions]
|
||||
);
|
||||
|
||||
const getActions = useCallback(
|
||||
([row]: [row: OnboardingShortRequestBankScrollerDto]): // context: IScrollerContext<OnboardingRequest>
|
||||
MenuItemProps[] => {
|
||||
if (isArchiveDto(row)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return getRowActions(
|
||||
[
|
||||
{
|
||||
text: locale.action.view,
|
||||
onClick: () => {},
|
||||
// onClick: () => viewAction(row),
|
||||
icon: EyeOpenIcon,
|
||||
authorities: [AUTHORITIES_ONBOARDING_BANK.VIEW],
|
||||
// guardians: viewGuardians,
|
||||
},
|
||||
{
|
||||
text: locale.action.edit,
|
||||
onClick: () => {},
|
||||
// onClick: () => editAction(row),
|
||||
icon: EditIcon,
|
||||
authorities: [AUTHORITIES_ONBOARDING_BANK.UPDATE],
|
||||
// guardians: editGuardians,
|
||||
},
|
||||
{
|
||||
text: locale.action.exportDocuments,
|
||||
onClick: () => {},
|
||||
// onClick: () =>
|
||||
// exportAction({
|
||||
// doc: row,
|
||||
// onSuccessExport: context.reload,
|
||||
// showLoader,
|
||||
// hideLoader,
|
||||
// }),
|
||||
icon: DownloadIcon,
|
||||
authorities: [AUTHORITIES_ONBOARDING_BANK.EXPORT_PF_AND_SCANS],
|
||||
// guardians: exportGuardians,
|
||||
},
|
||||
{
|
||||
text: locale.action.delete,
|
||||
onClick: () => {},
|
||||
// onClick: () => deleteAction({ doc: row, onSuccessRemove: context.reload }),
|
||||
icon: DeleteIcon,
|
||||
authorities: [AUTHORITIES_ONBOARDING_BANK.DELETE],
|
||||
// guardians: deleteGuardians,
|
||||
},
|
||||
],
|
||||
row
|
||||
);
|
||||
},
|
||||
[getRowActions]
|
||||
);
|
||||
|
||||
const onRowClick = useCallback(
|
||||
(row: OnboardingShortRequestBankScrollerDto) => {
|
||||
// if (isArchiveDto(row)) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// if (CLIENT_EDIT_STATUSES.includes(row.status)) {
|
||||
// editAction(row);
|
||||
// } else {
|
||||
// viewAction(row);
|
||||
// }
|
||||
router.push(`/onboarding-short/full/manage/${row.id}`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const fetcher = useCallback(
|
||||
async (data: IMetaData): Promise<ICollectionResponse<OnboardingShortRequestBankScrollerDto>> =>
|
||||
// TODO: это пока нам тут ненужно, мы без табов :)
|
||||
// if (archiveRequestsInScrollerEnabled && data.category === CATEGORY.ARCHIVE) {
|
||||
// const { category, ...metaData } = data;
|
||||
//
|
||||
// return onboardingAdminService.getListArchive(metaData);
|
||||
// }
|
||||
|
||||
onboardingShortAdminService.getList(data),
|
||||
[]
|
||||
);
|
||||
|
||||
// TODO: это пока нам тут ненужно, мы без табов :)
|
||||
// const categoryProps = useMemo(
|
||||
// () => ({
|
||||
// fetcher: async (data: IMetaData) => {
|
||||
// const [counterResult, archiveResult] = await Promise.all([
|
||||
// onboardingAdminService.getCounter(data),
|
||||
// new Promise<ICollectionResponse<OnboardingRequest> | null>(resolve => {
|
||||
// if (!archiveRequestsInScrollerEnabled) {
|
||||
// resolve(null);
|
||||
//
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // На беке нет категории архивных, поэтому наличие заявок проверяем, делая запрос на их получение.
|
||||
// const { category, ...metaData } = data;
|
||||
//
|
||||
// resolve(onboardingAdminService.getListArchive(metaData));
|
||||
// }),
|
||||
// ]);
|
||||
//
|
||||
// const categories = counterResult.map(({ name, count, desc }) => ({
|
||||
// value: name,
|
||||
// label: onboardingTranslator(desc),
|
||||
// count,
|
||||
// }));
|
||||
//
|
||||
// if (!archiveResult || archiveResult.total === 0) {
|
||||
// return categories;
|
||||
// }
|
||||
//
|
||||
// return [...categories, { value: CATEGORY.ARCHIVE, label: locale.scroller.archiveRequests.label }];
|
||||
// },
|
||||
// defaultCategory: CATEGORY.ALL,
|
||||
// }),
|
||||
// [archiveRequestsInScrollerEnabled]
|
||||
// );
|
||||
|
||||
const config: ScrollerProviderProps<OnboardingShortRequestBankScrollerDto> = {
|
||||
name: STORAGE_KEY_FILTERS,
|
||||
fetcher,
|
||||
columns,
|
||||
// categoryProps,
|
||||
filtersProps: {
|
||||
fastFiltersComponent: QuickFilters,
|
||||
commonFiltersComponent: CommonFilters,
|
||||
fields: FILTER_FIELDS,
|
||||
},
|
||||
sortProps: XS
|
||||
? undefined
|
||||
: {
|
||||
defaultSort: { [SORT_FIELD.CREATED_AT]: SORT_DIRECTION.DESC },
|
||||
},
|
||||
tableSettingsProps: {
|
||||
columns,
|
||||
defaultColumnSettings: columns.map(column => [column.name]),
|
||||
},
|
||||
tableProps: {
|
||||
rowActions: getActions,
|
||||
onRowClick,
|
||||
placeholder: <NoRequestsBanner />,
|
||||
minBodyHeight: '400px',
|
||||
},
|
||||
mobileTableProps: {
|
||||
card: Card,
|
||||
actionsTitle: locale.actionsTitle,
|
||||
placeholder: <NoRequestsBanner />,
|
||||
},
|
||||
activeFiltersProps: {
|
||||
getTags: getActiveFilterTags,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollerProvider {...config}>
|
||||
<Scroller />
|
||||
</ScrollerProvider>
|
||||
);
|
||||
};
|
||||
|
||||
ScrollerPage.displayName = 'ScrollerPage';
|
||||
|
||||
export default ScrollerPage;
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { Badge } from '@fractal-ui/extended';
|
||||
import { Text, Title } from '@fractal-ui/styling';
|
||||
import { CardForScroller, CardForScrollerItem, HeaderDelimiter } from '@fractal-ui/table';
|
||||
import { Wrapper } from 'components';
|
||||
import dayjs from 'dayjs';
|
||||
import type { BranchScrollerDto, OnboardingRequestBankScrollerDto } from 'interfaces/admin';
|
||||
import { locale } from 'localization';
|
||||
import { DOCUMENT_STATUS_COLOR_FRACTAL, STATUS_COLOR, STATUS_LABEL } from 'stream-constants';
|
||||
import { DATE_FORMAT } from '@platform/services';
|
||||
import { isArchiveDto } from '../interfaces';
|
||||
|
||||
/**
|
||||
* Свойства компонента карточки скроллера (для экранов XS).
|
||||
*/
|
||||
interface CardProps {
|
||||
/* Данные письма. */
|
||||
data: OnboardingRequestBankScrollerDto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Компонент карточки скроллера (для экранов XS).
|
||||
*/
|
||||
export const Card: React.FC<CardProps> = ({ data }) => {
|
||||
const isArchive = isArchiveDto(data);
|
||||
const { number, date, status } = data;
|
||||
|
||||
let shortName: string | undefined;
|
||||
let name: string | undefined;
|
||||
let inn = '';
|
||||
let branch: BranchScrollerDto | undefined;
|
||||
|
||||
if (isArchive) {
|
||||
({ shortName, name, inn } = data);
|
||||
}
|
||||
|
||||
if (!isArchive) {
|
||||
({ branch } = data);
|
||||
|
||||
const { bankClient } = data;
|
||||
|
||||
({ shortName, name, inn } = bankClient);
|
||||
}
|
||||
|
||||
return (
|
||||
<CardForScroller
|
||||
data={data}
|
||||
header={() => (
|
||||
<>
|
||||
<Title.H4>{`№ ${number}`}</Title.H4>
|
||||
<HeaderDelimiter />
|
||||
<Title.H4>{dayjs(date).format(DATE_FORMAT)}</Title.H4>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<CardForScrollerItem dataName="organization" header={locale.steps.organization}>
|
||||
{shortName || name || locale.scroller.card.noName}
|
||||
<Wrapper pt="2">
|
||||
<Text.P3Short>{`${locale.scroller.card.inn}: ${inn}`}</Text.P3Short>
|
||||
</Wrapper>
|
||||
</CardForScrollerItem>
|
||||
{!isArchive && branch && (
|
||||
<CardForScrollerItem dataName="branch" header={locale.fields.branch.label}>
|
||||
{branch.name}
|
||||
</CardForScrollerItem>
|
||||
)}
|
||||
{!isArchive && (
|
||||
<Badge size="S" type={DOCUMENT_STATUS_COLOR_FRACTAL[STATUS_COLOR[status]]}>
|
||||
{STATUS_LABEL[status]}
|
||||
</Badge>
|
||||
)}
|
||||
</CardForScroller>
|
||||
);
|
||||
};
|
||||
|
||||
Card.displayName = 'Card';
|
||||
@@ -0,0 +1,141 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Text, Wrapper } from '@fractal-ui/styling';
|
||||
import type { UnionColumnProps } from '@fractal-ui/table';
|
||||
import { Cell, CELL_TYPE } from '@fractal-ui/table';
|
||||
import { OnboardingIcons } from 'components';
|
||||
import dayjs from 'dayjs';
|
||||
import type { OnboardingRequestBankScrollerDto } from 'interfaces/admin';
|
||||
import { locale } from 'localization';
|
||||
import { DOCUMENT_STATUS_COLOR_FRACTAL, STATUS_COLOR, STATUS_LABEL } from 'stream-constants';
|
||||
import { SIGN_PLACE_ICONS, SIGN_PLACE_LABELS } from 'stream-constants/admin/signer-info';
|
||||
import { DATE_FORMAT } from '@platform/services';
|
||||
import { isArchiveDto } from '../interfaces';
|
||||
|
||||
/** Описание колонок таблицы. */
|
||||
export const COLUMNS: Record<string, UnionColumnProps<OnboardingRequestBankScrollerDto>> = {
|
||||
NUMBER: {
|
||||
id: 'number',
|
||||
accessor: 'number',
|
||||
name: 'number',
|
||||
hasSort: true,
|
||||
headerType: 'string',
|
||||
label: '№',
|
||||
width: 84,
|
||||
Cell({ number }: OnboardingRequestBankScrollerDto) {
|
||||
return <Cell cellName="number" cellType={CELL_TYPE.STRING} text={number} />;
|
||||
},
|
||||
},
|
||||
CREATED_AT: {
|
||||
id: 'createdAt',
|
||||
name: 'createdAt',
|
||||
hasSort: true,
|
||||
headerType: 'string',
|
||||
label: locale.manageRequest.tabs.rightsAndInvitations.date,
|
||||
width: 165,
|
||||
Cell({ date }: OnboardingRequestBankScrollerDto) {
|
||||
return <Cell cellName="createdAt" cellType={CELL_TYPE.STRING} text={dayjs(date).format(DATE_FORMAT)} />;
|
||||
},
|
||||
},
|
||||
ORGANIZATION: {
|
||||
id: 'organization',
|
||||
name: 'organization',
|
||||
headerType: 'string',
|
||||
label: locale.columns.bank.bankClient,
|
||||
width: 355,
|
||||
Cell(data: OnboardingRequestBankScrollerDto) {
|
||||
let shortName: string | undefined;
|
||||
let name: string | undefined;
|
||||
let inn = '';
|
||||
|
||||
if (isArchiveDto(data)) {
|
||||
({ shortName, name, inn } = data);
|
||||
}
|
||||
|
||||
if (!isArchiveDto(data)) {
|
||||
const { bankClient } = data;
|
||||
|
||||
({ shortName, name, inn } = bankClient);
|
||||
}
|
||||
|
||||
return (
|
||||
<Cell
|
||||
cellName="organization"
|
||||
cellType={CELL_TYPE.STRING}
|
||||
subText={`${locale.scroller.card.inn}: ${inn}`}
|
||||
text={shortName || name || locale.scroller.card.noName}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
BRANCH: {
|
||||
id: 'branch',
|
||||
name: 'branch',
|
||||
headerType: 'string',
|
||||
label: locale.fields.branch.label,
|
||||
width: 355,
|
||||
Cell(data: OnboardingRequestBankScrollerDto) {
|
||||
if (isArchiveDto(data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { branch } = data;
|
||||
|
||||
return <Cell cellName="branch" cellType={CELL_TYPE.STRING} text={branch.name} />;
|
||||
},
|
||||
},
|
||||
/** Колонка "Подписание договора". */
|
||||
SIGN_PLACE: {
|
||||
id: 'signPlace',
|
||||
name: 'signPlace',
|
||||
headerType: 'string',
|
||||
label: locale.fields.venue.label,
|
||||
width: 355,
|
||||
Cell({ lastManagerVisit, signPlace }: OnboardingRequestBankScrollerDto) {
|
||||
const Icon = useMemo(() => {
|
||||
if (lastManagerVisit) {
|
||||
return OnboardingIcons.DeliveryComplete;
|
||||
}
|
||||
|
||||
return SIGN_PLACE_ICONS[signPlace];
|
||||
}, [lastManagerVisit, signPlace]);
|
||||
|
||||
if (lastManagerVisit || signPlace) {
|
||||
return (
|
||||
<Cell cellName={'signPlace'}>
|
||||
<Wrapper alignItems="start" gap={2}>
|
||||
<Icon fill="FAINT" />
|
||||
<Text.P1>{lastManagerVisit ? locale.signPlace.departureComplete : SIGN_PLACE_LABELS[signPlace]}</Text.P1>
|
||||
</Wrapper>
|
||||
</Cell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
STATUS: {
|
||||
id: 'status',
|
||||
accessor: 'status',
|
||||
name: 'status',
|
||||
hasSort: true,
|
||||
headerType: 'string',
|
||||
label: locale.columns.bank.status,
|
||||
width: 170,
|
||||
Cell(data: OnboardingRequestBankScrollerDto) {
|
||||
if (isArchiveDto(data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { status } = data;
|
||||
|
||||
return (
|
||||
<Cell badgeType={DOCUMENT_STATUS_COLOR_FRACTAL[STATUS_COLOR[status]]} cellName="status" cellType={CELL_TYPE.STATUS}>
|
||||
<Text.P3Short>{STATUS_LABEL[status]}</Text.P3Short>
|
||||
</Cell>
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/** Список колонок. */
|
||||
export const columns = Object.values(COLUMNS);
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import type { FilterFormState } from '@base-components/scroller';
|
||||
import { useBreakpoints } from '@fractal-ui/styling';
|
||||
import { activeFilterFields, getActiveFilters } from 'utils';
|
||||
import type { IBranchV2 } from '@platform/services';
|
||||
import { ACTIVE_FILTER_FIELDS, FILTER_FIELD, FILTER_LABELS_BY_KEY } from '../constants';
|
||||
import { useBranches } from '../queries';
|
||||
|
||||
/** Хук с колбэком для получения тегов для компонента активных фильтров. */
|
||||
export const useGetActiveFilterTags = () => {
|
||||
const { XS } = useBreakpoints();
|
||||
const queryBranches = useBranches();
|
||||
|
||||
const branchesMap = useMemo(
|
||||
() =>
|
||||
queryBranches.data?.data?.reduce((acc, current) => {
|
||||
acc[current.id] = current;
|
||||
|
||||
return acc;
|
||||
}, {} as Record<IBranchV2['id'], IBranchV2>),
|
||||
[queryBranches.data]
|
||||
);
|
||||
|
||||
const getActiveFilterTags = useCallback(
|
||||
(values: FilterFormState) => {
|
||||
if (XS) return [];
|
||||
|
||||
const fields = {
|
||||
...ACTIVE_FILTER_FIELDS,
|
||||
[FILTER_FIELD.BRANCHES]: activeFilterFields.list({
|
||||
formatter: value => {
|
||||
if (value.length === 0 || !branchesMap) return;
|
||||
|
||||
if (value.length === 1) return branchesMap[value[0]].number;
|
||||
|
||||
return value.length.toString();
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const filters = getActiveFilters(values, fields, FILTER_LABELS_BY_KEY);
|
||||
|
||||
const tags = filters.map(filter => ({ label: filter.label, value: filter.value, fieldNames: [filter.fieldName] }));
|
||||
|
||||
return tags;
|
||||
},
|
||||
[XS, branchesMap]
|
||||
);
|
||||
|
||||
return { getActiveFilterTags };
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { MAX_POSSIBLE_DATE, MIN_POSSIBLE_DATE } from '../constants';
|
||||
|
||||
/** Функция получения максимально возможной для выбора даты в поле "Дата от". */
|
||||
export const getDateFromMaxDate = (dateTo: string | undefined) => (dateTo ? dayjs(dateTo).startOf('day').format() : MAX_POSSIBLE_DATE);
|
||||
|
||||
/** Функция получения минимально возможной для выбора даты в поле "Дата до". */
|
||||
export const getDateToMinDate = (dateFrom: string | undefined) => (dateFrom ? dayjs(dateFrom).startOf('day').format() : MIN_POSSIBLE_DATE);
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useScrollerContext } from '@base-components/scroller';
|
||||
import { SCROLLER_VIEW_TYPE } from './scroller/interfaces';
|
||||
|
||||
/** Хук для получения типа отображения скроллера. */
|
||||
export const useScrollerViewType = () => {
|
||||
const { currentCategory, categories, filterValues, queryCategories, queryList: queryItemsList } = useScrollerContext();
|
||||
|
||||
const scrollerViewType = useMemo<SCROLLER_VIEW_TYPE>(() => {
|
||||
const currentCategoryTab = categories.find(({ value }) => value === currentCategory);
|
||||
const currentCategoryCount = currentCategoryTab?.count || 0;
|
||||
const hasNoRequests = categories.every(({ count }) => count === 0);
|
||||
const hasNoRequestsInCurrentCategory = hasNoRequests || (!hasNoRequests && currentCategoryCount === 0);
|
||||
const hasFilterValues = Object.keys(filterValues).length > 0;
|
||||
|
||||
if (queryCategories.isFetched && hasNoRequests && !hasFilterValues) {
|
||||
return SCROLLER_VIEW_TYPE.NO_REQUESTS;
|
||||
}
|
||||
|
||||
if (queryCategories.isFetched && queryItemsList.isFetched) {
|
||||
if (hasFilterValues && hasNoRequestsInCurrentCategory) {
|
||||
return SCROLLER_VIEW_TYPE.NO_REQUESTS_WITH_FILTERS;
|
||||
}
|
||||
|
||||
if (hasNoRequestsInCurrentCategory) {
|
||||
return SCROLLER_VIEW_TYPE.NO_REQUESTS_IN_CURRENT_CATEGORY;
|
||||
}
|
||||
}
|
||||
|
||||
return SCROLLER_VIEW_TYPE.DEFAULT;
|
||||
}, [categories, currentCategory, filterValues, queryCategories.isFetched, queryItemsList.isFetched]);
|
||||
|
||||
return scrollerViewType;
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { DataResponseWithValidation, SaveResponseWithRawValidationResults } from 'interfaces';
|
||||
import type { OnboardingShortRequestBankScrollerDto } from 'interfaces/admin';
|
||||
import type { ShortOnboardingRequestDto } from 'pages/onboarding-short-new/interfaces';
|
||||
import type { IOnboardingAdminService } from 'services/onboarding-admin';
|
||||
import { onboardingAdminService } from 'services/onboarding-admin';
|
||||
import { transformValidation } from 'services/onboarding-common';
|
||||
import { request } from '@platform/core';
|
||||
import { getNewDictionaryService, type ICollectionResponse, type IDataResponse, type IMetaData } from '@platform/services';
|
||||
|
||||
/** Интерфейс клиентского сервиса онбординга. */
|
||||
export interface IOnboardingShortAdminService extends Omit<IOnboardingAdminService, 'delete' | 'get' | 'getList' | 'send' | 'update'> {
|
||||
/** Получение списка заявок на открытие первого счёта. */
|
||||
getList(metaData: IMetaData): Promise<ICollectionResponse<OnboardingShortRequestBankScrollerDto>>;
|
||||
/** Получение заявки по идентификатору. */
|
||||
get(id: string): Promise<ShortOnboardingRequestDto>;
|
||||
/** Удаление заявки. */
|
||||
delete(id: string): Promise<IDataResponse<ShortOnboardingRequestDto>>;
|
||||
/** Обновление данных заявок. */
|
||||
update(data: ShortOnboardingRequestDto): Promise<SaveResponseWithRawValidationResults<ShortOnboardingRequestDto>>;
|
||||
/** Отправка заявки. */
|
||||
send(id: string): Promise<SaveResponseWithRawValidationResults<ShortOnboardingRequestDto>>;
|
||||
}
|
||||
|
||||
const BASE_SHORT_URL = '/api/onboarding-short-bank';
|
||||
const ONBOARDING_SHORT_REQUEST_URL = `${BASE_SHORT_URL}/onboarding-short-request`;
|
||||
|
||||
const { getList, get } = getNewDictionaryService<any>(ONBOARDING_SHORT_REQUEST_URL);
|
||||
|
||||
/** Клиентский сервис онбординга. */
|
||||
export const onboardingShortAdminService: IOnboardingShortAdminService = {
|
||||
...onboardingAdminService,
|
||||
get,
|
||||
getList,
|
||||
update: async data => {
|
||||
const { data: response } = await request<DataResponseWithValidation<ShortOnboardingRequestDto>>({
|
||||
url: ONBOARDING_SHORT_REQUEST_URL,
|
||||
method: 'PUT',
|
||||
data: {
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.data) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return { ...transformValidation(response), rawValidationResult: response.validationResult };
|
||||
},
|
||||
delete: async id => {
|
||||
const { data: response } = await request<IDataResponse<ShortOnboardingRequestDto>>({
|
||||
url: `${ONBOARDING_SHORT_REQUEST_URL}/delete`,
|
||||
method: 'POST',
|
||||
data: { data: id },
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
asyncCreate: async ({ isSkipCheck1091, ...data }) => {
|
||||
const { data: response } = await request({
|
||||
url: `${ONBOARDING_SHORT_REQUEST_URL}/new/async${isSkipCheck1091 ? '?isSkipCheck1091=true' : ''}`,
|
||||
method: 'POST',
|
||||
data: { data },
|
||||
});
|
||||
|
||||
return response.data;
|
||||
},
|
||||
getAsyncTask: async id => {
|
||||
const { data: response } = await request({
|
||||
url: `${BASE_SHORT_URL}/async-task/${id}`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
},
|
||||
send: async id => {
|
||||
const { data: response } = await request<DataResponseWithValidation<ShortOnboardingRequestDto>>({
|
||||
url: `${ONBOARDING_SHORT_REQUEST_URL}/send`,
|
||||
method: 'POST',
|
||||
data: { data: id },
|
||||
});
|
||||
|
||||
if (!response.data) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return { ...transformValidation(response), rawValidationResult: response.validationResult };
|
||||
},
|
||||
checkActiveRequestBankBeforeSend: requestId =>
|
||||
request({
|
||||
url: `${ONBOARDING_SHORT_REQUEST_URL}/active-requests/check-send-bank`,
|
||||
method: 'POST',
|
||||
data: { data: requestId },
|
||||
}).then(r => r.data),
|
||||
checkActiveRequestUserBeforeSend: requestId =>
|
||||
request({
|
||||
url: `${ONBOARDING_SHORT_REQUEST_URL}/active-requests/check-send-user`,
|
||||
method: 'POST',
|
||||
data: { data: requestId },
|
||||
}).then(r => r.data),
|
||||
};
|
||||
@@ -4,10 +4,12 @@ import type { IOnboardingClientService } from 'services/onboarding-client';
|
||||
import { onboardingClientService } from 'services/onboarding-client';
|
||||
import { transformValidation } from 'services/onboarding-common';
|
||||
import { request } from '@platform/core';
|
||||
import { getNewDictionaryService, type IDataResponse } from '@platform/services';
|
||||
import { getNewDictionaryService, type ICollectionResponse, type IDataResponse, type IMetaData } from '@platform/services';
|
||||
|
||||
/** Интерфейс клиентского сервиса онбординга. */
|
||||
export interface IOnboardingShortClientService extends Omit<IOnboardingClientService, 'delete' | 'get' | 'send' | 'update'> {
|
||||
export interface IOnboardingShortClientService extends Omit<IOnboardingClientService, 'delete' | 'get' | 'getList' | 'send' | 'update'> {
|
||||
/** Получение списка заявок на открытие первого счёта. */
|
||||
getList(metaData: IMetaData): Promise<ICollectionResponse<ShortOnboardingRequestDto>>;
|
||||
/** Получение заявки по идентификатору. */
|
||||
get(id: string): Promise<ShortOnboardingRequestDto>;
|
||||
|
||||
@@ -24,11 +26,12 @@ export interface IOnboardingShortClientService extends Omit<IOnboardingClientSer
|
||||
const BASE_SHORT_URL = '/api/onboarding-short-client';
|
||||
const ONBOARDING_SHORT_REQUEST_URL = `${BASE_SHORT_URL}/onboarding-short-request`;
|
||||
|
||||
const { get } = getNewDictionaryService<ShortOnboardingRequestDto>(ONBOARDING_SHORT_REQUEST_URL);
|
||||
const { get, getList } = getNewDictionaryService<ShortOnboardingRequestDto>(ONBOARDING_SHORT_REQUEST_URL);
|
||||
|
||||
/** Клиентский сервис онбординга. */
|
||||
export const onboardingShortClientService: IOnboardingShortClientService = {
|
||||
...onboardingClientService,
|
||||
getList,
|
||||
get,
|
||||
update: async data => {
|
||||
const { data: response } = await request<DataResponseWithValidation<ShortOnboardingRequestDto>>({
|
||||
|
||||
Reference in New Issue
Block a user