From da55de5b5008cce0ea24def664e13f3239174324 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Fri, 14 Aug 2026 15:46:23 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20add=20=D0=B0=D0=B3=D0=B5=D0=B3=D0=BA?= =?UTF-8?q?=D1=83=20events?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/routes/AppRoutes.tsx | 5 + src/app/ui/AppBreadcrumbs.tsx | 34 ++- .../brokerage-event/api/useBrokerageEvents.ts | 25 ++ src/entities/brokerage-event/index.ts | 4 + src/pages/account-events/index.ts | 1 + .../ui/AccountEventsPage.test.tsx | 102 +++++++ .../account-events/ui/AccountEventsPage.tsx | 105 +++++++ src/pages/account/ui/AccountPage.test.tsx | 22 ++ src/pages/account/ui/AccountPage.view.tsx | 55 ++++ src/shared/api/generated/schema.d.ts | 201 +++++++++++++ src/shared/api/spec/openapi.json | 276 ++++++++++++++++++ src/shared/i18n/locales/ru.ts | 24 ++ src/test/handlers.ts | 7 + 13 files changed, 855 insertions(+), 6 deletions(-) create mode 100644 src/entities/brokerage-event/api/useBrokerageEvents.ts create mode 100644 src/entities/brokerage-event/index.ts create mode 100644 src/pages/account-events/index.ts create mode 100644 src/pages/account-events/ui/AccountEventsPage.test.tsx create mode 100644 src/pages/account-events/ui/AccountEventsPage.tsx diff --git a/src/app/routes/AppRoutes.tsx b/src/app/routes/AppRoutes.tsx index e6ad30f..8c7bf2c 100644 --- a/src/app/routes/AppRoutes.tsx +++ b/src/app/routes/AppRoutes.tsx @@ -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() { > } /> } /> + } + /> } diff --git a/src/app/ui/AppBreadcrumbs.tsx b/src/app/ui/AppBreadcrumbs.tsx index ce314f4..d78cdbe 100644 --- a/src/app/ui/AppBreadcrumbs.tsx +++ b/src/app/ui/AppBreadcrumbs.tsx @@ -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 ( ); @@ -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 ( ); + if (accountEvents) + return ( + + ); if (account) return ( ); diff --git a/src/entities/brokerage-event/api/useBrokerageEvents.ts b/src/entities/brokerage-event/api/useBrokerageEvents.ts new file mode 100644 index 0000000..2a4f8eb --- /dev/null +++ b/src/entities/brokerage-event/api/useBrokerageEvents.ts @@ -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 ?? "" } } }, + ), + }); +} diff --git a/src/entities/brokerage-event/index.ts b/src/entities/brokerage-event/index.ts new file mode 100644 index 0000000..79b8b4e --- /dev/null +++ b/src/entities/brokerage-event/index.ts @@ -0,0 +1,4 @@ +export { + type BrokerageEvent, + useBrokerageEvents, +} from "./api/useBrokerageEvents"; diff --git a/src/pages/account-events/index.ts b/src/pages/account-events/index.ts new file mode 100644 index 0000000..73763c3 --- /dev/null +++ b/src/pages/account-events/index.ts @@ -0,0 +1 @@ +export { AccountEventsPage } from "./ui/AccountEventsPage"; diff --git a/src/pages/account-events/ui/AccountEventsPage.test.tsx b/src/pages/account-events/ui/AccountEventsPage.test.tsx new file mode 100644 index 0000000..efe7532 --- /dev/null +++ b/src/pages/account-events/ui/AccountEventsPage.test.tsx @@ -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( + + + + + , + ); +} + +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( + + + + + , + ); + screen.getByRole("button", { name: "Повторить" }).click(); + expect(refetch).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/pages/account-events/ui/AccountEventsPage.tsx b/src/pages/account-events/ui/AccountEventsPage.tsx new file mode 100644 index 0000000..3e9d861 --- /dev/null +++ b/src/pages/account-events/ui/AccountEventsPage.tsx @@ -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 ; + if (events.isError) + return events.refetch()} />; + if (!events.data.events.length) return ; + return ; +} + +function AccountEventsPageLoading() { + const { t } = useTranslation(); + return ( + + ); +} + +function AccountEventsPageError({ retry }: { retry: () => void }) { + const { t } = useTranslation(); + return ( + {t("events.retry")}} + /> + ); +} + +function AccountEventsPageEmpty() { + const { t } = useTranslation(); + return ( + + {t("events.title")} + {t("events.hint")} + + + ); +} + +function AccountEventsPageLoaded({ events }: { events: BrokerageEvent[] }) { + const { t } = useTranslation(); + return ( + + {t("events.title")} + {t("events.hint")} + {events.map((event) => ( + + ))} + + ); +} + +function AccountEventRow({ event }: { event: BrokerageEvent }) { + const { t } = useTranslation(); + return ( + + + + {event.displayName} + + {formatCalendarDate(event.date)} ·{" "} + {t(`events.dateKind.${event.dateKind}`)} + + + + + {event.amount && event.currency + ? formatMoney({ currency: event.currency, value: event.amount }) + : t("events.noAmount")} + + + {t(`events.type.${event.type}`)} + + + + + ); +} + +function formatCalendarDate(date: string) { + return new Intl.DateTimeFormat("ru-RU", { + dateStyle: "medium", + timeZone: "UTC", + }).format(new Date(`${date}T00:00:00Z`)); +} diff --git a/src/pages/account/ui/AccountPage.test.tsx b/src/pages/account/ui/AccountPage.test.tsx index 0377d4d..7beca37 100644 --- a/src/pages/account/ui/AccountPage.test.tsx +++ b/src/pages/account/ui/AccountPage.test.tsx @@ -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( + , + ); + + await waitFor(() => + expect( + screen.getByText("Не удалось загрузить будущие события."), + ).toBeInTheDocument(), + ); + expect(screen.getByText("Вторая")).toBeInTheDocument(); + }); }); diff --git a/src/pages/account/ui/AccountPage.view.tsx b/src/pages/account/ui/AccountPage.view.tsx index 3b031df..922d0d3 100644 --- a/src/pages/account/ui/AccountPage.view.tsx +++ b/src/pages/account/ui/AccountPage.view.tsx @@ -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")} /> + ); } @@ -166,6 +171,7 @@ export function AccountPageLoaded({ accountId={accountId} accessToken={accessToken ?? null} /> + ); } @@ -211,6 +217,55 @@ function RecentOperations({ ); } +function RecentEvents({ + accountId, + accessToken, +}: { + accountId: string | undefined; + accessToken: string | null; +}) { + const { t } = useTranslation(); + const events = useBrokerageEvents(accountId, accessToken); + return ( +
+ {events.isLoading ? ( + {t("events.recentLoading")} + ) : null} + {events.isError ? ( + {t("events.recentError")} + ) : null} + {events.data?.events.length === 0 ? ( + {t("events.recentEmpty")} + ) : null} + {events.data?.events.slice(0, 5).map((event) => ( + + ))} + +
+ ); +} +function RecentEvent({ event }: { event: BrokerageEvent }) { + const { t } = useTranslation(); + return ( + + {`${t(`events.type.${event.type}`)} · ${event.displayName}`} + + {event.amount && event.currency + ? formatMoney({ currency: event.currency, value: event.amount }) + : t("events.noAmount")} + + + ); +} function PositionSection({ title, positions, diff --git a/src/shared/api/generated/schema.d.ts b/src/shared/api/generated/schema.d.ts index 6ca14ee..b990e65 100644 --- a/src/shared/api/generated/schema.d.ts +++ b/src/shared/api/generated/schema.d.ts @@ -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; 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"]; + }; + }; }; }; } diff --git a/src/shared/api/spec/openapi.json b/src/shared/api/spec/openapi.json index 8052431..4a62ace 100644 --- a/src/shared/api/spec/openapi.json +++ b/src/shared/api/spec/openapi.json @@ -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" + ] } } } diff --git a/src/shared/i18n/locales/ru.ts b/src/shared/i18n/locales/ru.ts index 573ddd9..5897db7 100644 --- a/src/shared/i18n/locales/ru.ts +++ b/src/shared/i18n/locales/ru.ts @@ -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", diff --git a/src/test/handlers.ts b/src/test/handlers.ts index 6f5e5bc..38ddb49 100644 --- a/src/test/handlers.ts +++ b/src/test/handlers.ts @@ -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: [], + }), + ), ];