fix: show saved events forecast window (WealthLedger/frontend#63)

This commit is contained in:
2026-11-05 07:14:14 +03:00
parent cadba8d075
commit 9efd37c5ff
8 changed files with 43 additions and 3 deletions
@@ -217,6 +217,7 @@ beforeEach(() => {
}), }),
), ),
incomeTotalsByCurrency: [], incomeTotalsByCurrency: [],
window: { from: "2026-09-01", to: "2026-10-01" },
}), }),
), ),
), ),
@@ -176,7 +176,11 @@ describe("Upcoming event date filters", () => {
({ request }) => { ({ request }) => {
searches.push(new URL(request.url).search); searches.push(new URL(request.url).search);
return HttpResponse.json(resource({ events: [] })); return HttpResponse.json(resource({
calculatedAt: "2026-08-14T00:00:00Z",
events: [],
window: { from: "2026-08-14", to: "2026-09-13" },
}));
}, },
), ),
); );
@@ -223,7 +227,11 @@ describe("Upcoming event date filters", () => {
"*/api/v1/brokerage-accounts/:accountId/events", "*/api/v1/brokerage-accounts/:accountId/events",
({ request }) => ({ request }) =>
new URL(request.url).searchParams.get("from") === defaultFrom new URL(request.url).searchParams.get("from") === defaultFrom
? HttpResponse.json(resource({ events: [] })) ? HttpResponse.json(resource({
calculatedAt: "2026-08-14T00:00:00Z",
events: [],
window: { from: "2026-08-14", to: "2026-09-13" },
}))
: HttpResponse.json({ code: "VALIDATION_ERROR" }, { status: 400 }), : HttpResponse.json({ code: "VALIDATION_ERROR" }, { status: 400 }),
), ),
); );
@@ -256,6 +264,7 @@ describe("Upcoming event date filters", () => {
calculatedAt: "2099-01-01T00:00:00Z", calculatedAt: "2099-01-01T00:00:00Z",
events: [], events: [],
incomeTotalsByCurrency: [], incomeTotalsByCurrency: [],
window: { from: "2099-01-01", to: "2099-01-31" },
}), }),
); );
}, },
@@ -264,6 +273,7 @@ describe("Upcoming event date filters", () => {
renderEvents(accountId, ["?from=2026-02-01&to=2026-02-28"]); renderEvents(accountId, ["?from=2026-02-01&to=2026-02-28"]);
const from = await screen.findByLabelText("Период"); const from = await screen.findByLabelText("Период");
expect(from).toHaveTextContent("01.02.2026 – 28.02.2026"); expect(from).toHaveTextContent("01.02.2026 – 28.02.2026");
expect(screen.getByText(/Прогноз от .* на 01\.01\.2099 – 31\.01\.2099/)).toBeVisible();
expect(screen.getByRole("button", { name: "Применить" })).toBeDisabled(); expect(screen.getByRole("button", { name: "Применить" })).toBeDisabled();
expect(searches).toEqual(["?from=2026-02-01&to=2026-02-28"]); expect(searches).toEqual(["?from=2026-02-01&to=2026-02-28"]);
@@ -17,6 +17,8 @@ const RETRIES_EXHAUSTED_MS = 6000;
const TEST_MS = 10_000; const TEST_MS = 10_000;
const snapshot = resource({ const snapshot = resource({
accountId: "account-1",
calculatedAt: "2026-08-14T00:00:00Z",
events: [ events: [
{ {
date: "2026-10-01", date: "2026-10-01",
@@ -32,6 +34,7 @@ const snapshot = resource({
}, },
], ],
incomeTotalsByCurrency: [{ currency: "RUB", value: "123.45" }], incomeTotalsByCurrency: [{ currency: "RUB", value: "123.45" }],
window: { from: "2026-08-14", to: "2026-09-13" },
}); });
function renderEvents() { function renderEvents() {
@@ -100,7 +103,13 @@ describe("AccountEventsPage read retry", () => {
async () => { async () => {
let isFailing = false; let isFailing = false;
let requests = 0; let requests = 0;
let imports = 0;
server.use( server.use(
http.post("*/api/v1/tbank-sync", () => {
imports += 1;
return HttpResponse.json({}, { status: 202 });
}),
http.get("*/api/v1/brokerage-accounts/:accountId/events", () => { http.get("*/api/v1/brokerage-accounts/:accountId/events", () => {
requests += 1; requests += 1;
@@ -111,6 +120,7 @@ describe("AccountEventsPage read retry", () => {
); );
const { queryClient } = renderEvents(); const { queryClient } = renderEvents();
await screen.findByText("Купон · Synthetic coupon"); await screen.findByText("Купон · Synthetic coupon");
expect(screen.getByText(/Прогноз от .* на 14\.08\.2026 – 13\.09\.2026/)).toBeVisible();
expect(screen.queryByRole("button", { name: "Повторить" })) expect(screen.queryByRole("button", { name: "Повторить" }))
.not.toBeInTheDocument(); .not.toBeInTheDocument();
@@ -127,6 +137,7 @@ describe("AccountEventsPage read retry", () => {
expect(screen.getByText("Не удалось обновить данные")).toBeInTheDocument(); expect(screen.getByText("Не удалось обновить данные")).toBeInTheDocument();
expect(screen.getByText("Купон · Synthetic coupon")).toBeInTheDocument(); expect(screen.getByText("Купон · Synthetic coupon")).toBeInTheDocument();
expect(screen.getByText(/Прогноз от .* на 14\.08\.2026 – 13\.09\.2026/)).toBeVisible();
expect(requests).toBe(4); expect(requests).toBe(4);
isFailing = false; isFailing = false;
@@ -136,6 +147,7 @@ describe("AccountEventsPage read retry", () => {
expect(screen.queryByText("Не удалось обновить данные")) expect(screen.queryByText("Не удалось обновить данные"))
.not.toBeInTheDocument()); .not.toBeInTheDocument());
expect(requests).toBe(5); expect(requests).toBe(5);
expect(imports).toBe(0);
}, },
); );
@@ -47,6 +47,7 @@ describe("AccountEventsPage", () => {
accountId: "account", accountId: "account",
calculatedAt: "2026-08-14T00:00:00Z", calculatedAt: "2026-08-14T00:00:00Z",
events: [], events: [],
window: { from: "2026-08-14", to: "2026-09-13" },
}, },
isError: false, isError: false,
isLoading: false, isLoading: false,
@@ -136,6 +137,7 @@ describe("AccountEventsPage", () => {
type: "offer", type: "offer",
}, },
], ],
window: { from: "2026-08-14", to: "2026-09-13" },
}, },
isError: false, isError: false,
isLoading: false, isLoading: false,
@@ -276,6 +278,7 @@ describe("AccountEventsPage", () => {
accountId: "all", accountId: "all",
calculatedAt: "2026-08-14T00:00:00Z", calculatedAt: "2026-08-14T00:00:00Z",
events: [...events], events: [...events],
window: { from: "2026-08-14", to: "2026-09-13" },
}, },
isError: false, isError: false,
isLoading: false, isLoading: false,
@@ -18,6 +18,7 @@ import {
type AccountEventsPageViewModel type AccountEventsPageViewModel
= | { = | {
allAccounts: boolean; allAccounts: boolean;
calculatedAt: string;
canApply: boolean; canApply: boolean;
draftPeriod: [null | string, null | string]; draftPeriod: [null | string, null | string];
events: BrokerageEvent[]; events: BrokerageEvent[];
@@ -31,6 +32,7 @@ type AccountEventsPageViewModel
source?: InvestmentResourceMeta; source?: InvestmentResourceMeta;
state: "loaded"; state: "loaded";
totalVisible: boolean; totalVisible: boolean;
window: { from: string; to: string };
} }
| { | {
onReset: () => void; onReset: () => void;
@@ -110,6 +112,7 @@ function useAccountEventsPageVm(): AccountEventsPageViewModel {
return { return {
allAccounts: accountId === "all", allAccounts: accountId === "all",
calculatedAt: events.data.calculatedAt,
canApply: canApply:
(draftFrom !== appliedFrom || draftTo !== appliedTo) (draftFrom !== appliedFrom || draftTo !== appliedTo)
&& isValidPeriod(draftFrom, draftTo), && isValidPeriod(draftFrom, draftTo),
@@ -133,6 +136,7 @@ function useAccountEventsPageVm(): AccountEventsPageViewModel {
source: events.meta, source: events.meta,
state: "loaded", state: "loaded",
totalVisible: events.meta?.completeness === "complete", totalVisible: events.meta?.completeness === "complete",
window: events.data.window,
}; };
} }
@@ -17,7 +17,7 @@ import {
brokerageEventGroupKey, brokerageEventGroupKey,
brokerageEventKey, brokerageEventKey,
} from "@/entities/brokerage-event"; } from "@/entities/brokerage-event";
import { groupItems } from "@/shared/lib"; import { formatDate, formatSnapshotTime, groupItems } from "@/shared/lib";
import { ReadRetry, SourceSyncStatus } from "@/shared/ui"; import { ReadRetry, SourceSyncStatus } from "@/shared/ui";
import type { AccountEventsPageViewModel } from "../AccountEventsPage.vm"; import type { AccountEventsPageViewModel } from "../AccountEventsPage.vm";
@@ -89,6 +89,14 @@ const AccountEventsPageLoaded: FC<AccountEventsPageLoadedProps> = (props) => {
<Text c="dimmed">{t("events.hint")}</Text> <Text c="dimmed">{t("events.hint")}</Text>
<Text c="dimmed" size="sm">
{t("events.forecastWindow", {
date: formatSnapshotTime(props.calculatedAt),
from: formatDate(props.window.from),
to: formatDate(props.window.to),
})}
</Text>
<Group align="end"> <Group align="end">
<DatePickerInput <DatePickerInput
allowSingleDateInRange allowSingleDateInRange
+1
View File
@@ -360,6 +360,7 @@ const ru = {
emptyTitle: "За выбранный период событий не найдено", emptyTitle: "За выбранный период событий не найдено",
errorText: "Попробуйте запросить прогноз еще раз.", errorText: "Попробуйте запросить прогноз еще раз.",
errorTitle: "Не удалось загрузить будущие события", errorTitle: "Не удалось загрузить будущие события",
forecastWindow: "Прогноз от {{date}} на {{from}} – {{to}}",
hint: "Прогноз рассчитан по текущей позиции до налогов. Покрытие зависит от доступных данных MOEX.", hint: "Прогноз рассчитан по текущей позиции до налогов. Покрытие зависит от доступных данных MOEX.",
noAmount: "Сумма неизвестна", noAmount: "Сумма неизвестна",
open: "Открыть будущие события", open: "Открыть будущие события",
+1
View File
@@ -66,6 +66,7 @@ const handlers = [
accountId: "account-1", accountId: "account-1",
calculatedAt: "2026-08-14T00:00:00Z", calculatedAt: "2026-08-14T00:00:00Z",
events: [], events: [],
window: { from: "2026-08-14", to: "2026-09-13" },
}), }),
), ),
), ),