feat(TEAMMSBMOB-14856): добавлен виджет счетов

This commit is contained in:
Смышляев Ильдар
2025-06-19 17:35:24 +05:00
parent f0242b030d
commit 12b7df2a43
63 changed files with 1047 additions and 0 deletions
@@ -0,0 +1,49 @@
import { lightTheme, Text } from '@fractal-ui/styling';
import styled from 'styled-components';
export const Account = styled.div`
display: flex;
gap: 16px;
padding: 16px 0;
width: 100%;
`;
export const Main = styled.div`
width: 100%;
display: flex;
justify-content: space-between;
`;
export const Head = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
`;
export const Info = styled.div`
display: flex;
flex-direction: column;
gap: 2px;
`;
export const ExpireText = styled(Text.P2)`
color: ${lightTheme.colors.text.error};
`;
export const SubText = styled(Text.P2)`
opacity: 0.56;
`;
export const Body = styled.div`
display: flex;
flex-direction: column;
align-items: end;
gap: 2px;
`;
export const Card = styled.div`
display: flex;
align-items: center;
gap: 4px;
opacity: 0.56;
`;
@@ -0,0 +1,83 @@
import { useCallback, useMemo } from 'react';
import { Text, Title } from '@fractal-ui/styling';
import * as S from './CreditAccount.styles';
import CashBlue from './assets/Cash_blue.png';
import type { CreditAccountProps } from './types';
import { DashboardAccount } from '@/shared/components';
import { getDaysLeft } from '@/shared/models/getDaysLeft/getDaysLeft';
import { getDeclensionDay } from '@/shared/models/getDeclensionDay/getDeclensionDay';
import { getFormattedBalance } from '@/shared/models/getFormattedMoney/getFormattedBalance';
const CreditAccount = ({ creditAccount }: CreditAccountProps) => {
const formattedBalance = useMemo(() => getFormattedBalance(creditAccount.balance), [creditAccount.balance]);
const formattedAvailableMoney = useMemo(
() => (creditAccount.availableMoney ? getFormattedBalance(creditAccount.availableMoney) : null),
[creditAccount.availableMoney]
);
const formattedExpire = useMemo(
() =>
creditAccount.expire
? {
money: getFormattedBalance(creditAccount.expire.money),
daysLeft: creditAccount.expire.timestamp ? getDaysLeft(Date.now(), creditAccount.expire.timestamp) : null,
}
: null,
[creditAccount.expire]
);
const formattedTillDate = useMemo(() => {
const options: Intl.DateTimeFormatOptions = {
day: '2-digit',
month: '2-digit',
year: '2-digit',
};
return new Date(creditAccount.tillDateTimestamp).toLocaleDateString('ru-RU', options).replaceAll('/', '.');
}, [creditAccount.tillDateTimestamp]);
const renderExpire = useCallback(() => {
if (formattedExpire?.money && formattedExpire?.daysLeft) {
return (
<S.ExpireText>
Через {formattedExpire.daysLeft} {getDeclensionDay(formattedExpire.daysLeft)}, {formattedExpire.money}
</S.ExpireText>
);
}
if (formattedExpire?.money) {
return <S.ExpireText>Просрочено {formattedExpire.money}</S.ExpireText>;
}
if (creditAccount.availableMoney) {
return <S.SubText>Доступно {formattedAvailableMoney}</S.SubText>;
}
}, [formattedExpire, formattedAvailableMoney, creditAccount.availableMoney]);
return (
<DashboardAccount
bodyContent={
<>
<Text.P1>До {formattedTillDate}</Text.P1>
<S.SubText>{creditAccount.accountCode}</S.SubText>
</>
}
headContent={
<>
<S.Info>
<Text.P1>{creditAccount.title}</Text.P1>
<Title.H3Bold>{formattedBalance}</Title.H3Bold>
</S.Info>
<S.Info>
{creditAccount.organization && <S.SubText>{creditAccount.organization}</S.SubText>}
{renderExpire()}
</S.Info>
</>
}
icon={CashBlue}
/>
);
};
export default CreditAccount;
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

@@ -0,0 +1 @@
export { default } from './CreditAccount';
@@ -0,0 +1,17 @@
export interface CreditAccountProps {
creditAccount: CreditAccount;
}
export interface CreditAccount {
id: string;
title: string;
balance: number;
expire?: {
money: number;
timestamp?: number;
};
availableMoney?: number;
organization?: string;
tillDateTimestamp: number;
accountCode: string;
}
@@ -0,0 +1,39 @@
import { Text } from '@fractal-ui/styling';
import styled from 'styled-components';
export const Account = styled.div`
display: flex;
gap: 16px;
padding: 16px 0;
width: 100%;
`;
export const Main = styled.div`
width: 100%;
display: flex;
justify-content: space-between;
`;
export const Head = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
`;
export const SubText = styled(Text.P2)`
opacity: 0.56;
`;
export const Body = styled.div`
display: flex;
flex-direction: column;
align-items: end;
gap: 2px;
`;
export const Card = styled.div`
display: flex;
align-items: center;
gap: 4px;
opacity: 0.56;
`;
@@ -0,0 +1,35 @@
import { useMemo } from 'react';
import { Text, Title } from '@fractal-ui/styling';
import * as S from './DepositAccount.styles';
import SafeBlack from './assets/Safe_black.png';
import type { DepositAccountProps } from './types';
import { DashboardAccount } from '@/shared/components';
import { getFormattedDate } from '@/shared/models/getFormattedDate/getFormattedDate';
import { getFormattedBalance } from '@/shared/models/getFormattedMoney/getFormattedBalance';
const DepositAccount = ({ depositAccount }: DepositAccountProps) => {
const formattedBalance = useMemo(() => getFormattedBalance(depositAccount.balance), [depositAccount.balance]);
const formattedEndDate = useMemo(() => getFormattedDate(depositAccount.endDateTimestamp), [depositAccount.endDateTimestamp]);
const formattedPercent = useMemo(() => depositAccount.depositPercent.toString().replace('.', ','), [depositAccount.depositPercent]);
return (
<DashboardAccount
bodyContent={<Text.P1>{formattedPercent}%</Text.P1>}
headContent={
<>
<Text.P2>{depositAccount.title}</Text.P2>
<Title.H3Bold>{formattedBalance}</Title.H3Bold>
{depositAccount.organization && <S.SubText>{depositAccount.organization}</S.SubText>}
<S.SubText>
На {depositAccount.forHowManyDays} дней до {formattedEndDate}
</S.SubText>
</>
}
icon={SafeBlack}
/>
);
};
export default DepositAccount;
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

@@ -0,0 +1 @@
export { default } from './DepositAccount';
@@ -0,0 +1,13 @@
export interface DepositAccountProps {
depositAccount: DepositAccount;
}
export interface DepositAccount {
id: string;
title: string;
balance: number;
forHowManyDays: number;
endDateTimestamp: number;
depositPercent: number;
organization?: string;
}
@@ -0,0 +1,34 @@
import styled from 'styled-components';
export const Account = styled.div`
display: flex;
gap: 16px;
padding: 16px 0;
width: 100%;
`;
export const Main = styled.div`
width: 100%;
display: flex;
justify-content: space-between;
`;
export const Head = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
`;
export const Body = styled.div`
display: flex;
flex-direction: column;
align-items: end;
gap: 2px;
`;
export const Card = styled.div`
display: flex;
align-items: center;
gap: 4px;
opacity: 0.56;
`;
@@ -0,0 +1,35 @@
import { useMemo } from 'react';
import { CardIcon } from '@fractal-ui/library';
import { Text, Title } from '@fractal-ui/styling';
import * as S from './FinancialAccount.styles';
import WalletIcon from './assets/Wallet.png';
import type { AccountProps } from './types';
import { DashboardAccount } from '@/shared/components';
import { getFormattedBalance } from '@/shared/models/getFormattedMoney/getFormattedBalance';
const FinancialAccount = ({ financialAccount }: AccountProps) => {
const formattedBalance = useMemo(() => getFormattedBalance(financialAccount.balance), [financialAccount.balance]);
return (
<DashboardAccount
bodyContent={
<>
<Text.P1> {financialAccount.lastFourDigitsCard}</Text.P1>
<S.Card>
<Text.P3>{financialAccount.linkedCardsCount}</Text.P3>
<CardIcon />
</S.Card>
</>
}
headContent={
<>
<Text.P1>{financialAccount.title}</Text.P1>
<Title.H3Bold>{formattedBalance}</Title.H3Bold>
</>
}
icon={WalletIcon}
/>
);
};
export default FinancialAccount;
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

@@ -0,0 +1 @@
export { default } from './FinancialAccount';
@@ -0,0 +1,11 @@
export interface AccountProps {
financialAccount: FinancialAccount;
}
export interface FinancialAccount {
id: string;
title: string;
balance: number;
lastFourDigitsCard: number;
linkedCardsCount: number;
}
@@ -0,0 +1,45 @@
import { CrossIcon } from '@fractal-ui/library';
import styled from 'styled-components';
export const Account = styled.div`
display: flex;
gap: 16px;
padding: 16px 0;
width: 100%;
`;
export const Icon = styled.img`
width: 40px;
height: 40px;
`;
export const Main = styled.div`
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
`;
export const Head = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
`;
export const Body = styled.div`
display: flex;
flex-direction: column;
align-items: end;
gap: 2px;
`;
export const Card = styled.div`
display: flex;
align-items: center;
gap: 4px;
opacity: 0.56;
`;
export const Cross = styled(CrossIcon)`
opacity: 0.56;
`;
@@ -0,0 +1,22 @@
import { ButtonIcon } from '@fractal-ui/core';
import { Badge } from '@fractal-ui/extended';
import { Text } from '@fractal-ui/styling';
import * as S from './GhostAccount.styles';
import type { GhostAccountProps } from './types';
const GhostAccount = ({ onClose, icon, badgeText, text }: GhostAccountProps) => (
<S.Account>
<S.Icon src={icon} />
<S.Main>
<S.Head>
<Badge size="XS" type="info">
{badgeText}
</Badge>
<Text.P2>{text}</Text.P2>
</S.Head>
<ButtonIcon dataAction="close-ghost-account" icon={S.Cross} variant="ghost" onClick={onClose} />
</S.Main>
</S.Account>
);
export default GhostAccount;
@@ -0,0 +1 @@
export { default } from './GhostAccount';
@@ -0,0 +1,6 @@
export interface GhostAccountProps {
onClose(): void;
icon: string;
text: string;
badgeText: string;
}
@@ -0,0 +1,5 @@
export { default as FinancialAccount } from './financialAccount';
export { default as DepositAccount } from './depositAccount';
export { default as CreditAccount } from './creditAccount';
export { default as GhostAccount } from './ghostAccount';
export { default as PaymentAccountSkeleton } from './paymentAccountSkeleton';
@@ -0,0 +1,13 @@
import styled from 'styled-components';
export const AccountSkeleton = styled.div`
display: flex;
gap: 24px;
align-items: center;
height: 88px;
`;
export const FullWidthSkeleton = styled.div`
width: 48px;
height: 48px;
`;
@@ -0,0 +1,16 @@
import { Skeleton } from '@fractal-ui/core';
import * as S from './PaymentAccountSkeleton.styles';
const PaymentAccountSkeleton = () => (
<S.AccountSkeleton>
<S.FullWidthSkeleton>
<Skeleton dataName="skeleton" height={48} variant="circular" width={48} />
</S.FullWidthSkeleton>
<Skeleton dataName="skeleton" height={24} width="100%" />
<Skeleton dataName="skeleton" height={24} width={136} />
</S.AccountSkeleton>
);
export default PaymentAccountSkeleton;
@@ -0,0 +1 @@
export { default } from './PaymentAccountSkeleton';
@@ -0,0 +1,61 @@
import { useMemo, useState } from 'react';
import { Button } from '@fractal-ui/core';
import CashSilver from './assets/Cash_silver.png';
import { SYSTEM_RESPONSE } from './constants';
import type { CreditAccountsProps } from './types';
import CreditAccount from '@/entities/creditAccount';
import GhostAccount from '@/entities/ghostAccount';
import { DashboardAccounts } from '@/shared/components';
import { MAX_ACCOUNTS_IN_DASHBOARD_COUNT } from '@/shared/constants/maxAccountsInDashboardCount';
const CreditAccounts = ({ creditAccounts, hasError }: CreditAccountsProps) => {
const [isGhostAccountVisible, setIsGhostAccountVisible] = useState(true);
const handleCloseGhostAccount = () => {
setIsGhostAccountVisible(false);
};
const handleOpenCreditAccount = () => {
// TODO:
};
const Accounts = useMemo(
() => creditAccounts.slice(0, MAX_ACCOUNTS_IN_DASHBOARD_COUNT).map(credit => <CreditAccount key={credit.id} creditAccount={credit} />),
[creditAccounts]
);
const Buttons = (
<>
<Button fullWidth dataAction="create-payment" shape="default" variant="primary">
Оформить кредит
</Button>
<Button fullWidth dataAction="TODO" shape="default" variant="secondary">
Показать все
</Button>
</>
);
return (
<DashboardAccounts
accounts={Accounts}
buttons={Buttons}
emptyStateButtonText={SYSTEM_RESPONSE.BUTTON_OPEN_CREDIT}
emptyStateDescriptionText={SYSTEM_RESPONSE.DESCRIPTION_HERE_YOUR_CREDITS}
ghostAccount={
isGhostAccountVisible && (
<GhostAccount
badgeText="Кредит на сумму до 10 млн ₽ "
icon={CashSilver}
text="Перейти к оформлению"
onClose={handleCloseGhostAccount}
/>
)
}
hasError={hasError}
onClickButtonEmptyState={handleOpenCreditAccount}
/>
);
};
export default CreditAccounts;
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

@@ -0,0 +1,4 @@
export const SYSTEM_RESPONSE = {
DESCRIPTION_HERE_YOUR_CREDITS: 'Здесь будут ваши кредиты',
BUTTON_OPEN_CREDIT: 'Открыть кредит',
};
@@ -0,0 +1 @@
export { default } from './CreditAccounts';
@@ -0,0 +1,6 @@
import type { CreditAccount } from '@/entities/creditAccount/models/types';
export interface CreditAccountsProps {
creditAccounts: CreditAccount[];
hasError: boolean;
}
@@ -0,0 +1,64 @@
import { useMemo, useState } from 'react';
import { Button } from '@fractal-ui/core';
import SafeWhite from './assets/Safe_white.png';
import { SYSTEM_RESPONSE } from './constants';
import type { DepositAccountsProps } from './types';
import DepositAccount from '@/entities/depositAccount';
import GhostAccount from '@/entities/ghostAccount';
import { DashboardAccounts } from '@/shared/components';
import { MAX_ACCOUNTS_IN_DASHBOARD_COUNT } from '@/shared/constants/maxAccountsInDashboardCount';
const DepositAccounts = ({ depositAccounts, hasError }: DepositAccountsProps) => {
const [isGhostAccountVisible, setIsGhostAccountVisible] = useState(true);
const handleCloseGhostAccount = () => {
setIsGhostAccountVisible(false);
};
const handleOpenDepositAccount = () => {
// TODO:
};
const Accounts = useMemo(
() =>
depositAccounts
.slice(0, MAX_ACCOUNTS_IN_DASHBOARD_COUNT)
.map(deposit => <DepositAccount key={deposit.id} depositAccount={deposit} />),
[depositAccounts]
);
const Buttons = (
<>
<Button fullWidth dataAction="create-payment" shape="default" variant="primary">
Открыть продукт
</Button>
<Button fullWidth dataAction="TODO" shape="default" variant="secondary">
Показать все
</Button>
</>
);
return (
<DashboardAccounts
accounts={Accounts}
buttons={Buttons}
emptyStateButtonText={SYSTEM_RESPONSE.BUTTON_OPEN_DEPOSIT}
emptyStateDescriptionText={SYSTEM_RESPONSE.DESCRIPTION_HERE_YOUR_DEPOSITS}
ghostAccount={
isGhostAccountVisible && (
<GhostAccount
badgeText="Персональная ставка по депозиту до 26%"
icon={SafeWhite}
text="Перейти к оформлению"
onClose={handleCloseGhostAccount}
/>
)
}
hasError={hasError}
onClickButtonEmptyState={handleOpenDepositAccount}
/>
);
};
export default DepositAccounts;
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

@@ -0,0 +1,4 @@
export const SYSTEM_RESPONSE = {
DESCRIPTION_HERE_YOUR_DEPOSITS: 'Здесь будут ваши депозиты и МНО',
BUTTON_OPEN_DEPOSIT: 'Открыть продукт',
};
@@ -0,0 +1 @@
export { default } from './DepositAccounts';
@@ -0,0 +1,6 @@
import type { DepositAccount } from '@/entities/depositAccount/models/types';
export interface DepositAccountsProps {
depositAccounts: DepositAccount[];
hasError: boolean;
}
@@ -0,0 +1,82 @@
import { useState } from 'react';
import type { MenuItemProps } from '@fractal-ui/composites';
import { ContextMenuBase } from '@fractal-ui/composites';
import { Button, ButtonIcon } from '@fractal-ui/core';
import { ContextMenuIcon } from '@fractal-ui/library';
import NewIcon from './assets/New.png';
import { SYSTEM_RESPONSE } from './constants';
import type { FinancialAccountsProps } from './types';
import FinancialAccount from '@/entities/financialAccount';
import GhostAccount from '@/entities/ghostAccount';
import { DashboardAccounts } from '@/shared/components';
import { MAX_ACCOUNTS_IN_DASHBOARD_COUNT } from '@/shared/constants/maxAccountsInDashboardCount';
const FinancialAccounts = ({ financialAccounts, hasError }: FinancialAccountsProps) => {
const [isGhostAccountVisible, setIsGhostAccountVisible] = useState(true);
const handleCloseGhostAccount = () => {
setIsGhostAccountVisible(false);
};
const handleRenameAccount = () => {
// TODO:
};
const handleCloseAccount = () => {
// TODO:
};
const handleOpenAccount = () => {
// TODO:
};
const accountsContextItems: MenuItemProps[] = [
{ text: 'Переименовать счёт', onClick: handleRenameAccount },
{ text: 'Закрыть счёт', onClick: handleCloseAccount },
];
const accountsContextItemsWithOpen: MenuItemProps[] = [{ text: 'Открыть счёт', onClick: handleOpenAccount }, ...accountsContextItems];
const Accounts = financialAccounts
.slice(0, MAX_ACCOUNTS_IN_DASHBOARD_COUNT)
.map(account => <FinancialAccount key={account.id} financialAccount={account} />);
const Buttons = (
<>
<Button fullWidth dataAction="create-payment" shape="default" variant="primary">
Создать платёж
</Button>
<Button fullWidth dataAction="TODO" shape="default" variant="secondary">
{financialAccounts.length > MAX_ACCOUNTS_IN_DASHBOARD_COUNT ? 'Показать все' : 'Открыть счёт'}
</Button>
<ContextMenuBase<HTMLButtonElement>
items={financialAccounts.length > MAX_ACCOUNTS_IN_DASHBOARD_COUNT ? accountsContextItemsWithOpen : accountsContextItems}
size="M"
>
{({ ref, toggleOpen }) => (
<ButtonIcon ref={ref} dataAction="TODO" icon={ContextMenuIcon} shape="default" variant="secondary" onClick={toggleOpen} />
)}
</ContextMenuBase>
</>
);
return (
<DashboardAccounts
accounts={Accounts}
buttons={Buttons}
emptyStateButtonText={SYSTEM_RESPONSE.BUTTON_OPEN_ACCOUNT}
emptyStateDescriptionText={SYSTEM_RESPONSE.DESCRIPTION_HERE_YOUR_ACCOUNTS}
ghostAccount={
isGhostAccountVisible && (
<GhostAccount badgeText="Счёт для бизнеса за 0 ₽" icon={NewIcon} text="Подписать заявку" onClose={handleCloseGhostAccount} />
)
}
hasError={hasError}
onClickButtonEmptyState={handleOpenAccount}
/>
);
};
export default FinancialAccounts;
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

@@ -0,0 +1,4 @@
export const SYSTEM_RESPONSE = {
DESCRIPTION_HERE_YOUR_ACCOUNTS: 'Здесь будут ваши счета',
BUTTON_OPEN_ACCOUNT: 'Открыть счёт',
};
@@ -0,0 +1 @@
export { default } from './FinancialAccounts';
@@ -0,0 +1,6 @@
import type { FinancialAccount } from '@/entities/financialAccount/types';
export interface FinancialAccountsProps {
financialAccounts: FinancialAccount[];
hasError: boolean;
}
@@ -0,0 +1,10 @@
import styled from 'styled-components';
export const Wrapper = styled.div`
display: flex;
flex-direction: column;
`;
export const SkeletonWrapper = styled.div`
padding-top: 8px;
`;
@@ -0,0 +1,16 @@
import { Skeleton } from '@fractal-ui/core';
import * as S from './FinancialDashboardSkeleton.styles';
import PaymentAccountSkeleton from '@/entities/paymentAccountSkeleton';
const FinancialDashboardSkeleton = () => (
<S.Wrapper>
<S.SkeletonWrapper>
<Skeleton dataName="skeleton-tabs" height={24} mb="8px" width={136} />
</S.SkeletonWrapper>
<PaymentAccountSkeleton />
<PaymentAccountSkeleton />
<PaymentAccountSkeleton />
</S.Wrapper>
);
export default FinancialDashboardSkeleton;
@@ -0,0 +1 @@
export { default } from './FinancialDashboardSkeleton';
@@ -0,0 +1,4 @@
export { default as FinancialAccount } from './financialAccounts';
export { default as DepositAccounts } from './depositAccounts';
export { default as CreditAccounts } from './creditAccounts';
export { default as FinancialDashboardSkeleton } from './financialDashboardSkeleton';
@@ -0,0 +1,32 @@
import styled from 'styled-components';
export const Account = styled.div`
display: flex;
gap: 16px;
padding: 16px 0;
width: 100%;
`;
export const Icon = styled.img`
min-width: 40px;
height: 40px;
`;
export const Main = styled.div`
width: 100%;
display: flex;
justify-content: space-between;
`;
export const Head = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
`;
export const Body = styled.div`
display: flex;
flex-direction: column;
align-items: end;
gap: 2px;
`;
@@ -0,0 +1,14 @@
import * as S from './DashboardAccount.styles';
import type { DashboardAccountProps } from './types';
const DashboardAccount = ({ icon, headContent, bodyContent }: DashboardAccountProps) => (
<S.Account>
<S.Icon alt="icon" src={icon} />
<S.Main>
<S.Head>{headContent}</S.Head>
<S.Body>{bodyContent}</S.Body>
</S.Main>
</S.Account>
);
export default DashboardAccount;
@@ -0,0 +1 @@
export { default } from './DashboardAccount';
@@ -0,0 +1,5 @@
export interface DashboardAccountProps {
icon: string; // динамически импортированная картинка
headContent: React.ReactNode | React.ReactNode[];
bodyContent: React.ReactNode | React.ReactNode[];
}
@@ -0,0 +1,41 @@
import { Text } from '@fractal-ui/styling';
import styled from 'styled-components';
export const Wrapper = styled.div`
display: flex;
flex-direction: column;
`;
export const SystemResponseWrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 22px 0;
button {
margin-top: 24px !important;
}
div {
padding-bottom: 0;
}
`;
export const SystemWrapperText = styled(Text.P2)`
opacity: 0.56;
padding: 0 !important;
`;
export const Accounts = styled.div`
display: flex;
flex-direction: column;
`;
export const ButtonsGroup = styled.div`
margin-top: 24px;
margin-bottom: 40px;
display: flex;
gap: 8px;
`;
@@ -0,0 +1,45 @@
import { Children } from 'react';
import { SystemResponse } from '@fractal-ui/extended';
import * as S from './DashboardAccounts.styles';
import type { DashboardAccountsProps } from './types';
const DashboardAccounts = ({
accounts,
ghostAccount,
hasError,
buttons,
onClickButtonEmptyState,
emptyStateButtonText,
emptyStateDescriptionText,
}: DashboardAccountsProps) => {
if (Children.count(accounts) === 0) {
return (
<S.SystemResponseWrapper>
<SystemResponse
mainButtonProps={{
dataAction: 'update',
children: emptyStateButtonText,
variant: 'primary',
shape: 'default',
onClick: onClickButtonEmptyState,
}}
size="M"
statusIcon="empty"
text={(<S.SystemWrapperText>{emptyStateDescriptionText}</S.SystemWrapperText>) as unknown as string}
/>
</S.SystemResponseWrapper>
);
}
return (
<S.Wrapper>
<S.Accounts>
{accounts}
{ghostAccount}
</S.Accounts>
{!hasError && <S.ButtonsGroup>{buttons}</S.ButtonsGroup>}
</S.Wrapper>
);
};
export default DashboardAccounts;
@@ -0,0 +1 @@
export { default } from './DashboardAccounts';
@@ -0,0 +1,9 @@
export interface DashboardAccountsProps {
accounts: React.ReactNode | React.ReactNode[];
ghostAccount?: React.ReactNode;
hasError: boolean;
buttons: React.ReactNode | React.ReactNode[];
emptyStateDescriptionText: string;
emptyStateButtonText: string;
onClickButtonEmptyState(): void;
}
@@ -0,0 +1,2 @@
export { default as DashboardAccounts } from './DashboardAccounts';
export { default as DashboardAccount } from './DashboardAccount';
@@ -0,0 +1 @@
export const MAX_ACCOUNTS_IN_DASHBOARD_COUNT = 3;
@@ -0,0 +1,5 @@
export const getDaysLeft = (timestampStart: number, timestampEnd: number) => {
const diffInMs = timestampEnd - timestampStart;
return Math.floor(diffInMs / (1000 * 60 * 60 * 24));
};
@@ -0,0 +1,11 @@
export const getDeclensionDay = (day: number) => {
const absDay = Math.abs(day);
const absDayString = absDay.toString();
const lastNumber = Number(absDayString.charAt(absDayString.length - 1));
if (lastNumber === 1) return 'день';
if ([2, 3, 4].includes(lastNumber)) return 'дня';
return 'дней';
};
@@ -0,0 +1,9 @@
export const getFormattedDate = (timestamp: number) =>
// Пример: 25 июня 2025
new Intl.DateTimeFormat('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric',
})
.format(new Date(timestamp))
.replace('.', '');
@@ -0,0 +1,2 @@
export const getFormattedBalance = (money: number, currency: Intl.NumberFormatOptions['currency'] = 'RUB', maximumFractionDigits = 2) =>
new Intl.NumberFormat('ru-RU', { style: 'currency', currency, maximumFractionDigits }).format(money);
+9
View File
@@ -0,0 +1,9 @@
declare module '*.png' {
const value: any;
export = value;
}
declare module '*.svg' {
const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
export default content;
}
@@ -0,0 +1,47 @@
import { SettingsIcon } from '@fractal-ui/library';
import { Text } from '@fractal-ui/styling';
import styled from 'styled-components';
export const FinancialDashboard = styled.div`
border-radius: 16px;
padding: 24px;
padding-bottom: 0;
display: flex;
flex-direction: column;
gap: 24px;
background-color: var(--bg-primary);
min-height: 328px;
`;
export const Head = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
`;
export const SystemResponseWrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
button {
margin-top: 24px !important;
}
div {
padding-bottom: 0;
}
`;
export const SystemWrapperText = styled(Text.P2)`
opacity: 0.56;
padding: 0 !important;
`;
export const Settings = styled(SettingsIcon)`
opacity: 0.56;
cursor: pointer;
`;
@@ -0,0 +1,78 @@
import { useState } from 'react';
import { ChipsGroup } from '@fractal-ui/composites';
import { SystemResponse } from '@fractal-ui/extended';
import * as S from './FinancialDashboard.styles';
import { FINANCE_CHIPS_OPTIONS, SYSTEM_RESPONSE } from './constants';
import type { FinancialDashboardProps } from './types';
import CreditAccounts from '@/features/creditAccounts';
import DepositAccounts from '@/features/depositAccounts';
import FinancialAccounts from '@/features/financialAccounts';
import FinancialDashboardSkeleton from '@/features/financialDashboardSkeleton';
const FinancialDashboard = ({ financialAccounts, depositAccounts, creditAccounts }: FinancialDashboardProps) => {
const [selectedTab, setSelectedTab] = useState(FINANCE_CHIPS_OPTIONS[0].value);
const handleChangeTab = (value: string) => {
setSelectedTab(value);
};
const renderSelectedTab = (tab: string) => {
switch (tab) {
case 'accounts':
return <FinancialAccounts financialAccounts={financialAccounts} hasError={false} />;
case 'deposits':
return <DepositAccounts depositAccounts={depositAccounts} hasError={false} />;
case 'credit':
return <CreditAccounts creditAccounts={creditAccounts} hasError={false} />;
case 'acquiring':
return <>acquiring</>;
default:
return null;
}
};
// TODO: поменять тоглеры
const hasError = false;
const isLoading = false;
const handleClickUpdate = () => {
// TODO:
};
return (
<S.FinancialDashboard>
{isLoading ? (
<FinancialDashboardSkeleton />
) : (
<>
<S.Head>
<ChipsGroup name="finance" options={FINANCE_CHIPS_OPTIONS} value={selectedTab} onChange={handleChangeTab} />
<S.Settings size="L" />
</S.Head>
{hasError ? (
<S.SystemResponseWrapper>
<SystemResponse
mainButtonProps={{
dataAction: 'update',
children: SYSTEM_RESPONSE.BUTTON_UPDATE,
variant: 'blue',
shape: 'default',
onClick: handleClickUpdate,
}}
size="M"
statusIcon="error"
text={(<S.SystemWrapperText>{SYSTEM_RESPONSE.DESCRIPTION_DATA_NOT_LOAD}</S.SystemWrapperText>) as unknown as string}
/>
</S.SystemResponseWrapper>
) : (
renderSelectedTab(selectedTab)
)}
</>
)}
</S.FinancialDashboard>
);
};
export default FinancialDashboard;
@@ -0,0 +1,25 @@
import type { ChipsOption } from '@fractal-ui/composites/dist-types/chips-group/types';
export const FINANCE_CHIPS_OPTIONS: ChipsOption[] = [
{
label: 'Счета',
value: 'accounts',
},
{
label: 'Депозиты и МНО',
value: 'deposits',
},
{
label: 'Кредиты',
value: 'credit',
},
{
label: 'Эквайринг',
value: 'acquiring',
},
];
export const SYSTEM_RESPONSE = {
DESCRIPTION_DATA_NOT_LOAD: 'Данные не загрузились, попробуйте обновить страницу',
BUTTON_UPDATE: 'Обновить',
};
@@ -0,0 +1 @@
export { default } from './FinancialDashboard';
@@ -0,0 +1,9 @@
import type { CreditAccount } from '@/entities/creditAccount/types';
import type { DepositAccount } from '@/entities/depositAccount/types';
import type { FinancialAccount } from '@/entities/financialAccount/types';
export interface FinancialDashboardProps {
financialAccounts: FinancialAccount[];
depositAccounts: DepositAccount[];
creditAccounts: CreditAccount[];
}
@@ -0,0 +1 @@
export { default as FinancialDashboard } from './FinancialDashboard';