From ed0b5388ab148ebf58048beda61022bbee0ab3aa Mon Sep 17 00:00:00 2001 From: codex-bot Date: Sat, 7 Nov 2026 10:50:51 +0300 Subject: [PATCH 1/2] fix: unify history and upcoming events labels (#161) (#163) --- src/app/routes/AppRoutes/AllAccounts.test.tsx | 8 +-- .../ui/AppBreadcrumbs/AppBreadcrumbs.test.tsx | 6 +-- .../AccountEventsPage.filters.test.tsx | 2 +- .../AccountEventsPage.test.tsx | 4 +- .../AccountOperationsPage.test.tsx | 8 +-- .../ui/AccountPage/AccountPage.test.tsx | 34 ++++++------- src/pages/bond/ui/BondPage/BondPage.test.tsx | 2 +- .../stock/ui/StockPage/StockPage.test.tsx | 2 +- src/shared/i18n/locales/ru.ts | 50 +++++++++---------- 9 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/app/routes/AppRoutes/AllAccounts.test.tsx b/src/app/routes/AppRoutes/AllAccounts.test.tsx index c2e8452..65c3258 100644 --- a/src/app/routes/AppRoutes/AllAccounts.test.tsx +++ b/src/app/routes/AppRoutes/AllAccounts.test.tsx @@ -227,9 +227,9 @@ beforeEach(() => { describe("All accounts routes", () => { it.each([ ["/accounts/all", "Все счета"], - ["/accounts/all/operations", "Журнал операций"], + ["/accounts/all/operations", "История операций"], ["/accounts/all/income?from=2026-08-01&to=2026-08-31", "Пассивный доход"], - ["/accounts/all/events", "Будущие события"], + ["/accounts/all/events", "Предстоящие события"], ])("opens %s directly", async (path, title) => { renderPage(path); expect( @@ -311,9 +311,9 @@ describe("All accounts routes", () => { ); for (const [suffix, tab] of [ - ["operations", "Операции"], + ["operations", "История операций"], ["income", "Доход"], - ["events", "События"], + ["events", "Предстоящие события"], ]) { fireEvent.click(screen.getByRole("tab", { name: tab })); expect(screen.getByRole("link", { name: "Подробнее" })) diff --git a/src/app/ui/AppBreadcrumbs/AppBreadcrumbs.test.tsx b/src/app/ui/AppBreadcrumbs/AppBreadcrumbs.test.tsx index e08231d..acc9bbe 100644 --- a/src/app/ui/AppBreadcrumbs/AppBreadcrumbs.test.tsx +++ b/src/app/ui/AppBreadcrumbs/AppBreadcrumbs.test.tsx @@ -55,9 +55,9 @@ describe("AppBreadcrumbs", () => { }); it.each([ - ["operations", "Операции"], - ["OPERATIONS", "Операции"], - ["events", "Будущие события"], + ["operations", "История операций"], + ["OPERATIONS", "История операций"], + ["events", "Предстоящие события"], ["income", "Пассивный доход"], ])("links to the account hierarchy from %s", (section, title) => { renderBreadcrumbs(`/accounts/account-1/${section}`, [ diff --git a/src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.filters.test.tsx b/src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.filters.test.tsx index 33cd2d6..25566ce 100644 --- a/src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.filters.test.tsx +++ b/src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.filters.test.tsx @@ -330,7 +330,7 @@ describe("Upcoming event date filters", () => { expect(screen.getByLabelText("URL")).toBeEmptyDOMElement(); expect(screen.getByRole("button", { name: "Применить" })).toBeDisabled(); expect( - screen.getByText("За выбранный период событий не найдено"), + screen.getByText("На выбранный период предстоящих событий не найдено"), ).toBeVisible(); }, ); diff --git a/src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.test.tsx b/src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.test.tsx index 3d32729..91850fb 100644 --- a/src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.test.tsx +++ b/src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.test.tsx @@ -174,7 +174,7 @@ describe("AccountEventsPage", () => { it("renders empty and error states with retry", async () => { const { rerender } = renderPage(); expect( - screen.getByText("За выбранный период событий не найдено"), + screen.getByText("На выбранный период предстоящих событий не найдено"), ).toBeInTheDocument(); query.mockReturnValue({ data: undefined, @@ -239,7 +239,7 @@ describe("AccountEventsPage", () => { "true", ); expect( - screen.queryByText("За выбранный период событий не найдено"), + screen.queryByText("На выбранный период предстоящих событий не найдено"), ).not.toBeInTheDocument(); }); diff --git a/src/pages/account-operations/ui/AccountOperationsPage/AccountOperationsPage.test.tsx b/src/pages/account-operations/ui/AccountOperationsPage/AccountOperationsPage.test.tsx index d43def2..7224ca8 100644 --- a/src/pages/account-operations/ui/AccountOperationsPage/AccountOperationsPage.test.tsx +++ b/src/pages/account-operations/ui/AccountOperationsPage/AccountOperationsPage.test.tsx @@ -440,7 +440,7 @@ describe("AccountOperationsPage", () => { renderPage(); expect( - await screen.findByRole("heading", { name: "Журнал операций" }), + await screen.findByRole("heading", { name: "История операций" }), ).toBeInTheDocument(); expect( screen.queryByRole("link", { name: "К счету" }), @@ -482,7 +482,7 @@ describe("AccountOperationsPage", () => { renderPage(); expect( - await screen.findByRole("heading", { name: "Журнал операций" }), + await screen.findByRole("heading", { name: "История операций" }), ).toBeInTheDocument(); await waitFor(() => expect(searches.length).toBeGreaterThan(0)); const countBefore = searches.length; @@ -521,7 +521,7 @@ describe("AccountOperationsPage", () => { renderPage(); expect( - await screen.findByRole("heading", { name: "Журнал операций" }), + await screen.findByRole("heading", { name: "История операций" }), ).toBeInTheDocument(); fireEvent.click(screen.getByRole("combobox", { name: "Категории" })); fireEvent.click(await screen.findByText("Налог")); @@ -579,7 +579,7 @@ describe("AccountOperationsPage", () => { ); renderPage(); expect( - await screen.findByText("Не удалось загрузить журнал", undefined, { + await screen.findByText("Не удалось загрузить историю операций", undefined, { timeout: RETRIES_EXHAUSTED_MS, }), ).toBeInTheDocument(); diff --git a/src/pages/account/ui/AccountPage/AccountPage.test.tsx b/src/pages/account/ui/AccountPage/AccountPage.test.tsx index 82bc249..1a6797f 100644 --- a/src/pages/account/ui/AccountPage/AccountPage.test.tsx +++ b/src/pages/account/ui/AccountPage/AccountPage.test.tsx @@ -850,9 +850,9 @@ describe("AccountPage states", () => { expect( screen.getByRole("heading", { name: "Портфель пока пуст" }), ).toBeInTheDocument(); - expect(screen.getByRole("link", { name: "Открыть журнал" })) + expect(screen.getByRole("link", { name: "Открыть историю операций" })) .toBeInTheDocument(); - expect(screen.getByRole("link", { name: "Открыть будущие события" })) + expect(screen.getByRole("link", { name: "Открыть предстоящие события" })) .toBeInTheDocument(); expect(screen.getByRole("link", { name: "Открыть пассивный доход" })) .toBeInTheDocument(); @@ -893,10 +893,10 @@ describe("AccountPage states", () => { }), ).toBeVisible(); expect( - screen.getByRole("status", { name: "Загрузка будущих событий" }), + screen.getByRole("status", { name: "Загрузка предстоящих событий" }), ).toBeVisible(); expect( - screen.queryByText("На ближайшие 30 дней событий не найдено."), + screen.queryByText("На ближайшие 30 дней предстоящих событий не найдено."), ).not.toBeInTheDocument(); expect(screen.getByText("Загрузка дохода текущего месяца")).toBeVisible(); expect(screen.queryByText("Нет текущих сумм")).not.toBeInTheDocument(); @@ -1076,7 +1076,7 @@ describe("AccountPage states", () => { />, ); - fireEvent.click(screen.getByRole("tab", { name: "Операции" })); + fireEvent.click(screen.getByRole("tab", { name: "История операций" })); expect( screen.getByText("Пока недоступно"), ).toBeInTheDocument(); @@ -1115,7 +1115,7 @@ describe("AccountPage states", () => { />, ); - fireEvent.click(screen.getByRole("tab", { name: "Операции" })); + fireEvent.click(screen.getByRole("tab", { name: "История операций" })); expect(screen.getByText("Сделка")).toBeInTheDocument(); expect(screen.getByText("−12 ₽")).toBeInTheDocument(); }); @@ -1169,8 +1169,8 @@ describe("AccountPage states", () => { />, ); - fireEvent.click(screen.getByRole("tab", { name: "События" })); - const section = screen.getByRole("tabpanel", { name: "События" }); + fireEvent.click(screen.getByRole("tab", { name: "Предстоящие события" })); + const section = screen.getByRole("tabpanel", { name: "Предстоящие события" }); const content = within(section); const text = section.textContent ?? ""; @@ -1211,15 +1211,15 @@ describe("AccountPage states", () => { />, ); - fireEvent.click(screen.getByRole("tab", { name: "События" })); + fireEvent.click(screen.getByRole("tab", { name: "Предстоящие события" })); const content = within( - screen.getByRole("tabpanel", { name: "События" }), + screen.getByRole("tabpanel", { name: "Предстоящие события" }), ); if (state === "loading") { const skeleton = content.getByRole("status", { - name: "Загрузка будущих событий", + name: "Загрузка предстоящих событий", }); // eslint-disable-next-line vitest/no-conditional-expect -- The fixed table case selects the expected outcome; every case asserts its own branch. @@ -1230,7 +1230,7 @@ describe("AccountPage states", () => { else { // eslint-disable-next-line vitest/no-conditional-expect -- The table case deterministically selects the empty state. expect( - content.getByText("На ближайшие 30 дней событий не найдено."), + content.getByText("На ближайшие 30 дней предстоящих событий не найдено."), ).toBeVisible(); } @@ -1251,7 +1251,7 @@ describe("AccountPage states", () => { />, ); - fireEvent.click(screen.getByRole("tab", { name: "События" })); + fireEvent.click(screen.getByRole("tab", { name: "Предстоящие события" })); expect( screen.getByText("Данные появятся здесь, когда чтение восстановится."), ).toBeInTheDocument(); @@ -1522,8 +1522,8 @@ describe("AccountPage states", () => { .toBeVisible(); expect(screen.queryByText("Вторая")).not.toBeInTheDocument(); - fireEvent.click(screen.getByRole("tab", { name: "Операции" })); - expect(screen.getByText("Последних событий пока нет.")).toBeVisible(); + fireEvent.click(screen.getByRole("tab", { name: "История операций" })); + expect(screen.getByText("Последних операций пока нет.")).toBeVisible(); }); it("links operation, event and income previews to their detail pages", () => { @@ -1538,8 +1538,8 @@ describe("AccountPage states", () => { ); for (const [tab, href] of [ - ["Операции", "/accounts/account-1/operations"], - ["События", "/accounts/account-1/events"], + ["История операций", "/accounts/account-1/operations"], + ["Предстоящие события", "/accounts/account-1/events"], ["Доход", "/accounts/account-1/income"], ]) { fireEvent.click(screen.getByRole("tab", { name: tab })); diff --git a/src/pages/bond/ui/BondPage/BondPage.test.tsx b/src/pages/bond/ui/BondPage/BondPage.test.tsx index caf8345..0cc2c40 100644 --- a/src/pages/bond/ui/BondPage/BondPage.test.tsx +++ b/src/pages/bond/ui/BondPage/BondPage.test.tsx @@ -351,7 +351,7 @@ describe("bond page", () => { screen.queryByRole("heading", { name: "Моя позиция" }), ).not.toBeInTheDocument(); expect( - screen.getByRole("heading", { name: "Моя история событий" }), + screen.getByRole("heading", { name: "История операций" }), ).toBeVisible(); }); it( diff --git a/src/pages/stock/ui/StockPage/StockPage.test.tsx b/src/pages/stock/ui/StockPage/StockPage.test.tsx index e66a4a3..a64e6b3 100644 --- a/src/pages/stock/ui/StockPage/StockPage.test.tsx +++ b/src/pages/stock/ui/StockPage/StockPage.test.tsx @@ -490,7 +490,7 @@ describe("stock page", () => { fireEvent.click(screen.getByRole("button", { name: "Далее" })); controls.recoverOperations(); expect(screen.queryByRole("button", { name: "Повторить" })).not.toBeInTheDocument(); - expect(screen.getByRole("heading", { name: "Моя история событий" })).toBeVisible(); + expect(screen.getByRole("heading", { name: "История операций" })).toBeVisible(); }, ); it("keeps the last quote without manual refresh", async () => { diff --git a/src/shared/i18n/locales/ru.ts b/src/shared/i18n/locales/ru.ts index e6583d2..a76db70 100644 --- a/src/shared/i18n/locales/ru.ts +++ b/src/shared/i18n/locales/ru.ts @@ -274,7 +274,7 @@ const ru = { COUPON_TYPE_VARIABLE: "Переменный", }, couponWindow: - "Календарь выпуска с {{from}} по {{to}}. Личные зачисления показаны в истории событий.", + "Календарь выпуска с {{from}} по {{to}}. Полученные выплаты показаны в истории операций.", couponYield: "Текущая купонная доходность, годовых", couponYieldNote: "Купонная доходность = ближайший купон × 365 / дней в купонном периоде / цена без НКД × 100 %. Это годовая оценка при сохранении размера купона и номинала, без налогов, комиссий и реинвестирования; для плавающего или переменного купона будущий доход изменится.", @@ -343,10 +343,10 @@ const ru = { breadcrumbs: { accountFallback: "Счет", accounts: "Брокерские счета", - events: "Будущие события", + events: "Предстоящие события", income: "Пассивный доход", label: "Хлебные крошки", - operations: "Операции", + operations: "История операций", settings: "Настройки", }, entryGroup: { @@ -356,20 +356,20 @@ const ru = { dateKind: { event: "Дата события", registry: "Дата реестра" }, emptyText: "Доступные источники не сообщили о купонах, дивидендах, погашениях или офертах в этом периоде.", - emptyTitle: "За выбранный период событий не найдено", - errorText: "Попробуйте запросить прогноз еще раз.", - errorTitle: "Не удалось загрузить будущие события", + emptyTitle: "На выбранный период предстоящих событий не найдено", + errorText: "Попробуйте загрузить предстоящие события ещё раз.", + errorTitle: "Не удалось загрузить предстоящие события", forecastWindow: "Прогноз от {{date}} на {{from}} – {{to}}", hint: "Прогноз рассчитан по текущей позиции до налогов. Покрытие зависит от доступных данных MOEX.", noAmount: "Сумма неизвестна", - open: "Открыть будущие события", + open: "Открыть предстоящие события", projectedAmount: "Ожидаемая сумма", - recentEmpty: "На ближайшие 30 дней событий не найдено.", - recentError: "Не удалось загрузить будущие события.", - recentLoading: "Загрузка будущих событий", - recentTitle: "Будущие события", + recentEmpty: "На ближайшие 30 дней предстоящих событий не найдено.", + recentError: "Не удалось загрузить предстоящие события.", + recentLoading: "Загрузка предстоящих событий", + recentTitle: "Предстоящие события", retry: "Повторить", - title: "Будущие события", + title: "Предстоящие события", type: { coupon: "Купон", dividend: "Дивиденд", @@ -451,14 +451,14 @@ const ru = { dividend: "Дивиденд", }, }, - errorText: "Попробуйте запросить операции еще раз.", - errorTitle: "Не удалось загрузить журнал", + errorText: "Попробуйте загрузить историю операций ещё раз.", + errorTitle: "Не удалось загрузить историю операций", from: "С", next: "Далее", noInstrument: "Без инструмента", noMoney: "Без движения денег", occurredAt: "Исполнено {{date}}", - open: "Открыть журнал", + open: "Открыть историю операций", operationType: { buy: "Покупка", coupon: "Купон", @@ -475,13 +475,13 @@ const ru = { previous: "Назад", price: "Цена: {{value}}", quantity: "{{value}} шт.", - recentEmpty: "Последних событий пока нет.", - recentError: "Не удалось загрузить последние события.", - recentLoading: "Загрузка последних событий", - recentTitle: "Последние события", + recentEmpty: "Последних операций пока нет.", + recentError: "Не удалось загрузить последние операции.", + recentLoading: "Загрузка последних операций", + recentTitle: "Последние операции", reset: "Сбросить", retry: "Повторить", - title: "Журнал операций", + title: "История операций", to: "По", }, navigation: { @@ -582,9 +582,9 @@ const ru = { }, tabs: { cash: "Деньги", - events: "События", + events: "Предстоящие события", income: "Доход", - operations: "Операции", + operations: "История операций", positions: "Позиции", }, title: "Портфель", @@ -637,10 +637,10 @@ const ru = { dividendNote: "Личная сумма — оценка по текущему количеству, не гарантия выплаты. Налоги не сопоставлены; дата выплаты компании может отличаться от дня зачисления на ваш счёт.", dividendWindow: - "События компании с {{from}} по {{to}}. Личные зачисления показаны отдельно в истории событий.", + "События компании с {{from}} по {{to}}. Полученные выплаты показаны отдельно в истории операций.", estimateUnavailable: "Данные позиции не удалось обновить. Личная оценка недоступна или рассчитана по сохранённой позиции.", - eventFilter: "Фильтр событий", + eventFilter: "Фильтр операций", eventPrice: "Цена: {{value}}", eventQuantity: "{{value}} шт.", exchanges: { @@ -656,7 +656,7 @@ const ru = { trades: "Сделки", }, forming: "Формируется", - history: "Моя история событий", + history: "История операций", historyNote: "Операции по этой акции на доступных открытых счетах, включая периоды до текущего года. Дата сделки — дата операции брокера; количество — исполненная часть. История закрытых счетов не включена.", incomplete: "Последняя свеча ещё формируется: её цена может измениться.", -- 2.54.0 From 850823e1f5fa63bdbc321e59b5ac60b51e6ac18b Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sat, 7 Nov 2026 10:33:38 +0300 Subject: [PATCH 2/2] feat: clarify grouped coupon payouts (WealthLedger/frontend#162) --- .../broker-operation/model/display.ts | 5 +++ .../ui/OperationGroup.test.tsx | 19 ++++++--- .../broker-operation/ui/OperationGroup.tsx | 39 ++++++++++++----- .../AccountIncomePage.test.tsx | 8 ++-- .../AccountOperationsPage.test.tsx | 4 +- .../ui/AccountPage/AccountPage.test.tsx | 2 +- src/shared/i18n/locales/ru.ts | 1 + .../ui/EntryGroup/EntryGroup.module.css | 42 +++++++++++++++++++ src/shared/ui/EntryGroup/EntryGroup.tsx | 37 +++++++++------- 9 files changed, 119 insertions(+), 38 deletions(-) diff --git a/src/entities/broker-operation/model/display.ts b/src/entities/broker-operation/model/display.ts index 53e86e1..4cadbc1 100644 --- a/src/entities/broker-operation/model/display.ts +++ b/src/entities/broker-operation/model/display.ts @@ -49,6 +49,10 @@ function entryLabelKey(operation: BrokerageOperation): string { return `ledger.category.${operation.category}`; } +function isCouponPayment(operation: BrokerageOperation): boolean { + return isOperationType(operation.type, OperationType.Coupon); +} + function isIncomePayment(operation: BrokerageOperation): boolean { return [OperationType.Coupon, OperationType.Dividend].some(type => isOperationType(operation.type, type), @@ -131,6 +135,7 @@ function paymentPresentation( export { brokerageOperationGroupKey, entryLabelKey, + isCouponPayment, isIncomePayment, isZeroDecimal, operationLabelKey, diff --git a/src/entities/broker-operation/ui/OperationGroup.test.tsx b/src/entities/broker-operation/ui/OperationGroup.test.tsx index 7c830fa..f2c6099 100644 --- a/src/entities/broker-operation/ui/OperationGroup.test.tsx +++ b/src/entities/broker-operation/ui/OperationGroup.test.tsx @@ -55,23 +55,27 @@ function renderGroup( } describe("OperationGroup", () => { - it("shows the header, exact total and separate card rows per account", () => { + it("shows the received coupon and positive totals in flat account rows", () => { renderGroup([ - operation("1", { id: "broker", name: "Брокерский" }, "0.1"), + { ...operation("1", { id: "broker", name: "Брокерский" }, "0.1"), description: "Выплата купонов СФО ВТБ РКС Эталон 04" }, operation("2", { id: "iis", name: "ИИС" }, "0.2"), ]); - expect(screen.getByText("Купон · BOND")).toBeInTheDocument(); + expect(screen.getByText("Выплата купонов СФО ВТБ РКС Эталон 04")).toBeInTheDocument(); expect(screen.getByText("+0,3 ₽")).toBeInTheDocument(); const accounts = within(screen.getByRole("list", { - name: /Купон · BOND, Исполнено 14\.08\.2026.*, \+0,3 ₽/, + name: /Выплата купонов СФО ВТБ РКС Эталон 04, Исполнено 14\.08\.2026.*, \+0,3 ₽/, })); expect(accounts.getByRole("listitem", { name: "Брокерский, +0,1 ₽" })) .toHaveTextContent("Купон"); + expect(accounts.getByText("+0,1 ₽")).toHaveClass(/positive/); expect(accounts.getByRole("listitem", { name: "ИИС, +0,2 ₽" })) .toHaveTextContent("Купон"); + expect(accounts.getByText("+0,2 ₽")).toHaveClass(/positive/); + expect(accounts.getByRole("listitem", { name: "ИИС, +0,2 ₽" })) + .not.toHaveClass("EntryGroup_accountCardRow"); expect(accounts.queryByText(/Исполнено/)).not.toBeInTheDocument(); expect(accounts.queryByRole("link")).not.toBeInTheDocument(); }); @@ -83,8 +87,10 @@ describe("OperationGroup", () => { ]); expect(screen.getByRole("list", { - name: /Купон · BOND, Исполнено .*Итог недоступен/, + name: /Выплата купонов BOND, Исполнено .*Итог недоступен/, })).toBeInTheDocument(); + expect(screen.getByText("Итог недоступен")).toBeInTheDocument(); + expect(screen.getByText("—")).not.toHaveClass(/positive/); }); it.each(["0", "0.000"])( @@ -114,6 +120,7 @@ describe("OperationGroup", () => { operation("2", { id: "iis", name: "ИИС" }, "-100"), ]); expect(screen.getByText("0 ₽")).toBeInTheDocument(); + expect(screen.getByText("−100 ₽")).toHaveClass(/negative/); }); it("renders repeated ids inside one group as separate rows", () => { @@ -123,7 +130,7 @@ describe("OperationGroup", () => { ]); expect(screen.getByText("+30 ₽")).toBeInTheDocument(); - const accounts = within(screen.getByRole("list", { name: /Купон/ })); + const accounts = within(screen.getByRole("list", { name: /Выплата купонов/ })); expect(accounts.getAllByText("Брокерский")).toHaveLength(1); expect(accounts.getAllByText("ИИС")).toHaveLength(1); expect(accounts.getAllByRole("listitem")).toHaveLength(2); diff --git a/src/entities/broker-operation/ui/OperationGroup.tsx b/src/entities/broker-operation/ui/OperationGroup.tsx index a5d73fb..fa36ce1 100644 --- a/src/entities/broker-operation/ui/OperationGroup.tsx +++ b/src/entities/broker-operation/ui/OperationGroup.tsx @@ -9,12 +9,13 @@ import { sumMoney, totalText, } from "@/shared/lib"; -import { AccountCardRow, EntryGroup, EntryGroupTotal } from "@/shared/ui"; +import { AccountCardRow, AccountRow, EntryGroup, EntryGroupTotal } from "@/shared/ui"; import type { BrokerageOperation } from "../api/useBrokerageOperations"; import { entryLabelKey, + isCouponPayment, isZeroDecimal, operationLabelKey, paymentPresentation, @@ -37,6 +38,8 @@ const OperationGroup: FC = (props) => { ? entryLabelKey(first) : operationLabelKey(first.type)); + const isCoupon = isCouponPayment(first); + const instrument = first.ticker ?? first.instrumentType ?? t("ledger.noInstrument"); @@ -52,9 +55,21 @@ const OperationGroup: FC = (props) => { : null; const date = formatSnapshotDay(first.occurredAt); - const title = `${typeLabel} · ${instrument}`; + const description = first.description.trim(); + let title = `${typeLabel} · ${instrument}`; + + if (isCoupon) { + title = description && description.toLocaleLowerCase() !== "купон" + ? description + : t("ledger.couponPayment", { instrument }); + } + const totalsText = totalText(totals, true); + const totalTone = isCoupon && totals?.length && totals.every(total => !isZeroDecimal(total.value) && !total.value.startsWith("-")) + ? "positive" + : "neutral"; + const accessibleName = [ title, t("payout.executedAt", { date }), @@ -63,13 +78,14 @@ const OperationGroup: FC = (props) => { return ( } valueLabel={typeLabel} wrap @@ -80,14 +96,15 @@ const OperationGroup: FC = (props) => { {props.entries.map(({ key, operation }) => { const payment = paymentPresentation(operation); - return ( - - ); + const accountRowProps = { + accountName: operation.account?.name ?? "—", + value: payment.amount, + valueLabel: t(payment.label), + }; + + return isCoupon + ? + : ; })} ); diff --git a/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.test.tsx b/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.test.tsx index 2af0697..f0ae8d8 100644 --- a/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.test.tsx +++ b/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.test.tsx @@ -817,11 +817,11 @@ describe("income period and snapshot boundary", () => { ); renderPage(undefined, "all"); - expect(await screen.findByText("Купон · BOND")).toBeInTheDocument(); + expect(await screen.findByText("Выплата купонов BOND")).toBeInTheDocument(); expect(screen.getByText("+30 ₽")).toBeInTheDocument(); const accounts = within(screen.getByRole("list", { - name: /Купон · BOND, Исполнено/, + name: /Выплата купонов BOND, Исполнено/, })); expect(accounts.getByText("Брокерский")).toBeInTheDocument(); @@ -856,7 +856,7 @@ describe("income period and snapshot boundary", () => { expect(await screen.findByText("+30 ₽")).toBeInTheDocument(); const accounts = within(screen.getByRole("list", { - name: /Купон · BOND/, + name: /Выплата купонов BOND/, })); expect(accounts.getAllByRole("listitem")).toHaveLength(2); @@ -889,7 +889,7 @@ describe("income period and snapshot boundary", () => { ); const view = renderPage(undefined, "all"); - await screen.findByText("Купон · BOND"); + await screen.findByText("Выплата купонов BOND"); expect(screen.queryByText("+30 ₽")).not.toBeInTheDocument(); view.unmount(); diff --git a/src/pages/account-operations/ui/AccountOperationsPage/AccountOperationsPage.test.tsx b/src/pages/account-operations/ui/AccountOperationsPage/AccountOperationsPage.test.tsx index 7224ca8..e411897 100644 --- a/src/pages/account-operations/ui/AccountOperationsPage/AccountOperationsPage.test.tsx +++ b/src/pages/account-operations/ui/AccountOperationsPage/AccountOperationsPage.test.tsx @@ -787,10 +787,10 @@ describe("all-account operation groups", () => { fireEvent.click(screen.getByRole("button", { name: "Далее" })); const accounts = await screen.findByRole("list", { - name: /Купон · BOND, Исполнено/, + name: /Выплата купонов BOND, Исполнено/, }); - expect(screen.getByText("Купон · BOND")).toBeInTheDocument(); + expect(screen.getByText("Выплата купонов BOND")).toBeInTheDocument(); expect(screen.getByText("+200 ₽")).toBeInTheDocument(); expect(within(accounts).getAllByRole("listitem")).toHaveLength(2); diff --git a/src/pages/account/ui/AccountPage/AccountPage.test.tsx b/src/pages/account/ui/AccountPage/AccountPage.test.tsx index 1a6797f..5ce9057 100644 --- a/src/pages/account/ui/AccountPage/AccountPage.test.tsx +++ b/src/pages/account/ui/AccountPage/AccountPage.test.tsx @@ -1655,7 +1655,7 @@ describe("AccountPage states", () => { await screen.findByRole("heading", { level: 1, name: "Все счета" }); const operationGroup = screen.getByRole("list", { - name: /Купон · BOND, Исполнено/, + name: /Выплата купонов BOND, Исполнено/, }); expect(screen.getByText("+500 ₽")).toBeInTheDocument(); diff --git a/src/shared/i18n/locales/ru.ts b/src/shared/i18n/locales/ru.ts index a76db70..0970b59 100644 --- a/src/shared/i18n/locales/ru.ts +++ b/src/shared/i18n/locales/ru.ts @@ -443,6 +443,7 @@ const ru = { transfer: "Перевод", }, clearPeriod: "Очистить период", + couponPayment: "Выплата купонов {{instrument}}", emptyText: "Измените период или фильтры.", emptyTitle: "Операций нет", entryLabel: { diff --git a/src/shared/ui/EntryGroup/EntryGroup.module.css b/src/shared/ui/EntryGroup/EntryGroup.module.css index 527ed80..6d4f613 100644 --- a/src/shared/ui/EntryGroup/EntryGroup.module.css +++ b/src/shared/ui/EntryGroup/EntryGroup.module.css @@ -4,6 +4,40 @@ background: var(--wl-color-subtle); } +.flat { + background: var(--wl-color-surface); +} + +.flat > :first-child { + border: 0; + background: transparent; +} + +.flat .accounts { + padding-bottom: var(--mantine-spacing-md); +} + +.flat .accountRow { + padding-block: var(--mantine-spacing-md); +} + +.flat .accountRow:last-child { + padding-bottom: 0; +} + +.flat .accountRow > :first-child { + min-width: 0; + overflow-wrap: anywhere; +} + +.positive { + color: var(--wl-color-positive); +} + +.negative { + color: var(--wl-color-negative); +} + .accounts { padding: 0 var(--mantine-spacing-md) var(--mantine-spacing-sm); list-style: none; @@ -38,4 +72,12 @@ .accountRow { align-items: flex-start; } + + .flat .accountRow { + flex-wrap: wrap; + } + + .flat .accountValue { + white-space: normal; + } } diff --git a/src/shared/ui/EntryGroup/EntryGroup.tsx b/src/shared/ui/EntryGroup/EntryGroup.tsx index 9f44271..c1c9b93 100644 --- a/src/shared/ui/EntryGroup/EntryGroup.tsx +++ b/src/shared/ui/EntryGroup/EntryGroup.tsx @@ -9,11 +9,15 @@ import classes from "./EntryGroup.module.css"; type AccountRowProps = { accountName: string; + tone?: "negative" + | "neutral" + | "positive"; value: string; valueLabel: string; }; type EntryGroupProps = PropsWithChildren<{ + flat?: boolean; header: ReactNode; label: string; }>; @@ -25,13 +29,14 @@ type EntryGroupTotalProps = { const AccountRow: FC = (props) => { const accessibleName = `${props.accountName}, ${props.value}`; + const valueClass = props.tone ? classes[props.tone] : undefined; return (
  • {props.accountName} - {props.value} + {props.value} {props.valueLabel} @@ -53,20 +58,24 @@ const AccountCardRow: FC = props => (
  • ); -const EntryGroup: FC = props => ( - - {props.header} +const EntryGroup: FC = (props) => { + const groupClass = props.flat ? `${classes.group} ${classes.flat}` : classes.group; - - {props.children} - - -); + return ( + + {props.header} + + + {props.children} + + + ); +}; const EntryGroupTotal: FC = (props) => { const { t } = useTranslation(); -- 2.54.0