Pull request #142: feat(TEAMMSBMOB-16592): фильтрация счетов, копирование реквизитов

Merge in MCB_FE/mcb-platform-monorepo from story/TEAMMSBMOB-15375 to develop

* commit 'a5ac148050afeaa8e02321a76a59bc2977248dfa':
  feat(TEAMMSBMOB-16592): объекты вынесены из функций
  feat(TEAMMSBMOB-16592): добавлено копирование реквизитов
  feat(TEAMMSBMOB-16592): добавлены моки и фильтрация на раздел счета
This commit is contained in:
Ильдар Смышляев
2025-08-11 12:11:08 +03:00
70 changed files with 915 additions and 430 deletions
@@ -0,0 +1,3 @@
const ACCOUNT_DETAILS_ENDPOINT = '/client-dictionary/dictionary/client/account-details/get-details';
export { ACCOUNT_DETAILS_ENDPOINT };
@@ -0,0 +1,3 @@
export * from './mocks';
export * from './types';
export * from './endpoints';
@@ -0,0 +1,10 @@
import { rest } from 'msw';
import { ACCOUNT_DETAILS_ENDPOINT } from '../endpoints';
import type { AccountDetailsResponseDto } from '../types';
import { ACCOUNT_DETAILS_MOCK } from './mocks';
const getAccountDetailsHandler = rest.post<never, never, AccountDetailsResponseDto>(ACCOUNT_DETAILS_ENDPOINT, async (_, res, ctx) =>
res(ctx.delay(500), ctx.json(ACCOUNT_DETAILS_MOCK))
);
export { getAccountDetailsHandler };
@@ -0,0 +1,5 @@
import { getAccountDetailsHandler } from './getAccountDetails';
const accountDetailsHandlers = [getAccountDetailsHandler];
export { accountDetailsHandlers };
@@ -0,0 +1,23 @@
import type { AccountDetailsResponseDto } from '../types';
const ACCOUNT_DETAILS_MOCK: AccountDetailsResponseDto = {
data: {
organization: 'ООО "ВеллЭнерджи"',
inn: '781633333333',
kpp: '773601001',
ogrn: '1029665556574',
account: '40522210012345',
bik: '044521234',
bankName: 'ООО "Банк"',
correspondentAccount: '30176550605316000601',
beneficiary: 'ООО "ВеллЭнерджи"',
beneficiaryAccount: '40522210012345',
beneficiaryTransitAccount: '12342543212345',
swiftCode: 'SWIFT',
beneficiaryBank: 'ООО "Банк"',
beneficiaryBankAddress: 'г. Москва, ул. Пушкина, д. 1',
},
};
export { ACCOUNT_DETAILS_MOCK };
@@ -0,0 +1,22 @@
interface AccountDetailsDto {
organization: string;
inn: string;
kpp: string;
ogrn: string;
account: string;
bik: string;
bankName: string;
correspondentAccount: string;
beneficiary: string;
beneficiaryAccount: string;
beneficiaryTransitAccount: string;
swiftCode: string;
beneficiaryBank: string;
beneficiaryBankAddress: string;
}
interface AccountDetailsResponseDto {
data: AccountDetailsDto;
}
export type { AccountDetailsDto, AccountDetailsResponseDto };
@@ -0,0 +1 @@
export * from './client-dictionary';
+1
View File
@@ -5,3 +5,4 @@ export * from './msb-accounts';
export * from './treasury-deals-client';
export * from '@tanstack/react-query';
export * from './uaa-client';
export * from './client-dictionary-client';
+4 -2
View File
@@ -1,3 +1,4 @@
import { accountDetailsHandlers } from '../client-dictionary-client';
import { inquiriesHandlers } from '../inquiries';
import { financialAccountsHandlers, clientOrganizationsHandlers } from '../msb-accounts';
import { statementsHandlers } from '../statements';
@@ -8,10 +9,10 @@ import {
fetchFullDocumentsHandlers,
cancelDocumentsHandlers,
earlyRefundDocumentsHandlers,
fetchTreasuryDealsCurrenciesHandlers,
fetchDepositGeneralAgreementsHandlers,
fetchDepositDocumentNewForEditHandlers,
fetchTreasuryDealsRatesHandlers
fetchTreasuryDealsRatesHandlers,
fetchTreasuryDealsCurrenciesHandlers,
} from '../treasury-deals-client';
import { userProfileHandlers } from '../uaa-client';
import { fetchRublePaymentClientHandlers } from './fetchRublePaymentClient';
@@ -37,6 +38,7 @@ const handlers = [
...earlyRefundDocumentsHandlers,
...fetchTreasuryDealsCurrenciesHandlers,
...fetchTreasuryDealsRatesHandlers,
...accountDetailsHandlers,
];
export { handlers };
@@ -17,24 +17,32 @@ const FINANCIAL_ACCOUNTS_MOCK: FinancialAccountsResponseDto = {
accountId: '6856e7d9-d490-4fcb-aa2e-0b9a8d0f7aa2',
accountNumber: '1111111111114184',
currencyCode: 'RUB',
currentBalance: 2_100_100,
currentBalance: 7_070_100,
turnoverUpdatedAt: new Date('2025-07-07'),
contractType: {
code: 'BASE',
name: 'Расчетный счёт',
code: 'OTHER',
name: 'Счёт застройщика',
},
accountKind: {
code: 'BASE',
name: 'Спецсчёт',
code: 'OTHER',
name: 'Счёт застройщика',
},
},
{
accountId: '6856e7d9-d490-4fcb-aa2e-0b9a8d0f9aa0',
accountNumber: '1111111111113220',
currencyCode: 'RUB',
currentBalance: 1_000_100,
currentBalance: 2_070_100,
turnoverUpdatedAt: new Date('2025-07-02'),
ecoAccountName: 'Наименование счёта',
contractType: {
code: 'BASE',
name: 'Расчётный счёт',
},
accountKind: {
code: 'BASE',
name: 'Спецсчёт',
},
},
],
},
@@ -49,15 +57,15 @@ const FINANCIAL_ACCOUNTS_MOCK: FinancialAccountsResponseDto = {
accountId: '6856e7d9-d490-4fcb-aa2e-0b9a8d0f7aa4',
accountNumber: '1111111111114020',
currencyCode: 'RUB',
currentBalance: 12_440_100,
turnoverUpdatedAt: new Date('2025-07-18'),
currentBalance: 18_060_100,
turnoverUpdatedAt: new Date('2025-06-18'),
contractType: {
code: 'OTHER',
name: 'Расчетный счёт',
name: 'Счёт платежного агрегатора',
},
accountKind: {
code: 'BASE',
name: 'Спецсчёт',
code: 'OTHER',
name: 'Счёт платежного агрегатора',
},
},
],
@@ -70,36 +78,52 @@ const FINANCIAL_ACCOUNTS_MOCK: FinancialAccountsResponseDto = {
},
accounts: [
{
accountId: '6856e7d9-d490-4fcb-aa2e-0b9a8d0f7aa3',
accountId: '6856e7d9-d490-4fcb-aa2e-0b9a8d0f4aa4',
accountNumber: '1111111111114020',
currencyCode: 'RUB',
currentBalance: 12_980_100,
currentBalance: 23_070_100,
turnoverUpdatedAt: new Date('2025-07-23'),
ecoAccountName: 'Наименование счёта',
accountBlockState: 'PARTIAL',
},
{
accountId: '6856e7d9-d490-4fcb-aa2e-0b9a8d0f9aa6',
accountNumber: '1111111111114321',
currencyCode: 'CNY',
currentBalance: 525_222,
turnoverUpdatedAt: new Date('2025-07-12'),
contractType: {
code: 'BASE',
name: 'Расчетный счёт',
code: 'OTHER',
name: 'Транзитный счёт',
},
accountKind: {
code: 'BASE',
name: 'Спецсчёт',
name: 'Транзитный счёт',
},
},
{
accountId: '6856e7d9-d490-4fcb-aa2e-0b9a8d0f9aa9',
accountId: '6856e7d9-d490-4fcb-aa2e-0b9c1d0f9aa6',
accountNumber: '1111111111114321',
currencyCode: 'CNY',
currentBalance: 12_500,
turnoverUpdatedAt: new Date('2025-05-12'),
contractType: {
code: 'BASE',
name: 'Расчётный счёт',
},
accountKind: {
code: 'BASE',
name: 'Расчётный счёт',
},
},
{
accountId: '6856e7d9-d490-4fcb-aa2e-2b9a8d1f9aa9',
accountNumber: '1111111111114020',
currencyCode: 'RUB',
currentBalance: 12_980_100,
currentBalance: 13_070_100,
turnoverUpdatedAt: new Date('2025-07-13'),
ecoAccountName: 'Наименование счёта',
contractType: {
code: 'OTHER',
name: 'Счёт корпоративной карты',
},
accountKind: {
code: 'OTHER',
name: 'Счёт корпоративной карты',
},
},
],
},
@@ -122,30 +146,30 @@ const FINANCIAL_ACCOUNTS_MOCK: FinancialAccountsResponseDto = {
accountId: '6856e7d9-d490-4fcb-aa2e-0b9a8d0f7aa3',
accountNumber: '1111111111114020',
currencyCode: 'RUB',
currentBalance: 14_980_100,
turnoverUpdatedAt: new Date('2025-07-22'),
currentBalance: 22_060_100,
turnoverUpdatedAt: new Date('2025-06-22'),
contractType: {
code: 'OTHER',
name: 'Расчетный счёт',
name: 'Транзитный счёт',
},
accountKind: {
code: 'BASE',
name: 'Спецсчёт',
name: 'Транзитный счёт',
},
},
{
accountId: '6856e7d9-d490-4fcb-aa2e-0b9a8d0f9aa6',
accountNumber: '1111111111114321',
currencyCode: 'CNY',
currentBalance: 425_222,
turnoverUpdatedAt: new Date('2025-07-30'),
currentBalance: 30_070,
turnoverUpdatedAt: new Date('2025-06-30'),
contractType: {
code: 'OTHER',
name: 'Расчетный счёт',
name: 'Карточный счет',
},
accountKind: {
code: 'BASE',
name: 'Спецсчёт',
name: 'Карточный счет',
},
accountBlockState: 'FULL',
},
@@ -153,31 +177,48 @@ const FINANCIAL_ACCOUNTS_MOCK: FinancialAccountsResponseDto = {
accountId: '6856e7d9-d490-4fcb-aa2e-0b9a8d0f9aa9',
accountNumber: '1111111111114020',
currencyCode: 'RUB',
currentBalance: 12_980_100,
currentBalance: 1_070_100,
turnoverUpdatedAt: new Date('2025-07-01'),
ecoAccountName: 'Наименование счёта',
contractType: {
code: 'OTHER',
name: 'Счёт исполнителя ГК',
},
accountKind: {
code: 'BASE',
name: 'Счёт исполнителя ГК',
},
},
{
accountId: '6856e7d9-d490-4fcb-aa2e-0b9a8d0f9aa3',
accountNumber: '1111111111114020',
currencyCode: 'RUB',
currentBalance: 14_980_100,
currentBalance: 4_070_100,
turnoverUpdatedAt: new Date('2025-07-04'),
ecoAccountName: 'Наименование счёта',
contractType: {
code: 'BASE',
name: 'Расчётный счёт',
},
accountKind: {
code: 'BASE',
name: 'Расчётный счёт',
},
},
{
accountId: '6856e7d9-d490-4fcb-aa2e-0b9a8d0f9aa2',
accountNumber: '1111155111114321',
currencyCode: 'CNY',
currentBalance: 425_222,
currentBalance: 30_070,
turnoverUpdatedAt: new Date('2025-07-30'),
accountBlockState: 'FULL',
contractType: {
code: 'BASE',
name: 'Расчетный счёт',
code: 'OTHER',
name: 'Счёт корпоративной карты',
},
accountKind: {
code: 'BASE',
name: 'Спецсчёт',
code: 'OTHER',
name: 'Счёт корпоративной карты',
},
},
],
+1
View File
@@ -4,3 +4,4 @@ export * from './ui';
export * from './ui';
export * from './types';
export * from './context';
export * from './model';
+1
View File
@@ -8,3 +8,4 @@ export * from './useElementHeight';
export * from './useRefetchData';
export * from './getFormattedBalance';
export * from './files';
export * from './sortByDate';
+1
View File
@@ -0,0 +1 @@
export * from './useUserAccounts';
@@ -0,0 +1,10 @@
const accountTypeNames = {
PAYMENT_ACCOUNT: 'Расчётный счёт',
CURRENCY_ACCOUNT: 'Валютный счёт',
TRANSIT_ACCOUNT: 'Транзитный счёт',
DEPOSIT_ACCOUNT: 'Депозитный счёт',
CORPORATE_CARD_ACCOUNT: 'Счёт корпоративной карты',
SPECIAL_ACCOUNT: 'Спецсчет',
};
export { accountTypeNames };
@@ -1,12 +1,13 @@
import type { AccountGroupDto } from '@msb/http';
import type { Currency } from '@msb/shared';
import { accountTypeNames, type Currency, sortByDate } from '@msb/shared';
import { includedAccountTypeNames } from './includedAccountTypeNames';
import type { FinancialAccountModel } from './types';
import { sortByDate } from '@/shared/lib';
/**
* Обрабатывает наименование счета.
* Добавляет наименование организации, если организаций больше 1.
* Оставляет последние 4 цифры от номера счета.
* Сравнивает с 5 типами счета, если ни к одному не подходит, то подставляет тип счета как "Спецсчет".
*/
const getMappedAccountsToUI = (accountGroup: AccountGroupDto[]): FinancialAccountModel[] => {
const hasIncludeOrganization = accountGroup.length > 1;
@@ -19,11 +20,17 @@ const getMappedAccountsToUI = (accountGroup: AccountGroupDto[]): FinancialAccoun
mappingAccount.ecoAccountName ||
(mappingAccount.contractType?.code === 'BASE' ? mappingAccount.accountKind?.name || '' : mappingAccount.contractType?.name || '');
const accountTypeName =
mappingAccount.contractType?.code === 'BASE' ? mappingAccount.accountKind?.name || '' : mappingAccount.contractType?.name || '';
const accountType = includedAccountTypeNames.includes(accountTypeName) ? accountTypeName : accountTypeNames.SPECIAL_ACCOUNT;
return {
id: mappingAccount.accountId,
organization: hasIncludeOrganization ? group.bankClientDto.shortName : undefined,
currencyCode: mappingAccount.currencyCode as Currency,
title: accountName,
accountType,
balance: mappingAccount.currentBalance,
lastFourDigitsCard: mappingAccount.accountNumber.slice(-4),
status: mappingAccount.accountBlockState,
@@ -0,0 +1,11 @@
import { accountTypeNames } from './accountTypeNames';
const includedAccountTypeNames = [
accountTypeNames.PAYMENT_ACCOUNT,
accountTypeNames.CURRENCY_ACCOUNT,
accountTypeNames.TRANSIT_ACCOUNT,
accountTypeNames.DEPOSIT_ACCOUNT,
accountTypeNames.CORPORATE_CARD_ACCOUNT,
];
export { includedAccountTypeNames };
@@ -1,3 +1,4 @@
export * from './types';
export * from './useUserAccounts';
export * from './getMappedAccountsToUI';
export * from './accountTypeNames';
@@ -1,9 +1,10 @@
import type { AccountStatusDto } from '@msb/http';
import type { Currency } from '@/shared/model/balanceCurrency/types';
import type { Currency } from '@msb/shared';
interface FinancialAccountModel {
id: string;
title: string;
accountType: string;
balance: number;
currencyCode: Currency;
lastFourDigitsCard: string;
@@ -1,7 +1,8 @@
import { useMemo, useRef } from 'react';
import { useAccounts } from '@msb/http';
import { useAppContext } from '@msb/shared';
import { getMappedAccountsToUI, getOrganizationIntersectionWithAccounts } from '@/entities/FinancialAccount';
import { getMappedAccountsToUI } from './getMappedAccountsToUI';
import { getOrganizationIntersectionWithAccounts } from './getOrganizationIntersectionWithAccounts';
/**
* Хук, для работы с пересечением всех счетов и счетов организаций, доступных пользователю.
@@ -12,6 +12,7 @@ import * as S from './MultiSelectButton.styles';
interface Props {
header: string;
onClose?(): void;
}
const MultiSelectButton = ({
@@ -20,6 +21,7 @@ const MultiSelectButton = ({
value = [],
name,
onChange,
onClose,
optionTemplate,
disabled,
}: BaseMultiSelectProps & Props) => {
@@ -31,6 +33,7 @@ const MultiSelectButton = ({
const handleSelect = useCallback(
(selected: OptionProps, event) => {
const newValue = value.includes(selected.value) ? value.filter(v => v !== selected.value) : [...value, selected.value];
onChange?.(newValue, event);
},
[onChange, value]
@@ -65,7 +68,11 @@ const MultiSelectButton = ({
const handleClose = useCallback(() => {
setIsOpened(false);
}, [setIsOpened]);
if (onClose) {
onClose();
}
}, [setIsOpened, onClose]);
const contextValues = useMemo(
() => ({ value, search: '', onClick: handleSelect, optionTemplate }),
@@ -0,0 +1,9 @@
import { type AccountDetailsResponseDto, ACCOUNT_DETAILS_ENDPOINT, network } from '@msb/http';
const fetchAccountDetails = async (accountId: string): Promise<AccountDetailsResponseDto> => {
const response = await network.client.post<AccountDetailsResponseDto>(ACCOUNT_DETAILS_ENDPOINT, accountId);
return response.data;
};
export { fetchAccountDetails };
@@ -0,0 +1,2 @@
export * from './fetchAccountDetails';
export * from './queryKeys';
@@ -0,0 +1 @@
export const QUERY_KEY_FINANCIAL_ACCOUNTS = 'financial-accounts';
@@ -0,0 +1 @@
export * from './fetchAccountDetails';
@@ -0,0 +1 @@
export { useAccountDetails } from './model';
@@ -0,0 +1 @@
export { useAccountDetails } from './useAccountDetails';
@@ -0,0 +1,15 @@
import { type AccountDetailsResponseDto, useMutation } from '@msb/http';
import { fetchAccountDetails } from '../api';
const useAccountDetails = () => {
const {
data: accountDetails,
error: accountDetailsError,
isLoading: isAccountDetailsLoading,
mutate: mutateAccountDetails,
} = useMutation<AccountDetailsResponseDto, Error | undefined, string>((accountId: string) => fetchAccountDetails(accountId));
return { mutateAccountDetails, accountDetails, accountDetailsError, isAccountDetailsLoading };
};
export { useAccountDetails };
@@ -1,6 +1,10 @@
const LOCALIZATION = {
ACCOUNTS: 'Счета',
OPEN_ACCOUNT: 'Открыть счёт',
EMPTY_STATE_TEXT: 'Здесь будут ваши счета',
SOMETHING_WENT_WRONG: 'Что-то не сработало',
SOON_FIX: 'Скоро мы это исправим. Попробуйте ещё раз позже',
RETURN_BACK: 'Вернуться назад',
};
export { LOCALIZATION };
@@ -2,7 +2,7 @@ import styled from '@emotion/styled';
import { MEDIA } from '@msb/shared';
const Page = styled.div(({ theme }) => ({
borderRadius: '16px',
borderRadius: '16px 16px 0 0',
padding: '24px',
height: '100%',
width: '100%',
@@ -46,4 +46,12 @@ const CreatePaymentMobileWrapper = styled.div(({ theme }) => ({
borderRadius: '16px 16px 0 0',
}));
export { HeaderWrapper, Page, PageWrapper, CreatePaymentMobileWrapper };
const SystemResponseWrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
`;
export { HeaderWrapper, Page, PageWrapper, CreatePaymentMobileWrapper, SystemResponseWrapper };
@@ -1,6 +1,8 @@
import type { ReactElement } from 'react';
import { useCallback, useMemo, type ReactElement } from 'react';
import { Button } from '@msb/fractal-ui-core';
import { MEDIA, PageNavigation, useMediaQuery } from '@msb/shared';
import { SystemResponse } from '@msb/fractal-ui-extended';
import { MEDIA, PageNavigation, PATHS, useMediaQuery, useUserAccounts } from '@msb/shared';
import { useHistory } from 'react-router-dom';
import { LOCALIZATION } from '../constants';
import * as S from './AccountsMainPage.styles';
import { AccountsPageSkeleton } from '@/shared/ui';
@@ -11,31 +13,77 @@ const AccountsMainPage = (): ReactElement => {
// TODO:
};
// TODO: убрать при добавлении моков
const isLoading = false;
const isMobile = useMediaQuery(MEDIA.mobile);
const { financialAccounts, isFinancialAccountLoading, hasFinancialAccountsLastFailedError } = useUserAccounts();
const history = useHistory();
const handleClickBack = useCallback(() => {
history.push(PATHS.HOME);
}, [history]);
const accountsContent = useMemo(() => {
if (hasFinancialAccountsLastFailedError)
return (
<S.SystemResponseWrapper>
<SystemResponse
description={LOCALIZATION.SOON_FIX}
mainButtonProps={{
dataAction: 'return-back',
variant: 'primary',
shape: 'default',
children: LOCALIZATION.RETURN_BACK,
onClick: handleClickBack,
}}
size="M"
statusIcon="error"
text={LOCALIZATION.SOMETHING_WENT_WRONG}
/>
</S.SystemResponseWrapper>
);
if (financialAccounts.length === 0)
return (
<S.SystemResponseWrapper>
<SystemResponse
mainButtonProps={{
dataAction: 'open-account',
variant: 'primary',
shape: 'default',
children: LOCALIZATION.OPEN_ACCOUNT,
onClick: handleOpenAccount,
}}
size="M"
statusIcon="empty"
text={LOCALIZATION.EMPTY_STATE_TEXT}
/>
</S.SystemResponseWrapper>
);
return <AccountsTable accounts={financialAccounts} />;
}, [financialAccounts, hasFinancialAccountsLastFailedError, handleClickBack]);
return (
<S.PageWrapper>
<S.Page>
{isLoading ? (
{isFinancialAccountLoading ? (
<AccountsPageSkeleton />
) : (
<>
<S.HeaderWrapper>
<PageNavigation title={LOCALIZATION.ACCOUNTS} />
{!isMobile && (
{!isMobile && financialAccounts.length > 0 && !hasFinancialAccountsLastFailedError && (
<Button dataAction="open-account" shape="default" variant="primary" onClick={handleOpenAccount}>
{LOCALIZATION.OPEN_ACCOUNT}
</Button>
)}
</S.HeaderWrapper>
<AccountsTable />
{accountsContent}
</>
)}
</S.Page>
{isMobile && (
{isMobile && financialAccounts.length > 0 && (
<S.CreatePaymentMobileWrapper>
<Button fullWidth dataAction="open-account" shape="default" size="M" variant="primary" onClick={handleOpenAccount}>
{LOCALIZATION.OPEN_ACCOUNT}
@@ -0,0 +1,3 @@
const capitalizeFirstLetter = (text: string) => text.charAt(0).toUpperCase() + text.slice(1);
export { capitalizeFirstLetter };
@@ -0,0 +1 @@
export { capitalizeFirstLetter } from './capitalizeFirstLetter';
@@ -0,0 +1 @@
export { useCopyToClipboard } from './useCopyToClipboard';
@@ -0,0 +1,33 @@
import { useSnackbar } from '@fractal-ui/overlays';
const useCopyToClipboard = (successMessage: string) => {
const { showSnackbarMessage } = useSnackbar();
const copyText = async (text: string) => {
if (navigator.clipboard) {
await navigator.clipboard.writeText(text);
} else {
// Поддержка старых iOS Safari
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
}
showSnackbarMessage({
message: successMessage,
type: 'success',
});
};
return { copyText };
};
export { useCopyToClipboard };
@@ -0,0 +1,3 @@
const TABLE_DATA_RENDER_COUNT = 15 as number;
export { TABLE_DATA_RENDER_COUNT };
@@ -1,3 +1,5 @@
const ORGANIZATION_KEY = 'organizations';
const ACCOUNT_TYPE_KEY = 'accountTypes';
const CURRENCY_KEY = 'currencies';
export { ORGANIZATION_KEY };
export { ORGANIZATION_KEY, ACCOUNT_TYPE_KEY, CURRENCY_KEY };
@@ -1,5 +1,4 @@
export * from './localization';
export * from './tableColumns';
export * from './tableData';
export * from './form';
export * from './organizationOptions';
export * from './constants';
@@ -15,6 +15,7 @@ const LOCALIZATION = {
ORDER_STATEMENT: 'Заказать выписку',
COPY_REQUISITES: 'Скопировать реквизиты',
ACTIONS: 'Действия',
REQUISITES_COPIED: 'Реквизиты скопированы',
};
export { LOCALIZATION };
@@ -1,19 +0,0 @@
import type { BaseOptionsProps } from '@fractal-ui/composites';
// TODO: удалить при добавлении моков
const ORGANIZATIONS_OPTIONS: BaseOptionsProps[] = [
{
label: 'ООО «Идеальный мир»',
value: 1,
},
{
label: 'ЗАО «Технологии будущего»',
value: 2,
},
{
label: 'ИП Иванов Иван Сергеевич',
value: 3,
},
];
export { ORGANIZATIONS_OPTIONS };
@@ -1,49 +1,49 @@
import type { CellDataProps, UnionColumnProps } from '@fractal-ui/table';
import type { AccountTableColumn } from '../model';
import type { FinancialAccountModel } from '@msb/shared';
import { AccountCell, ActionsAndStatusCell, AvailableBalanceCell, RequisitesCell } from '../ui/Cells';
import { LOCALIZATION } from './localization';
const DESKTOP_COLUMNS: Array<UnionColumnProps<AccountTableColumn>> = [
const DESKTOP_COLUMNS: Array<UnionColumnProps<FinancialAccountModel>> = [
{
accessor: 'account',
name: 'account',
accessor: 'title',
name: 'title',
headerType: 'string',
label: LOCALIZATION.ACCOUNT,
width: 280,
Cell({ account }: CellDataProps<AccountTableColumn>) {
return <AccountCell icon={account.icon} name={account.name} organization={account.organization} />;
Cell({ title, organization }: CellDataProps<FinancialAccountModel>) {
return <AccountCell name={title} organization={organization} />;
},
},
{
accessor: 'availableBalance',
accessor: 'balance',
headerType: 'number',
label: LOCALIZATION.AVAILABLE_BALANCE,
name: 'availableBalance',
name: 'balance',
width: 237,
disableResizing: true,
Cell({ availableBalance }: CellDataProps<AccountTableColumn>) {
return <AvailableBalanceCell availableBalance={availableBalance} />;
Cell({ balance, currencyCode }: CellDataProps<FinancialAccountModel>) {
return <AvailableBalanceCell balance={balance} currency={currencyCode} />;
},
},
{
accessor: 'requisites',
accessor: 'lastFourDigitsCard',
headerType: 'number',
label: LOCALIZATION.REQUISITES,
name: 'requisites',
name: 'lastFourDigitsCard',
disableResizing: true,
width: 131,
Cell({ requisites }: CellDataProps<AccountTableColumn>) {
return <RequisitesCell requisites={requisites} />;
Cell({ lastFourDigitsCard, id, currencyCode }: CellDataProps<FinancialAccountModel>) {
return <RequisitesCell accountId={id} currency={currencyCode} lastFourDigitsCard={lastFourDigitsCard} />;
},
},
{
accessor: 'statusAndActions',
accessor: 'status',
headerType: 'functional',
name: 'statusAndActions',
name: 'status',
maxWidth: 288,
minWidth: 210,
Cell({ statusAndActions }: CellDataProps<AccountTableColumn>) {
return <ActionsAndStatusCell canCreatePayment={statusAndActions.canCreatePayment} status={statusAndActions.status} />;
Cell({ status }: CellDataProps<FinancialAccountModel>) {
return <ActionsAndStatusCell status={status} />;
},
},
];
@@ -1,58 +0,0 @@
import { EagleIcon, WalletIcon } from '@fractal-ui/library';
import type { AccountTableColumn } from '../model';
// TODO: удалить при добавлении моков
const TABLE_DATA: AccountTableColumn[] = [
{
account: {
name: 'Расчётный',
organization: 'ООО «Идеальный мир»',
icon: WalletIcon,
},
availableBalance: '12 980 100,00 ₽',
requisites: '• 3658',
statusAndActions: {
canCreatePayment: true,
status: 'FULL',
},
},
{
account: {
name: 'Расчётный',
organization: 'ООО «Идеальный мир»',
icon: WalletIcon,
},
availableBalance: '12 980 100,00 ₽',
requisites: '• 3658',
statusAndActions: {
canCreatePayment: true,
status: 'PARTIAL',
},
},
{
account: {
name: 'Спецсчёт',
organization: 'ЗАО «Технологии будущего»',
icon: EagleIcon,
},
availableBalance: '750 300,00 $',
requisites: '• 4872',
statusAndActions: {
canCreatePayment: false,
},
},
{
account: {
name: 'Расчётный',
organization: 'ИП Иванов Иван Сергеевич',
icon: WalletIcon,
},
availableBalance: '5 320 500,00 ₽',
requisites: '• 2941',
statusAndActions: {
canCreatePayment: true,
},
},
];
export { TABLE_DATA };
@@ -0,0 +1,28 @@
import type { FinancialAccountModel } from '@msb/shared';
import type { OptionsKey } from '../model';
/**
* Фильтрует массив счетов по выбранным фильтрам.
* @param accounts - Массив счетов.
* @param filterValues - Объект фильтров.
* @returns - Отфильтрованный массив счетов.
*/
const filterAccounts = (accounts: FinancialAccountModel[], filterValues: Record<OptionsKey, any>) => {
let filtered = accounts;
if (filterValues.organizations.length > 0) {
filtered = accounts.filter(account => filterValues.organizations.includes(account.organization!));
}
if (filterValues.accountTypes.length > 0) {
filtered = filtered.filter(account => filterValues.accountTypes.includes(account.accountType));
}
if (filterValues.currencies.length > 0) {
filtered = filtered.filter(account => filterValues.currencies.includes(account.currencyCode));
}
return filtered;
};
export { filterAccounts };
@@ -0,0 +1,46 @@
import type { AccountDetailsDto } from '@msb/http';
import type { Currency } from '@msb/shared';
const rubRequisiteWords: Record<string, string> = {
organization: 'Организация',
inn: 'ИНН',
kpp: 'КПП',
ogrn: 'ОГРН',
account: 'Номер расчетного счета',
bik: 'БИК банка',
bankName: 'Наименование банка',
correspondentAccount: 'Корреспондентский счет банка',
};
const currencyRequisiteWords: Record<string, string> = {
beneficiary: 'Бенефициар\\Beneficiary',
beneficiaryAccount: 'Счёт бенефициара\\Beneficiary account number',
beneficiaryTransitAccount: 'Транзитный счёт бенефициара\\Beneficiary transit account number',
swiftCode: 'SWIFT-код банка\\SWIFT-code',
beneficiaryBank: 'Банк бенефициара\\Beneficiary bank',
beneficiaryBankAddress: 'Адрес банка бенефициара\\Beneficiary bank address',
};
/**
* Форматирует реквизиты в строку для копирования.
* @param requisites - Реквизиты.
* @param currency - Код валюты.
* @returns - Отформатированная строка.
*/
const formatAccountDetailsToCopy = (requisites: AccountDetailsDto, currency: Currency) => {
if (currency === 'RUB') {
const requisitesArray = Object.entries(requisites)
.map(([key, value]) => (rubRequisiteWords[key] ? `${rubRequisiteWords[key]} - ${value}` : null))
.filter(Boolean);
return requisitesArray.join('\n');
}
const requisitesArray = Object.entries(requisites)
.map(([key, value]) => (currencyRequisiteWords[key] ? `${currencyRequisiteWords[key]} - ${value}` : null))
.filter(Boolean);
return requisitesArray.join('\n');
};
export { formatAccountDetailsToCopy };
@@ -0,0 +1,4 @@
export * from './sortTableData';
export * from './formatAccountDetailsToCopy';
export * from './sortAccountTypes';
export * from './filterAccounts';
@@ -0,0 +1,25 @@
import { accountTypeNames } from '@msb/shared';
const accountTypesPriorities = {
[accountTypeNames.PAYMENT_ACCOUNT]: 5,
[accountTypeNames.CURRENCY_ACCOUNT]: 4,
[accountTypeNames.TRANSIT_ACCOUNT]: 3,
[accountTypeNames.DEPOSIT_ACCOUNT]: 2,
[accountTypeNames.CORPORATE_CARD_ACCOUNT]: 1,
[accountTypeNames.SPECIAL_ACCOUNT]: 0,
};
/**
* Сортирует массив типов счетов по приоритету.
* @param types - Массив типов.
* @returns - Отсортированный массив типов.
*/
const sortAccountTypes = (types: string[]) =>
types.sort((firstType, secondType) => {
const firstTypePriority = accountTypesPriorities[firstType] || 0;
const secondTypePriority = accountTypesPriorities[secondType] || 0;
return secondTypePriority - firstTypePriority;
});
export { sortAccountTypes };
@@ -0,0 +1,26 @@
import type { AccountStatusDto } from '@msb/http';
import { sortByDate, type FinancialAccountModel } from '@msb/shared';
const statusPriority: Record<AccountStatusDto, number> = {
FULL: 2,
PARTIAL: 1,
};
/**
* Сортирует счета по статусу и дате.
* @param accounts - Массив счетов.
* @param isAscending - Флаг для сортировки по возрастанию (не влияет на статус).
* @returns - Отсортированный массив.
*/
const sortTableData = (accounts: FinancialAccountModel[], isAscending = false) => {
const sortedByDate = sortByDate(accounts, 'turnoverUpdatedAt', isAscending);
return sortedByDate.sort((firstAccount, secondAccount) => {
const firstAccountPriority = firstAccount.status ? statusPriority[firstAccount.status] : 0;
const secondAccountPriority = secondAccount.status ? statusPriority[secondAccount.status] : 0;
return secondAccountPriority - firstAccountPriority;
});
};
export { sortTableData };
@@ -1,23 +1,11 @@
import type { IconComponent, IconName } from '@fractal-ui/library';
import type { IconName } from '@fractal-ui/library';
import type { RowActionsMenuItemProps } from '@fractal-ui/table';
import type { AccountStatusDto } from '@msb/http';
interface AccountTableColumn {
account: {
name: string;
organization?: string;
icon: IconComponent;
};
availableBalance: string;
requisites: string;
statusAndActions: {
status?: AccountStatusDto;
canCreatePayment: boolean;
};
}
import type { ACCOUNT_TYPE_KEY, CURRENCY_KEY, ORGANIZATION_KEY } from '../constants';
interface RowAction extends RowActionsMenuItemProps {
iconName: IconName;
}
export type { AccountTableColumn, RowAction };
type OptionsKey = typeof ACCOUNT_TYPE_KEY | typeof CURRENCY_KEY | typeof ORGANIZATION_KEY;
export type { RowAction, OptionsKey };
@@ -0,0 +1,41 @@
import styled from '@emotion/styled';
import { MEDIA } from '@msb/shared';
const FiltersForm = styled.form<{ $haveOrganizations: boolean }>`
padding-top: 12px;
display: flex;
gap: 16px;
* {
white-space: nowrap;
}
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
@media ${MEDIA.mobile} {
margin: 0 -16px;
overflow: scroll;
& button:first-child {
margin-left: 16px;
}
& button:last-child {
margin-right: 16px;
}
}
@media ${MEDIA.desktop} {
display: grid;
grid-template-columns: repeat(${({ $haveOrganizations }) => ($haveOrganizations ? 3 : 2)}, 1fr);
& > * {
min-width: 0;
}
}
`;
export { FiltersForm };
@@ -0,0 +1,147 @@
import { useMemo, useState } from 'react';
import { createField } from '@fractal-ui/form';
import type { BaseOptionsProps } from '@msb/fractal-ui-composites';
import type { FinancialAccountModel } from '@msb/shared';
import { MEDIA, MultiSelect, MultiSelectButton, useMediaQuery } from '@msb/shared';
import { Form } from 'react-final-form';
import { ACCOUNT_TYPE_KEY, CURRENCY_KEY, LOCALIZATION, ORGANIZATION_KEY } from '../../constants';
import { filterAccounts, sortAccountTypes, sortTableData } from '../../lib';
import type { OptionsKey } from '../../model';
import * as S from './AccountsFiltersForm.styles';
import { capitalizeFirstLetter } from '@/shared/lib';
const MultiSelectField = createField(MultiSelect);
const MultiSelectButtonField = createField(MultiSelectButton);
const initialValues = { [ORGANIZATION_KEY]: [], [ACCOUNT_TYPE_KEY]: [], [CURRENCY_KEY]: [] };
interface Props {
accounts: FinancialAccountModel[];
onFiltersApply(filteredAccounts: FinancialAccountModel[]): void;
}
const AccountsFiltersForm = ({ accounts, onFiltersApply }: Props) => {
const handleSubmit = () => {
// TODO:
};
const options = useMemo<Record<OptionsKey, BaseOptionsProps[]>>(() => {
const uniqueOrganizations = new Set(accounts.map(account => account.organization).filter(Boolean) as string[]);
const organizations = Array.from(uniqueOrganizations).map(organization => ({
label: organization,
value: organization,
}));
const uniqueAccountTypes = new Set(sortAccountTypes(accounts.map(account => account.accountType)));
const accountTypes = Array.from(uniqueAccountTypes).map(accountType => ({
label: accountType,
value: accountType,
}));
const uniqueCurrencies = new Set(accounts.map(account => account.currencyCode));
const currencies = Array.from(uniqueCurrencies).map(currency => {
const currencyName = capitalizeFirstLetter(new Intl.DisplayNames(['ru'], { type: 'currency' }).of(currency) || '');
return {
label: `${currency} ${currencyName === currency ? '' : currencyName}`,
value: currency,
};
});
return {
organizations,
accountTypes,
currencies,
};
}, [accounts]);
const [lastFormValues, setLastFormValues] = useState<Record<OptionsKey, any>>({ accountTypes: [], currencies: [], organizations: [] });
const handleApplyFilters = (values: Record<OptionsKey, any>) => {
if (JSON.stringify(values) === JSON.stringify(lastFormValues)) {
return;
}
onFiltersApply(sortTableData(filterAccounts(accounts, values)));
setLastFormValues(values);
};
const isDesktop = useMediaQuery(MEDIA.desktop);
return (
<Form
initialValues={initialValues}
render={({ values }: { values: Record<OptionsKey, string[]> }) => (
<S.FiltersForm $haveOrganizations={options.organizations.length > 1} data-form-type="accounts">
{isDesktop ? (
<>
{options.organizations.length > 1 && (
<MultiSelectField
autoComplete="off"
clearButtonVisibility={values[ORGANIZATION_KEY].length > 0 ? 'always' : 'never'}
moreText={`${LOCALIZATION.ORGANIZATIONS}: `}
name={ORGANIZATION_KEY}
options={options.organizations}
placeholder={LOCALIZATION.ALL_ORGANIZATIONS}
size="S"
onBeforeClose={() => handleApplyFilters(values)}
/>
)}
<MultiSelectField
autoComplete="off"
clearButtonVisibility={values[ACCOUNT_TYPE_KEY].length > 0 ? 'always' : 'never'}
moreText={`${LOCALIZATION.ACCOUNT_TYPE}: `}
name={ACCOUNT_TYPE_KEY}
options={options.accountTypes}
placeholder={LOCALIZATION.ACCOUNT_TYPE}
size="S"
onBeforeClose={() => handleApplyFilters(values)}
/>
<MultiSelectField
autoComplete="off"
clearButtonVisibility={values[CURRENCY_KEY].length > 0 ? 'always' : 'never'}
moreText={`${LOCALIZATION.CURRENCY}: `}
name={CURRENCY_KEY}
options={options.currencies}
placeholder={LOCALIZATION.CURRENCY}
size="S"
onBeforeClose={() => handleApplyFilters(values)}
/>
</>
) : (
<>
{options.organizations.length > 1 && (
<MultiSelectButtonField
header={LOCALIZATION.ALL_ORGANIZATIONS}
name={ORGANIZATION_KEY}
options={options.organizations}
onClose={() => handleApplyFilters(values)}
/>
)}
<MultiSelectButtonField
header={LOCALIZATION.ACCOUNT_TYPE}
name={ACCOUNT_TYPE_KEY}
options={options.accountTypes}
onClose={() => handleApplyFilters(values)}
/>
<MultiSelectButtonField
header={LOCALIZATION.CURRENCY}
name={CURRENCY_KEY}
options={options.currencies}
onClose={() => handleApplyFilters(values)}
/>
</>
)}
</S.FiltersForm>
)}
onSubmit={handleSubmit}
/>
);
};
export { AccountsFiltersForm };
@@ -0,0 +1 @@
export { AccountsFiltersForm } from './AccountsFiltersForm';
@@ -1,65 +1,29 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { MEDIA } from '@msb/shared';
const TableWithButtons = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
width: 100%;
height: 100%;
`;
const FiltersForm = styled.form`
padding-top: 12px;
display: flex;
gap: 16px;
* {
white-space: nowrap;
const accountsTableGlobalStyles = css`
[data-table-type='accounts'] [data-name='context-menu-cell'] {
height: 100%;
align-items: center;
svg {
opacity: 0.56;
}
}
scrollbar-width: none;
&::-webkit-scrollbar {
[data-form-type='accounts'] [data-action='clear'] {
display: none;
}
@media ${MEDIA.mobile} {
margin: 0 -16px;
overflow: scroll;
& button:first-child {
margin-left: 16px;
}
& button:last-child {
margin-right: 16px;
}
}
@media ${MEDIA.desktop} {
& > * {
flex: 1 1 0;
min-width: 0;
}
[data-form-type='accounts'] [data-role='tag'] {
padding-right: 8px;
}
`;
const CheckboxWrapper = styled.div<{ $isChecked: boolean }>(({ theme, $isChecked }) => ({
padding: '16px 24px',
backgroundColor: $isChecked ? theme.colors.bg.selected : theme.colors.bg.primary,
}));
const MobileTable = styled.div`
display: flex;
flex-direction: column;
padding-top: 16px;
`;
const BottomSheetButton = styled.div`
padding: 16px;
display: flex;
align-items: center;
gap: 8px;
`;
export { TableWithButtons, FiltersForm, CheckboxWrapper, MobileTable, BottomSheetButton };
export { TableWithButtons, accountsTableGlobalStyles };
@@ -1,64 +1,48 @@
import { useState } from 'react';
import { css, Global } from '@emotion/react';
import { createField } from '@fractal-ui/form';
import { CopyIcon, DocumentIcon, Icon, PlusIcon } from '@fractal-ui/library';
import { Table } from '@fractal-ui/table';
import { Global } from '@emotion/react';
import { CopyIcon, DocumentIcon, PlusIcon } from '@fractal-ui/library';
import { CardScroller, Table } from '@fractal-ui/table';
import { ScrollContainer } from '@msb/fractal-ui-core';
import { BottomSheet } from '@msb/fractal-ui-overlays';
import { Text } from '@msb/fractal-ui-styling';
import { MEDIA, MultiSelect, MultiSelectButton, useMediaQuery } from '@msb/shared';
import { Form } from 'react-final-form';
import { DESKTOP_COLUMNS, LOCALIZATION, ORGANIZATION_KEY, ORGANIZATIONS_OPTIONS, TABLE_DATA } from '../constants';
import type { AccountTableColumn, RowAction } from '../model';
import type { FinancialAccountModel } from '@msb/shared';
import { MEDIA, PATHS, useMediaQuery } from '@msb/shared';
import { useHistory } from 'react-router-dom';
import { DESKTOP_COLUMNS, LOCALIZATION, TABLE_DATA_RENDER_COUNT } from '../constants';
import { formatAccountDetailsToCopy, sortTableData } from '../lib';
import type { RowAction } from '../model';
import { AccountsFiltersForm } from './AccountsFiltersForm';
import * as S from './AccountsTable.styles';
import { MobileCard } from './MobileCard';
import { BULLET_UNICODE } from '@/shared/constants';
import { useAccountDetails } from '@/entities/Account';
import { useCopyToClipboard } from '@/shared/model';
const GlobalStylesActionsButton = () => (
<Global
styles={css`
[data-table-type='accounts'] [data-name='context-menu-cell'] {
height: 100%;
align-items: center;
svg {
opacity: 0.56;
}
}
`}
/>
);
const AccountsTable = () => {
const handleSubmit = () => {
// TODO:
};
interface Props {
accounts: FinancialAccountModel[];
}
const AccountsTable = ({ accounts }: Props) => {
const isDesktop = useMediaQuery(MEDIA.desktop);
const isMobile = useMediaQuery(MEDIA.mobile);
const [openedAccountBottomSheet, setOpenedAccountBottomSheet] = useState<AccountTableColumn | null>(null);
const handleOpenBottomSheet = (account: AccountTableColumn) => {
setOpenedAccountBottomSheet(account);
};
const handleCloseBottomSheet = () => {
setOpenedAccountBottomSheet(null);
};
const history = useHistory();
const handleCreatePayment = () => {
// TODO:
history.push(PATHS.PAYMENTS);
};
const handleOrderStatement = () => {
// TODO:
history.push(PATHS.STATEMENTS_AND_INQUIRIES);
};
const handleCopyRequisites = () => {
// TODO:
const { mutateAccountDetails } = useAccountDetails();
const { copyText } = useCopyToClipboard(LOCALIZATION.REQUISITES_COPIED);
const handleCopyRequisites = (row: FinancialAccountModel) => {
mutateAccountDetails(row.id, {
onSuccess: data => copyText(formatAccountDetailsToCopy(data.data, row.currencyCode)),
});
};
const ROW_ACTIONS: RowAction[] = [
const getRowActions = (rows: FinancialAccountModel[]): RowAction[] => [
{
dataAction: 'create-payment',
text: LOCALIZATION.CREATE_PAYMENT,
@@ -78,93 +62,48 @@ const AccountsTable = () => {
text: LOCALIZATION.COPY_REQUISITES,
iconName: 'Copy',
icon: CopyIcon,
onClick: handleCopyRequisites,
onClick: () => handleCopyRequisites(rows[0]),
},
];
const MultiSelectField = createField(MultiSelect);
const MultiSelectButtonField = createField(MultiSelectButton);
const [filteredAccounts, setFilteredAccounts] = useState(sortTableData(accounts));
const handleFiltersApply = (accountsWithFilters: FinancialAccountModel[]) => {
setFilteredAccounts(accountsWithFilters);
};
const [renderCount, setRenderCount] = useState(TABLE_DATA_RENDER_COUNT);
// eslint-disable-next-line @typescript-eslint/require-await
const handleEndTable = async () => {
setRenderCount(prevValue => prevValue + TABLE_DATA_RENDER_COUNT);
};
return (
<S.TableWithButtons>
<BottomSheet
header={LOCALIZATION.ACTIONS}
isOpen={Boolean(openedAccountBottomSheet)}
subTitle={
openedAccountBottomSheet
? `${openedAccountBottomSheet?.account.name} ${BULLET_UNICODE} ${openedAccountBottomSheet?.requisites}`
: ''
}
onClose={handleCloseBottomSheet}
>
{ROW_ACTIONS.map(action => (
<S.BottomSheetButton key={action.dataAction}>
<Icon name={action.iconName} /> <Text.P1>{action.text}</Text.P1>
</S.BottomSheetButton>
))}
</BottomSheet>
<Form
initialValues={{ [ORGANIZATION_KEY]: [] }}
render={() => (
<S.FiltersForm>
{isDesktop ? (
<>
{ORGANIZATIONS_OPTIONS.length > 1 && (
<MultiSelectField
autoComplete="off"
moreText={`${LOCALIZATION.ORGANIZATIONS}: `}
name={ORGANIZATION_KEY}
options={ORGANIZATIONS_OPTIONS}
placeholder={LOCALIZATION.ALL_ORGANIZATIONS}
size="S"
/>
)}
<MultiSelectField autoComplete="off" name="accountType" options={[]} placeholder={LOCALIZATION.ACCOUNT_TYPE} size="S" />
<MultiSelectField autoComplete="off" name="currency" options={[]} placeholder={LOCALIZATION.CURRENCY} size="S" />
</>
) : (
<>
{ORGANIZATIONS_OPTIONS.length > 1 && (
<MultiSelectButtonField header={LOCALIZATION.ALL_ORGANIZATIONS} name={ORGANIZATION_KEY} options={ORGANIZATIONS_OPTIONS} />
)}
<MultiSelectButtonField header={LOCALIZATION.ACCOUNT_TYPE} name={'accountType'} options={ORGANIZATIONS_OPTIONS} />
<MultiSelectButtonField header={LOCALIZATION.CURRENCY} name={'currency'} options={ORGANIZATIONS_OPTIONS} />
</>
)}
</S.FiltersForm>
)}
onSubmit={handleSubmit}
/>
{/* // Нужно, чтобы достучаться до нужных атрибутов, по другому стили не накладываются на эти компоненты */}
<Global styles={S.accountsTableGlobalStyles} />
<AccountsFiltersForm accounts={accounts} onFiltersApply={handleFiltersApply} />
{isMobile ? (
<S.MobileTable>
{TABLE_DATA.map(account => (
<MobileCard
key={account.requisites}
balance={account.availableBalance}
lastFourDigitsAccount={account.requisites}
name={account.account.name}
organization={account.account.organization}
status={account.statusAndActions.status}
onClickActions={() => handleOpenBottomSheet(account)}
/>
))}
</S.MobileTable>
<CardScroller
card={MobileCard}
data={filteredAccounts.map(account => ({ ...account, rowActions: getRowActions([account]) })).slice(0, renderCount)}
lazyLoadProps={{ onIntersecting: handleEndTable }}
/>
) : (
<ScrollContainer style={{ height: 460 }}>
<GlobalStylesActionsButton />
<div data-table-type="accounts">
<Table
hasBorderTop
columns={DESKTOP_COLUMNS}
data={TABLE_DATA}
hasColumnsDrag={false}
isStickyRowActionsColumn={!isDesktop}
rowActions={isDesktop ? undefined : () => ROW_ACTIONS}
/>
</div>
<ScrollContainer data-table-type="accounts" style={{ height: '100%' }}>
{/* // TODO: в таблице на планшетах row actions открываются как dropdown, а не drawer */}
<Table
hasBorderTop
withInnerScrolling
columns={DESKTOP_COLUMNS}
data={filteredAccounts.slice(0, renderCount)}
hasColumnsDrag={false}
isStickyRowActionsColumn={!isDesktop}
lazyLoadProps={{ onIntersecting: handleEndTable }}
rowActions={isDesktop ? undefined : getRowActions}
/>
</ScrollContainer>
)}
</S.TableWithButtons>
@@ -1,17 +1,16 @@
import type { IconComponent } from '@fractal-ui/library';
import { Pictogram } from '@fractal-ui/library';
import { Pictogram, WalletIcon } from '@fractal-ui/library';
import { Text } from '@msb/fractal-ui-styling';
import * as S from './Cells.styles';
interface Props {
icon: IconComponent;
name: string;
organization?: string;
}
const AccountCell = ({ icon, name, organization }: Props) => (
const AccountCell = ({ name, organization }: Props) => (
<S.Cell>
<Pictogram backgroundColor="bg.secondary" icon={icon} opacity={0.56} size="L" />
{/* // TODO: иконки из типа счета */}
<Pictogram backgroundColor="bg.secondary" icon={WalletIcon} opacity={0.56} size="L" />
<S.Info>
<Text.P2>{name}</Text.P2>
@@ -2,22 +2,24 @@ import { PlusIcon } from '@fractal-ui/library';
import { ButtonIcon } from '@msb/fractal-ui-core';
import { Badge } from '@msb/fractal-ui-extended';
import type { AccountStatusDto } from '@msb/http';
import { MEDIA, useMediaQuery } from '@msb/shared';
import { MEDIA, PATHS, useMediaQuery } from '@msb/shared';
import { useHistory } from 'react-router-dom';
import { LOCALIZATION } from '../../constants';
import * as S from './Cells.styles';
interface Props {
status?: AccountStatusDto;
canCreatePayment: boolean;
}
const ActionsAndStatusCell = ({ status, canCreatePayment }: Props) => {
const ActionsAndStatusCell = ({ status }: Props) => {
const history = useHistory();
const handleOpenStatements = () => {
// TODO:
history.push(PATHS.STATEMENTS_AND_INQUIRIES);
};
const handleCreatePayments = () => {
// TODO:
history.push(PATHS.PAYMENTS);
};
const isDesktop = useMediaQuery(MEDIA.desktop);
@@ -38,16 +40,14 @@ const ActionsAndStatusCell = ({ status, canCreatePayment }: Props) => {
{isDesktop && (
<>
<S.Document onClick={handleOpenStatements} />
{canCreatePayment && (
<ButtonIcon
dataAction="create-payment"
icon={PlusIcon}
shape="default"
size="XS"
variant="blue"
onClick={handleCreatePayments}
/>
)}
<ButtonIcon
dataAction="create-payment"
icon={PlusIcon}
shape="default"
size="XS"
variant="blue"
onClick={handleCreatePayments}
/>
</>
)}
</S.ActionsWrapper>
@@ -1,16 +1,23 @@
import { useMemo } from 'react';
import { Title } from '@msb/fractal-ui-styling';
import { getFormattedBalance } from '@msb/shared';
import * as S from './Cells.styles';
interface Props {
availableBalance: string;
balance: number;
currency: string;
}
const AvailableBalanceCell = ({ availableBalance }: Props) => (
<S.Cell $justify="end">
<S.Wrapper>
<Title.H5>{availableBalance}</Title.H5>
</S.Wrapper>
</S.Cell>
);
const AvailableBalanceCell = ({ balance, currency }: Props) => {
const formattedBalance = useMemo(() => getFormattedBalance(balance, currency), [balance, currency]);
return (
<S.Cell $justify="end">
<S.Wrapper>
<Title.H5>{formattedBalance}</Title.H5>
</S.Wrapper>
</S.Cell>
);
};
export { AvailableBalanceCell };
@@ -1,14 +1,33 @@
import { Spinner } from '@msb/fractal-ui-core';
import { Text } from '@msb/fractal-ui-styling';
import type { Currency } from '@msb/shared';
import { MEDIA, useMediaQuery } from '@msb/shared';
import { LOCALIZATION } from '../../constants';
import { formatAccountDetailsToCopy } from '../../lib';
import * as S from './Cells.styles';
import { useAccountDetails } from '@/entities/Account';
import { BULLET_UNICODE } from '@/shared/constants';
import { useCopyToClipboard } from '@/shared/model';
interface Props {
requisites: string;
lastFourDigitsCard: string;
accountId: string;
currency: Currency;
}
const RequisitesCell = ({ requisites }: Props) => {
const handleCopyRequisites = () => {
// TODO:
const RequisitesCell = ({ lastFourDigitsCard, accountId, currency }: Props) => {
const { mutateAccountDetails, accountDetails, isAccountDetailsLoading } = useAccountDetails();
const { copyText } = useCopyToClipboard(LOCALIZATION.REQUISITES_COPIED);
const handleClickCopy = () => {
if (accountDetails) {
copyText(formatAccountDetailsToCopy(accountDetails.data, currency));
} else {
mutateAccountDetails(accountId, {
onSuccess: data => copyText(formatAccountDetailsToCopy(data.data, currency)),
});
}
};
const isDesktop = useMediaQuery(MEDIA.desktop);
@@ -16,8 +35,10 @@ const RequisitesCell = ({ requisites }: Props) => {
return (
<S.Cell $justify="end">
<S.Wrapper>
<Text.P2>{requisites}</Text.P2>
{isDesktop && <S.Copy onClick={handleCopyRequisites} />}
<Text.P2>
{BULLET_UNICODE} {lastFourDigitsCard}
</Text.P2>
{isDesktop && (isAccountDetailsLoading ? <Spinner dataName="account-details" size="XS" /> : <S.Copy onClick={handleClickCopy} />)}
</S.Wrapper>
</S.Cell>
);
@@ -1,25 +1,49 @@
import styled from '@emotion/styled';
import { ContextMenuIcon } from '@fractal-ui/library';
import { Text } from '@msb/fractal-ui-styling';
import { ButtonIcon } from '@fractal-ui/core';
import { CardForScroller } from '@fractal-ui/table';
import { Text, Title } from '@msb/fractal-ui-styling';
const Card = styled.div`
padding-bottom: 16px;
const Card = styled(CardForScroller)`
padding: 16px 0 0 0;
margin-bottom: 0px;
display: flex;
flex-direction: column;
align-items: start;
gap: 8px;
position: relative;
box-shadow: none;
[data-testid='card-header'] {
position: absolute;
top: 0;
right: 0;
margin: 16px;
}
`;
const Balance = styled(Title.H4)`
&&& {
font-size: 18px;
}
`;
const Organization = styled(Text.P3)`
opacity: 0.56;
`;
const MoreIcon = styled(ContextMenuIcon)`
const BottomSheetButton = styled.div`
padding: 16px;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
`;
const ButtonMore = styled(ButtonIcon)`
position: absolute;
top: 0;
right: 0;
opacity: 0.56;
margin: 16px;
`;
export { Card, Organization, MoreIcon };
export { Card, Organization, Balance, BottomSheetButton, ButtonMore };
@@ -1,40 +1,65 @@
import { useMemo, useState } from 'react';
import { ContextMenuIcon, Icon } from '@fractal-ui/library';
import type { CardForScrollerProps } from '@fractal-ui/table';
import { Divider } from '@msb/fractal-ui-core';
import { Badge } from '@msb/fractal-ui-extended';
import { Text, Title } from '@msb/fractal-ui-styling';
import type { AccountStatusDto } from '@msb/http';
import { BottomSheet } from '@msb/fractal-ui-overlays';
import { Text } from '@msb/fractal-ui-styling';
import type { FinancialAccountModel } from '@msb/shared';
import { getFormattedBalance } from '@msb/shared';
import { LOCALIZATION } from '../../constants';
import type { RowAction } from '../../model';
import * as S from './MobileCard.styles';
import { BULLET_UNICODE } from '@/shared/constants';
interface Props {
status?: AccountStatusDto;
name: string;
lastFourDigitsAccount: string;
balance: string;
organization?: string;
onClickActions(): void;
}
const MobileCard: React.FC<CardForScrollerProps<FinancialAccountModel & { rowActions: RowAction[] }>> = ({ data }) => {
const formattedBalance = useMemo(() => getFormattedBalance(data.balance, data.currencyCode), [data]);
const MobileCard = ({ status, name, lastFourDigitsAccount, balance, organization, onClickActions }: Props) => (
<S.Card>
<S.MoreIcon onClick={onClickActions} />
{status &&
(status === 'FULL' ? (
<Badge size="XS" type="error">
{LOCALIZATION.STATUS_FULL_BLOCK}
</Badge>
) : (
<Badge size="XS" type="warning">
{LOCALIZATION.STATUS_PARTIAL_BLOCK}
</Badge>
))}
<Text.P3>
{name} {BULLET_UNICODE} {lastFourDigitsAccount}
</Text.P3>
<Title.H4>{balance}</Title.H4>
{organization && <S.Organization>{organization}</S.Organization>}
<Divider height="1" width="100%" />
</S.Card>
);
const [isBottomSheetOpened, setIsBottomSheetOpened] = useState(false);
const openBottomSheet = () => {
setIsBottomSheetOpened(true);
};
const closeBottomSheet = () => {
setIsBottomSheetOpened(false);
};
return (
<S.Card data={data}>
<BottomSheet
header={LOCALIZATION.ACTIONS}
isOpen={isBottomSheetOpened}
subTitle={`${data.title} ${BULLET_UNICODE} ${data.lastFourDigitsCard}`}
onClose={closeBottomSheet}
>
{data.rowActions.map(action => (
<S.BottomSheetButton key={action.dataAction} onClick={action.onClick}>
<Icon name={action.iconName} /> <Text.P1>{action.text}</Text.P1>
</S.BottomSheetButton>
))}
</BottomSheet>
<S.ButtonMore dataAction="open-bottomsheet" icon={ContextMenuIcon} size="S" variant="ghost" onClick={openBottomSheet} />
{data.status &&
(data.status === 'FULL' ? (
<Badge size="XS" type="error">
{LOCALIZATION.STATUS_FULL_BLOCK}
</Badge>
) : (
<Badge size="XS" type="warning">
{LOCALIZATION.STATUS_PARTIAL_BLOCK}
</Badge>
))}
<Text.P3>
{data.title} {BULLET_UNICODE} {data.lastFourDigitsCard}
</Text.P3>
<S.Balance>{formattedBalance}</S.Balance>
{data.organization && <S.Organization>{data.organization}</S.Organization>}
<Divider height="1" mb="0" width="100%" />
</S.Card>
);
};
export { MobileCard };
@@ -1,3 +1 @@
export { FinancialAccount } from './ui';
export * from './model';
export * from './lib';
@@ -1 +0,0 @@
export * from './getOrganizationIntersectionWithAccounts';
@@ -1,9 +1,8 @@
import { useMemo } from 'react';
import { WalletIcon } from '@fractal-ui/library';
import { Text, Title } from '@fractal-ui/styling';
import { useMediaQuery, MEDIA } from '@msb/shared';
import { useMediaQuery, MEDIA, type FinancialAccountModel } from '@msb/shared';
import { LOCALIZATION } from '../constants';
import type { FinancialAccountModel } from '../model';
import * as S from './FinancialAccount.styles';
import { BULLET_UNICODE } from '@/shared/constants/unicodes';
import { getFormattedBalance } from '@/shared/lib/getFormattedBalance/getFormattedBalance';
@@ -1,10 +1,9 @@
import { type ReactElement } from 'react';
import { PlusIcon } from '@fractal-ui/library';
import { Footer, GhostBanners, PageLayoutWithSections, PATHS } from '@msb/shared';
import { Footer, GhostBanners, PageLayoutWithSections, PATHS, useUserAccounts } from '@msb/shared';
import { useHistory } from 'react-router-dom';
import { ADD_NEW_PRODUCT, banners } from '../constants';
import * as S from './MainPage.styles';
import { useUserAccounts } from '@/entities/FinancialAccount';
import { Balance } from '@/widgets/Balance';
import { FinancialDashboard } from '@/widgets/FinancialDashboard';
import { ProductCarousel } from '@/widgets/ProductCarousel';
@@ -1,4 +1,3 @@
export * from './getDaysLeft';
export * from './getFormattedBalance';
export * from './getFormattedDate';
export * from './sortByDate';
@@ -1,4 +1,4 @@
import type { FinancialAccountModel } from '@/entities/FinancialAccount';
import type { FinancialAccountModel } from '@msb/shared';
import type { BalanceCurrency, Currency } from '@/shared/model/balanceCurrency/types';
/**
@@ -1,13 +1,12 @@
import { useMemo, useState } from 'react';
import { Text, Title } from '@fractal-ui/styling';
import { MEDIA, useMediaQuery } from '@msb/shared';
import { type FinancialAccountModel, MEDIA, useMediaQuery } from '@msb/shared';
import { BALANCE_AD, BALANCE_STATISTICS } from '../../constants/balance';
import LOCALIZATION from '../../constants/localization';
import { getTotalBalance } from '../../lib';
import { BalanceAd } from '../BalanceAd';
import { BalanceStatistics } from '../BalanceStatistics';
import * as S from './Balance.styles';
import type { FinancialAccountModel } from '@/entities/FinancialAccount';
import { getFormattedBalance } from '@/shared/lib/getFormattedBalance/getFormattedBalance';
import { BalanceCurrencies } from '@/widgets/Balance/ui/BalanceCurrencies';
@@ -3,10 +3,10 @@ import type { MenuItemProps } from '@fractal-ui/composites';
import { ContextMenuBase } from '@fractal-ui/composites';
import { ButtonIcon } from '@fractal-ui/core';
import { ContextMenuIcon, WalletIcon } from '@fractal-ui/library';
import { useMediaQuery, MEDIA, PATHS } from '@msb/shared';
import { type FinancialAccountModel, useMediaQuery, MEDIA, PATHS } from '@msb/shared';
import { useHistory } from 'react-router-dom';
import LOCALIZATION from '../../../constants/localization';
import { FinancialAccount, type FinancialAccountModel } from '@/entities/FinancialAccount';
import { FinancialAccount } from '@/entities/FinancialAccount';
import { GhostAccount } from '@/entities/GhostAccount';
import { MAX_ACCOUNTS_IN_DASHBOARD_COUNT } from '@/shared/constants/maxAccountsInDashboardCount';
import { DashboardAccounts, NoWrapEllipsisButton } from '@/shared/ui/components';
@@ -1,7 +1,7 @@
import type { ReactElement } from 'react';
import { useState } from 'react';
import { SystemResponse } from '@fractal-ui/extended';
import { MEDIA, useMediaQuery } from '@msb/shared';
import { type FinancialAccountModel, MEDIA, useMediaQuery } from '@msb/shared';
import { CREDIT_ACCOUNTS, DEPOSIT_ACCOUNTS } from '../../constants/accounts';
import { FINANCE_CHIPS_OPTIONS } from '../../constants/constants';
import LOCALIZATION from '../../constants/localization';
@@ -13,7 +13,6 @@ import { FinancialDashboardSkeleton } from '../FinancialDashboardSkeleton';
import * as S from './FinancialDashboard.styles';
import type { CreditAccountModel } from '@/entities/CreditAccount';
import type { DepositAccountModel } from '@/entities/DepositAccount';
import type { FinancialAccountModel } from '@/entities/FinancialAccount';
interface Props {
financialAccounts: FinancialAccountModel[];