refactor: clarify frontend module ownership
Frontend verify / commits (pull_request) Failing after 1m19s
Frontend verify / security (pull_request) Skipped
Frontend verify / verify (pull_request) Skipped

This commit is contained in:
2026-09-29 07:07:36 +03:00
parent 7c28a423dd
commit f9f0f6921e
91 changed files with 1292 additions and 1050 deletions
@@ -1,6 +1,5 @@
import { useParams } from "react-router";
import type { BrokerageOperation } from "@/entities/broker-operation";
import type { Portfolio } from "@/entities/portfolio";
import { useBrokerageOperations } from "@/entities/broker-operation";
@@ -9,17 +8,25 @@ import {
useBrokerageAccounts,
} from "@/entities/brokerage-account";
import {
type BrokerageEventsPreviewState,
useBrokerageEvents,
} from "@/entities/brokerage-event";
import { type IncomeSummary, useIncomeSummary } from "@/entities/income";
import { useIncomeSummary } from "@/entities/income";
import { usePortfolio } from "@/entities/portfolio";
import { useScreenSync } from "@/features/tbank-connection";
import { apiErrorCode } from "@/shared/api";
import { type AccountPagePositionSection, toAccountPagePositionSections } from "../../model/positions";
const RECENT_EVENTS_LIMIT = 5;
import {
type AccountPagePositionSection,
toAccountPagePositionSections,
} from "../../model/positions";
import {
type AccountPageIncomeSummary,
type AccountPageRecentEvents,
type AccountPageRecentOperations,
toIncomeSummary,
toRecentEvents,
toRecentOperations,
} from "../../model/previews";
type AccountPageContent = {
account?: BrokerageAccount;
@@ -38,131 +45,18 @@ type AccountPageEmptyViewModel = AccountPageContent & {
state: "empty";
};
type AccountPageIncomeSummary
= | {
lastSuccessfulAt?: null | string;
refreshError?: boolean;
stale?: boolean;
state: "loaded";
summary: IncomeSummary;
to: string;
}
| { retry: () => Promise<unknown> | 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<unknown> | undefined; state: "error" }
| { state: "loading" };
function toIncomeSummary(
accountId: string | undefined,
income: ReturnType<typeof useIncomeSummary>,
): 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<typeof useBrokerageEvents>,
): 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<typeof useBrokerageOperations>,
): 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);
@@ -197,7 +91,10 @@ function useAccountPageViewModel(): AccountPageViewModel {
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 incomeSummary = toIncomeSummary(
accountId, income, () => income.refetch(),
);
const refresh = {
partial: portfolio.meta?.availability === "partial",
@@ -235,9 +132,4 @@ function useAccountPageViewModel(): AccountPageViewModel {
};
}
export {
type AccountPageIncomeSummary,
type AccountPageRecentEvents,
type AccountPageRecentOperations,
useAccountPageViewModel,
};
export { useAccountPageViewModel };