feat: add агегку events

This commit is contained in:
2026-08-14 15:50:25 +03:00
parent 719a6ed8b7
commit da55de5b50
13 changed files with 855 additions and 6 deletions
+5
View File
@@ -1,6 +1,7 @@
import { Navigate, Route, Routes, useLocation } from "react-router";
import { useAuth } from "@/features/auth";
import { AccountPage } from "@/pages/account";
import { AccountEventsPage } from "@/pages/account-events";
import { AccountOperationsPage } from "@/pages/account-operations";
import { AccountsPage } from "@/pages/accounts";
import { LoginPage } from "@/pages/login";
@@ -60,6 +61,10 @@ export function AppRoutes() {
>
<Route path="/accounts" element={<AccountsPage />} />
<Route path="/accounts/:accountId" element={<AccountPage />} />
<Route
path="/accounts/:accountId/events"
element={<AccountEventsPage />}
/>
<Route
path="/accounts/:accountId/operations"
element={<AccountOperationsPage />}
+28 -6
View File
@@ -38,9 +38,11 @@ function BreadcrumbTrail({ items }: { items: Crumb[] }) {
function AccountBreadcrumbs({
accountId,
events,
operations,
}: {
accountId: string;
events: boolean;
operations: boolean;
}) {
const { accessToken } = useAuth();
@@ -53,16 +55,22 @@ function AccountBreadcrumbs({
return (
<BreadcrumbTrail
items={
operations
events
? [
{ label: t("breadcrumbs.accounts"), to: "/accounts" },
{ label: accountName, to: accountPath },
{ label: t("breadcrumbs.operations") },
]
: [
{ label: t("breadcrumbs.accounts"), to: "/accounts" },
{ label: accountName },
{ label: t("breadcrumbs.events") },
]
: operations
? [
{ label: t("breadcrumbs.accounts"), to: "/accounts" },
{ label: accountName, to: accountPath },
{ label: t("breadcrumbs.operations") },
]
: [
{ label: t("breadcrumbs.accounts"), to: "/accounts" },
{ label: accountName },
]
}
/>
);
@@ -74,6 +82,10 @@ export function AppBreadcrumbs() {
path: "/accounts/:accountId/operations",
end: true,
});
const accountEvents = useMatch({
path: "/accounts/:accountId/events",
end: true,
});
const account = useMatch({ path: "/accounts/:accountId", end: true });
const accounts = useMatch({ path: "/accounts", end: true });
const settings = useMatch({ path: "/settings", end: true });
@@ -82,13 +94,23 @@ export function AppBreadcrumbs() {
return (
<AccountBreadcrumbs
accountId={accountOperations.params.accountId ?? ""}
events={false}
operations
/>
);
if (accountEvents)
return (
<AccountBreadcrumbs
accountId={accountEvents.params.accountId ?? ""}
events
operations={false}
/>
);
if (account)
return (
<AccountBreadcrumbs
accountId={account.params.accountId ?? ""}
events={false}
operations={false}
/>
);
@@ -0,0 +1,25 @@
import { useQuery } from "@tanstack/react-query";
import { apiQueryClient } from "@/shared/api";
import type { paths } from "@/shared/api/generated/schema";
type EventsEndpoint =
paths["/api/v1/brokerage-accounts/{accountId}/events"]["get"];
export type BrokerageEvent =
EventsEndpoint["responses"][200]["content"]["application/json"]["events"][number];
export function useBrokerageEvents(
accountId: string | undefined,
accessToken: string | null,
) {
return useQuery({
queryKey: ["brokerage-events", accountId ?? ""] as const,
enabled: Boolean(accountId && accessToken),
staleTime: 30_000,
retry: false,
...apiQueryClient.queryOptions(
"get",
"/api/v1/brokerage-accounts/{accountId}/events",
{ params: { path: { accountId: accountId ?? "" } } },
),
});
}
+4
View File
@@ -0,0 +1,4 @@
export {
type BrokerageEvent,
useBrokerageEvents,
} from "./api/useBrokerageEvents";
+1
View File
@@ -0,0 +1 @@
export { AccountEventsPage } from "./ui/AccountEventsPage";
@@ -0,0 +1,102 @@
import { MantineProvider } from "@mantine/core";
import { render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useBrokerageEvents } from "@/entities/brokerage-event";
import { AccountEventsPage } from "./AccountEventsPage";
vi.mock("@/entities/brokerage-event", () => ({
useBrokerageEvents: vi.fn(),
}));
vi.mock("@/features/auth", () => ({
useAuth: () => ({ accessToken: "token" }),
}));
const query = vi.mocked(useBrokerageEvents);
function renderPage() {
return render(
<MantineProvider>
<MemoryRouter>
<AccountEventsPage />
</MemoryRouter>
</MantineProvider>,
);
}
describe("AccountEventsPage", () => {
beforeEach(() => {
query.mockReturnValue({
data: {
accountId: "account",
calculatedAt: "2026-08-14T00:00:00Z",
events: [],
},
isError: false,
isLoading: false,
refetch: vi.fn(),
} as never);
});
it("renders events and marks an offer without an amount", () => {
query.mockReturnValue({
data: {
accountId: "account",
calculatedAt: "2026-08-14T00:00:00Z",
events: [
{
amount: "1250",
currency: "RUB",
date: "2026-08-20",
dateKind: "event",
displayName: "ОФЗ 26238",
figi: "FIGI",
instrumentUid: "UID",
ticker: "OFZ",
type: "coupon",
},
{
amount: null,
currency: null,
date: "2026-08-25",
dateKind: "event",
displayName: "ОФЗ 26238",
figi: "FIGI",
instrumentUid: "UID",
ticker: "OFZ",
type: "offer",
},
],
},
isError: false,
isLoading: false,
refetch: vi.fn(),
} as never);
renderPage();
expect(screen.getByText("1 250 ₽")).toBeInTheDocument();
expect(screen.getByText("Сумма неизвестна")).toBeInTheDocument();
});
it("renders empty and retryable error states", () => {
const refetch = vi.fn();
const { rerender } = renderPage();
expect(
screen.getByText("На ближайшие 30 дней событий не найдено"),
).toBeInTheDocument();
query.mockReturnValue({
data: undefined,
isError: true,
isLoading: false,
refetch,
} as never);
rerender(
<MantineProvider>
<MemoryRouter>
<AccountEventsPage />
</MemoryRouter>
</MantineProvider>,
);
screen.getByRole("button", { name: "Повторить" }).click();
expect(refetch).toHaveBeenCalledOnce();
});
});
@@ -0,0 +1,105 @@
import { Button, Card, Group, Stack, Text, Title } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router";
import {
type BrokerageEvent,
useBrokerageEvents,
} from "@/entities/brokerage-event";
import { formatMoney } from "@/entities/portfolio";
import { useAuth } from "@/features/auth";
import { StateView } from "@/shared/ui";
export function AccountEventsPage() {
const { accountId } = useParams();
const { accessToken } = useAuth();
const events = useBrokerageEvents(accountId, accessToken);
if (events.isLoading) return <AccountEventsPageLoading />;
if (events.isError)
return <AccountEventsPageError retry={() => events.refetch()} />;
if (!events.data.events.length) return <AccountEventsPageEmpty />;
return <AccountEventsPageLoaded events={events.data.events} />;
}
function AccountEventsPageLoading() {
const { t } = useTranslation();
return (
<StateView
loading
title={t("states.loadingTitle")}
text={t("states.loadingText")}
/>
);
}
function AccountEventsPageError({ retry }: { retry: () => void }) {
const { t } = useTranslation();
return (
<StateView
error
title={t("events.errorTitle")}
text={t("events.errorText")}
action={<Button onClick={retry}>{t("events.retry")}</Button>}
/>
);
}
function AccountEventsPageEmpty() {
const { t } = useTranslation();
return (
<Stack maw={960} gap="lg">
<Title order={1}>{t("events.title")}</Title>
<Text c="dimmed">{t("events.hint")}</Text>
<StateView title={t("events.emptyTitle")} text={t("events.emptyText")} />
</Stack>
);
}
function AccountEventsPageLoaded({ events }: { events: BrokerageEvent[] }) {
const { t } = useTranslation();
return (
<Stack maw={960} gap="lg">
<Title order={1}>{t("events.title")}</Title>
<Text c="dimmed">{t("events.hint")}</Text>
{events.map((event) => (
<AccountEventRow
event={event}
key={`${event.type}-${event.date}-${event.ticker}`}
/>
))}
</Stack>
);
}
function AccountEventRow({ event }: { event: BrokerageEvent }) {
const { t } = useTranslation();
return (
<Card withBorder>
<Group justify="space-between" align="flex-start" wrap="wrap">
<Stack gap={2}>
<Text fw={700}>{event.displayName}</Text>
<Text c="dimmed" size="sm">
{formatCalendarDate(event.date)} ·{" "}
{t(`events.dateKind.${event.dateKind}`)}
</Text>
</Stack>
<Stack gap={2} align="flex-end">
<Text fw={700}>
{event.amount && event.currency
? formatMoney({ currency: event.currency, value: event.amount })
: t("events.noAmount")}
</Text>
<Text c="dimmed" size="sm">
{t(`events.type.${event.type}`)}
</Text>
</Stack>
</Group>
</Card>
);
}
function formatCalendarDate(date: string) {
return new Intl.DateTimeFormat("ru-RU", {
dateStyle: "medium",
timeZone: "UTC",
}).format(new Date(`${date}T00:00:00Z`));
}
+22
View File
@@ -141,4 +141,26 @@ describe("AccountPage states", () => {
);
expect(screen.getByText("Вторая")).toBeInTheDocument();
});
it("keeps the portfolio visible when upcoming events fail", async () => {
server.use(
http.get("*/api/v1/brokerage-accounts/:accountId/events", () =>
HttpResponse.json({ code: "MOEX_UNAVAILABLE" }, { status: 503 }),
),
);
renderPage(
<AccountPageLoaded
accountId="account-1"
accessToken="token"
portfolio={portfolio}
/>,
);
await waitFor(() =>
expect(
screen.getByText("Не удалось загрузить будущие события."),
).toBeInTheDocument(),
);
expect(screen.getByText("Вторая")).toBeInTheDocument();
});
});
+55
View File
@@ -14,6 +14,10 @@ import { Link } from "react-router";
import { useBrokerageOperations } from "@/entities/broker-operation";
import type { BrokerageAccount } from "@/entities/brokerage-account";
import { accountTypeKey } from "@/entities/brokerage-account";
import {
type BrokerageEvent,
useBrokerageEvents,
} from "@/entities/brokerage-event";
import type { Portfolio } from "@/entities/portfolio";
import { formatMoney, formatSnapshotTime } from "@/entities/portfolio";
import { StateView } from "@/shared/ui";
@@ -99,6 +103,7 @@ export function AccountPageEmpty({
text={t("portfolio.emptyText")}
/>
<RecentOperations accountId={accountId} accessToken={accessToken} />
<RecentEvents accountId={accountId} accessToken={accessToken} />
</Stack>
);
}
@@ -166,6 +171,7 @@ export function AccountPageLoaded({
accountId={accountId}
accessToken={accessToken ?? null}
/>
<RecentEvents accountId={accountId} accessToken={accessToken ?? null} />
</Stack>
);
}
@@ -211,6 +217,55 @@ function RecentOperations({
</Section>
);
}
function RecentEvents({
accountId,
accessToken,
}: {
accountId: string | undefined;
accessToken: string | null;
}) {
const { t } = useTranslation();
const events = useBrokerageEvents(accountId, accessToken);
return (
<Section title={t("events.recentTitle")}>
{events.isLoading ? (
<Text c="dimmed">{t("events.recentLoading")}</Text>
) : null}
{events.isError ? (
<Alert color="red">{t("events.recentError")}</Alert>
) : null}
{events.data?.events.length === 0 ? (
<Text c="dimmed">{t("events.recentEmpty")}</Text>
) : null}
{events.data?.events.slice(0, 5).map((event) => (
<RecentEvent
key={`${event.type}-${event.date}-${event.ticker}`}
event={event}
/>
))}
<Button
component={Link}
to={`/accounts/${accountId}/events`}
variant="subtle"
>
{t("events.open")}
</Button>
</Section>
);
}
function RecentEvent({ event }: { event: BrokerageEvent }) {
const { t } = useTranslation();
return (
<Paper className={classes.cash} withBorder>
<Text>{`${t(`events.type.${event.type}`)} · ${event.displayName}`}</Text>
<Text fw={700}>
{event.amount && event.currency
? formatMoney({ currency: event.currency, value: event.amount })
: t("events.noAmount")}
</Text>
</Paper>
);
}
function PositionSection({
title,
positions,
+201
View File
@@ -148,6 +148,22 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/brokerage-accounts/{accountId}/events": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["UpcomingEventsController_get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
@@ -245,6 +261,26 @@ export interface components {
positions: components["schemas"]["PortfolioPositionDto"][];
cashBalances: components["schemas"]["MoneyDto"][];
};
UpcomingEventDto: {
/** @enum {string} */
type: "coupon" | "dividend" | "maturity" | "offer";
/** Format: date */
date: string;
/** @enum {string} */
dateKind: "event" | "registry";
figi: string | null;
instrumentUid: string | null;
ticker: string;
displayName: string;
currency: string | null;
amount: string | null;
};
UpcomingEventsDto: {
accountId: string;
/** Format: date-time */
calculatedAt: string;
events: components["schemas"]["UpcomingEventDto"][];
};
};
responses: never;
parameters: never;
@@ -311,6 +347,15 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Upstream service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
};
};
AuthController_login: {
@@ -370,6 +415,15 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Upstream service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
};
};
AuthController_refresh: {
@@ -425,6 +479,15 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Upstream service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
};
};
AuthController_logout: {
@@ -478,6 +541,15 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Upstream service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
};
};
AuthController_me: {
@@ -533,6 +605,15 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Upstream service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
};
};
TbankConnectionController_status: {
@@ -588,6 +669,15 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Upstream service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
};
};
TbankConnectionController_put: {
@@ -647,6 +737,15 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Upstream service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
};
};
TbankConnectionController_delete: {
@@ -700,6 +799,15 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Upstream service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
};
};
BrokerageAccountsController_list: {
@@ -755,6 +863,15 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Upstream service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
};
};
BrokerageOperationsController_get: {
@@ -818,6 +935,15 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Upstream service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
};
};
PortfolioController_get: {
@@ -875,6 +1001,81 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Upstream service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
};
};
UpcomingEventsController_get: {
parameters: {
query?: never;
header?: never;
path: {
accountId: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["UpcomingEventsDto"];
};
};
/** @description Invalid request */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Authentication required */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Resource not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Upstream service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
};
};
}
+276
View File
@@ -65,6 +65,16 @@
}
}
}
},
"503": {
"description": "Upstream service unavailable",
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsDto"
}
}
}
}
},
"tags": [
@@ -136,6 +146,16 @@
}
}
}
},
"503": {
"description": "Upstream service unavailable",
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsDto"
}
}
}
}
},
"tags": [
@@ -197,6 +217,16 @@
}
}
}
},
"503": {
"description": "Upstream service unavailable",
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsDto"
}
}
}
}
},
"tags": [
@@ -251,6 +281,16 @@
}
}
}
},
"503": {
"description": "Upstream service unavailable",
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsDto"
}
}
}
}
},
"tags": [
@@ -312,6 +352,16 @@
}
}
}
},
"503": {
"description": "Upstream service unavailable",
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsDto"
}
}
}
}
},
"security": [
@@ -378,6 +428,16 @@
}
}
}
},
"503": {
"description": "Upstream service unavailable",
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsDto"
}
}
}
}
},
"security": [
@@ -452,6 +512,16 @@
}
}
}
},
"503": {
"description": "Upstream service unavailable",
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsDto"
}
}
}
}
},
"security": [
@@ -509,6 +579,16 @@
}
}
}
},
"503": {
"description": "Upstream service unavailable",
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsDto"
}
}
}
}
},
"security": [
@@ -578,6 +658,16 @@
}
}
}
},
"503": {
"description": "Upstream service unavailable",
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsDto"
}
}
}
}
},
"security": [
@@ -709,6 +799,16 @@
}
}
}
},
"503": {
"description": "Upstream service unavailable",
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsDto"
}
}
}
}
},
"security": [
@@ -784,6 +884,101 @@
}
}
}
},
"503": {
"description": "Upstream service unavailable",
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsDto"
}
}
}
}
},
"security": [
{
"bearer": []
}
],
"tags": [
"brokerage-accounts"
]
}
},
"/api/v1/brokerage-accounts/{accountId}/events": {
"get": {
"operationId": "UpcomingEventsController_get",
"parameters": [
{
"name": "accountId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpcomingEventsDto"
}
}
}
},
"400": {
"description": "Invalid request",
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsDto"
}
}
}
},
"401": {
"description": "Authentication required",
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsDto"
}
}
}
},
"404": {
"description": "Resource not found",
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsDto"
}
}
}
},
"500": {
"description": "Internal server error",
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsDto"
}
}
}
},
"503": {
"description": "Upstream service unavailable",
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsDto"
}
}
}
}
},
"security": [
@@ -1194,6 +1389,87 @@
"positions",
"cashBalances"
]
},
"UpcomingEventDto": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"coupon",
"dividend",
"maturity",
"offer"
]
},
"date": {
"type": "string",
"format": "date"
},
"dateKind": {
"type": "string",
"enum": [
"event",
"registry"
]
},
"figi": {
"type": "string",
"nullable": true
},
"instrumentUid": {
"type": "string",
"nullable": true
},
"ticker": {
"type": "string"
},
"displayName": {
"type": "string"
},
"currency": {
"type": "string",
"nullable": true
},
"amount": {
"type": "string",
"nullable": true
}
},
"required": [
"type",
"date",
"dateKind",
"figi",
"instrumentUid",
"ticker",
"displayName",
"currency",
"amount"
]
},
"UpcomingEventsDto": {
"type": "object",
"properties": {
"accountId": {
"type": "string"
},
"calculatedAt": {
"type": "string",
"format": "date-time"
},
"events": {
"type": "array",
"items": {
"$ref": "#/components/schemas/UpcomingEventDto"
}
}
},
"required": [
"accountId",
"calculatedAt",
"events"
]
}
}
}
+24
View File
@@ -7,6 +7,7 @@ export const ru = {
accounts: "Брокерские счета",
accountFallback: "Счет",
operations: "Операции",
events: "Будущие события",
settings: "Настройки",
},
auth: {
@@ -98,6 +99,29 @@ export const ru = {
other: "Прочее",
},
},
events: {
title: "Будущие события",
open: "Открыть будущие события",
recentTitle: "Будущие события",
recentLoading: "Загрузка будущих событий",
recentError: "Не удалось загрузить будущие события.",
recentEmpty: "На ближайшие 30 дней событий не найдено.",
hint: "Прогноз рассчитан по текущей позиции до налогов. Покрытие зависит от доступных данных MOEX.",
emptyTitle: "На ближайшие 30 дней событий не найдено",
emptyText:
"Доступные источники не сообщили о купонах, дивидендах, погашениях или офертах в этом периоде.",
errorTitle: "Не удалось загрузить будущие события",
errorText: "Попробуйте запросить прогноз еще раз.",
retry: "Повторить",
noAmount: "Сумма неизвестна",
dateKind: { event: "Дата события", registry: "Дата реестра" },
type: {
coupon: "Купон",
dividend: "Дивиденд",
maturity: "Погашение",
offer: "Оферта",
},
},
settings: { title: "Настройки" },
tbank: {
title: "T-Bank Invest",
+7
View File
@@ -45,4 +45,11 @@ export const handlers = [
observedAt: "2026-08-14T00:00:00Z",
}),
),
http.get("*/api/v1/brokerage-accounts/:accountId/events", () =>
HttpResponse.json({
accountId: "account-1",
calculatedAt: "2026-08-14T00:00:00Z",
events: [],
}),
),
];