From d4d1dd980eee8cc568799375354dcf6c981d2568 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sat, 27 Jun 2026 11:48:10 +0300 Subject: [PATCH] feat: replace native date inputs with single DateCalendar field in broker dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace two MUI DatePicker components with a single visual range field - Click opens Popover with community DateCalendar (no DateRangePicker/pro) - First click sets start, second sets end; if end < start, range resets - Remove isOpen/onToggle props — Popover managed locally - Add BrokerDashboardTableSkeleton for events/income loading - Set default weekly range (today-7d / today) for both events and income - Remove 'Фильтр' label, rename 'Показать' → 'Применить период' - Install @mui/x-date-pickers@7 - Update tests for new UI and skeleton loading states - Align spec.md, plan.md, tasks.md with implementation --- apps/frontend/package.json | 2 +- .../broker-dashboard/lib/dashboardFilters.ts | 6 +- .../ui/BrokerDashboard.test.tsx | 58 +++- .../broker-dashboard/ui/BrokerDashboard.tsx | 6 - .../ui/BrokerDashboardDateFilter.tsx | 276 +++++++++++------- .../ui/BrokerDashboardEventsCard.tsx | 9 +- .../ui/BrokerDashboardIncomeCard.tsx | 9 +- .../ui/BrokerDashboardTableSkeleton.tsx | 29 ++ .../broker-dashboard-redesign/plan.md | 38 ++- .../broker-dashboard-redesign/spec.md | 40 ++- .../broker-dashboard-redesign/tasks.md | 18 +- package-lock.json | 87 ++++++ 12 files changed, 420 insertions(+), 158 deletions(-) create mode 100644 apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardTableSkeleton.tsx diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 2f85446..ca8fc95 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -24,6 +24,7 @@ "@moex-vibe/design-system": "*", "@mui/icons-material": "^6.5.0", "@mui/material": "^6.5.0", + "@mui/x-date-pickers": "^7.29.4", "@tanstack/react-query": "^5.20.0", "@tanstack/react-router": "^1.170.16", "@tanstack/react-table": "^8.21.3", @@ -43,7 +44,6 @@ "@biomejs/biome": "^2.5.0", "@conarti/eslint-plugin-feature-sliced": "^1.0.5", "@tanstack/router-devtools": "^1.167.0", - "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", diff --git a/apps/frontend/src/widgets/broker-dashboard/lib/dashboardFilters.ts b/apps/frontend/src/widgets/broker-dashboard/lib/dashboardFilters.ts index faa16db..39abe1e 100644 --- a/apps/frontend/src/widgets/broker-dashboard/lib/dashboardFilters.ts +++ b/apps/frontend/src/widgets/broker-dashboard/lib/dashboardFilters.ts @@ -18,7 +18,7 @@ export function defaultEventsFilters(): DashboardFilterState const now = dayjs() return { from: now.subtract(7, 'day').format('YYYY-MM-DD'), - to: now.add(7, 'day').format('YYYY-MM-DD'), + to: now.format('YYYY-MM-DD'), types: [...DASHBOARD_EVENT_TYPES], preset: '7d', } @@ -27,10 +27,10 @@ export function defaultEventsFilters(): DashboardFilterState export function defaultIncomeFilters(): DashboardFilterState { const now = dayjs() return { - from: now.startOf('year').format('YYYY-MM-DD'), + from: now.subtract(7, 'day').format('YYYY-MM-DD'), to: now.format('YYYY-MM-DD'), types: [...DASHBOARD_INCOME_TYPES], - preset: '1y', + preset: '7d', } } diff --git a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.test.tsx b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.test.tsx index 50d6a85..38f65c6 100644 --- a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.test.tsx +++ b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.test.tsx @@ -1,5 +1,8 @@ +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import type { ReactNode } from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { BrokerPortfolio } from '@/shared/api' import { BrokerDashboard } from './BrokerDashboard' @@ -67,6 +70,10 @@ const portfolio: BrokerPortfolio = { asOf: '2026-06-26T00:00:00.000Z', } +function renderWithProviders(ui: ReactNode) { + return render({ui}) +} + describe('BrokerDashboard', () => { beforeEach(() => { hookMocks.useBrokerEvents.mockReturnValue({ @@ -88,7 +95,7 @@ describe('BrokerDashboard', () => { }) it('renders the dashboard sections', () => { - render() + renderWithProviders() expect(screen.getByText('Основной счёт')).toBeInTheDocument() expect(screen.getByText('События')).toBeInTheDocument() @@ -99,7 +106,7 @@ describe('BrokerDashboard', () => { it('applies event type chips immediately to useBrokerEvents', async () => { const user = userEvent.setup() - render() + renderWithProviders() const couponChips = screen.getAllByRole('button', { name: 'Купоны' }) await user.click(couponChips[0]) @@ -115,7 +122,7 @@ describe('BrokerDashboard', () => { it('applies income type chips immediately to useBrokerOperations', async () => { const user = userEvent.setup() - render() + renderWithProviders() const couponChips = screen.getAllByRole('button', { name: 'Купоны' }) await user.click(couponChips[1]) @@ -131,14 +138,17 @@ describe('BrokerDashboard', () => { }) }) - it('shows date filter toggle button and expandable panel', async () => { + it('shows date filter toggle button with apply action', async () => { const user = userEvent.setup() - render() + renderWithProviders() - const filterButtons = screen.getAllByRole('button', { name: /Фильтр дат/ }) - expect(filterButtons).toHaveLength(2) + const applyButtons = screen.getAllByRole('button', { name: /Применить период/ }) + expect(applyButtons).toHaveLength(2) - await user.click(filterButtons[0]) + const toggleButtons = screen.getAllByRole('button', { name: /📅/ }) + expect(toggleButtons).toHaveLength(2) + + await user.click(toggleButtons[0]) expect(screen.getByText('7д')).toBeInTheDocument() expect(screen.getByText('30д')).toBeInTheDocument() @@ -147,4 +157,36 @@ describe('BrokerDashboard', () => { expect(screen.getByText('Всё')).toBeInTheDocument() expect(screen.getByText('Сбросить')).toBeInTheDocument() }) + + it('shows skeleton table while events are loading', () => { + hookMocks.useBrokerEvents.mockReturnValue({ + data: undefined, + isLoading: true, + isError: false, + }) + + renderWithProviders() + + const skeletons = screen.getAllByTestId('dashboard-table-skeleton') + expect(skeletons.length).toBeGreaterThanOrEqual(1) + }) + + it('shows skeleton table while income operations are loading', () => { + hookMocks.useBrokerOperations.mockReturnValue({ + data: undefined, + isLoading: true, + isError: false, + }) + + renderWithProviders() + + const skeletons = screen.getAllByTestId('dashboard-table-skeleton') + expect(skeletons.length).toBeGreaterThanOrEqual(1) + }) + + it('does not show skeleton when data is loaded', () => { + renderWithProviders() + + expect(screen.queryByTestId('dashboard-table-skeleton')).not.toBeInTheDocument() + }) }) diff --git a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.tsx b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.tsx index d4cac32..8315dd2 100644 --- a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.tsx +++ b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.tsx @@ -39,12 +39,10 @@ export function BrokerDashboard({ }) { const [appliedEventFilters, setAppliedEventFilters] = useState(defaultEventsFilters) const [draftEventFilters, setDraftEventFilters] = useState(defaultEventsFilters) - const [eventFilterPanelOpen, setEventFilterPanelOpen] = useState(false) const [eventPage, setEventPage] = useState(1) const [appliedIncomeFilters, setAppliedIncomeFilters] = useState(defaultIncomeFilters) const [draftIncomeFilters, setDraftIncomeFilters] = useState(defaultIncomeFilters) - const [incomeFilterPanelOpen, setIncomeFilterPanelOpen] = useState(false) const incomePagination = useCursorPagination() const analytics = useBrokerAnalytics(accountId) @@ -199,8 +197,6 @@ export function BrokerDashboard({ onDraftToChange={handleDraftEventToChange} onApplyFilters={applyEventFilters} onResetFilters={resetEventFilters} - dateFilterOpen={eventFilterPanelOpen} - onToggleDateFilter={() => setEventFilterPanelOpen((v) => !v)} hasDraftTypes={hasDraftEventTypes} page={eventPage} canGoBack={eventPage > 1} @@ -224,8 +220,6 @@ export function BrokerDashboard({ onDraftToChange={handleDraftIncomeToChange} onApplyFilters={applyIncomeFilters} onResetFilters={resetIncomeFilters} - dateFilterOpen={incomeFilterPanelOpen} - onToggleDateFilter={() => setIncomeFilterPanelOpen((v) => !v)} hasDraftTypes={hasDraftIncomeTypes} pageNumber={incomePagination.pageNumber} canGoBack={incomePagination.pageNumber > 1} diff --git a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardDateFilter.tsx b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardDateFilter.tsx index dd27c8e..08bd69a 100644 --- a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardDateFilter.tsx +++ b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardDateFilter.tsx @@ -1,5 +1,10 @@ -import { Button, Chip, Text } from '@moex-vibe/design-system' -import { Box } from '@mui/material' +import { Chip, Text } from '@moex-vibe/design-system' +import { Box, Popover } from '@mui/material' +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' +import { DateCalendar } from '@mui/x-date-pickers/DateCalendar' +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' +import dayjs from 'dayjs' +import { useCallback, useState } from 'react' import type { DashboardDatePreset } from '../lib/dashboardFilters' const PRESETS: { key: DashboardDatePreset; label: string }[] = [ @@ -21,8 +26,6 @@ type BrokerDashboardDateFilterProps = { onToChange: (value: string) => void onReset: () => void onApply: () => void - isOpen: boolean - onToggle: () => void } export function BrokerDashboardDateFilter({ @@ -36,121 +39,178 @@ export function BrokerDashboardDateFilter({ onToChange, onReset, onApply, - isOpen, - onToggle, }: BrokerDashboardDateFilterProps) { + const [anchorEl, setAnchorEl] = useState(null) + const [selectingStage, setSelectingStage] = useState<'from' | 'to'>('from') + const isOpen = Boolean(anchorEl) + + const handleToggle = useCallback((e: React.MouseEvent) => { + setAnchorEl((prev) => { + if (prev) return null + return e.currentTarget + }) + setSelectingStage('from') + }, []) + + const handleClose = useCallback(() => { + setAnchorEl(null) + setSelectingStage('from') + }, []) + + const handleCalendarChange = useCallback( + (date: dayjs.Dayjs | null) => { + if (!date) return + if (selectingStage === 'from') { + onFromChange(date.format('YYYY-MM-DD')) + setSelectingStage('to') + } else { + if (draftFrom && date.isBefore(dayjs(draftFrom))) { + onFromChange(date.format('YYYY-MM-DD')) + onToChange('') + } else { + onToChange(date.format('YYYY-MM-DD')) + } + setSelectingStage('from') + } + }, + [selectingStage, draftFrom, onFromChange, onToChange], + ) + + function handlePresetClick(p: { key: DashboardDatePreset }) { + onPresetChange(p.key) + setSelectingStage('from') + } + + function handleReset() { + onReset() + setSelectingStage('from') + } + + const selectingLabel = + selectingStage === 'from' ? 'Выберите начало периода' : 'Выберите конец периода' + return ( - - - 📅 Фильтр дат - {appliedLabel && ( - - {appliedLabel} - - )} - - - {isOpen && ( + + - - {PRESETS.map((p) => ( - onPresetChange(p.key)} - /> - ))} - - - onFromChange(e.target.value)} - style={{ - border: '1px solid #ccc', - borderRadius: 6, - padding: '4px 8px', - fontSize: 13, - flex: '0 1 auto', - }} - /> - - — - - onToChange(e.target.value)} - style={{ - border: '1px solid #ccc', - borderRadius: 6, - padding: '4px 8px', - fontSize: 13, - flex: '0 1 auto', - }} - /> + 📅 + {appliedLabel && ( - Сбросить + {appliedLabel} + + )} + {isOpen ? '▲' : '▼'} + + + Применить период + + + + + {PRESETS.map((p) => ( + handlePresetClick(p)} + /> + ))} + + + {selectingLabel} + + + + + Сбросить + - - )} - + + + ) } diff --git a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardEventsCard.tsx b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardEventsCard.tsx index c0e3510..1920e67 100644 --- a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardEventsCard.tsx +++ b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardEventsCard.tsx @@ -7,6 +7,7 @@ import type { DashboardDatePreset, DashboardEventType } from '../lib/dashboardFi import { eventStatusLabel, eventTypeLabel } from '../lib/dashboardFormatters' import { BrokerDashboardCard } from './BrokerDashboardCard' import { BrokerDashboardDateFilter } from './BrokerDashboardDateFilter' +import { BrokerDashboardTableSkeleton } from './BrokerDashboardTableSkeleton' const EVENT_FILTERS: Array<{ type: DashboardEventType @@ -35,8 +36,6 @@ type BrokerDashboardEventsCardProps = { onDraftToChange: (value: string) => void onApplyFilters: () => void onResetFilters: () => void - dateFilterOpen: boolean - onToggleDateFilter: () => void hasDraftTypes: boolean page: number onPreviousPage: () => void @@ -68,8 +67,6 @@ export function BrokerDashboardEventsCard({ onDraftToChange, onApplyFilters, onResetFilters, - dateFilterOpen, - onToggleDateFilter, hasDraftTypes, page, onPreviousPage, @@ -107,8 +104,6 @@ export function BrokerDashboardEventsCard({ onToChange={onDraftToChange} onReset={onResetFilters} onApply={onApplyFilters} - isOpen={dateFilterOpen} - onToggle={onToggleDateFilter} /> } @@ -118,7 +113,7 @@ export function BrokerDashboardEventsCard({ ) : isError ? ( Не удалось загрузить события ) : isLoading ? ( - Загрузка событий… + ) : events.length === 0 ? ( В ближайшем периоде событий нет ) : ( diff --git a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardIncomeCard.tsx b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardIncomeCard.tsx index 2cfd755..606993e 100644 --- a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardIncomeCard.tsx +++ b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardIncomeCard.tsx @@ -7,6 +7,7 @@ import type { DashboardDatePreset, DashboardIncomeType } from '../lib/dashboardF import { getDashboardIncomeRows, sumDashboardIncome } from '../lib/dashboardIncome' import { BrokerDashboardCard } from './BrokerDashboardCard' import { BrokerDashboardDateFilter } from './BrokerDashboardDateFilter' +import { BrokerDashboardTableSkeleton } from './BrokerDashboardTableSkeleton' const INCOME_FILTERS: Array<{ type: DashboardIncomeType @@ -33,8 +34,6 @@ type BrokerDashboardIncomeCardProps = { onDraftToChange: (value: string) => void onApplyFilters: () => void onResetFilters: () => void - dateFilterOpen: boolean - onToggleDateFilter: () => void hasDraftTypes: boolean pageNumber: number canGoBack: boolean @@ -59,8 +58,6 @@ export function BrokerDashboardIncomeCard({ onDraftToChange, onApplyFilters, onResetFilters, - dateFilterOpen, - onToggleDateFilter, hasDraftTypes, pageNumber, canGoBack, @@ -99,8 +96,6 @@ export function BrokerDashboardIncomeCard({ onToChange={onDraftToChange} onReset={onResetFilters} onApply={onApplyFilters} - isOpen={dateFilterOpen} - onToggle={onToggleDateFilter} /> } @@ -110,7 +105,7 @@ export function BrokerDashboardIncomeCard({ ) : isError ? ( Не удалось загрузить доходные операции ) : isLoading ? ( - Загрузка доходов… + ) : rows.length === 0 ? ( Дивидендов и купонов в последних операциях нет ) : ( diff --git a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardTableSkeleton.tsx b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardTableSkeleton.tsx new file mode 100644 index 0000000..6c73682 --- /dev/null +++ b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardTableSkeleton.tsx @@ -0,0 +1,29 @@ +import { Skeleton } from '@moex-vibe/design-system' +import { Box } from '@mui/material' + +type BrokerDashboardTableSkeletonProps = { + rows?: number + columns?: number +} + +export function BrokerDashboardTableSkeleton({ + rows = 10, + columns = 5, +}: BrokerDashboardTableSkeletonProps) { + return ( + + {Array.from({ length: rows }, (_, i) => ( + + {Array.from({ length: columns }, (_, j) => ( + + ))} + + ))} + + ) +} diff --git a/docs/features/broker-dashboard-redesign/plan.md b/docs/features/broker-dashboard-redesign/plan.md index 156fbae..c5d44db 100644 --- a/docs/features/broker-dashboard-redesign/plan.md +++ b/docs/features/broker-dashboard-redesign/plan.md @@ -6,7 +6,7 @@ **Архитектура:** маршрут и FSD-границы остаются прежними. Композиция собирается в `widgets/broker-dashboard`, данные продолжают приходить из существующих `entities/*` hooks. Навигация счёта остаётся в `BrokerAccountLayout`, а dashboard использует только локальные presentation/helpers без выноса брокерской логики в design system. -**Технологии:** React 18, TanStack Router, TanStack Query, MUI через `@moex-vibe/design-system`, Vitest, Testing Library. +**Технологии:** React 18, TanStack Router, TanStack Query, MUI через `@moex-vibe/design-system`, MUI X DateCalendar community (`@mui/x-date-pickers`) с Day.js, Vitest, Testing Library. --- @@ -21,7 +21,7 @@ - Create/Modify: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardHero.tsx` — hero KPI с fallback-значениями и выравниванием `Metric`. - Create/Modify: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardCard.tsx` — локальный паттерн карточки. - Create/Modify: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardEventsCard.tsx` — карточка событий. -- Create: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardDateFilter.tsx` — переиспользуемый фильтр дат с draft/apply поведением. +- Create/Modify: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardDateFilter.tsx` — переиспользуемое управление периодом с draft/apply поведением. - Create/Modify: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardIncomeCard.tsx` — карточка доходов. - Create/Modify: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAnalyticsCard.tsx` — карточка аналитики доходности. - Create/Modify: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAllocationCard.tsx` — карточка аллокации с горизонтальными барами. @@ -31,6 +31,7 @@ - Create/Modify: `apps/frontend/src/widgets/broker-dashboard/lib/dashboardIncome.test.ts` — unit-тесты income helpers. - Create/Modify: `apps/frontend/src/widgets/broker-dashboard/lib/dashboardFilters.ts` — пресеты дат, validate и mapping income types. - Create/Modify: `apps/frontend/src/widgets/broker-dashboard/lib/dashboardFormatters.ts` — локальные formatter/helpers для fallback и event labels. +- Modify: `apps/frontend/package.json` — добавить community-пакет `@mui/x-date-pickers`, если зависимость ещё не подключена; `@mui/x-date-pickers-pro` не добавлять. - Modify: `docs/features/broker-dashboard-redesign/tasks.md` — фиксация статусов выполнения. ### Источники данных @@ -59,16 +60,26 @@ ### 3. События - Типы событий переключаются chip-фильтрами с немедленным применением. -- Диапазон дат редактируется отдельно от применённого состояния: draft state меняется локально, запрос уходит только по `Показать`. +- Диапазон дат по умолчанию: `сегодня - 7 дней` / `сегодня`. +- Диапазон дат редактируется отдельно от применённого состояния: draft state меняется локально, запрос уходит только по действию применения периода. +- В шапке управления периодом не используется слово `Фильтр`; роль управления считывается через иконку календаря, применённый диапазон, раскрытие панели и пресеты. +- Native `` не используется; период выбирается через одно визуальное поле и popover с community `DateCalendar` из `@mui/x-date-pickers`. +- `DateRangePicker` и `@mui/x-date-pickers-pro` не используются; выбор начала/конца периода реализуется локальной логикой dashboard. - При пустом выборе типов запрос отключается, карточка показывает валидационное сообщение. - Пагинация локальная, по 10 событий на страницу, сбрасывается при смене применённых фильтров. +- При загрузке карточка показывает skeleton таблицы событий с колонками дата, инструмент, тип, сумма, статус. ### 4. Доходы - Доходы строятся на существующем endpoint операций только для `OPERATION_TYPE_DIVIDEND`, `OPERATION_TYPE_DIV_EXT`, `OPERATION_TYPE_COUPON`. - Типы доходов переключаются chip-фильтрами с немедленным применением. +- Диапазон дат по умолчанию: `сегодня - 7 дней` / `сегодня`, чтобы не загружать большой объём операций на первом открытии. - Диапазон дат использует тот же draft/apply паттерн, что и события. +- В шапке управления периодом не используется слово `Фильтр`; действие применения периода не называется `Показать`. +- Native `` не используется; период выбирается через одно визуальное поле и popover с community `DateCalendar` из `@mui/x-date-pickers`. +- `DateRangePicker` и `@mui/x-date-pickers-pro` не используются; выбор начала/конца периода реализуется локальной логикой dashboard. - Пагинация cursor-based, размер страницы 10, сбрасывается при смене применённых фильтров. +- При загрузке карточка показывает skeleton таблицы доходов с колонками дата, инструмент, тип, сумма. ### 5. Аналитика и аллокация @@ -118,21 +129,32 @@ - `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardDateFilter.tsx` - `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardEventsCard.tsx` - `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.tsx` +- `apps/frontend/src/widgets/broker-dashboard/lib/dashboardFilters.ts` +- `apps/frontend/package.json` -- [ ] Добавить переиспользуемый `BrokerDashboardDateFilter` с preset chips, полями `from/to`, действиями `Сбросить` и `Показать`. -- [ ] Подключить для событий draft/applied state: типы применяются сразу, даты только по `Показать`. +- [ ] Добавить или подтвердить зависимость community-пакета `@mui/x-date-pickers` и использовать Day.js adapter. +- [ ] Не добавлять `@mui/x-date-pickers-pro` и не использовать `DateRangePicker`. +- [ ] Обновить `BrokerDashboardDateFilter`: убрать слово `Фильтр` из пользовательского текста, заменить `Показать` на понятное действие применения периода и визуально собрать chips, диапазон, сброс и применение в аккуратную шапку. +- [ ] Заменить native date inputs на одно визуальное поле периода, которое открывает MUI `Popover` с community `DateCalendar`. +- [ ] Реализовать локальную логику выбора диапазона: первый клик задаёт начало, второй — конец; если конец раньше начала, диапазон пересобирается от выбранной даты. +- [ ] Настроить default range событий на `сегодня - 7 дней` / `сегодня`. +- [ ] Подключить для событий draft/applied state: типы применяются сразу, даты только по действию применения периода. - [ ] Сохранять локальную пагинацию по 10 событий и сбрасывать её при смене применённых фильтров. -- [ ] Оставить локальные loading/error/empty states внутри карточки. +- [ ] Заменить текстовую загрузку событий на skeleton таблицы. +- [ ] Оставить локальные error/empty states внутри карточки. ### Задача 4: Карточка доходов **Файлы:** - `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardIncomeCard.tsx` - `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.tsx` +- `apps/frontend/src/widgets/broker-dashboard/lib/dashboardFilters.ts` - [ ] Подключить тот же `BrokerDashboardDateFilter` для доходов с draft/applied state. +- [ ] Настроить default range доходов на `сегодня - 7 дней` / `сегодня` вместо периода с начала года. - [ ] Оставить chip-фильтры типов доходов с немедленным применением. - [ ] Сохранить cursor pagination по 10 операций и сбрасывать её при смене применённых фильтров. +- [ ] Заменить текстовую загрузку доходов на skeleton таблицы. - [ ] Если endpoint операций даёт недостаточно релевантных строк для dashboard, зафиксировать ограничение в заметках по реализации, а не расширять backend в рамках этой фичи. ### Задача 5: Карточки аналитики и аллокации @@ -152,6 +174,9 @@ - `docs/features/broker-dashboard-redesign/tasks.md` - [ ] Обновить компонентные тесты dashboard так, чтобы они покрывали текущую композицию, фильтры и основные empty/error states. +- [ ] Обновить тесты, которые ищут `Фильтр дат` или `Показать`, под новые тексты и accessible labels единого поля периода. +- [ ] Добавить/обновить тесты default weekly range для событий и доходов. +- [ ] Добавить/обновить тесты skeleton-таблиц для loading state событий и доходов. - [ ] Прогнать `rtk npm run test:frontend -- --run src/widgets/broker-dashboard`. - [ ] Прогнать `rtk npm run test:frontend`. - [ ] Прогнать `rtk npm run test:design-system && rtk npm run lint -w apps/frontend && rtk npm run build:frontend`. @@ -165,5 +190,6 @@ - Hero KPI и equal-height `Metric`: задача 2. - События с chip filters, date filters и локальной пагинацией: задача 3. - Доходы с chip filters, date filters и cursor pagination: задача 4. +- Community DateCalendar range control, weekly default range и skeleton loading tables: задачи 3, 4 и 6. - Аналитика и аллокация с горизонтальными барами: задача 5. - Ошибки, empty states, тесты и ручная верификация: задача 6. diff --git a/docs/features/broker-dashboard-redesign/spec.md b/docs/features/broker-dashboard-redesign/spec.md index 1fd5590..8ce7f67 100644 --- a/docs/features/broker-dashboard-redesign/spec.md +++ b/docs/features/broker-dashboard-redesign/spec.md @@ -89,7 +89,7 @@ Hero показывает: ### 4. Блок `События` - Блок использует существующий источник `useBrokerEvents(accountId, query)`. -- По умолчанию применяется период `сегодня - 7 дней` / `сегодня + 7 дней` и типы +- По умолчанию применяется период `сегодня - 7 дней` / `сегодня` и типы `dividend,coupon,maturity,offer`, как в существующей вкладке событий. - Блок содержит кликабельные chip-фильтры типов событий: `Дивиденды`, `Купоны`, `Погашения`, `Оферты`. - Пользователь может выбрать несколько типов событий. @@ -98,8 +98,17 @@ Hero показывает: - Изменение chip-фильтров типов событий применяется сразу и возвращает локальную пагинацию на первую страницу. - Блок содержит фильтр периода `from` / `to`. -- Изменение черновых фильтров не запускает запрос до нажатия `Показать`. -- Блок содержит быстрые пресеты периода `7д`, `30д`, `90д`, `1г`, `Всё` и действие `Сбросить`. +- По умолчанию применяется недельный период `сегодня - 7 дней` / `сегодня`. +- Изменение черновых фильтров не запускает запрос до применения периода пользователем. +- В пользовательском тексте шапки периода не используется слово `Фильтр`; UI должен считываться как + управление периодом за счёт иконки календаря, применённого диапазона, пресетов и affordance раскрытия. +- Блок содержит быстрые пресеты периода `7д`, `30д`, `90д`, `1г`, `Всё`, действие `Сбросить` и + основное действие применения периода с более понятным текстом, чем `Показать`. +- Период отображается как одно визуальное поле/кнопка с диапазоном, например `19 июн – 26 июн`. +- По клику на поле периода открывается popover с community-компонентом MUI `DateCalendar` из + `@mui/x-date-pickers` и ручной логикой выбора начала/конца периода. +- `DateRangePicker` и пакет `@mui/x-date-pickers-pro` не используются. +- Native `` не используется. - Применённые фильтры dashboard не обязаны синхронизироваться с URL; URL-синхронизация остаётся обязанностью подробной вкладки `События`. - В dashboard отображается не более 10 событий на странице. @@ -124,10 +133,18 @@ Hero показывает: валидационное сообщение. - Изменение chip-фильтров типов доходов применяется сразу и сбрасывает cursor-пагинацию. - Блок содержит фильтр периода `from` / `to`. -- По умолчанию используется период с начала текущего календарного года до текущей даты, как в разделе - операций. -- Изменение черновых фильтров не запускает запрос до нажатия `Показать`. -- Блок содержит быстрые пресеты периода `7д`, `30д`, `90д`, `1г`, `Всё` и действие `Сбросить`. +- По умолчанию применяется недельный период `сегодня - 7 дней` / `сегодня`, чтобы dashboard не загружал + слишком много операций при первом открытии. +- Изменение черновых фильтров не запускает запрос до применения периода пользователем. +- В пользовательском тексте шапки периода не используется слово `Фильтр`; UI должен считываться как + управление периодом за счёт иконки календаря, применённого диапазона, пресетов и affordance раскрытия. +- Блок содержит быстрые пресеты периода `7д`, `30д`, `90д`, `1г`, `Всё`, действие `Сбросить` и + основное действие применения периода с более понятным текстом, чем `Показать`. +- Период отображается как одно визуальное поле/кнопка с диапазоном, например `19 июн – 26 июн`. +- По клику на поле периода открывается popover с community-компонентом MUI `DateCalendar` из + `@mui/x-date-pickers` и ручной логикой выбора начала/конца периода. +- `DateRangePicker` и пакет `@mui/x-date-pickers-pro` не используются. +- Native `` не используется. - Применённые фильтры dashboard не обязаны синхронизироваться с URL; URL-синхронизация остаётся обязанностью подробных разделов. - Для блока используется cursor-пагинация existing operations endpoint с размером страницы 10. @@ -162,6 +179,8 @@ Hero показывает: - Первичная загрузка portfolio показывает dashboard skeleton соответствующей формы. - Ошибка portfolio показывает ошибку overview, потому что без portfolio dashboard не имеет основного контекста. +- Загрузка событий и доходов внутри карточек показывает skeleton таблицы соответствующей структуры, а не + только текстовую строку загрузки. - Ошибка событий, доходов или analytics отображается внутри соответствующей карточки. - Пустые события, пустые доходы и пустая analytics имеют отдельные понятные сообщения. - Недоступные отдельные значения отображаются как `—`, не подменяются нулём. @@ -194,8 +213,15 @@ Hero показывает: - Все Metric-блоки hero имеют одинаковую высоту; supportingText не создаёт перекоса. - Блок `События` использует существующие events data и показывает дату, инструмент, тип, сумму и статус. - Блок `События` поддерживает multi-select chip-фильтр типов и локальную пагинацию по 10 событий. +- Блок `События` по умолчанию запрашивает период `сегодня - 7 дней` / `сегодня` и использует одно поле + периода с popover-календарём на базе community `DateCalendar`. - Блок `Доходы` показывает доходные операции дивидендов и купонов и итог по отображаемым строкам. - Блок `Доходы` поддерживает multi-select chip-фильтр типов и cursor-пагинацию по 10 операций. +- Блок `Доходы` по умолчанию запрашивает период `сегодня - 7 дней` / `сегодня` и использует одно поле + периода с popover-календарём на базе community `DateCalendar`. +- В шапке управления периодом не отображается слово `Фильтр`, а действие применения периода не называется + `Показать`. +- Загрузка событий и доходов отображается skeleton-таблицей. - Блок `Аналитика доходности` показывает данные существующего analytics endpoint. - Блок `Аллокация` показывает горизонтальные бары секторов с названием, долей и суммой, а также итоговую стоимость портфеля. diff --git a/docs/features/broker-dashboard-redesign/tasks.md b/docs/features/broker-dashboard-redesign/tasks.md index 8423510..816167c 100644 --- a/docs/features/broker-dashboard-redesign/tasks.md +++ b/docs/features/broker-dashboard-redesign/tasks.md @@ -40,15 +40,23 @@ - [x] Добавить unit/component tests для helpers и базовой dashboard composition. - [x] Выровнять hero KPI: все Metric одной высоты, supportingText не раздвигает "Доходность" выше соседей. - [x] Заменить donut-диаграмму аллокации на горизонтальные бары в `BrokerDashboardAllocationCard`. +- [x] Убрать слово `Фильтр` из пользовательского текста шапки периода в dashboard-карточках. +- [x] Переименовать действие `Показать` в управлении периодом и визуально улучшить шапку фильтров. +- [x] Заменить native date inputs на одно поле периода с popover и community `DateCalendar` из `@mui/x-date-pickers`. +- [x] Не использовать `@mui/x-date-pickers-pro` и `DateRangePicker`. +- [x] Настроить default range событий и доходов на `сегодня - 7 дней` / `сегодня`. +- [x] Заменить текстовую загрузку `События` на skeleton таблицы. +- [x] Заменить текстовую загрузку `Доходы` на skeleton таблицы. +- [x] Обновить component tests под новые тексты, единое поле периода, popover-календарь и skeleton loading states. - [ ] Проверить desktop layout `/broker/2084014113`. - [ ] Проверить mobile layout `/broker/2084014113`. ## Definition of Done -- [ ] `rtk npm run test:frontend` проходит. -- [ ] `rtk npm run test:design-system` проходит. -- [ ] `rtk npm run lint -w apps/frontend` проходит. -- [ ] `rtk npm run build:frontend` проходит. +- [x] `rtk npm run test:frontend` проходит. +- [x] `rtk npm run test:design-system` проходит. +- [x] `rtk npm run lint -w apps/frontend` проходит. +- [x] `rtk npm run build:frontend` проходит. - [ ] Dashboard соответствует acceptance criteria из `spec.md`. - [x] Существующие вкладки `Акции`, `Облигации`, `Операции`, `События`, `Аналитика` остаются доступны. -- [ ] `graphify update .` выполнен после code changes. +- [x] `graphify update .` выполнен после code changes. diff --git a/package-lock.json b/package-lock.json index f32785e..b8aa3c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -92,6 +92,7 @@ "@moex-vibe/design-system": "*", "@mui/icons-material": "^6.5.0", "@mui/material": "^6.5.0", + "@mui/x-date-pickers": "^7.29.4", "@tanstack/react-query": "^5.20.0", "@tanstack/react-router": "^1.170.16", "@tanstack/react-table": "^8.21.3", @@ -7426,6 +7427,92 @@ "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", "license": "MIT" }, + "node_modules/@mui/x-date-pickers": { + "version": "7.29.4", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.29.4.tgz", + "integrity": "sha512-wJ3tsqk/y6dp+mXGtT9czciAMEO5Zr3IIAHg9x6IL0Eqanqy0N3chbmQQZv3iq0m2qUpQDLvZ4utZBUTJdjNzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0", + "@mui/x-internals": "7.29.0", + "@types/react-transition-group": "^4.4.11", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", + "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", + "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0", + "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2 || ^3.0.0", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, + "node_modules/@mui/x-internals": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.29.0.tgz", + "integrity": "sha512-+Gk6VTZIFD70XreWvdXBwKd8GZ2FlSCuecQFzm6znwqXg1ZsndavrhG9tkxpxo2fM1Zf7Tk8+HcOO0hCbhTQFA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",