feat: replace native date inputs with single DateCalendar field in broker dashboard

- 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
This commit is contained in:
Sergey Krylov 2026-06-27 11:48:10 +03:00
parent 0116c34a9f
commit d4d1dd980e
12 changed files with 420 additions and 158 deletions

View File

@ -24,6 +24,7 @@
"@moex-vibe/design-system": "*", "@moex-vibe/design-system": "*",
"@mui/icons-material": "^6.5.0", "@mui/icons-material": "^6.5.0",
"@mui/material": "^6.5.0", "@mui/material": "^6.5.0",
"@mui/x-date-pickers": "^7.29.4",
"@tanstack/react-query": "^5.20.0", "@tanstack/react-query": "^5.20.0",
"@tanstack/react-router": "^1.170.16", "@tanstack/react-router": "^1.170.16",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
@ -43,7 +44,6 @@
"@biomejs/biome": "^2.5.0", "@biomejs/biome": "^2.5.0",
"@conarti/eslint-plugin-feature-sliced": "^1.0.5", "@conarti/eslint-plugin-feature-sliced": "^1.0.5",
"@tanstack/router-devtools": "^1.167.0", "@tanstack/router-devtools": "^1.167.0",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",

View File

@ -18,7 +18,7 @@ export function defaultEventsFilters(): DashboardFilterState<DashboardEventType>
const now = dayjs() const now = dayjs()
return { return {
from: now.subtract(7, 'day').format('YYYY-MM-DD'), 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], types: [...DASHBOARD_EVENT_TYPES],
preset: '7d', preset: '7d',
} }
@ -27,10 +27,10 @@ export function defaultEventsFilters(): DashboardFilterState<DashboardEventType>
export function defaultIncomeFilters(): DashboardFilterState<DashboardIncomeType> { export function defaultIncomeFilters(): DashboardFilterState<DashboardIncomeType> {
const now = dayjs() const now = dayjs()
return { return {
from: now.startOf('year').format('YYYY-MM-DD'), from: now.subtract(7, 'day').format('YYYY-MM-DD'),
to: now.format('YYYY-MM-DD'), to: now.format('YYYY-MM-DD'),
types: [...DASHBOARD_INCOME_TYPES], types: [...DASHBOARD_INCOME_TYPES],
preset: '1y', preset: '7d',
} }
} }

View File

@ -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 { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event' import userEvent from '@testing-library/user-event'
import type { ReactNode } from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { BrokerPortfolio } from '@/shared/api' import type { BrokerPortfolio } from '@/shared/api'
import { BrokerDashboard } from './BrokerDashboard' import { BrokerDashboard } from './BrokerDashboard'
@ -67,6 +70,10 @@ const portfolio: BrokerPortfolio = {
asOf: '2026-06-26T00:00:00.000Z', asOf: '2026-06-26T00:00:00.000Z',
} }
function renderWithProviders(ui: ReactNode) {
return render(<LocalizationProvider dateAdapter={AdapterDayjs}>{ui}</LocalizationProvider>)
}
describe('BrokerDashboard', () => { describe('BrokerDashboard', () => {
beforeEach(() => { beforeEach(() => {
hookMocks.useBrokerEvents.mockReturnValue({ hookMocks.useBrokerEvents.mockReturnValue({
@ -88,7 +95,7 @@ describe('BrokerDashboard', () => {
}) })
it('renders the dashboard sections', () => { it('renders the dashboard sections', () => {
render(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />) renderWithProviders(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
expect(screen.getByText('Основной счёт')).toBeInTheDocument() expect(screen.getByText('Основной счёт')).toBeInTheDocument()
expect(screen.getByText('События')).toBeInTheDocument() expect(screen.getByText('События')).toBeInTheDocument()
@ -99,7 +106,7 @@ describe('BrokerDashboard', () => {
it('applies event type chips immediately to useBrokerEvents', async () => { it('applies event type chips immediately to useBrokerEvents', async () => {
const user = userEvent.setup() const user = userEvent.setup()
render(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />) renderWithProviders(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
const couponChips = screen.getAllByRole('button', { name: 'Купоны' }) const couponChips = screen.getAllByRole('button', { name: 'Купоны' })
await user.click(couponChips[0]) await user.click(couponChips[0])
@ -115,7 +122,7 @@ describe('BrokerDashboard', () => {
it('applies income type chips immediately to useBrokerOperations', async () => { it('applies income type chips immediately to useBrokerOperations', async () => {
const user = userEvent.setup() const user = userEvent.setup()
render(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />) renderWithProviders(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
const couponChips = screen.getAllByRole('button', { name: 'Купоны' }) const couponChips = screen.getAllByRole('button', { name: 'Купоны' })
await user.click(couponChips[1]) 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() const user = userEvent.setup()
render(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />) renderWithProviders(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
const filterButtons = screen.getAllByRole('button', { name: /Фильтр дат/ }) const applyButtons = screen.getAllByRole('button', { name: /Применить период/ })
expect(filterButtons).toHaveLength(2) 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('7д')).toBeInTheDocument()
expect(screen.getByText('30д')).toBeInTheDocument() expect(screen.getByText('30д')).toBeInTheDocument()
@ -147,4 +157,36 @@ describe('BrokerDashboard', () => {
expect(screen.getByText('Всё')).toBeInTheDocument() expect(screen.getByText('Всё')).toBeInTheDocument()
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(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
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(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
const skeletons = screen.getAllByTestId('dashboard-table-skeleton')
expect(skeletons.length).toBeGreaterThanOrEqual(1)
})
it('does not show skeleton when data is loaded', () => {
renderWithProviders(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
expect(screen.queryByTestId('dashboard-table-skeleton')).not.toBeInTheDocument()
})
}) })

View File

@ -39,12 +39,10 @@ export function BrokerDashboard({
}) { }) {
const [appliedEventFilters, setAppliedEventFilters] = useState(defaultEventsFilters) const [appliedEventFilters, setAppliedEventFilters] = useState(defaultEventsFilters)
const [draftEventFilters, setDraftEventFilters] = useState(defaultEventsFilters) const [draftEventFilters, setDraftEventFilters] = useState(defaultEventsFilters)
const [eventFilterPanelOpen, setEventFilterPanelOpen] = useState(false)
const [eventPage, setEventPage] = useState(1) const [eventPage, setEventPage] = useState(1)
const [appliedIncomeFilters, setAppliedIncomeFilters] = useState(defaultIncomeFilters) const [appliedIncomeFilters, setAppliedIncomeFilters] = useState(defaultIncomeFilters)
const [draftIncomeFilters, setDraftIncomeFilters] = useState(defaultIncomeFilters) const [draftIncomeFilters, setDraftIncomeFilters] = useState(defaultIncomeFilters)
const [incomeFilterPanelOpen, setIncomeFilterPanelOpen] = useState(false)
const incomePagination = useCursorPagination() const incomePagination = useCursorPagination()
const analytics = useBrokerAnalytics(accountId) const analytics = useBrokerAnalytics(accountId)
@ -199,8 +197,6 @@ export function BrokerDashboard({
onDraftToChange={handleDraftEventToChange} onDraftToChange={handleDraftEventToChange}
onApplyFilters={applyEventFilters} onApplyFilters={applyEventFilters}
onResetFilters={resetEventFilters} onResetFilters={resetEventFilters}
dateFilterOpen={eventFilterPanelOpen}
onToggleDateFilter={() => setEventFilterPanelOpen((v) => !v)}
hasDraftTypes={hasDraftEventTypes} hasDraftTypes={hasDraftEventTypes}
page={eventPage} page={eventPage}
canGoBack={eventPage > 1} canGoBack={eventPage > 1}
@ -224,8 +220,6 @@ export function BrokerDashboard({
onDraftToChange={handleDraftIncomeToChange} onDraftToChange={handleDraftIncomeToChange}
onApplyFilters={applyIncomeFilters} onApplyFilters={applyIncomeFilters}
onResetFilters={resetIncomeFilters} onResetFilters={resetIncomeFilters}
dateFilterOpen={incomeFilterPanelOpen}
onToggleDateFilter={() => setIncomeFilterPanelOpen((v) => !v)}
hasDraftTypes={hasDraftIncomeTypes} hasDraftTypes={hasDraftIncomeTypes}
pageNumber={incomePagination.pageNumber} pageNumber={incomePagination.pageNumber}
canGoBack={incomePagination.pageNumber > 1} canGoBack={incomePagination.pageNumber > 1}

View File

@ -1,5 +1,10 @@
import { Button, Chip, Text } from '@moex-vibe/design-system' import { Chip, Text } from '@moex-vibe/design-system'
import { Box } from '@mui/material' 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' import type { DashboardDatePreset } from '../lib/dashboardFilters'
const PRESETS: { key: DashboardDatePreset; label: string }[] = [ const PRESETS: { key: DashboardDatePreset; label: string }[] = [
@ -21,8 +26,6 @@ type BrokerDashboardDateFilterProps = {
onToChange: (value: string) => void onToChange: (value: string) => void
onReset: () => void onReset: () => void
onApply: () => void onApply: () => void
isOpen: boolean
onToggle: () => void
} }
export function BrokerDashboardDateFilter({ export function BrokerDashboardDateFilter({
@ -36,15 +39,63 @@ export function BrokerDashboardDateFilter({
onToChange, onToChange,
onReset, onReset,
onApply, onApply,
isOpen,
onToggle,
}: BrokerDashboardDateFilterProps) { }: BrokerDashboardDateFilterProps) {
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
const [selectingStage, setSelectingStage] = useState<'from' | 'to'>('from')
const isOpen = Boolean(anchorEl)
const handleToggle = useCallback((e: React.MouseEvent<HTMLElement>) => {
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 ( return (
<LocalizationProvider dateAdapter={AdapterDayjs}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
<Box <Box
component="button" component="button"
type="button" type="button"
onClick={onToggle} onClick={handleToggle}
sx={{ sx={{
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
@ -60,7 +111,7 @@ export function BrokerDashboardDateFilter({
'&:hover': { bgcolor: 'grey.700' }, '&:hover': { bgcolor: 'grey.700' },
}} }}
> >
📅 Фильтр дат 📅
{appliedLabel && ( {appliedLabel && (
<Box <Box
sx={{ sx={{
@ -76,22 +127,38 @@ export function BrokerDashboardDateFilter({
{appliedLabel} {appliedLabel}
</Box> </Box>
)} )}
<Box sx={{ fontSize: 10, ml: 0.25, color: 'grey.400' }}>{isOpen ? '▲' : '▼'}</Box>
</Box> </Box>
<Button variant="primary" size="small" onClick={onApply} disabled={!hasDraftTypes}>
Показать
</Button>
{isOpen && (
<Box <Box
component="button"
type="button"
onClick={onApply}
disabled={!hasDraftTypes}
sx={{ sx={{
width: '100%', display: 'inline-flex',
mt: 0.5, alignItems: 'center',
p: 1.5, gap: 1,
border: '1px solid', bgcolor: hasDraftTypes ? 'primary.main' : 'grey.400',
borderColor: 'divider', color: 'common.white',
borderRadius: 2, border: 'none',
bgcolor: 'grey.50', borderRadius: 1.5,
px: 1.5,
py: 0.75,
fontSize: 13,
cursor: hasDraftTypes ? 'pointer' : 'default',
'&:hover': hasDraftTypes ? { bgcolor: 'primary.dark' } : {},
}} }}
> >
Применить период
</Box>
<Popover
open={isOpen}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
sx={{ mt: 0.5 }}
>
<Box sx={{ p: 2, minWidth: 320 }}>
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mb: 1 }}> <Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mb: 1 }}>
{PRESETS.map((p) => ( {PRESETS.map((p) => (
<Chip <Chip
@ -99,42 +166,34 @@ export function BrokerDashboardDateFilter({
label={p.label} label={p.label}
tone={p.key === preset ? 'primary' : 'neutral'} tone={p.key === preset ? 'primary' : 'neutral'}
selected={p.key === preset} selected={p.key === preset}
onClick={() => onPresetChange(p.key)} onClick={() => handlePresetClick(p)}
/> />
))} ))}
</Box> </Box>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap' }}> <Text variant="caption" tone="secondary" sx={{ mb: 1, display: 'block' }}>
<input {selectingLabel}
type="date"
value={draftFrom}
onChange={(e) => onFromChange(e.target.value)}
style={{
border: '1px solid #ccc',
borderRadius: 6,
padding: '4px 8px',
fontSize: 13,
flex: '0 1 auto',
}}
/>
<Text variant="body" tone="muted">
</Text> </Text>
<input <DateCalendar
type="date" value={draftFrom ? dayjs(draftFrom) : null}
value={draftTo} onChange={handleCalendarChange}
onChange={(e) => onToChange(e.target.value)} sx={{
style={{ '& .MuiPickersDay-root': {
border: '1px solid #ccc', ...(draftFrom && draftTo
borderRadius: 6, ? {
padding: '4px 8px', '&.Mui-selected': {
fontSize: 13, bgcolor: 'primary.main',
flex: '0 1 auto', color: 'common.white',
},
}
: {}),
},
}} }}
/> />
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1 }}>
<Box <Box
component="button" component="button"
type="button" type="button"
onClick={onReset} onClick={handleReset}
sx={{ sx={{
background: 'none', background: 'none',
border: 'none', border: 'none',
@ -150,7 +209,8 @@ export function BrokerDashboardDateFilter({
</Box> </Box>
</Box> </Box>
</Box> </Box>
)} </Popover>
</Box> </Box>
</LocalizationProvider>
) )
} }

View File

@ -7,6 +7,7 @@ import type { DashboardDatePreset, DashboardEventType } from '../lib/dashboardFi
import { eventStatusLabel, eventTypeLabel } from '../lib/dashboardFormatters' import { eventStatusLabel, eventTypeLabel } from '../lib/dashboardFormatters'
import { BrokerDashboardCard } from './BrokerDashboardCard' import { BrokerDashboardCard } from './BrokerDashboardCard'
import { BrokerDashboardDateFilter } from './BrokerDashboardDateFilter' import { BrokerDashboardDateFilter } from './BrokerDashboardDateFilter'
import { BrokerDashboardTableSkeleton } from './BrokerDashboardTableSkeleton'
const EVENT_FILTERS: Array<{ const EVENT_FILTERS: Array<{
type: DashboardEventType type: DashboardEventType
@ -35,8 +36,6 @@ type BrokerDashboardEventsCardProps = {
onDraftToChange: (value: string) => void onDraftToChange: (value: string) => void
onApplyFilters: () => void onApplyFilters: () => void
onResetFilters: () => void onResetFilters: () => void
dateFilterOpen: boolean
onToggleDateFilter: () => void
hasDraftTypes: boolean hasDraftTypes: boolean
page: number page: number
onPreviousPage: () => void onPreviousPage: () => void
@ -68,8 +67,6 @@ export function BrokerDashboardEventsCard({
onDraftToChange, onDraftToChange,
onApplyFilters, onApplyFilters,
onResetFilters, onResetFilters,
dateFilterOpen,
onToggleDateFilter,
hasDraftTypes, hasDraftTypes,
page, page,
onPreviousPage, onPreviousPage,
@ -107,8 +104,6 @@ export function BrokerDashboardEventsCard({
onToChange={onDraftToChange} onToChange={onDraftToChange}
onReset={onResetFilters} onReset={onResetFilters}
onApply={onApplyFilters} onApply={onApplyFilters}
isOpen={dateFilterOpen}
onToggle={onToggleDateFilter}
/> />
</Box> </Box>
} }
@ -118,7 +113,7 @@ export function BrokerDashboardEventsCard({
) : isError ? ( ) : isError ? (
<Text tone="negative">Не удалось загрузить события</Text> <Text tone="negative">Не удалось загрузить события</Text>
) : isLoading ? ( ) : isLoading ? (
<Text tone="muted">Загрузка событий</Text> <BrokerDashboardTableSkeleton rows={5} columns={5} />
) : events.length === 0 ? ( ) : events.length === 0 ? (
<Text tone="muted">В ближайшем периоде событий нет</Text> <Text tone="muted">В ближайшем периоде событий нет</Text>
) : ( ) : (

View File

@ -7,6 +7,7 @@ import type { DashboardDatePreset, DashboardIncomeType } from '../lib/dashboardF
import { getDashboardIncomeRows, sumDashboardIncome } from '../lib/dashboardIncome' import { getDashboardIncomeRows, sumDashboardIncome } from '../lib/dashboardIncome'
import { BrokerDashboardCard } from './BrokerDashboardCard' import { BrokerDashboardCard } from './BrokerDashboardCard'
import { BrokerDashboardDateFilter } from './BrokerDashboardDateFilter' import { BrokerDashboardDateFilter } from './BrokerDashboardDateFilter'
import { BrokerDashboardTableSkeleton } from './BrokerDashboardTableSkeleton'
const INCOME_FILTERS: Array<{ const INCOME_FILTERS: Array<{
type: DashboardIncomeType type: DashboardIncomeType
@ -33,8 +34,6 @@ type BrokerDashboardIncomeCardProps = {
onDraftToChange: (value: string) => void onDraftToChange: (value: string) => void
onApplyFilters: () => void onApplyFilters: () => void
onResetFilters: () => void onResetFilters: () => void
dateFilterOpen: boolean
onToggleDateFilter: () => void
hasDraftTypes: boolean hasDraftTypes: boolean
pageNumber: number pageNumber: number
canGoBack: boolean canGoBack: boolean
@ -59,8 +58,6 @@ export function BrokerDashboardIncomeCard({
onDraftToChange, onDraftToChange,
onApplyFilters, onApplyFilters,
onResetFilters, onResetFilters,
dateFilterOpen,
onToggleDateFilter,
hasDraftTypes, hasDraftTypes,
pageNumber, pageNumber,
canGoBack, canGoBack,
@ -99,8 +96,6 @@ export function BrokerDashboardIncomeCard({
onToChange={onDraftToChange} onToChange={onDraftToChange}
onReset={onResetFilters} onReset={onResetFilters}
onApply={onApplyFilters} onApply={onApplyFilters}
isOpen={dateFilterOpen}
onToggle={onToggleDateFilter}
/> />
</Box> </Box>
} }
@ -110,7 +105,7 @@ export function BrokerDashboardIncomeCard({
) : isError ? ( ) : isError ? (
<Text tone="negative">Не удалось загрузить доходные операции</Text> <Text tone="negative">Не удалось загрузить доходные операции</Text>
) : isLoading ? ( ) : isLoading ? (
<Text tone="muted">Загрузка доходов</Text> <BrokerDashboardTableSkeleton rows={5} columns={4} />
) : rows.length === 0 ? ( ) : rows.length === 0 ? (
<Text tone="muted">Дивидендов и купонов в последних операциях нет</Text> <Text tone="muted">Дивидендов и купонов в последних операциях нет</Text>
) : ( ) : (

View File

@ -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 (
<Box sx={{ display: 'grid', gap: 1.5, py: 1 }} data-testid="dashboard-table-skeleton">
{Array.from({ length: rows }, (_, i) => (
<Box key={i} sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
{Array.from({ length: columns }, (_, j) => (
<Skeleton
key={j}
height={16}
width={j === 0 ? 80 : j === columns - 1 ? 60 : 100}
shape="rounded"
/>
))}
</Box>
))}
</Box>
)
}

View File

@ -6,7 +6,7 @@
**Архитектура:** маршрут и FSD-границы остаются прежними. Композиция собирается в `widgets/broker-dashboard`, данные продолжают приходить из существующих `entities/*` hooks. Навигация счёта остаётся в `BrokerAccountLayout`, а dashboard использует только локальные presentation/helpers без выноса брокерской логики в design system. **Архитектура:** маршрут и 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/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/BrokerDashboardCard.tsx` — локальный паттерн карточки.
- Create/Modify: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardEventsCard.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/BrokerDashboardIncomeCard.tsx` — карточка доходов.
- Create/Modify: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAnalyticsCard.tsx` — карточка аналитики доходности. - Create/Modify: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAnalyticsCard.tsx` — карточка аналитики доходности.
- Create/Modify: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAllocationCard.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/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/dashboardFilters.ts` — пресеты дат, validate и mapping income types.
- Create/Modify: `apps/frontend/src/widgets/broker-dashboard/lib/dashboardFormatters.ts` — локальные formatter/helpers для fallback и event labels. - 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` — фиксация статусов выполнения. - Modify: `docs/features/broker-dashboard-redesign/tasks.md` — фиксация статусов выполнения.
### Источники данных ### Источники данных
@ -59,16 +60,26 @@
### 3. События ### 3. События
- Типы событий переключаются chip-фильтрами с немедленным применением. - Типы событий переключаются chip-фильтрами с немедленным применением.
- Диапазон дат редактируется отдельно от применённого состояния: draft state меняется локально, запрос уходит только по `Показать`. - Диапазон дат по умолчанию: `сегодня - 7 дней` / `сегодня`.
- Диапазон дат редактируется отдельно от применённого состояния: draft state меняется локально, запрос уходит только по действию применения периода.
- В шапке управления периодом не используется слово `Фильтр`; роль управления считывается через иконку календаря, применённый диапазон, раскрытие панели и пресеты.
- Native `<input type="date">` не используется; период выбирается через одно визуальное поле и popover с community `DateCalendar` из `@mui/x-date-pickers`.
- `DateRangePicker` и `@mui/x-date-pickers-pro` не используются; выбор начала/конца периода реализуется локальной логикой dashboard.
- При пустом выборе типов запрос отключается, карточка показывает валидационное сообщение. - При пустом выборе типов запрос отключается, карточка показывает валидационное сообщение.
- Пагинация локальная, по 10 событий на страницу, сбрасывается при смене применённых фильтров. - Пагинация локальная, по 10 событий на страницу, сбрасывается при смене применённых фильтров.
- При загрузке карточка показывает skeleton таблицы событий с колонками дата, инструмент, тип, сумма, статус.
### 4. Доходы ### 4. Доходы
- Доходы строятся на существующем endpoint операций только для `OPERATION_TYPE_DIVIDEND`, `OPERATION_TYPE_DIV_EXT`, `OPERATION_TYPE_COUPON`. - Доходы строятся на существующем endpoint операций только для `OPERATION_TYPE_DIVIDEND`, `OPERATION_TYPE_DIV_EXT`, `OPERATION_TYPE_COUPON`.
- Типы доходов переключаются chip-фильтрами с немедленным применением. - Типы доходов переключаются chip-фильтрами с немедленным применением.
- Диапазон дат по умолчанию: `сегодня - 7 дней` / `сегодня`, чтобы не загружать большой объём операций на первом открытии.
- Диапазон дат использует тот же draft/apply паттерн, что и события. - Диапазон дат использует тот же draft/apply паттерн, что и события.
- В шапке управления периодом не используется слово `Фильтр`; действие применения периода не называется `Показать`.
- Native `<input type="date">` не используется; период выбирается через одно визуальное поле и popover с community `DateCalendar` из `@mui/x-date-pickers`.
- `DateRangePicker` и `@mui/x-date-pickers-pro` не используются; выбор начала/конца периода реализуется локальной логикой dashboard.
- Пагинация cursor-based, размер страницы 10, сбрасывается при смене применённых фильтров. - Пагинация cursor-based, размер страницы 10, сбрасывается при смене применённых фильтров.
- При загрузке карточка показывает skeleton таблицы доходов с колонками дата, инструмент, тип, сумма.
### 5. Аналитика и аллокация ### 5. Аналитика и аллокация
@ -118,21 +129,32 @@
- `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardDateFilter.tsx` - `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/BrokerDashboardEventsCard.tsx`
- `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.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`, действиями `Сбросить` и `Показать`. - [ ] Добавить или подтвердить зависимость community-пакета `@mui/x-date-pickers` и использовать Day.js adapter.
- [ ] Подключить для событий draft/applied state: типы применяются сразу, даты только по `Показать`. - [ ] Не добавлять `@mui/x-date-pickers-pro` и не использовать `DateRangePicker`.
- [ ] Обновить `BrokerDashboardDateFilter`: убрать слово `Фильтр` из пользовательского текста, заменить `Показать` на понятное действие применения периода и визуально собрать chips, диапазон, сброс и применение в аккуратную шапку.
- [ ] Заменить native date inputs на одно визуальное поле периода, которое открывает MUI `Popover` с community `DateCalendar`.
- [ ] Реализовать локальную логику выбора диапазона: первый клик задаёт начало, второй — конец; если конец раньше начала, диапазон пересобирается от выбранной даты.
- [ ] Настроить default range событий на `сегодня - 7 дней` / `сегодня`.
- [ ] Подключить для событий draft/applied state: типы применяются сразу, даты только по действию применения периода.
- [ ] Сохранять локальную пагинацию по 10 событий и сбрасывать её при смене применённых фильтров. - [ ] Сохранять локальную пагинацию по 10 событий и сбрасывать её при смене применённых фильтров.
- [ ] Оставить локальные loading/error/empty states внутри карточки. - [ ] Заменить текстовую загрузку событий на skeleton таблицы.
- [ ] Оставить локальные error/empty states внутри карточки.
### Задача 4: Карточка доходов ### Задача 4: Карточка доходов
**Файлы:** **Файлы:**
- `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardIncomeCard.tsx` - `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardIncomeCard.tsx`
- `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.tsx` - `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.tsx`
- `apps/frontend/src/widgets/broker-dashboard/lib/dashboardFilters.ts`
- [ ] Подключить тот же `BrokerDashboardDateFilter` для доходов с draft/applied state. - [ ] Подключить тот же `BrokerDashboardDateFilter` для доходов с draft/applied state.
- [ ] Настроить default range доходов на `сегодня - 7 дней` / `сегодня` вместо периода с начала года.
- [ ] Оставить chip-фильтры типов доходов с немедленным применением. - [ ] Оставить chip-фильтры типов доходов с немедленным применением.
- [ ] Сохранить cursor pagination по 10 операций и сбрасывать её при смене применённых фильтров. - [ ] Сохранить cursor pagination по 10 операций и сбрасывать её при смене применённых фильтров.
- [ ] Заменить текстовую загрузку доходов на skeleton таблицы.
- [ ] Если endpoint операций даёт недостаточно релевантных строк для dashboard, зафиксировать ограничение в заметках по реализации, а не расширять backend в рамках этой фичи. - [ ] Если endpoint операций даёт недостаточно релевантных строк для dashboard, зафиксировать ограничение в заметках по реализации, а не расширять backend в рамках этой фичи.
### Задача 5: Карточки аналитики и аллокации ### Задача 5: Карточки аналитики и аллокации
@ -152,6 +174,9 @@
- `docs/features/broker-dashboard-redesign/tasks.md` - `docs/features/broker-dashboard-redesign/tasks.md`
- [ ] Обновить компонентные тесты dashboard так, чтобы они покрывали текущую композицию, фильтры и основные empty/error states. - [ ] Обновить компонентные тесты 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 -- --run src/widgets/broker-dashboard`.
- [ ] Прогнать `rtk npm run test:frontend`. - [ ] Прогнать `rtk npm run test:frontend`.
- [ ] Прогнать `rtk npm run test:design-system && rtk npm run lint -w apps/frontend && rtk npm run build: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. - Hero KPI и equal-height `Metric`: задача 2.
- События с chip filters, date filters и локальной пагинацией: задача 3. - События с chip filters, date filters и локальной пагинацией: задача 3.
- Доходы с chip filters, date filters и cursor pagination: задача 4. - Доходы с chip filters, date filters и cursor pagination: задача 4.
- Community DateCalendar range control, weekly default range и skeleton loading tables: задачи 3, 4 и 6.
- Аналитика и аллокация с горизонтальными барами: задача 5. - Аналитика и аллокация с горизонтальными барами: задача 5.
- Ошибки, empty states, тесты и ручная верификация: задача 6. - Ошибки, empty states, тесты и ручная верификация: задача 6.

View File

@ -89,7 +89,7 @@ Hero показывает:
### 4. Блок `События` ### 4. Блок `События`
- Блок использует существующий источник `useBrokerEvents(accountId, query)`. - Блок использует существующий источник `useBrokerEvents(accountId, query)`.
- По умолчанию применяется период `сегодня - 7 дней` / `сегодня + 7 дней` и типы - По умолчанию применяется период `сегодня - 7 дней` / `сегодня` и типы
`dividend,coupon,maturity,offer`, как в существующей вкладке событий. `dividend,coupon,maturity,offer`, как в существующей вкладке событий.
- Блок содержит кликабельные chip-фильтры типов событий: `Дивиденды`, `Купоны`, `Погашения`, `Оферты`. - Блок содержит кликабельные chip-фильтры типов событий: `Дивиденды`, `Купоны`, `Погашения`, `Оферты`.
- Пользователь может выбрать несколько типов событий. - Пользователь может выбрать несколько типов событий.
@ -98,8 +98,17 @@ Hero показывает:
- Изменение chip-фильтров типов событий применяется сразу и возвращает локальную пагинацию на первую - Изменение chip-фильтров типов событий применяется сразу и возвращает локальную пагинацию на первую
страницу. страницу.
- Блок содержит фильтр периода `from` / `to`. - Блок содержит фильтр периода `from` / `to`.
- Изменение черновых фильтров не запускает запрос до нажатия `Показать`. - По умолчанию применяется недельный период `сегодня - 7 дней` / `сегодня`.
- Блок содержит быстрые пресеты периода `7д`, `30д`, `90д`, `1г`, `Всё` и действие `Сбросить`. - Изменение черновых фильтров не запускает запрос до применения периода пользователем.
- В пользовательском тексте шапки периода не используется слово `Фильтр`; UI должен считываться как
управление периодом за счёт иконки календаря, применённого диапазона, пресетов и affordance раскрытия.
- Блок содержит быстрые пресеты периода `7д`, `30д`, `90д`, `1г`, `Всё`, действие `Сбросить` и
основное действие применения периода с более понятным текстом, чем `Показать`.
- Период отображается как одно визуальное поле/кнопка с диапазоном, например `19 июн 26 июн`.
- По клику на поле периода открывается popover с community-компонентом MUI `DateCalendar` из
`@mui/x-date-pickers` и ручной логикой выбора начала/конца периода.
- `DateRangePicker` и пакет `@mui/x-date-pickers-pro` не используются.
- Native `<input type="date">` не используется.
- Применённые фильтры dashboard не обязаны синхронизироваться с URL; URL-синхронизация остаётся - Применённые фильтры dashboard не обязаны синхронизироваться с URL; URL-синхронизация остаётся
обязанностью подробной вкладки `События`. обязанностью подробной вкладки `События`.
- В dashboard отображается не более 10 событий на странице. - В dashboard отображается не более 10 событий на странице.
@ -124,10 +133,18 @@ Hero показывает:
валидационное сообщение. валидационное сообщение.
- Изменение chip-фильтров типов доходов применяется сразу и сбрасывает cursor-пагинацию. - Изменение chip-фильтров типов доходов применяется сразу и сбрасывает cursor-пагинацию.
- Блок содержит фильтр периода `from` / `to`. - Блок содержит фильтр периода `from` / `to`.
- По умолчанию используется период с начала текущего календарного года до текущей даты, как в разделе - По умолчанию применяется недельный период `сегодня - 7 дней` / `сегодня`, чтобы dashboard не загружал
операций. слишком много операций при первом открытии.
- Изменение черновых фильтров не запускает запрос до нажатия `Показать`. - Изменение черновых фильтров не запускает запрос до применения периода пользователем.
- Блок содержит быстрые пресеты периода `7д`, `30д`, `90д`, `1г`, `Всё` и действие `Сбросить`. - В пользовательском тексте шапки периода не используется слово `Фильтр`; UI должен считываться как
управление периодом за счёт иконки календаря, применённого диапазона, пресетов и affordance раскрытия.
- Блок содержит быстрые пресеты периода `7д`, `30д`, `90д`, `1г`, `Всё`, действие `Сбросить` и
основное действие применения периода с более понятным текстом, чем `Показать`.
- Период отображается как одно визуальное поле/кнопка с диапазоном, например `19 июн 26 июн`.
- По клику на поле периода открывается popover с community-компонентом MUI `DateCalendar` из
`@mui/x-date-pickers` и ручной логикой выбора начала/конца периода.
- `DateRangePicker` и пакет `@mui/x-date-pickers-pro` не используются.
- Native `<input type="date">` не используется.
- Применённые фильтры dashboard не обязаны синхронизироваться с URL; URL-синхронизация остаётся - Применённые фильтры dashboard не обязаны синхронизироваться с URL; URL-синхронизация остаётся
обязанностью подробных разделов. обязанностью подробных разделов.
- Для блока используется cursor-пагинация existing operations endpoint с размером страницы 10. - Для блока используется cursor-пагинация existing operations endpoint с размером страницы 10.
@ -162,6 +179,8 @@ Hero показывает:
- Первичная загрузка portfolio показывает dashboard skeleton соответствующей формы. - Первичная загрузка portfolio показывает dashboard skeleton соответствующей формы.
- Ошибка portfolio показывает ошибку overview, потому что без portfolio dashboard не имеет основного - Ошибка portfolio показывает ошибку overview, потому что без portfolio dashboard не имеет основного
контекста. контекста.
- Загрузка событий и доходов внутри карточек показывает skeleton таблицы соответствующей структуры, а не
только текстовую строку загрузки.
- Ошибка событий, доходов или analytics отображается внутри соответствующей карточки. - Ошибка событий, доходов или analytics отображается внутри соответствующей карточки.
- Пустые события, пустые доходы и пустая analytics имеют отдельные понятные сообщения. - Пустые события, пустые доходы и пустая analytics имеют отдельные понятные сообщения.
- Недоступные отдельные значения отображаются как `—`, не подменяются нулём. - Недоступные отдельные значения отображаются как `—`, не подменяются нулём.
@ -194,8 +213,15 @@ Hero показывает:
- Все Metric-блоки hero имеют одинаковую высоту; supportingText не создаёт перекоса. - Все Metric-блоки hero имеют одинаковую высоту; supportingText не создаёт перекоса.
- Блок `События` использует существующие events data и показывает дату, инструмент, тип, сумму и статус. - Блок `События` использует существующие events data и показывает дату, инструмент, тип, сумму и статус.
- Блок `События` поддерживает multi-select chip-фильтр типов и локальную пагинацию по 10 событий. - Блок `События` поддерживает multi-select chip-фильтр типов и локальную пагинацию по 10 событий.
- Блок `События` по умолчанию запрашивает период `сегодня - 7 дней` / `сегодня` и использует одно поле
периода с popover-календарём на базе community `DateCalendar`.
- Блок `Доходы` показывает доходные операции дивидендов и купонов и итог по отображаемым строкам. - Блок `Доходы` показывает доходные операции дивидендов и купонов и итог по отображаемым строкам.
- Блок `Доходы` поддерживает multi-select chip-фильтр типов и cursor-пагинацию по 10 операций. - Блок `Доходы` поддерживает multi-select chip-фильтр типов и cursor-пагинацию по 10 операций.
- Блок `Доходы` по умолчанию запрашивает период `сегодня - 7 дней` / `сегодня` и использует одно поле
периода с popover-календарём на базе community `DateCalendar`.
- В шапке управления периодом не отображается слово `Фильтр`, а действие применения периода не называется
`Показать`.
- Загрузка событий и доходов отображается skeleton-таблицей.
- Блок `Аналитика доходности` показывает данные существующего analytics endpoint. - Блок `Аналитика доходности` показывает данные существующего analytics endpoint.
- Блок `Аллокация` показывает горизонтальные бары секторов с названием, долей и суммой, а также - Блок `Аллокация` показывает горизонтальные бары секторов с названием, долей и суммой, а также
итоговую стоимость портфеля. итоговую стоимость портфеля.

View File

@ -40,15 +40,23 @@
- [x] Добавить unit/component tests для helpers и базовой dashboard composition. - [x] Добавить unit/component tests для helpers и базовой dashboard composition.
- [x] Выровнять hero KPI: все Metric одной высоты, supportingText не раздвигает "Доходность" выше соседей. - [x] Выровнять hero KPI: все Metric одной высоты, supportingText не раздвигает "Доходность" выше соседей.
- [x] Заменить donut-диаграмму аллокации на горизонтальные бары в `BrokerDashboardAllocationCard`. - [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`. - [ ] Проверить desktop layout `/broker/2084014113`.
- [ ] Проверить mobile layout `/broker/2084014113`. - [ ] Проверить mobile layout `/broker/2084014113`.
## Definition of Done ## Definition of Done
- [ ] `rtk npm run test:frontend` проходит. - [x] `rtk npm run test:frontend` проходит.
- [ ] `rtk npm run test:design-system` проходит. - [x] `rtk npm run test:design-system` проходит.
- [ ] `rtk npm run lint -w apps/frontend` проходит. - [x] `rtk npm run lint -w apps/frontend` проходит.
- [ ] `rtk npm run build:frontend` проходит. - [x] `rtk npm run build:frontend` проходит.
- [ ] Dashboard соответствует acceptance criteria из `spec.md`. - [ ] Dashboard соответствует acceptance criteria из `spec.md`.
- [x] Существующие вкладки `Акции`, `Облигации`, `Операции`, `События`, `Аналитика` остаются доступны. - [x] Существующие вкладки `Акции`, `Облигации`, `Операции`, `События`, `Аналитика` остаются доступны.
- [ ] `graphify update .` выполнен после code changes. - [x] `graphify update .` выполнен после code changes.

87
package-lock.json generated
View File

@ -92,6 +92,7 @@
"@moex-vibe/design-system": "*", "@moex-vibe/design-system": "*",
"@mui/icons-material": "^6.5.0", "@mui/icons-material": "^6.5.0",
"@mui/material": "^6.5.0", "@mui/material": "^6.5.0",
"@mui/x-date-pickers": "^7.29.4",
"@tanstack/react-query": "^5.20.0", "@tanstack/react-query": "^5.20.0",
"@tanstack/react-router": "^1.170.16", "@tanstack/react-router": "^1.170.16",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
@ -7426,6 +7427,92 @@
"integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==",
"license": "MIT" "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": { "node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",