import { useParams } from "react-router"; import type { BrokerageOperation } from "@/entities/broker-operation"; import type { Portfolio } from "@/entities/portfolio"; import { useBrokerageOperations } from "@/entities/broker-operation"; import { type BrokerageAccount, useBrokerageAccounts, } from "@/entities/brokerage-account"; import { type BrokerageEventsPreviewState, useBrokerageEvents, } from "@/entities/brokerage-event"; import { type IncomeSummary, useIncomeSummary } from "@/entities/income"; import { usePortfolio } from "@/entities/portfolio"; import { useStocks } from "@/entities/stock"; import { useScreenSync } from "@/features/tbank-connection"; import { apiErrorCode } from "@/shared/api"; import { type AccountPagePositionSection, toAccountPagePositionSections } from "../../model/positions"; const RECENT_EVENTS_LIMIT = 5; type AccountPageContent = { account?: BrokerageAccount; accountId?: string; incomeSummary: AccountPageIncomeSummary; partial?: boolean; recentEvents: AccountPageRecentEvents; recentOperations: AccountPageRecentOperations; refreshError: boolean; refreshing: boolean; retry: () => Promise | undefined; sourceError?: boolean; }; type AccountPageEmptyViewModel = AccountPageContent & { state: "empty"; }; type AccountPageIncomeSummary = | { lastSuccessfulAt?: null | string; refreshError?: boolean; stale?: boolean; state: "loaded"; summary: IncomeSummary; to: string; } | { retry: () => Promise | undefined; state: "error"; to: string } | { state: "empty" | "loading" | "partial"; to: string; }; type AccountPageLoadedViewModel = AccountPageContent & { portfolio: Portfolio; positionSections: AccountPagePositionSection[]; state: "loaded"; }; type AccountPageRecentEvents = BrokerageEventsPreviewState; type AccountPageRecentOperations = | { items: BrokerageOperation[]; lastSuccessfulAt?: null | string; refreshError?: boolean; stale?: boolean; state: "loaded" | "partial"; to: string; } | { state: "empty" | "error" | "loading"; to: string; }; type AccountPageViewModel = | AccountPageEmptyViewModel | AccountPageLoadedViewModel | { code?: string; retry: () => Promise | undefined; state: "error" } | { state: "loading" }; function toIncomeSummary( accountId: string | undefined, income: ReturnType, ): AccountPageIncomeSummary { const to = `/accounts/${accountId}/income`; if (!income.data && !income.isError && income.meta?.sync.state !== "failed") return { state: "loading", to }; if (income.meta?.availability === "partial") return { state: "partial", to }; if (!income.data && (income.isError || income.meta?.sync.state === "failed")) return { retry: () => income.refetch(), state: "error", to }; if (!income.data?.totalsByCurrency.length) return { state: "empty", to }; return { lastSuccessfulAt: income.meta?.lastSuccessfulAt, refreshError: income.isError || income.meta?.sync.state === "failed", stale: income.meta?.freshness === "stale", state: "loaded", summary: income.data, to, }; } function toRecentEvents( accountId: string | undefined, events: ReturnType, ): AccountPageRecentEvents { const to = `/accounts/${accountId}/events`; if (!events.data && !events.isError && events.meta?.sync.state !== "failed") return { state: "loading", to }; if (events.meta?.availability === "partial") return { items: events.data?.events.slice(0, RECENT_EVENTS_LIMIT) ?? [], state: "partial", to, }; if (!events.data && (events.isError || events.meta?.sync.state === "failed")) return { state: "error", to }; const items = events.data?.events.slice(0, RECENT_EVENTS_LIMIT) ?? []; if (items.length === 0) return { state: "empty", to }; return { items, lastSuccessfulAt: events.meta?.lastSuccessfulAt, refreshError: events.isError || events.meta?.sync.state === "failed", stale: events.meta?.freshness === "stale", state: "loaded", to, }; } function toRecentOperations( accountId: string | undefined, operations: ReturnType, ): AccountPageRecentOperations { const to = `/accounts/${accountId}/operations`; if ( !operations.data && !operations.isError && operations.meta?.sync.state !== "failed" ) return { state: "loading", to }; if (operations.meta?.availability === "partial") return { items: operations.data?.items ?? [], state: "partial", to }; if ( !operations.data && (operations.isError || operations.meta?.sync.state === "failed") ) return { state: "error", to }; if (!operations.data?.items.length) return { state: "empty", to }; return { items: operations.data.items, lastSuccessfulAt: operations.meta?.lastSuccessfulAt, refreshError: operations.isError || operations.meta?.sync.state === "failed", stale: operations.meta?.freshness === "stale", state: "loaded", to, }; } function useAccountPageViewModel(): AccountPageViewModel { const { accountId } = useParams(); const portfolio = usePortfolio(accountId); const stocks = useStocks( portfolio.data?.positions.flatMap(position => position.type === "STOCK" && position.instrumentUid ? [position.instrumentUid] : [], ) ?? [], ); const operations = useBrokerageOperations(accountId, { limit: 5, }); const events = useBrokerageEvents(accountId); const income = useIncomeSummary(accountId); const accounts = useBrokerageAccounts(); const sync = useScreenSync({ accountId, screen: "account" }, [ accounts, portfolio, operations, events, income, ]); if (!portfolio.data) { if (portfolio.isError || portfolio.meta?.sync.state === "failed") return { code: apiErrorCode(portfolio.error), retry: sync.retry, state: "error" as const, }; return { state: "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); const refresh = { partial: portfolio.meta?.availability === "partial", refreshError: portfolio.isError || sync.transportError || sync.sourceError, refreshing: portfolio.isFetching || sync.refreshing, retry: sync.retry, sourceError: sync.sourceError, }; if ( portfolio.meta?.availability === "complete" && portfolio.data.positions.length === 0 && portfolio.data.cashBalances.length === 0 ) return { account, accountId, incomeSummary, recentEvents, recentOperations, state: "empty" as const, ...refresh, }; return { account, accountId, incomeSummary, portfolio: portfolio.data, positionSections: toAccountPagePositionSections( portfolio.data, stocks, ), recentEvents, recentOperations, state: "loaded" as const, ...refresh, }; } export { type AccountPageIncomeSummary, type AccountPageRecentEvents, type AccountPageRecentOperations, useAccountPageViewModel, };