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:
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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: 'Счёт корпоративной карты',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from './ui';
|
||||
export * from './ui';
|
||||
export * from './types';
|
||||
export * from './context';
|
||||
export * from './model';
|
||||
|
||||
@@ -8,3 +8,4 @@ export * from './useElementHeight';
|
||||
export * from './useRefetchData';
|
||||
export * from './getFormattedBalance';
|
||||
export * from './files';
|
||||
export * from './sortByDate';
|
||||
|
||||
@@ -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 };
|
||||
+9
-2
@@ -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
@@ -1,3 +1,4 @@
|
||||
export * from './types';
|
||||
export * from './useUserAccounts';
|
||||
export * from './getMappedAccountsToUI';
|
||||
export * from './accountTypeNames';
|
||||
+2
-1
@@ -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;
|
||||
+2
-1
@@ -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 }),
|
||||
|
||||
+9
@@ -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 };
|
||||
|
||||
+41
@@ -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 };
|
||||
+147
@@ -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';
|
||||
|
||||
|
||||
+2
-2
@@ -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
-2
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user