Compare commits

..

No commits in common. "6f4b46c964902da9c17a583280400e6b56b9f1ab" and "595d05915127d9d30e5ef2eee3f4c7195a83afbb" have entirely different histories.

6 changed files with 25 additions and 136 deletions

View File

@ -80,10 +80,7 @@ describe('useBrokerEvents', () => {
it('reuses cache when query key matches', async () => {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
queryClient.setQueryData(
['broker', 'events', 'acc-1', '2026-06-22', '2026-06-29'],
mockEventsData,
);
queryClient.setQueryData(['broker', 'events', 'acc-1', query], mockEventsData);
const { result } = renderHook(() => useBrokerEvents('acc-1', query), {
wrapper: createWrapper(queryClient),

View File

@ -3,11 +3,10 @@ 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<BrokerEventsData>({
queryKey: ['broker', 'events', accountId, from, to],
queryKey: ['broker', 'events', accountId, query],
enabled: Boolean(accountId),
queryFn: async () => (await getBrokerEvents(accountId!, { from, to })).data,
queryFn: async () => (await getBrokerEvents(accountId!, query)).data,
staleTime: 300_000,
retry: 2,
refetchOnWindowFocus: false,

View File

@ -12,22 +12,9 @@ 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 }) => <h2>{children}</h2>,
Text: ({ children }: { children: ReactNode }) => <span>{children}</span>,
TextField: ({ label, value, onChange, ...props }: any) => (
<input
aria-label={label || ''}
value={value || ''}
onChange={onChange || (() => {})}
type={props.type || 'text'}
/>
),
}));
import { useBrokerEvents } from '@/entities/broker-event';
@ -192,40 +179,7 @@ describe('BrokerEventsPage', () => {
} as unknown as ReturnType<typeof useBrokerEvents>);
render(<BrokerEventsPage />, { wrapper: createWrapper() });
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<typeof useBrokerEvents>);
render(<BrokerEventsPage />, { wrapper: createWrapper() });
expect(screen.getByLabelText('С')).toBeInTheDocument();
expect(screen.getByLabelText('По')).toBeInTheDocument();
expect(screen.getByText('На ближайшие 7 дней событий нет')).toBeInTheDocument();
});
it('renders events heading, summary and table', () => {

View File

@ -1,11 +1,8 @@
import { useState, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Box } from '@mui/material';
import { Heading, Text, TextField } from '@moex-vibe/design-system';
import dayjs from 'dayjs';
import { Heading, Text } from '@moex-vibe/design-system';
import { useBrokerEvents } from '@/entities/broker-event';
import { useBrokerAccountContext } from '@/widgets/broker-account-layout';
import { formatBrokerCurrencyValue, formatBrokerDate } from '@/shared/lib/formatters';
import { formatBrokerCurrencyValue } from '@/shared/lib/formatters';
function eventTypeLabel(type: string): string {
switch (type) {
@ -22,41 +19,26 @@ function eventTypeLabel(type: string): string {
}
}
function defaultPeriod(): { from: string; to: string } {
const now = dayjs();
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);
return {
from: now.format('YYYY-MM-DD'),
to: now.add(7, 'day').format('YYYY-MM-DD'),
from: from.toISOString().slice(0, 10),
to: to.toISOString().slice(0, 10),
};
}
export function BrokerEventsPage() {
const { accountId } = useBrokerAccountContext();
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 period = defaultPeriod();
const events = useBrokerEvents(accountId, period);
const ev = events.data;
@ -68,32 +50,6 @@ export function BrokerEventsPage() {
<Heading level={2} id="broker-events-heading">
События
</Heading>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
<TextField
label="С"
type="date"
value={from}
onChange={(e) => {
const newParams = new URLSearchParams(searchParams);
newParams.set('from', e.target.value);
setSearchParams(newParams, { replace: true });
}}
InputLabelProps={{ shrink: true }}
/>
<TextField
label="По"
type="date"
value={to}
onChange={(e) => {
const newParams = new URLSearchParams(searchParams);
newParams.set('to', e.target.value);
setSearchParams(newParams, { replace: true });
}}
InputLabelProps={{ shrink: true }}
error={!!dateError}
helperText={dateError}
/>
</Box>
</Box>
{events.error ? (
@ -106,7 +62,7 @@ export function BrokerEventsPage() {
</Text>
) : ev && ev.items.length === 0 ? (
<Text component="p" tone="muted">
В выбранном диапазоне событий нет
На ближайшие 7 дней событий нет
</Text>
) : ev ? (
<Box sx={{ display: 'grid', gap: 2 }}>
@ -130,9 +86,7 @@ export function BrokerEventsPage() {
<Text variant="caption" tone="secondary">
Ближайшее
</Text>
<Box sx={{ fontWeight: 700 }}>
{formatBrokerDate(ev.summary.nearestEventDate) ?? '-'}
</Box>
<Box sx={{ fontWeight: 700 }}>{formatDate(ev.summary.nearestEventDate)}</Box>
</Box>
<Box>
<Text variant="caption" tone="secondary">
@ -212,7 +166,7 @@ export function BrokerEventsPage() {
component="td"
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
>
{formatBrokerDate(item.eventDate) ?? '-'}
{formatDate(item.eventDate)}
</Box>
<Box
component="td"

View File

@ -207,15 +207,3 @@ 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. При невалидных датах — показ ошибки под полем, запрос не выполняется.

View File

@ -57,11 +57,8 @@
- [x] Добавить новый раздел `События` в `BrokerAccountLayout`.
- [x] Добавить маршрут `/broker/:accountId/events`.
- [x] Реализовать выбор диапазона дат.
- [x] Добавить два TextField[type=date] из дизайн-системы на страницу событий.
- [x] Синхронизировать from/to с URL-параметрами (useSearchParams).
- [x] dayjs для валидации, форматирования и дефолтов (todaytoday+7d).
- [x] Валидация to >= from с показом ошибки под полем.
- [ ] Реализовать выбор диапазона дат.
(Deferred: первая версия использует фиксированный период today+7d. UI для выбора дат — follow-up.)
- [x] Реализовать summary по выбранному периоду.
- [x] Реализовать список событий с признаком `estimate`.
- [ ] Разделить денежные и неденежные события на уровне представления.