203 lines
5.7 KiB
TypeScript
203 lines
5.7 KiB
TypeScript
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<typeof readRetry>;
|
|
|
|
type AccountPageViewModel
|
|
= | AccountPageEmptyViewModel
|
|
| AccountPageLoadedViewModel
|
|
| { code?: string; readRetry: AccountPageReadRetry; state: "error" }
|
|
| { state: "loading" };
|
|
|
|
function useAccountPageVm(): 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<typeof selection>) =>
|
|
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 { useAccountPageVm };
|