refactor: little structure refactor
This commit is contained in:
@@ -1,260 +1,28 @@
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useParams } from "react-router";
|
||||
import {
|
||||
accountTypeKey,
|
||||
type BrokerageAccount,
|
||||
} from "@/entities/brokerage-account";
|
||||
import type { Portfolio } from "@/entities/portfolio";
|
||||
import {
|
||||
formatMoney,
|
||||
formatSnapshotTime,
|
||||
portfolioErrorCode,
|
||||
usePortfolio,
|
||||
} from "@/entities/portfolio";
|
||||
import { useAuth } from "@/features/auth";
|
||||
import { StateView } from "@/shared/ui";
|
||||
import classes from "./AccountPage.module.css";
|
||||
|
||||
const accountsQueryKey = ["brokerage-accounts"] as const;
|
||||
AccountPageEmpty,
|
||||
AccountPageError,
|
||||
AccountPageLoaded,
|
||||
AccountPageLoading,
|
||||
} from "./AccountPage.view";
|
||||
import { useAccountPageViewModel } from "./AccountPage.vm";
|
||||
|
||||
export function AccountPage() {
|
||||
const { accountId } = useParams();
|
||||
const { accessToken } = useAuth();
|
||||
const portfolio = usePortfolio(accountId, accessToken);
|
||||
const account = useQueryClient()
|
||||
.getQueryData<BrokerageAccount[]>(accountsQueryKey)
|
||||
?.find((item) => item.id === accountId);
|
||||
|
||||
if (portfolio.isFetching) return <AccountPageLoading />;
|
||||
if (portfolio.isError)
|
||||
return (
|
||||
<AccountPageError
|
||||
code={portfolioErrorCode(portfolio.error)}
|
||||
retry={() => portfolio.refetch()}
|
||||
/>
|
||||
);
|
||||
if (!portfolio.data) return <AccountPageLoading />;
|
||||
if (!portfolio.data.positions.length && !portfolio.data.cashBalances.length)
|
||||
return <AccountPageEmpty account={account} />;
|
||||
return <AccountPageLoaded account={account} portfolio={portfolio.data} />;
|
||||
}
|
||||
|
||||
export function AccountPageLoading() {
|
||||
const { t } = useTranslation();
|
||||
const viewModel = useAccountPageViewModel();
|
||||
if (viewModel.state === "loading") return <AccountPageLoading />;
|
||||
if (viewModel.state === "error") return <AccountPageError {...viewModel} />;
|
||||
if (viewModel.state === "empty")
|
||||
return <AccountPageEmpty account={viewModel.account} />;
|
||||
return (
|
||||
<StateView
|
||||
loading
|
||||
title={t("states.loadingTitle")}
|
||||
text={t("states.loadingText")}
|
||||
<AccountPageLoaded
|
||||
account={viewModel.account}
|
||||
portfolio={viewModel.portfolio}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountPageError({
|
||||
code,
|
||||
retry,
|
||||
}: {
|
||||
code?: string;
|
||||
retry: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (code === "NOT_FOUND")
|
||||
return (
|
||||
<StateView
|
||||
error
|
||||
title={t("portfolio.accountUnavailableTitle")}
|
||||
text={t("portfolio.accountUnavailableText")}
|
||||
action={
|
||||
<Button component={Link} to="/accounts">
|
||||
{t("portfolio.back")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
if (code === "TBANK_NOT_CONNECTED" || code === "INVALID_TBANK_TOKEN")
|
||||
return (
|
||||
<StateView
|
||||
error
|
||||
title={t("portfolio.connectionErrorTitle")}
|
||||
text={t("portfolio.connectionErrorText")}
|
||||
action={
|
||||
<Button component={Link} to="/settings">
|
||||
{t("accounts.openSettings")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
if (code === "TBANK_UNAVAILABLE")
|
||||
return (
|
||||
<StateView
|
||||
error
|
||||
title={t("accounts.unavailableTitle")}
|
||||
text={t("accounts.unavailableText")}
|
||||
action={<Button onClick={retry}>{t("accounts.retry")}</Button>}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<StateView
|
||||
error
|
||||
title={t("states.errorTitle")}
|
||||
text={t("states.errorText")}
|
||||
action={<Button onClick={retry}>{t("accounts.retry")}</Button>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountPageEmpty({ account }: { account?: BrokerageAccount }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Stack>
|
||||
<AccountHeading account={account} />
|
||||
<StateView
|
||||
title={t("portfolio.emptyTitle")}
|
||||
text={t("portfolio.emptyText")}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountPageLoaded({
|
||||
account,
|
||||
portfolio,
|
||||
}: {
|
||||
account?: BrokerageAccount;
|
||||
portfolio: Portfolio;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const stocks = portfolio.positions.filter(
|
||||
(position) => position.type === "STOCK",
|
||||
);
|
||||
const bonds = portfolio.positions.filter(
|
||||
(position) => position.type === "BOND",
|
||||
);
|
||||
const otherPositions = portfolio.positions.filter(
|
||||
(position) => position.type !== "STOCK" && position.type !== "BOND",
|
||||
);
|
||||
return (
|
||||
<Stack className={classes.page} gap="lg">
|
||||
<AccountHeading account={account} />
|
||||
{portfolio.isStale ? (
|
||||
<Alert color="blue">{t("portfolio.staleWarning")}</Alert>
|
||||
) : null}
|
||||
<Text c="dimmed" size="sm">
|
||||
{t("portfolio.updatedAt", {
|
||||
date: formatSnapshotTime(portfolio.lastSuccessfulAt),
|
||||
})}
|
||||
</Text>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }}>
|
||||
{portfolio.totalsByCurrency.map((total) => (
|
||||
<Paper className={classes.total} key={total.currency} withBorder>
|
||||
<Text c="dimmed" size="xs">
|
||||
{total.currency}
|
||||
</Text>
|
||||
<Text fw={800} size="xl">
|
||||
{formatMoney(total)}
|
||||
</Text>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
{stocks.length ? (
|
||||
<PositionSection title={t("portfolio.stocks")} positions={stocks} />
|
||||
) : null}
|
||||
{bonds.length ? (
|
||||
<PositionSection title={t("portfolio.bonds")} positions={bonds} />
|
||||
) : null}
|
||||
{otherPositions.length ? (
|
||||
<PositionSection
|
||||
title={t("portfolio.otherPositions")}
|
||||
positions={otherPositions}
|
||||
/>
|
||||
) : null}
|
||||
<Section title={t("portfolio.cashBalances")}>
|
||||
{portfolio.cashBalances.map((balance) => (
|
||||
<Paper className={classes.cash} key={balance.currency} withBorder>
|
||||
<Text>{balance.currency}</Text>
|
||||
<Text fw={700}>{formatMoney(balance)}</Text>
|
||||
</Paper>
|
||||
))}
|
||||
</Section>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function PositionSection({
|
||||
title,
|
||||
positions,
|
||||
}: {
|
||||
title: string;
|
||||
positions: Portfolio["positions"];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Section title={title}>
|
||||
{positions.map((position) => (
|
||||
<Card
|
||||
className={classes.row}
|
||||
key={`${position.figi}-${position.displayName}-${position.quantity}`}
|
||||
withBorder
|
||||
>
|
||||
<Group justify="space-between" align="flex-start" wrap="nowrap">
|
||||
<Stack gap={2}>
|
||||
<Text fw={700}>{position.displayName}</Text>
|
||||
<Text c="dimmed" size="sm">
|
||||
{t("portfolio.quantity", { value: position.quantity })}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack gap={2} align="flex-end">
|
||||
<Text fw={700}>{formatMoney(position.currentValue)}</Text>
|
||||
<Text c="dimmed" size="sm">
|
||||
{t("portfolio.return", {
|
||||
value: formatMoney(position.absoluteReturn),
|
||||
})}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
function AccountHeading({ account }: { account?: BrokerageAccount }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Stack gap={2}>
|
||||
<Title order={1}>{account?.name ?? t("portfolio.title")}</Title>
|
||||
{account ? (
|
||||
<Text c="dimmed" size="sm">
|
||||
{t(accountTypeKey(account.type))}
|
||||
</Text>
|
||||
) : null}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function Section({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<Title order={2}>{title}</Title>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
export {
|
||||
AccountPageEmpty,
|
||||
AccountPageError,
|
||||
AccountPageLoaded,
|
||||
AccountPageLoading,
|
||||
} from "./AccountPage.view";
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
import type { BrokerageAccount } from "@/entities/brokerage-account";
|
||||
import { accountTypeKey } from "@/entities/brokerage-account";
|
||||
import type { Portfolio } from "@/entities/portfolio";
|
||||
import { formatMoney, formatSnapshotTime } from "@/entities/portfolio";
|
||||
import { StateView } from "@/shared/ui";
|
||||
import classes from "./AccountPage.module.css";
|
||||
|
||||
export function AccountPageLoading() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StateView
|
||||
loading
|
||||
title={t("states.loadingTitle")}
|
||||
text={t("states.loadingText")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export function AccountPageError({
|
||||
code,
|
||||
retry,
|
||||
}: {
|
||||
code?: string;
|
||||
retry: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (code === "NOT_FOUND")
|
||||
return (
|
||||
<StateView
|
||||
error
|
||||
title={t("portfolio.accountUnavailableTitle")}
|
||||
text={t("portfolio.accountUnavailableText")}
|
||||
action={
|
||||
<Button component={Link} to="/accounts">
|
||||
{t("portfolio.back")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
if (code === "TBANK_NOT_CONNECTED" || code === "INVALID_TBANK_TOKEN")
|
||||
return (
|
||||
<StateView
|
||||
error
|
||||
title={t("portfolio.connectionErrorTitle")}
|
||||
text={t("portfolio.connectionErrorText")}
|
||||
action={
|
||||
<Button component={Link} to="/settings">
|
||||
{t("accounts.openSettings")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
if (code === "TBANK_UNAVAILABLE")
|
||||
return (
|
||||
<StateView
|
||||
error
|
||||
title={t("accounts.unavailableTitle")}
|
||||
text={t("accounts.unavailableText")}
|
||||
action={<Button onClick={retry}>{t("accounts.retry")}</Button>}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<StateView
|
||||
error
|
||||
title={t("states.errorTitle")}
|
||||
text={t("states.errorText")}
|
||||
action={<Button onClick={retry}>{t("accounts.retry")}</Button>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export function AccountPageEmpty({ account }: { account?: BrokerageAccount }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Stack>
|
||||
<AccountHeading account={account} />
|
||||
<StateView
|
||||
title={t("portfolio.emptyTitle")}
|
||||
text={t("portfolio.emptyText")}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
export function AccountPageLoaded({
|
||||
account,
|
||||
portfolio,
|
||||
}: {
|
||||
account?: BrokerageAccount;
|
||||
portfolio: Portfolio;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const stocks = portfolio.positions.filter((p) => p.type === "STOCK");
|
||||
const bonds = portfolio.positions.filter((p) => p.type === "BOND");
|
||||
const other = portfolio.positions.filter(
|
||||
(p) => p.type !== "STOCK" && p.type !== "BOND",
|
||||
);
|
||||
return (
|
||||
<Stack className={classes.page} gap="lg">
|
||||
<AccountHeading account={account} />
|
||||
{portfolio.isStale ? (
|
||||
<Alert color="blue">{t("portfolio.staleWarning")}</Alert>
|
||||
) : null}
|
||||
<Text c="dimmed" size="sm">
|
||||
{t("portfolio.updatedAt", {
|
||||
date: formatSnapshotTime(portfolio.lastSuccessfulAt),
|
||||
})}
|
||||
</Text>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }}>
|
||||
{portfolio.totalsByCurrency.map((total) => (
|
||||
<Paper className={classes.total} key={total.currency} withBorder>
|
||||
<Text c="dimmed" size="xs">
|
||||
{total.currency}
|
||||
</Text>
|
||||
<Text fw={800} size="xl">
|
||||
{formatMoney(total)}
|
||||
</Text>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
{stocks.length ? (
|
||||
<PositionSection title={t("portfolio.stocks")} positions={stocks} />
|
||||
) : null}
|
||||
{bonds.length ? (
|
||||
<PositionSection title={t("portfolio.bonds")} positions={bonds} />
|
||||
) : null}
|
||||
{other.length ? (
|
||||
<PositionSection
|
||||
title={t("portfolio.otherPositions")}
|
||||
positions={other}
|
||||
/>
|
||||
) : null}
|
||||
<Section title={t("portfolio.cashBalances")}>
|
||||
{portfolio.cashBalances.map((balance) => (
|
||||
<Paper className={classes.cash} key={balance.currency} withBorder>
|
||||
<Text>{balance.currency}</Text>
|
||||
<Text fw={700}>{formatMoney(balance)}</Text>
|
||||
</Paper>
|
||||
))}
|
||||
</Section>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
function PositionSection({
|
||||
title,
|
||||
positions,
|
||||
}: {
|
||||
title: string;
|
||||
positions: Portfolio["positions"];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Section title={title}>
|
||||
{positions.map((position) => (
|
||||
<Card
|
||||
className={classes.row}
|
||||
key={`${position.figi}-${position.displayName}-${position.quantity}`}
|
||||
withBorder
|
||||
>
|
||||
<Group justify="space-between" align="flex-start" wrap="nowrap">
|
||||
<Stack gap={2}>
|
||||
<Text fw={700}>{position.displayName}</Text>
|
||||
<Text c="dimmed" size="sm">
|
||||
{t("portfolio.quantity", { value: position.quantity })}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack gap={2} align="flex-end">
|
||||
<Text fw={700}>{formatMoney(position.currentValue)}</Text>
|
||||
<Text c="dimmed" size="sm">
|
||||
{t("portfolio.return", {
|
||||
value: formatMoney(position.absoluteReturn),
|
||||
})}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
function AccountHeading({ account }: { account?: BrokerageAccount }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Stack gap={2}>
|
||||
<Title order={1}>{account?.name ?? t("portfolio.title")}</Title>
|
||||
{account ? (
|
||||
<Text c="dimmed" size="sm">
|
||||
{t(accountTypeKey(account.type))}
|
||||
</Text>
|
||||
) : null}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
function Section({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<Title order={2}>{title}</Title>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useParams } from "react-router";
|
||||
import type { BrokerageAccount } from "@/entities/brokerage-account";
|
||||
import { portfolioErrorCode, usePortfolio } from "@/entities/portfolio";
|
||||
import { useAuth } from "@/features/auth";
|
||||
|
||||
const accountsQueryKey = ["brokerage-accounts"] as const;
|
||||
|
||||
export function useAccountPageViewModel() {
|
||||
const { accountId } = useParams();
|
||||
const { accessToken } = useAuth();
|
||||
const portfolio = usePortfolio(accountId, accessToken);
|
||||
const account = useQueryClient()
|
||||
.getQueryData<BrokerageAccount[]>(accountsQueryKey)
|
||||
?.find((item) => item.id === accountId);
|
||||
if (portfolio.isError)
|
||||
return {
|
||||
state: "error" as const,
|
||||
code: portfolioErrorCode(portfolio.error),
|
||||
retry: () => portfolio.refetch(),
|
||||
};
|
||||
if (portfolio.isFetching || !portfolio.data)
|
||||
return { state: "loading" as const };
|
||||
if (!portfolio.data.positions.length && !portfolio.data.cashBalances.length)
|
||||
return { state: "empty" as const, account };
|
||||
return { state: "loaded" as const, account, portfolio: portfolio.data };
|
||||
}
|
||||
@@ -1,103 +1,21 @@
|
||||
import { Button, Stack, Title } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
import {
|
||||
BrokerageAccountList,
|
||||
errorCode,
|
||||
useBrokerageAccounts,
|
||||
} from "@/entities/brokerage-account";
|
||||
import { useAuth } from "@/features/auth";
|
||||
import { StateView } from "@/shared/ui";
|
||||
AccountsPageEmpty,
|
||||
AccountsPageError,
|
||||
AccountsPageLoaded,
|
||||
AccountsPageLoading,
|
||||
} from "./AccountsPage.view";
|
||||
import { useAccountsPageViewModel } from "./AccountsPage.vm";
|
||||
|
||||
export function AccountsPage() {
|
||||
const { t } = useTranslation();
|
||||
const { accessToken } = useAuth();
|
||||
const accounts = useBrokerageAccounts(accessToken);
|
||||
const code = errorCode(accounts.error);
|
||||
if (accounts.isFetching) return <AccountsPageLoading />;
|
||||
if (accounts.isError)
|
||||
return <AccountsPageError code={code} retry={() => accounts.refetch()} />;
|
||||
if (!accounts.data?.length) return <AccountsPageEmpty />;
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={1}>{t("accounts.title")}</Title>
|
||||
<BrokerageAccountList accounts={accounts.data} />
|
||||
</Stack>
|
||||
);
|
||||
const viewModel = useAccountsPageViewModel();
|
||||
if (viewModel.state === "loading") return <AccountsPageLoading />;
|
||||
if (viewModel.state === "error") return <AccountsPageError {...viewModel} />;
|
||||
if (viewModel.state === "empty") return <AccountsPageEmpty />;
|
||||
return <AccountsPageLoaded accounts={viewModel.accounts} />;
|
||||
}
|
||||
|
||||
export function AccountsPageLoading() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StateView
|
||||
loading
|
||||
title={t("states.loadingTitle")}
|
||||
text={t("states.loadingText")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountsPageError({
|
||||
code,
|
||||
retry,
|
||||
}: {
|
||||
code?: string;
|
||||
retry?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const settingsAction = (
|
||||
<Button component={Link} to="/settings" variant="light">
|
||||
{t("accounts.openSettings")}
|
||||
</Button>
|
||||
);
|
||||
if (code === "TBANK_NOT_CONNECTED")
|
||||
return (
|
||||
<StateView
|
||||
error
|
||||
title={t("accounts.notConnectedTitle")}
|
||||
text={t("accounts.notConnectedText")}
|
||||
action={settingsAction}
|
||||
/>
|
||||
);
|
||||
if (code === "INVALID_TBANK_TOKEN")
|
||||
return (
|
||||
<StateView
|
||||
error
|
||||
title={t("accounts.invalidTokenTitle")}
|
||||
text={t("accounts.invalidTokenText")}
|
||||
action={settingsAction}
|
||||
/>
|
||||
);
|
||||
if (code === "TBANK_UNAVAILABLE")
|
||||
return (
|
||||
<StateView
|
||||
error
|
||||
title={t("accounts.unavailableTitle")}
|
||||
text={t("accounts.unavailableText")}
|
||||
action={
|
||||
<Stack align="flex-start">
|
||||
<Button onClick={retry}>{t("accounts.retry")}</Button>
|
||||
{settingsAction}
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<StateView
|
||||
error
|
||||
title={t("states.errorTitle")}
|
||||
text={t("states.errorText")}
|
||||
action={<Button onClick={retry}>{t("accounts.retry")}</Button>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountsPageEmpty() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StateView
|
||||
title={t("accounts.emptyTitle")}
|
||||
text={t("accounts.emptyText")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export {
|
||||
AccountsPageEmpty,
|
||||
AccountsPageError,
|
||||
AccountsPageLoading,
|
||||
} from "./AccountsPage.view";
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Button, Stack, Title } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
import type { BrokerageAccount } from "@/entities/brokerage-account";
|
||||
import { BrokerageAccountList } from "@/entities/brokerage-account";
|
||||
import { StateView } from "@/shared/ui";
|
||||
|
||||
export function AccountsPageLoading() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StateView
|
||||
loading
|
||||
title={t("states.loadingTitle")}
|
||||
text={t("states.loadingText")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountsPageError({
|
||||
code,
|
||||
retry,
|
||||
}: {
|
||||
code?: string;
|
||||
retry?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const settingsAction = (
|
||||
<Button component={Link} to="/settings" variant="light">
|
||||
{t("accounts.openSettings")}
|
||||
</Button>
|
||||
);
|
||||
if (code === "TBANK_NOT_CONNECTED")
|
||||
return (
|
||||
<StateView
|
||||
error
|
||||
title={t("accounts.notConnectedTitle")}
|
||||
text={t("accounts.notConnectedText")}
|
||||
action={settingsAction}
|
||||
/>
|
||||
);
|
||||
if (code === "INVALID_TBANK_TOKEN")
|
||||
return (
|
||||
<StateView
|
||||
error
|
||||
title={t("accounts.invalidTokenTitle")}
|
||||
text={t("accounts.invalidTokenText")}
|
||||
action={settingsAction}
|
||||
/>
|
||||
);
|
||||
if (code === "TBANK_UNAVAILABLE")
|
||||
return (
|
||||
<StateView
|
||||
error
|
||||
title={t("accounts.unavailableTitle")}
|
||||
text={t("accounts.unavailableText")}
|
||||
action={
|
||||
<Stack align="flex-start">
|
||||
<Button onClick={retry}>{t("accounts.retry")}</Button>
|
||||
{settingsAction}
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<StateView
|
||||
error
|
||||
title={t("states.errorTitle")}
|
||||
text={t("states.errorText")}
|
||||
action={<Button onClick={retry}>{t("accounts.retry")}</Button>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountsPageEmpty() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StateView
|
||||
title={t("accounts.emptyTitle")}
|
||||
text={t("accounts.emptyText")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountsPageLoaded({
|
||||
accounts,
|
||||
}: {
|
||||
accounts: BrokerageAccount[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={1}>{t("accounts.title")}</Title>
|
||||
<BrokerageAccountList accounts={accounts} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { errorCode, useBrokerageAccounts } from "@/entities/brokerage-account";
|
||||
import { useAuth } from "@/features/auth";
|
||||
|
||||
export function useAccountsPageViewModel() {
|
||||
const { accessToken } = useAuth();
|
||||
const accounts = useBrokerageAccounts(accessToken);
|
||||
if (accounts.isFetching) return { state: "loading" as const };
|
||||
if (accounts.isError)
|
||||
return {
|
||||
state: "error" as const,
|
||||
code: errorCode(accounts.error),
|
||||
retry: () => accounts.refetch(),
|
||||
};
|
||||
if (!accounts.data?.length) return { state: "empty" as const };
|
||||
return { state: "loaded" as const, accounts: accounts.data };
|
||||
}
|
||||
@@ -8,22 +8,15 @@ export function NotFoundPage() {
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={1}>{t("notFound.title")}</Title>
|
||||
<NotFoundPageEmpty />
|
||||
<StateView
|
||||
title={t("states.emptyTitle")}
|
||||
text={t("states.emptyText")}
|
||||
action={
|
||||
<Button component={Link} to="/accounts">
|
||||
{t("notFound.action")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function NotFoundPageEmpty() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StateView
|
||||
title={t("states.emptyTitle")}
|
||||
text={t("states.emptyText")}
|
||||
action={
|
||||
<Button component={Link} to="/accounts">
|
||||
{t("notFound.action")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user