Files
frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts
T
ksv741 3c226f8c29
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 12s
Frontend verify / verify (push) Successful in 7m41s
feat: render hourly investment states
2026-10-09 21:13:34 +03:00

140 lines
3.7 KiB
TypeScript

import { useParams } from "react-router";
import type { Portfolio } from "@/entities/portfolio";
import type { InvestmentResourceMeta } from "@/shared/api";
import { useBrokerageOperations } from "@/entities/broker-operation";
import {
type BrokerageAccount,
useBrokerageAccounts,
} from "@/entities/brokerage-account";
import {
useBrokerageEvents,
} from "@/entities/brokerage-event";
import { useIncomeSummary } from "@/entities/income";
import { usePortfolio } from "@/entities/portfolio";
import { useScreenSync } from "@/features/tbank-connection";
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "@/shared/api";
import { useInitialAvailability } from "@/shared/lib";
import {
type AccountPagePositionSection,
toAccountPagePositionSections,
} from "../../model/positions";
import {
type AccountPageIncomeSummary,
type AccountPageRecentEvents,
type AccountPageRecentOperations,
toIncomeSummary,
toRecentEvents,
toRecentOperations,
} from "../../model/previews";
type AccountPageContent = {
account?: BrokerageAccount;
accountId?: string;
incomeSummary: AccountPageIncomeSummary;
partial?: boolean;
recentEvents: AccountPageRecentEvents;
recentOperations: AccountPageRecentOperations;
};
type AccountPageEmptyViewModel = AccountPageContent & {
state: "empty";
};
type AccountPageLoadedViewModel = AccountPageContent & {
portfolio: Portfolio;
positionSections: AccountPagePositionSection[];
source?: InvestmentResourceMeta;
state: "loaded";
};
type AccountPageViewModel
= | AccountPageEmptyViewModel
| AccountPageLoadedViewModel
| { code?: string; state: "error" }
| { state: "loading" };
function isPortfolioAccessFailure(code?: string) {
return isApiErrorCode(code, ApiErrorCode.Forbidden)
|| isApiErrorCode(code, ApiErrorCode.InvalidTbankToken)
|| isApiErrorCode(code, ApiErrorCode.NotFound)
|| isApiErrorCode(code, ApiErrorCode.TbankNotConnected);
}
function useAccountPageViewModel(): AccountPageViewModel {
const { accountId } = useParams();
const portfolio = usePortfolio(accountId);
const operations = useBrokerageOperations(accountId, {
limit: 5,
});
const events = useBrokerageEvents(accountId);
const income = useIncomeSummary(accountId);
const accounts = useBrokerageAccounts();
useScreenSync({ accountId, screen: "account" }, [
accounts,
portfolio,
operations,
events,
income,
]);
const code = apiErrorCode(portfolio.error);
const isAccessFailure = isPortfolioAccessFailure(code);
const availability = useInitialAvailability({
hasContent: !!portfolio.data,
identity: accountId ?? "account",
isError: portfolio.isError || portfolio.meta?.lastAttempt.state === "failed",
isFetching: portfolio.isFetching,
});
if (isAccessFailure) return { code, state: "error" as const };
if (!portfolio.data)
return {
state: availability === "unavailable" ? "error" : "loading",
} as const;
const account = accounts.data?.find(item => item.id === accountId);
const recentOperations = toRecentOperations(accountId, operations);
const recentEvents = toRecentEvents(accountId, events);
const incomeSummary = toIncomeSummary(accountId, income);
if (
portfolio.meta?.availability === "available"
&& portfolio.data.positions.length === 0
&& portfolio.data.cashBalances.length === 0
)
return {
account,
accountId,
incomeSummary,
recentEvents,
recentOperations,
state: "empty" as const,
};
const isPartial = portfolio.meta?.completeness === "incomplete";
return {
account,
accountId,
incomeSummary,
partial: isPartial,
portfolio: portfolio.data,
positionSections: toAccountPagePositionSections(portfolio.data),
recentEvents,
recentOperations,
source: portfolio.meta,
state: "loaded" as const,
};
}
export { useAccountPageViewModel };