From 3027b4f0f0c30f3fc1d59d95f176cf2d4df986a0 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Mon, 22 Jun 2026 06:56:18 +0300 Subject: [PATCH 1/3] docs: update plan and tasks for broker events date range selection --- docs/features/broker-events-and-payouts/plan.md | 12 ++++++++++++ docs/features/broker-events-and-payouts/tasks.md | 7 +++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/features/broker-events-and-payouts/plan.md b/docs/features/broker-events-and-payouts/plan.md index 9a1b22d..447e1b3 100644 --- a/docs/features/broker-events-and-payouts/plan.md +++ b/docs/features/broker-events-and-payouts/plan.md @@ -207,3 +207,15 @@ Overview получает компактный блок ближайших со - `npm run build -w apps/frontend` - `npm run build -w apps/docs` если публичные docs менялись - `npm run codegen -w apps/frontend` если Swagger-контракт обновлён + +### Frontend date range UI + +Даты хранятся в URL-параметрах `from` / `to` для возможности поделиться ссылкой. На странице — два `TextField[type=date]` из дизайн-системы. dayjs для форматирования, валидации и дефолтов. + +Поток данных: + +``` +useSearchParams → from/to → useBrokerEvents(query) → TanStack Query (автоrefetch) +``` + +Дефолт при первом визите без параметров: today – today+7d. Валидация: to >= from. При невалидных датах — показ ошибки под полем, запрос не выполняется. diff --git a/docs/features/broker-events-and-payouts/tasks.md b/docs/features/broker-events-and-payouts/tasks.md index 6c75278..8bb904c 100644 --- a/docs/features/broker-events-and-payouts/tasks.md +++ b/docs/features/broker-events-and-payouts/tasks.md @@ -57,8 +57,11 @@ - [x] Добавить новый раздел `События` в `BrokerAccountLayout`. - [x] Добавить маршрут `/broker/:accountId/events`. -- [ ] Реализовать выбор диапазона дат. - (Deferred: первая версия использует фиксированный период today+7d. UI для выбора дат — follow-up.) +- [x] Реализовать выбор диапазона дат. + - [x] Добавить два TextField[type=date] из дизайн-системы на страницу событий. + - [x] Синхронизировать from/to с URL-параметрами (useSearchParams). + - [x] dayjs для валидации, форматирования и дефолтов (today–today+7d). + - [x] Валидация to >= from с показом ошибки под полем. - [x] Реализовать summary по выбранному периоду. - [x] Реализовать список событий с признаком `estimate`. - [ ] Разделить денежные и неденежные события на уровне представления. -- 2.47.2 From 0ddd9b5f80cfa693b4f51240e2e73fbfee533752 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Mon, 22 Jun 2026 06:58:59 +0300 Subject: [PATCH 2/3] feat: add date range selection to broker events page --- .../ui/BrokerEventsPage.test.tsx | 48 ++++++++++- .../broker-events/ui/BrokerEventsPage.tsx | 84 ++++++++++++++----- 2 files changed, 112 insertions(+), 20 deletions(-) diff --git a/apps/frontend/src/pages/broker-events/ui/BrokerEventsPage.test.tsx b/apps/frontend/src/pages/broker-events/ui/BrokerEventsPage.test.tsx index 5457673..3618303 100644 --- a/apps/frontend/src/pages/broker-events/ui/BrokerEventsPage.test.tsx +++ b/apps/frontend/src/pages/broker-events/ui/BrokerEventsPage.test.tsx @@ -12,9 +12,22 @@ vi.mock('@/widgets/broker-account-layout', () => ({ useBrokerAccountContext: () => ({ accountId: 'acc-1', portfolio: null }), })); +const mockSetSearchParams = vi.fn(); +vi.mock('react-router-dom', () => ({ + useSearchParams: () => [new URLSearchParams(), mockSetSearchParams], +})); + vi.mock('@moex-vibe/design-system', () => ({ Heading: ({ children }: { children: ReactNode }) =>

{children}

, Text: ({ children }: { children: ReactNode }) => {children}, + TextField: ({ label, value, onChange, ...props }: any) => ( + {})} + type={props.type || 'text'} + /> + ), })); import { useBrokerEvents } from '@/entities/broker-event'; @@ -179,7 +192,40 @@ describe('BrokerEventsPage', () => { } as unknown as ReturnType); render(, { wrapper: createWrapper() }); - expect(screen.getByText('На ближайшие 7 дней событий нет')).toBeInTheDocument(); + expect(screen.getByText('В выбранном диапазоне событий нет')).toBeInTheDocument(); + }); + + it('renders date range inputs', () => { + vi.mocked(useBrokerEvents).mockReturnValue({ + data: mockData, + isLoading: false, + isError: false, + error: null, + isSuccess: true, + isPending: false, + dataUpdatedAt: Date.now(), + errorUpdatedAt: 0, + failureCount: 0, + failureReason: null, + errorUpdateCount: 0, + isFetched: true, + isFetchedAfterMount: true, + isFetching: false, + isInitialLoading: false, + isPaused: false, + isLoadingError: false, + isRefetchError: false, + isPlaceholderData: false, + isStale: false, + refetch: vi.fn(), + promise: Promise.resolve(mockData), + status: 'success', + fetchStatus: 'idle', + } as unknown as ReturnType); + + render(, { wrapper: createWrapper() }); + expect(screen.getByLabelText('С')).toBeInTheDocument(); + expect(screen.getByLabelText('По')).toBeInTheDocument(); }); it('renders events heading, summary and table', () => { diff --git a/apps/frontend/src/pages/broker-events/ui/BrokerEventsPage.tsx b/apps/frontend/src/pages/broker-events/ui/BrokerEventsPage.tsx index fb97604..aed5ec4 100644 --- a/apps/frontend/src/pages/broker-events/ui/BrokerEventsPage.tsx +++ b/apps/frontend/src/pages/broker-events/ui/BrokerEventsPage.tsx @@ -1,8 +1,11 @@ +import { useState, useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; import { Box } from '@mui/material'; -import { Heading, Text } from '@moex-vibe/design-system'; +import { Heading, Text, TextField } from '@moex-vibe/design-system'; +import dayjs from 'dayjs'; import { useBrokerEvents } from '@/entities/broker-event'; import { useBrokerAccountContext } from '@/widgets/broker-account-layout'; -import { formatBrokerCurrencyValue } from '@/shared/lib/formatters'; +import { formatBrokerCurrencyValue, formatBrokerDate } from '@/shared/lib/formatters'; function eventTypeLabel(type: string): string { switch (type) { @@ -19,26 +22,41 @@ function eventTypeLabel(type: string): string { } } -function formatDate(value: string | null): string { - if (!value) return '-'; - return new Date(value).toLocaleDateString('ru-RU'); -} - -function defaultPeriod() { - const now = new Date(); - const from = new Date(now); - const to = new Date(now); - to.setDate(to.getDate() + 7); +function defaultPeriod(): { from: string; to: string } { + const now = dayjs(); return { - from: from.toISOString().slice(0, 10), - to: to.toISOString().slice(0, 10), + from: now.format('YYYY-MM-DD'), + to: now.add(7, 'day').format('YYYY-MM-DD'), }; } export function BrokerEventsPage() { const { accountId } = useBrokerAccountContext(); - const period = defaultPeriod(); - const events = useBrokerEvents(accountId, period); + const [searchParams, setSearchParams] = useSearchParams(); + const [initialized, setInitialized] = useState(false); + + useEffect(() => { + const from = searchParams.get('from'); + const to = searchParams.get('to'); + if (!from || !to || !dayjs(from).isValid() || !dayjs(to).isValid()) { + const def = defaultPeriod(); + setSearchParams({ from: def.from, to: def.to }, { replace: true }); + } + setInitialized(true); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const from = searchParams.get('from') ?? ''; + const to = searchParams.get('to') ?? ''; + const period = initialized ? { from, to } : defaultPeriod(); + + const validFrom = dayjs(from); + const validTo = dayjs(to); + const dateError = + from && to && validFrom.isValid() && validTo.isValid() && validTo.isBefore(validFrom) + ? '"По" не может быть раньше "С"' + : ''; + + const events = useBrokerEvents(dateError ? undefined : accountId, period); const ev = events.data; @@ -50,6 +68,32 @@ export function BrokerEventsPage() { События + + { + const newParams = new URLSearchParams(searchParams); + newParams.set('from', e.target.value); + setSearchParams(newParams, { replace: true }); + }} + InputLabelProps={{ shrink: true }} + /> + { + const newParams = new URLSearchParams(searchParams); + newParams.set('to', e.target.value); + setSearchParams(newParams, { replace: true }); + }} + InputLabelProps={{ shrink: true }} + error={!!dateError} + helperText={dateError} + /> + {events.error ? ( @@ -62,7 +106,7 @@ export function BrokerEventsPage() { ) : ev && ev.items.length === 0 ? ( - На ближайшие 7 дней событий нет + В выбранном диапазоне событий нет ) : ev ? ( @@ -86,7 +130,9 @@ export function BrokerEventsPage() { Ближайшее - {formatDate(ev.summary.nearestEventDate)} + + {formatBrokerDate(ev.summary.nearestEventDate) ?? '-'} + @@ -166,7 +212,7 @@ export function BrokerEventsPage() { component="td" sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }} > - {formatDate(item.eventDate)} + {formatBrokerDate(item.eventDate) ?? '-'} Date: Mon, 22 Jun 2026 19:56:53 +0300 Subject: [PATCH 3/3] fix: flatten queryKey to primitives for reliable TanStack Query key comparison --- .../src/entities/broker-event/model/useBrokerEvents.test.tsx | 5 ++++- .../src/entities/broker-event/model/useBrokerEvents.ts | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/entities/broker-event/model/useBrokerEvents.test.tsx b/apps/frontend/src/entities/broker-event/model/useBrokerEvents.test.tsx index dc4db4b..46f2881 100644 --- a/apps/frontend/src/entities/broker-event/model/useBrokerEvents.test.tsx +++ b/apps/frontend/src/entities/broker-event/model/useBrokerEvents.test.tsx @@ -80,7 +80,10 @@ describe('useBrokerEvents', () => { it('reuses cache when query key matches', async () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); - queryClient.setQueryData(['broker', 'events', 'acc-1', query], mockEventsData); + queryClient.setQueryData( + ['broker', 'events', 'acc-1', '2026-06-22', '2026-06-29'], + mockEventsData, + ); const { result } = renderHook(() => useBrokerEvents('acc-1', query), { wrapper: createWrapper(queryClient), diff --git a/apps/frontend/src/entities/broker-event/model/useBrokerEvents.ts b/apps/frontend/src/entities/broker-event/model/useBrokerEvents.ts index 01d463a..ca4f06a 100644 --- a/apps/frontend/src/entities/broker-event/model/useBrokerEvents.ts +++ b/apps/frontend/src/entities/broker-event/model/useBrokerEvents.ts @@ -3,10 +3,11 @@ import type { BrokerEventsData } from '@/shared/api/responses'; import { getBrokerEvents, type BrokerEventsQuery } from '../api/brokerEventApi'; export function useBrokerEvents(accountId: string | undefined, query: BrokerEventsQuery) { + const { from, to } = query; return useQuery({ - queryKey: ['broker', 'events', accountId, query], + queryKey: ['broker', 'events', accountId, from, to], enabled: Boolean(accountId), - queryFn: async () => (await getBrokerEvents(accountId!, query)).data, + queryFn: async () => (await getBrokerEvents(accountId!, { from, to })).data, staleTime: 300_000, retry: 2, refetchOnWindowFocus: false, -- 2.47.2