feat(короткая заявка): TEAMMSBDAY-2309 [ОНБ_МСБ] добавил скроллер на фрактале

This commit is contained in:
Valeev Bulat
2025-07-17 17:16:45 +03:00
committed by akondratov
parent 3a3edc50a3
commit 41cabd0853
59 changed files with 3525 additions and 2820 deletions
+2 -1
View File
@@ -1797,5 +1797,6 @@
"action.fine": "Хорошо",
"action.documentsRequested.header": "Документы запрошены",
"documentsRequested.afterUploadDocs.description": "После загрузки документов клиентом, вы сможете завершить формирование заявки и отправить ее в ЕСК",
"documentsRequested.result.success": "Заявка отправлена в ЕСК"
"documentsRequested.result.success": "Заявка отправлена в ЕСК",
"fields.venue.label": "Место встречи"
}
+1
View File
@@ -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';
+14
View File
@@ -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],
};
+1
View File
@@ -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';
+10
View File
@@ -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
View File
@@ -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;
}
+4 -1
View File
@@ -5469,5 +5469,8 @@
},
"documentsRequested.result.success": {
"ru": "Заявка отправлена в ЕСК"
},
"fields.venue.label": {
"ru": "Место встречи"
}
}
}
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],
};
-4
View File
@@ -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,
},
});
+5 -5
View File
@@ -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;
}
+3 -3
View File
@@ -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>;
+6 -2
View File
@@ -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;
}
-15
View File
@@ -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';
+101
View File
@@ -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();
+1
View File
@@ -0,0 +1 @@
export * from './scroller';
+28
View File
@@ -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;
};
+99
View File
@@ -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),
};
+6 -3
View File
@@ -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>>({