import { useState } from "react"; import { useTranslation } from "react-i18next"; 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, isBrokerageAccessLoss, readRetry } from "@/shared/api"; import { useInitialAvailability } from "@/shared/lib"; import { type Concentration, concentrationCurrencies, type ConcentrationLabels, type Slice, toConcentration, } from "../../model/concentration"; import { type AccountPagePositionSection, toAccountPagePositionSections, toUnrealizedReturns, } 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; readRetry: AccountPageReadRetry; recentEvents: AccountPageRecentEvents; recentOperations: AccountPageRecentOperations; source?: InvestmentResourceMeta; }; type AccountPageEmptyViewModel = AccountPageContent & { state: "empty"; }; type AccountPageLoadedViewModel = AccountPageContent & { concentration: Concentration | null; currency: null | string; onCurrencyChange: (currency: null | string) => void; onSliceChange: (slice: Slice) => void; onTabChange: (tab: null | string) => void; portfolio: Portfolio; positionSections: AccountPagePositionSection[]; slice: Slice; state: "loaded"; tab: string; unrealizedReturns: Portfolio["totalsByCurrency"]; }; type AccountPageReadRetry = ReturnType; type AccountPageViewModel = | AccountPageEmptyViewModel | AccountPageLoadedViewModel | { code?: string; readRetry: AccountPageReadRetry; state: "error" } | { state: "loading" }; function useAccountPageViewModel(): AccountPageViewModel { const { accountId } = useParams(); const { t } = useTranslation(); const [selection, setSelection] = useState<{ accountId?: string; currency: null | string; slice: Slice; tab: string; }>({ accountId, currency: null, slice: "stocks", tab: "positions" }); const current = selection.accountId === accountId ? selection : { accountId, currency: null, slice: "stocks" as const, tab: "positions", }; const updateSelection = (change: Partial) => setSelection({ ...current, ...change }); 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); // The portfolio drives this screen; the retry repeats only its GET. const retry = readRetry([portfolio]); const isAccessFailure = isBrokerageAccessLoss(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, readRetry: retry, state: "error" as const }; if (!portfolio.data) return availability === "unavailable" ? { code, readRetry: retry, state: "error" as const } : { 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); if ( portfolio.meta?.availability === "available" && portfolio.data.positions.length === 0 && portfolio.data.cashBalances.length === 0 ) return { account, accountId, incomeSummary, readRetry: retry, recentEvents, recentOperations, source: portfolio.meta, state: "empty" as const, }; const isPartial = portfolio.meta?.completeness === "incomplete"; const currencies = concentrationCurrencies(portfolio.data); const currency = current.currency && currencies.includes(current.currency) ? current.currency : currencies[0] ?? null; const labels: ConcentrationLabels = { missingIssuer: t("portfolio.concentration.missingIssuer"), missingSector: t("portfolio.concentration.missingSector"), other: t("portfolio.concentration.other"), sector: code => t(`stock.sectors.${code}`, { defaultValue: code }), }; const concentration = accountId !== "all" && current.tab === "concentration" ? toConcentration(portfolio.data, current.slice, currency, labels) : null; return { account, accountId, concentration, currency, incomeSummary, onCurrencyChange: value => updateSelection({ currency: value }), onSliceChange: slice => updateSelection({ slice }), onTabChange: tab => updateSelection({ tab: tab ?? "positions" }), partial: isPartial, portfolio: portfolio.data, positionSections: toAccountPagePositionSections(portfolio.data), readRetry: retry, recentEvents, recentOperations, slice: current.slice, source: portfolio.meta, state: "loaded" as const, tab: current.tab, unrealizedReturns: toUnrealizedReturns(portfolio.data.positions), }; } export { useAccountPageViewModel };