feat(TEAMMSBMOB-14856): добавлен виджет счетов
This commit is contained in:
@@ -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';
|
||||
+13
@@ -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;
|
||||
}
|
||||
+10
@@ -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;
|
||||
`;
|
||||
+16
@@ -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';
|
||||
+32
@@ -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[];
|
||||
}
|
||||
+41
@@ -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);
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user