fix(frontend): align broker dashboard HTML parity
This commit is contained in:
parent
58b8075583
commit
fee179d8e9
@ -18,65 +18,72 @@ export function AppLayout() {
|
|||||||
background: 'var(--color-surface)',
|
background: 'var(--color-surface)',
|
||||||
borderBottom: '1px solid #e0e0e0',
|
borderBottom: '1px solid #e0e0e0',
|
||||||
padding: '12px 24px',
|
padding: '12px 24px',
|
||||||
display: 'flex',
|
display: 'grid',
|
||||||
alignItems: 'center',
|
gridTemplateColumns: 'minmax(0, 1fr) auto',
|
||||||
flexWrap: 'wrap',
|
gap: 12,
|
||||||
gap: 24,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Link
|
<div style={{ display: 'grid', gap: 12 }}>
|
||||||
to="/"
|
<div style={{ display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap' }}>
|
||||||
style={{
|
<Link
|
||||||
fontSize: 20,
|
to="/"
|
||||||
fontWeight: 700,
|
style={{
|
||||||
color: 'var(--color-text)',
|
fontSize: 20,
|
||||||
textDecoration: 'none',
|
fontWeight: 700,
|
||||||
}}
|
color: 'var(--color-text)',
|
||||||
>
|
textDecoration: 'none',
|
||||||
MoexVibe
|
}}
|
||||||
</Link>
|
>
|
||||||
<div style={{ flex: '1 1 280px', minWidth: 220, maxWidth: 420 }}>
|
MoexVibe
|
||||||
<SearchBar />
|
</Link>
|
||||||
|
<div style={{ flex: '1 1 280px', minWidth: 220, maxWidth: 420 }}>
|
||||||
|
<SearchBar />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav
|
||||||
|
style={{ display: 'flex', gap: 20, flexWrap: 'wrap' }}
|
||||||
|
aria-label="Основная навигация"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
to="/portfolios"
|
||||||
|
style={{
|
||||||
|
fontSize: 14,
|
||||||
|
color: 'var(--color-text)',
|
||||||
|
textDecoration: 'none',
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Портфели
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/broker"
|
||||||
|
style={{
|
||||||
|
fontSize: 14,
|
||||||
|
color: 'var(--color-text)',
|
||||||
|
textDecoration: 'none',
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Брокер
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/screener"
|
||||||
|
style={{
|
||||||
|
fontSize: 14,
|
||||||
|
color: 'var(--color-text)',
|
||||||
|
textDecoration: 'none',
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Скринер
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
|
||||||
to="/portfolios"
|
|
||||||
style={{
|
|
||||||
fontSize: 14,
|
|
||||||
color: 'var(--color-text)',
|
|
||||||
textDecoration: 'none',
|
|
||||||
fontWeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Портфели
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/broker"
|
|
||||||
style={{
|
|
||||||
fontSize: 14,
|
|
||||||
color: 'var(--color-text)',
|
|
||||||
textDecoration: 'none',
|
|
||||||
fontWeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Брокер
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/screener"
|
|
||||||
style={{
|
|
||||||
fontSize: 14,
|
|
||||||
color: 'var(--color-text)',
|
|
||||||
textDecoration: 'none',
|
|
||||||
fontWeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Скринер
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 'auto',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
gap: 12,
|
gap: 12,
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -7,8 +7,8 @@ import { useBrokerPortfolio } from '@/entities/broker-account'
|
|||||||
import type { BrokerPortfolio } from '@/shared/api'
|
import type { BrokerPortfolio } from '@/shared/api'
|
||||||
|
|
||||||
const baseLinkStyle: React.CSSProperties = {
|
const baseLinkStyle: React.CSSProperties = {
|
||||||
padding: '10px 12px',
|
padding: '10px 14px',
|
||||||
borderRadius: 8,
|
borderRadius: 999,
|
||||||
color: 'var(--color-text-secondary)',
|
color: 'var(--color-text-secondary)',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
@ -16,6 +16,7 @@ const baseLinkStyle: React.CSSProperties = {
|
|||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
}
|
}
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
@ -54,8 +55,6 @@ export function BrokerAccountLayout({ children }: { children: ReactNode }) {
|
|||||||
gap: 0.5,
|
gap: 0.5,
|
||||||
overflowX: 'auto',
|
overflowX: 'auto',
|
||||||
scrollbarWidth: 'thin',
|
scrollbarWidth: 'thin',
|
||||||
borderBottom: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
pb: 0.5,
|
pb: 0.5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -70,7 +69,7 @@ export function BrokerAccountLayout({ children }: { children: ReactNode }) {
|
|||||||
...baseLinkStyle,
|
...baseLinkStyle,
|
||||||
color: 'var(--color-primary)',
|
color: 'var(--color-primary)',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
background: 'rgba(25, 118, 210, 0.08)',
|
background: 'rgba(25, 118, 210, 0.1)',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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.format('YYYY-MM-DD'),
|
to: now.add(7, 'day').format('YYYY-MM-DD'),
|
||||||
types: [...DASHBOARD_EVENT_TYPES],
|
types: [...DASHBOARD_EVENT_TYPES],
|
||||||
preset: '7d',
|
preset: '7d',
|
||||||
}
|
}
|
||||||
@ -40,7 +40,9 @@ export function applyDatePreset<T extends string>(
|
|||||||
): DashboardFilterState<T> {
|
): DashboardFilterState<T> {
|
||||||
const now = dayjs()
|
const now = dayjs()
|
||||||
const next = { ...filters, preset }
|
const next = { ...filters, preset }
|
||||||
if (preset === 'all') return { ...next, from: '', to: '' }
|
if (preset === 'all') {
|
||||||
|
return { ...next, from: '2000-01-01', to: '2099-12-31' }
|
||||||
|
}
|
||||||
const amount = preset === '1y' ? 1 : Number.parseInt(preset, 10)
|
const amount = preset === '1y' ? 1 : Number.parseInt(preset, 10)
|
||||||
const unit = preset === '1y' ? 'year' : 'day'
|
const unit = preset === '1y' ? 'year' : 'day'
|
||||||
return {
|
return {
|
||||||
@ -50,6 +52,25 @@ export function applyDatePreset<T extends string>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function applyEventDatePreset(
|
||||||
|
filters: DashboardFilterState<DashboardEventType>,
|
||||||
|
preset: DashboardDatePreset,
|
||||||
|
): DashboardFilterState<DashboardEventType> {
|
||||||
|
const now = dayjs()
|
||||||
|
const next = { ...filters, preset }
|
||||||
|
if (preset === 'all') {
|
||||||
|
return { ...next, from: '2000-01-01', to: '2099-12-31' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount = preset === '1y' ? 1 : Number.parseInt(preset, 10)
|
||||||
|
const unit = preset === '1y' ? 'year' : 'day'
|
||||||
|
return {
|
||||||
|
...next,
|
||||||
|
from: now.subtract(amount, unit).format('YYYY-MM-DD'),
|
||||||
|
to: now.add(amount, unit).format('YYYY-MM-DD'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function validateDashboardFilters<T extends string>(
|
export function validateDashboardFilters<T extends string>(
|
||||||
filters: DashboardFilterState<T>,
|
filters: DashboardFilterState<T>,
|
||||||
): string {
|
): string {
|
||||||
|
|||||||
@ -96,10 +96,10 @@ describe('moneyToneToColor', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('incomeTypeTone', () => {
|
describe('incomeTypeTone', () => {
|
||||||
it('maps each income label to a stable tone', () => {
|
it('maps each income label to a stable tone matching HTML parity (coupon=info)', () => {
|
||||||
expect(incomeTypeTone('Дивиденд')).toBe('success')
|
expect(incomeTypeTone('Дивиденд')).toBe('success')
|
||||||
expect(incomeTypeTone('Дивиденд (внешний)')).toBe('info')
|
expect(incomeTypeTone('Дивиденд (внешний)')).toBe('success')
|
||||||
expect(incomeTypeTone('Купон')).toBe('warning')
|
expect(incomeTypeTone('Купон')).toBe('info')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -65,11 +65,10 @@ export function eventTypeTone(type: BrokerEventItem['type']): TypeTone {
|
|||||||
export function incomeTypeTone(label: DashboardIncomeTypeLabel): TypeTone {
|
export function incomeTypeTone(label: DashboardIncomeTypeLabel): TypeTone {
|
||||||
switch (label) {
|
switch (label) {
|
||||||
case 'Дивиденд':
|
case 'Дивиденд':
|
||||||
return 'success'
|
|
||||||
case 'Дивиденд (внешний)':
|
case 'Дивиденд (внешний)':
|
||||||
return 'info'
|
return 'success'
|
||||||
case 'Купон':
|
case 'Купон':
|
||||||
return 'warning'
|
return 'info'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -210,11 +210,31 @@ describe('BrokerDashboard', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows date filter toggle button with apply action and accessible name', async () => {
|
it('requests future events in the default dashboard range', () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
try {
|
||||||
|
vi.setSystemTime(new Date('2026-06-27T12:00:00.000Z'))
|
||||||
|
|
||||||
|
renderWithProviders(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
|
||||||
|
|
||||||
|
expect(hookMocks.useBrokerEvents).toHaveBeenCalledWith(
|
||||||
|
'acc-1',
|
||||||
|
expect.objectContaining({
|
||||||
|
from: '2026-06-20',
|
||||||
|
to: '2026-07-04',
|
||||||
|
}),
|
||||||
|
{ enabled: true },
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows date filter toggle button with refresh action and accessible name', async () => {
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
renderWithProviders(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
|
renderWithProviders(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
|
||||||
|
|
||||||
const applyButtons = screen.getAllByRole('button', { name: /Применить период/ })
|
const applyButtons = screen.getAllByRole('button', { name: 'Обновить' })
|
||||||
expect(applyButtons).toHaveLength(2)
|
expect(applyButtons).toHaveLength(2)
|
||||||
|
|
||||||
const toggleButtons = screen.getAllByRole('button', { name: /^Период/ })
|
const toggleButtons = screen.getAllByRole('button', { name: /^Период/ })
|
||||||
@ -228,6 +248,7 @@ describe('BrokerDashboard', () => {
|
|||||||
expect(screen.getByText('1г')).toBeInTheDocument()
|
expect(screen.getByText('1г')).toBeInTheDocument()
|
||||||
expect(screen.getByText('Всё')).toBeInTheDocument()
|
expect(screen.getByText('Всё')).toBeInTheDocument()
|
||||||
expect(screen.getByText('Сбросить')).toBeInTheDocument()
|
expect(screen.getByText('Сбросить')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('button', { name: 'Применить период' })).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not render a text chevron glyph inside the period toggle button', () => {
|
it('does not render a text chevron glyph inside the period toggle button', () => {
|
||||||
@ -386,6 +407,46 @@ describe('BrokerDashboard', () => {
|
|||||||
expect(screen.getByText('Ожидается')).toBeInTheDocument()
|
expect(screen.getByText('Ожидается')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('renders HTML-parity card headings, toolbar label and footer summary for events', () => {
|
||||||
|
mockEventsLoaded([
|
||||||
|
buildEvent({ id: 'evt-1' }),
|
||||||
|
buildEvent({ id: 'evt-2', ticker: 'HEAD', name: 'HeadHunter Group', type: 'dividend' }),
|
||||||
|
])
|
||||||
|
hookMocks.useBrokerEvents.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
items: [
|
||||||
|
buildEvent({ id: 'evt-1' }),
|
||||||
|
buildEvent({ id: 'evt-2', ticker: 'HEAD', name: 'HeadHunter Group', type: 'dividend' }),
|
||||||
|
],
|
||||||
|
summary: { eventCount: 48 },
|
||||||
|
asOf: '2026-06-27T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
isLoading: false,
|
||||||
|
isError: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
renderWithProviders(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
|
||||||
|
|
||||||
|
const eventsSection = screen.getByLabelText('События')
|
||||||
|
expect(screen.getByText('2 из 48')).toBeInTheDocument()
|
||||||
|
expect(within(eventsSection).getAllByText('Тип').length).toBeGreaterThan(0)
|
||||||
|
expect(
|
||||||
|
within(eventsSection).getByText('Показано 2 событий за выбранный период'),
|
||||||
|
).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders HTML-parity count badge and footer summary for income', () => {
|
||||||
|
mockOperationsLoaded([
|
||||||
|
buildOperation({ id: 'op-1' }),
|
||||||
|
buildOperation({ id: 'op-2', ticker: 'IRAO', name: 'Интер РАО' }),
|
||||||
|
])
|
||||||
|
|
||||||
|
renderWithProviders(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('2 операции')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText(/Показано 2 · Итого:/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it('uses negative tone when event actual amount is negative', () => {
|
it('uses negative tone when event actual amount is negative', () => {
|
||||||
mockEventsLoaded([
|
mockEventsLoaded([
|
||||||
buildEvent({
|
buildEvent({
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import type { BrokerPortfolio } from '@/shared/api'
|
|||||||
import { useCursorPagination } from '@/shared/lib/useCursorPagination'
|
import { useCursorPagination } from '@/shared/lib/useCursorPagination'
|
||||||
import {
|
import {
|
||||||
applyDatePreset,
|
applyDatePreset,
|
||||||
|
applyEventDatePreset,
|
||||||
type DashboardDatePreset,
|
type DashboardDatePreset,
|
||||||
type DashboardEventType,
|
type DashboardEventType,
|
||||||
type DashboardIncomeType,
|
type DashboardIncomeType,
|
||||||
@ -151,7 +152,7 @@ export function BrokerDashboard({
|
|||||||
}, [incomePagination])
|
}, [incomePagination])
|
||||||
|
|
||||||
function handleDraftEventPresetChange(preset: DashboardDatePreset) {
|
function handleDraftEventPresetChange(preset: DashboardDatePreset) {
|
||||||
setDraftEventFilters((filters) => applyDatePreset(filters, preset))
|
setDraftEventFilters((filters) => applyEventDatePreset(filters, preset))
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDraftEventFromChange(value: string) {
|
function handleDraftEventFromChange(value: string) {
|
||||||
@ -194,6 +195,7 @@ export function BrokerDashboard({
|
|||||||
onApplyFilters={applyEventFilters}
|
onApplyFilters={applyEventFilters}
|
||||||
onResetFilters={resetEventFilters}
|
onResetFilters={resetEventFilters}
|
||||||
hasDraftTypes={hasDraftEventTypes}
|
hasDraftTypes={hasDraftEventTypes}
|
||||||
|
totalCount={events.data?.summary?.eventCount}
|
||||||
page={eventPage}
|
page={eventPage}
|
||||||
canGoBack={eventPage > 1}
|
canGoBack={eventPage > 1}
|
||||||
canGoForward={eventPage * eventPageSize < eventItems.length}
|
canGoForward={eventPage * eventPageSize < eventItems.length}
|
||||||
@ -217,6 +219,7 @@ export function BrokerDashboard({
|
|||||||
onApplyFilters={applyIncomeFilters}
|
onApplyFilters={applyIncomeFilters}
|
||||||
onResetFilters={resetIncomeFilters}
|
onResetFilters={resetIncomeFilters}
|
||||||
hasDraftTypes={hasDraftIncomeTypes}
|
hasDraftTypes={hasDraftIncomeTypes}
|
||||||
|
visibleCount={operations.data?.items?.length ?? 0}
|
||||||
pageNumber={incomePagination.pageNumber}
|
pageNumber={incomePagination.pageNumber}
|
||||||
canGoBack={incomePagination.pageNumber > 1}
|
canGoBack={incomePagination.pageNumber > 1}
|
||||||
canGoForward={operations.data?.hasNext ?? false}
|
canGoForward={operations.data?.hasNext ?? false}
|
||||||
|
|||||||
@ -31,6 +31,14 @@ export function BrokerDashboardAllocationCard({ portfolio }: { portfolio: Broker
|
|||||||
<Text tone="muted">Нет данных для распределения</Text>
|
<Text tone="muted">Нет данных для распределения</Text>
|
||||||
) : (
|
) : (
|
||||||
<Box sx={{ display: 'grid', gap: 1.5 }}>
|
<Box sx={{ display: 'grid', gap: 1.5 }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap' }}>
|
||||||
|
<Text variant="caption" tone="secondary">
|
||||||
|
Структура портфеля
|
||||||
|
</Text>
|
||||||
|
<Text variant="body" sx={{ fontWeight: 700 }}>
|
||||||
|
{formatBrokerMoney(portfolio.totals.portfolio)}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
{sectors.map((sector) => (
|
{sectors.map((sector) => (
|
||||||
<Box key={sector.key}>
|
<Box key={sector.key}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
||||||
|
|||||||
@ -72,20 +72,35 @@ export function BrokerDashboardAnalyticsCard({
|
|||||||
const value = data[field]
|
const value = data[field]
|
||||||
const tone = analyticsTone(field, value)
|
const tone = analyticsTone(field, value)
|
||||||
return (
|
return (
|
||||||
<Metric
|
<Box
|
||||||
key={field}
|
key={field}
|
||||||
label={label}
|
sx={{
|
||||||
value={
|
borderRadius: 2,
|
||||||
<Box
|
border: '1px solid',
|
||||||
component="span"
|
borderColor: tone === 'negative' ? 'error.light' : 'divider',
|
||||||
data-testid={testId}
|
bgcolor:
|
||||||
data-tone={tone}
|
tone === 'positive'
|
||||||
sx={{ color: moneyToneToColor(tone), fontWeight: 700 }}
|
? 'rgba(46, 125, 50, 0.06)'
|
||||||
>
|
: tone === 'negative'
|
||||||
{analyticsDisplay(field, value, data.currency)}
|
? 'rgba(211, 47, 47, 0.06)'
|
||||||
</Box>
|
: 'grey.50',
|
||||||
}
|
p: 1.5,
|
||||||
/>
|
}}
|
||||||
|
>
|
||||||
|
<Metric
|
||||||
|
label={label}
|
||||||
|
value={
|
||||||
|
<Box
|
||||||
|
component="span"
|
||||||
|
data-testid={testId}
|
||||||
|
data-tone={tone}
|
||||||
|
sx={{ color: moneyToneToColor(tone), fontWeight: 700 }}
|
||||||
|
>
|
||||||
|
{analyticsDisplay(field, value, data.currency)}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import type { ReactNode } from 'react'
|
|||||||
|
|
||||||
type BrokerDashboardCardProps = {
|
type BrokerDashboardCardProps = {
|
||||||
title: string
|
title: string
|
||||||
|
badge?: ReactNode
|
||||||
action?: ReactNode
|
action?: ReactNode
|
||||||
filters?: ReactNode
|
filters?: ReactNode
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
@ -13,6 +14,7 @@ type BrokerDashboardCardProps = {
|
|||||||
|
|
||||||
export function BrokerDashboardCard({
|
export function BrokerDashboardCard({
|
||||||
title,
|
title,
|
||||||
|
badge,
|
||||||
action,
|
action,
|
||||||
filters,
|
filters,
|
||||||
children,
|
children,
|
||||||
@ -38,15 +40,36 @@ export function BrokerDashboardCard({
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'flex-start',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
gap: 2,
|
gap: 2,
|
||||||
mb: 1.5,
|
mb: 1.5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Heading level={2} size="section">
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
|
||||||
{title}
|
<Heading level={2} size="section">
|
||||||
</Heading>
|
{title}
|
||||||
|
</Heading>
|
||||||
|
{badge ? (
|
||||||
|
<Box
|
||||||
|
component="span"
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
minHeight: 24,
|
||||||
|
px: 1,
|
||||||
|
borderRadius: 999,
|
||||||
|
bgcolor: 'grey.100',
|
||||||
|
color: 'text.secondary',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 700,
|
||||||
|
lineHeight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{badge}
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
{action}
|
{action}
|
||||||
</Box>
|
</Box>
|
||||||
{filters && <Box sx={{ mb: 1.5 }}>{filters}</Box>}
|
{filters && <Box sx={{ mb: 1.5 }}>{filters}</Box>}
|
||||||
|
|||||||
@ -88,9 +88,16 @@ export function BrokerDashboardDateFilter({
|
|||||||
setSelectingStage('from')
|
setSelectingStage('from')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleApply() {
|
||||||
|
onApply()
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
|
||||||
const selectingLabel =
|
const selectingLabel =
|
||||||
selectingStage === 'from' ? 'Выберите начало периода' : 'Выберите конец периода'
|
selectingStage === 'from' ? 'Выберите начало периода' : 'Выберите конец периода'
|
||||||
const periodAriaLabel = appliedLabel ? `Период: ${appliedLabel}` : 'Период'
|
const periodAriaLabel = appliedLabel ? `Период: ${appliedLabel}` : 'Период'
|
||||||
|
const rangeInverted = Boolean(draftFrom && draftTo) && dayjs(draftTo).isBefore(dayjs(draftFrom))
|
||||||
|
const canApply = hasDraftTypes && !rangeInverted
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||||
@ -151,29 +158,6 @@ export function BrokerDashboardDateFilter({
|
|||||||
<ExpandMoreRounded sx={{ fontSize: 16 }} />
|
<ExpandMoreRounded sx={{ fontSize: 16 }} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
|
||||||
component="button"
|
|
||||||
type="button"
|
|
||||||
onClick={onApply}
|
|
||||||
disabled={!hasDraftTypes}
|
|
||||||
sx={{
|
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 1,
|
|
||||||
bgcolor: hasDraftTypes ? 'primary.main' : 'grey.400',
|
|
||||||
color: 'common.white',
|
|
||||||
border: 'none',
|
|
||||||
borderRadius: 1,
|
|
||||||
px: 1.5,
|
|
||||||
py: 0.5,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 600,
|
|
||||||
cursor: hasDraftTypes ? 'pointer' : 'default',
|
|
||||||
'&:hover': hasDraftTypes ? { bgcolor: 'primary.dark' } : {},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Применить период
|
|
||||||
</Box>
|
|
||||||
<Popover
|
<Popover
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
@ -212,7 +196,9 @@ export function BrokerDashboardDateFilter({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1 }}>
|
<Box
|
||||||
|
sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 1 }}
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
component="button"
|
component="button"
|
||||||
type="button"
|
type="button"
|
||||||
@ -230,9 +216,54 @@ export function BrokerDashboardDateFilter({
|
|||||||
>
|
>
|
||||||
Сбросить
|
Сбросить
|
||||||
</Box>
|
</Box>
|
||||||
|
<Box
|
||||||
|
component="button"
|
||||||
|
type="button"
|
||||||
|
onClick={handleApply}
|
||||||
|
disabled={!canApply}
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
bgcolor: canApply ? 'primary.main' : 'grey.400',
|
||||||
|
color: 'common.white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 1,
|
||||||
|
px: 1.5,
|
||||||
|
py: 0.75,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 700,
|
||||||
|
cursor: canApply ? 'pointer' : 'default',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Применить период
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
<Box
|
||||||
|
component="button"
|
||||||
|
type="button"
|
||||||
|
onClick={onApply}
|
||||||
|
disabled={!canApply}
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
bgcolor: canApply ? 'primary.main' : 'grey.400',
|
||||||
|
color: 'common.white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 1,
|
||||||
|
px: 1.5,
|
||||||
|
py: 0.5,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 700,
|
||||||
|
cursor: canApply ? 'pointer' : 'default',
|
||||||
|
'&:hover': canApply ? { bgcolor: 'primary.dark' } : {},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Обновить
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</LocalizationProvider>
|
</LocalizationProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -79,6 +79,7 @@ type BrokerDashboardEventsCardProps = {
|
|||||||
onApplyFilters: () => void
|
onApplyFilters: () => void
|
||||||
onResetFilters: () => void
|
onResetFilters: () => void
|
||||||
hasDraftTypes: boolean
|
hasDraftTypes: boolean
|
||||||
|
totalCount?: number
|
||||||
page: number
|
page: number
|
||||||
onPreviousPage: () => void
|
onPreviousPage: () => void
|
||||||
onNextPage: () => void
|
onNextPage: () => void
|
||||||
@ -124,6 +125,7 @@ export function BrokerDashboardEventsCard({
|
|||||||
onApplyFilters,
|
onApplyFilters,
|
||||||
onResetFilters,
|
onResetFilters,
|
||||||
hasDraftTypes,
|
hasDraftTypes,
|
||||||
|
totalCount,
|
||||||
page,
|
page,
|
||||||
onPreviousPage,
|
onPreviousPage,
|
||||||
onNextPage,
|
onNextPage,
|
||||||
@ -135,6 +137,7 @@ export function BrokerDashboardEventsCard({
|
|||||||
return (
|
return (
|
||||||
<BrokerDashboardCard
|
<BrokerDashboardCard
|
||||||
title="События"
|
title="События"
|
||||||
|
badge={events.length > 0 ? `${events.length} из ${totalCount ?? events.length}` : undefined}
|
||||||
action={<Link to={`/broker/${encodeURIComponent(accountId)}/events`}>Все события</Link>}
|
action={<Link to={`/broker/${encodeURIComponent(accountId)}/events`}>Все события</Link>}
|
||||||
filters={
|
filters={
|
||||||
<BrokerDashboardTableToolbar
|
<BrokerDashboardTableToolbar
|
||||||
@ -260,16 +263,39 @@ export function BrokerDashboardEventsCard({
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1, alignItems: 'center' }}>
|
<Box
|
||||||
<Button variant="secondary" size="small" onClick={onPreviousPage} disabled={!canGoBack}>
|
sx={{
|
||||||
←
|
display: 'flex',
|
||||||
</Button>
|
justifyContent: 'space-between',
|
||||||
<Text variant="body" tone="secondary">
|
gap: 1,
|
||||||
{page}
|
alignItems: 'center',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text variant="caption" tone="secondary">
|
||||||
|
Показано {events.length} событий за выбранный период
|
||||||
</Text>
|
</Text>
|
||||||
<Button variant="secondary" size="small" onClick={onNextPage} disabled={!canGoForward}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
→
|
<Button
|
||||||
</Button>
|
variant="secondary"
|
||||||
|
size="small"
|
||||||
|
onClick={onPreviousPage}
|
||||||
|
disabled={!canGoBack}
|
||||||
|
>
|
||||||
|
←
|
||||||
|
</Button>
|
||||||
|
<Text variant="body" tone="secondary">
|
||||||
|
{page}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="small"
|
||||||
|
onClick={onNextPage}
|
||||||
|
disabled={!canGoForward}
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -73,6 +73,7 @@ type BrokerDashboardIncomeCardProps = {
|
|||||||
onApplyFilters: () => void
|
onApplyFilters: () => void
|
||||||
onResetFilters: () => void
|
onResetFilters: () => void
|
||||||
hasDraftTypes: boolean
|
hasDraftTypes: boolean
|
||||||
|
visibleCount: number
|
||||||
pageNumber: number
|
pageNumber: number
|
||||||
canGoBack: boolean
|
canGoBack: boolean
|
||||||
canGoForward: boolean
|
canGoForward: boolean
|
||||||
@ -97,6 +98,7 @@ export function BrokerDashboardIncomeCard({
|
|||||||
onApplyFilters,
|
onApplyFilters,
|
||||||
onResetFilters,
|
onResetFilters,
|
||||||
hasDraftTypes,
|
hasDraftTypes,
|
||||||
|
visibleCount,
|
||||||
pageNumber,
|
pageNumber,
|
||||||
canGoBack,
|
canGoBack,
|
||||||
canGoForward,
|
canGoForward,
|
||||||
@ -109,6 +111,7 @@ export function BrokerDashboardIncomeCard({
|
|||||||
return (
|
return (
|
||||||
<BrokerDashboardCard
|
<BrokerDashboardCard
|
||||||
title="Доходы"
|
title="Доходы"
|
||||||
|
badge={rows.length > 0 ? `${visibleCount} операции` : undefined}
|
||||||
action={<Link to={`/broker/${encodeURIComponent(accountId)}/operations`}>Все операции</Link>}
|
action={<Link to={`/broker/${encodeURIComponent(accountId)}/operations`}>Все операции</Link>}
|
||||||
filters={
|
filters={
|
||||||
<BrokerDashboardTableToolbar
|
<BrokerDashboardTableToolbar
|
||||||
@ -223,25 +226,45 @@ export function BrokerDashboardIncomeCard({
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Text variant="caption" tone="secondary">
|
<Box
|
||||||
Показано: {rows.length} · Итого:{' '}
|
sx={{
|
||||||
{total
|
display: 'flex',
|
||||||
? `${total.value >= 0 ? '+' : '−'}${formatDashboardCurrency({
|
justifyContent: 'space-between',
|
||||||
currency: total.currency,
|
gap: 1,
|
||||||
value: Math.abs(total.value),
|
alignItems: 'center',
|
||||||
})}`
|
flexWrap: 'wrap',
|
||||||
: '—'}
|
}}
|
||||||
</Text>
|
>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1, alignItems: 'center' }}>
|
<Text variant="caption" tone="secondary">
|
||||||
<Button variant="secondary" size="small" onClick={onPreviousPage} disabled={!canGoBack}>
|
Показано {rows.length} · Итого:{' '}
|
||||||
←
|
{total
|
||||||
</Button>
|
? `${total.value >= 0 ? '+' : '−'}${formatDashboardCurrency({
|
||||||
<Text variant="body" tone="secondary">
|
currency: total.currency,
|
||||||
{pageNumber}
|
value: Math.abs(total.value),
|
||||||
|
})}`
|
||||||
|
: '—'}
|
||||||
</Text>
|
</Text>
|
||||||
<Button variant="secondary" size="small" onClick={onNextPage} disabled={!canGoForward}>
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1, alignItems: 'center' }}>
|
||||||
→
|
<Button
|
||||||
</Button>
|
variant="secondary"
|
||||||
|
size="small"
|
||||||
|
onClick={onPreviousPage}
|
||||||
|
disabled={!canGoBack}
|
||||||
|
>
|
||||||
|
←
|
||||||
|
</Button>
|
||||||
|
<Text variant="body" tone="secondary">
|
||||||
|
{pageNumber}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="small"
|
||||||
|
onClick={onNextPage}
|
||||||
|
disabled={!canGoForward}
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export function BrokerDashboardTableToolbar({ chips, children }: BrokerDashboard
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: { xs: '1fr', md: 'minmax(0, 1fr) auto' },
|
gridTemplateColumns: { xs: '1fr', md: 'minmax(0, 1fr) auto auto' },
|
||||||
gap: 1.25,
|
gap: 1.25,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
border: '1px solid',
|
border: '1px solid',
|
||||||
@ -21,7 +21,12 @@ export function BrokerDashboardTableToolbar({ chips, children }: BrokerDashboard
|
|||||||
p: 1,
|
p: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.75, minWidth: 0 }}>{chips}</Box>
|
<Box sx={{ display: 'grid', gap: 0.75, minWidth: 0 }}>
|
||||||
|
<Box sx={{ color: 'text.secondary', fontSize: 12, fontWeight: 700, lineHeight: 1 }}>
|
||||||
|
Тип
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.75, minWidth: 0 }}>{chips}</Box>
|
||||||
|
</Box>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -65,7 +65,8 @@
|
|||||||
### 3. События
|
### 3. События
|
||||||
|
|
||||||
- Типы событий переключаются chip-фильтрами с немедленным применением.
|
- Типы событий переключаются chip-фильтрами с немедленным применением.
|
||||||
- Диапазон дат по умолчанию: `сегодня - 7 дней` / `сегодня`.
|
- Диапазон дат по умолчанию: `сегодня - 7 дней` / `сегодня + 7 дней`, чтобы обзор сразу показывал
|
||||||
|
ближайшие будущие события.
|
||||||
- Диапазон дат редактируется отдельно от применённого состояния: draft state меняется локально, запрос уходит только по действию применения периода.
|
- Диапазон дат редактируется отдельно от применённого состояния: draft state меняется локально, запрос уходит только по действию применения периода.
|
||||||
- В шапке управления периодом не используется слово `Фильтр`; роль управления считывается через иконку календаря, применённый диапазон, раскрытие панели и пресеты.
|
- В шапке управления периодом не используется слово `Фильтр`; роль управления считывается через иконку календаря, применённый диапазон, раскрытие панели и пресеты.
|
||||||
- Native `<input type="date">` не используется; период выбирается через одно визуальное поле и popover с community `DateCalendar` из `@mui/x-date-pickers`.
|
- Native `<input type="date">` не используется; период выбирается через одно визуальное поле и popover с community `DateCalendar` из `@mui/x-date-pickers`.
|
||||||
@ -73,6 +74,7 @@
|
|||||||
- При пустом выборе типов запрос отключается, карточка показывает валидационное сообщение.
|
- При пустом выборе типов запрос отключается, карточка показывает валидационное сообщение.
|
||||||
- Пагинация локальная, по 10 событий на страницу, сбрасывается при смене применённых фильтров.
|
- Пагинация локальная, по 10 событий на страницу, сбрасывается при смене применённых фильтров.
|
||||||
- При загрузке карточка показывает skeleton таблицы событий с колонками дата, инструмент, тип, сумма, статус.
|
- При загрузке карточка показывает skeleton таблицы событий с колонками дата, инструмент, тип, сумма, статус.
|
||||||
|
- В toolbar карточки используются count badge, label `Тип`, кнопка `Обновить` и footer-summary по паттерну HTML-эталона.
|
||||||
|
|
||||||
### 4. Доходы
|
### 4. Доходы
|
||||||
|
|
||||||
@ -118,6 +120,7 @@
|
|||||||
используя крупный `Heading size="title"`.
|
используя крупный `Heading size="title"`.
|
||||||
- `BrokerDashboardDateFilter` должен использовать иконку раскрытия вместо текстового символа и сохранять
|
- `BrokerDashboardDateFilter` должен использовать иконку раскрытия вместо текстового символа и сохранять
|
||||||
единый toolbar-паттерн для `События` и `Доходы`.
|
единый toolbar-паттерн для `События` и `Доходы`.
|
||||||
|
- Для `События` period presets должны поддерживать будущую часть диапазона, а не обрезаться текущим днём.
|
||||||
- Таблицы `События` и `Доходы` должны иметь `thead`, type badges, двухстрочный инструмент при наличии
|
- Таблицы `События` и `Доходы` должны иметь `thead`, type badges, двухстрочный инструмент при наличии
|
||||||
названия и semantic amount colors.
|
названия и semantic amount colors.
|
||||||
- `BrokerDashboardAnalyticsCard` должен окрашивать KPI-карточки по смыслу и показывать RUB через `₽`.
|
- `BrokerDashboardAnalyticsCard` должен окрашивать KPI-карточки по смыслу и показывать RUB через `₽`.
|
||||||
@ -168,7 +171,7 @@
|
|||||||
- [ ] Обновить `BrokerDashboardDateFilter`: убрать слово `Фильтр` из пользовательского текста, заменить `Показать` на понятное действие применения периода и визуально собрать chips, диапазон, сброс и применение в аккуратную шапку.
|
- [ ] Обновить `BrokerDashboardDateFilter`: убрать слово `Фильтр` из пользовательского текста, заменить `Показать` на понятное действие применения периода и визуально собрать chips, диапазон, сброс и применение в аккуратную шапку.
|
||||||
- [ ] Заменить native date inputs на одно визуальное поле периода, которое открывает MUI `Popover` с community `DateCalendar`.
|
- [ ] Заменить native date inputs на одно визуальное поле периода, которое открывает MUI `Popover` с community `DateCalendar`.
|
||||||
- [ ] Реализовать локальную логику выбора диапазона: первый клик задаёт начало, второй — конец; если конец раньше начала, диапазон пересобирается от выбранной даты.
|
- [ ] Реализовать локальную логику выбора диапазона: первый клик задаёт начало, второй — конец; если конец раньше начала, диапазон пересобирается от выбранной даты.
|
||||||
- [ ] Настроить default range событий на `сегодня - 7 дней` / `сегодня`.
|
- [ ] Настроить default range событий на `сегодня - 7 дней` / `сегодня + 7 дней`.
|
||||||
- [ ] Подключить для событий draft/applied state: типы применяются сразу, даты только по действию применения периода.
|
- [ ] Подключить для событий draft/applied state: типы применяются сразу, даты только по действию применения периода.
|
||||||
- [ ] Сохранять локальную пагинацию по 10 событий и сбрасывать её при смене применённых фильтров.
|
- [ ] Сохранять локальную пагинацию по 10 событий и сбрасывать её при смене применённых фильтров.
|
||||||
- [ ] Заменить текстовую загрузку событий на skeleton таблицы.
|
- [ ] Заменить текстовую загрузку событий на skeleton таблицы.
|
||||||
|
|||||||
@ -105,7 +105,7 @@ Hero показывает:
|
|||||||
### 4. Блок `События`
|
### 4. Блок `События`
|
||||||
|
|
||||||
- Блок использует существующий источник `useBrokerEvents(accountId, query)`.
|
- Блок использует существующий источник `useBrokerEvents(accountId, query)`.
|
||||||
- По умолчанию применяется период `сегодня - 7 дней` / `сегодня` и типы
|
- По умолчанию применяется период `сегодня - 7 дней` / `сегодня + 7 дней` и типы
|
||||||
`dividend,coupon,maturity,offer`, как в существующей вкладке событий.
|
`dividend,coupon,maturity,offer`, как в существующей вкладке событий.
|
||||||
- Блок содержит кликабельные chip-фильтры типов событий: `Дивиденды`, `Купоны`, `Погашения`, `Оферты`.
|
- Блок содержит кликабельные chip-фильтры типов событий: `Дивиденды`, `Купоны`, `Погашения`, `Оферты`.
|
||||||
- Пользователь может выбрать несколько типов событий.
|
- Пользователь может выбрать несколько типов событий.
|
||||||
@ -114,7 +114,8 @@ Hero показывает:
|
|||||||
- Изменение chip-фильтров типов событий применяется сразу и возвращает локальную пагинацию на первую
|
- Изменение chip-фильтров типов событий применяется сразу и возвращает локальную пагинацию на первую
|
||||||
страницу.
|
страницу.
|
||||||
- Блок содержит фильтр периода `from` / `to`.
|
- Блок содержит фильтр периода `from` / `to`.
|
||||||
- По умолчанию применяется недельный период `сегодня - 7 дней` / `сегодня`.
|
- По умолчанию применяется недельный период `сегодня - 7 дней` / `сегодня + 7 дней`, чтобы обзор
|
||||||
|
сразу показывал ближайшие будущие события.
|
||||||
- Изменение черновых фильтров не запускает запрос до применения периода пользователем.
|
- Изменение черновых фильтров не запускает запрос до применения периода пользователем.
|
||||||
- В пользовательском тексте шапки периода не используется слово `Фильтр`; UI должен считываться как
|
- В пользовательском тексте шапки периода не используется слово `Фильтр`; UI должен считываться как
|
||||||
управление периодом за счёт иконки календаря, применённого диапазона, пресетов и affordance раскрытия.
|
управление периодом за счёт иконки календаря, применённого диапазона, пресетов и affordance раскрытия.
|
||||||
@ -257,7 +258,7 @@ Hero показывает:
|
|||||||
- В таблице `События` инструмент отображается двумя строками при наличии названия, тип отображается
|
- В таблице `События` инструмент отображается двумя строками при наличии названия, тип отображается
|
||||||
бейджем, сумма и статус имеют семантические цвета.
|
бейджем, сумма и статус имеют семантические цвета.
|
||||||
- Блок `События` поддерживает multi-select chip-фильтр типов и локальную пагинацию по 10 событий.
|
- Блок `События` поддерживает multi-select chip-фильтр типов и локальную пагинацию по 10 событий.
|
||||||
- Блок `События` по умолчанию запрашивает период `сегодня - 7 дней` / `сегодня` и использует одно поле
|
- Блок `События` по умолчанию запрашивает период `сегодня - 7 дней` / `сегодня + 7 дней` и использует одно поле
|
||||||
периода с popover-календарём на базе community `DateCalendar`.
|
периода с popover-календарём на базе community `DateCalendar`.
|
||||||
- Блок `Доходы` показывает доходные операции дивидендов и купонов и итог по отображаемым строкам.
|
- Блок `Доходы` показывает доходные операции дивидендов и купонов и итог по отображаемым строкам.
|
||||||
- В таблице `Доходы` инструмент отображается двумя строками при наличии названия, тип отображается
|
- В таблице `Доходы` инструмент отображается двумя строками при наличии названия, тип отображается
|
||||||
|
|||||||
@ -44,7 +44,8 @@
|
|||||||
- [x] Переименовать действие `Показать` в управлении периодом и визуально улучшить шапку фильтров.
|
- [x] Переименовать действие `Показать` в управлении периодом и визуально улучшить шапку фильтров.
|
||||||
- [x] Заменить native date inputs на одно поле периода с popover и community `DateCalendar` из `@mui/x-date-pickers`.
|
- [x] Заменить native date inputs на одно поле периода с popover и community `DateCalendar` из `@mui/x-date-pickers`.
|
||||||
- [x] Не использовать `@mui/x-date-pickers-pro` и `DateRangePicker`.
|
- [x] Не использовать `@mui/x-date-pickers-pro` и `DateRangePicker`.
|
||||||
- [x] Настроить default range событий и доходов на `сегодня - 7 дней` / `сегодня`.
|
- [x] Настроить default range событий на `сегодня - 7 дней` / `сегодня + 7 дней`, доходов — на
|
||||||
|
`сегодня - 7 дней` / `сегодня`.
|
||||||
- [x] Заменить текстовую загрузку `События` на skeleton таблицы.
|
- [x] Заменить текстовую загрузку `События` на skeleton таблицы.
|
||||||
- [x] Заменить текстовую загрузку `Доходы` на skeleton таблицы.
|
- [x] Заменить текстовую загрузку `Доходы` на skeleton таблицы.
|
||||||
- [x] Обновить component tests под новые тексты, единое поле периода, popover-календарь и skeleton loading states.
|
- [x] Обновить component tests под новые тексты, единое поле периода, popover-календарь и skeleton loading states.
|
||||||
@ -56,18 +57,27 @@
|
|||||||
- [x] Согласовать статический визуальный эталон `docs/research/2026-06-27-broker-account-redesign.html`.
|
- [x] Согласовать статический визуальный эталон `docs/research/2026-06-27-broker-account-redesign.html`.
|
||||||
- [x] Обновить `spec.md` под HTML parity: компактные заголовки, единый toolbar, бейджи, цвета, подписи инструментов, `₽`.
|
- [x] Обновить `spec.md` под HTML parity: компактные заголовки, единый toolbar, бейджи, цвета, подписи инструментов, `₽`.
|
||||||
- [x] Обновить `plan.md` под перенос HTML parity в React-компоненты.
|
- [x] Обновить `plan.md` под перенос HTML parity в React-компоненты.
|
||||||
- [ ] Добавить `apps/frontend/src/widgets/broker-dashboard/lib/dashboardVisual.ts` с helpers для money tone, type tone, currency symbol и instrument display.
|
- [x] Добавить `apps/frontend/src/widgets/broker-dashboard/lib/dashboardVisual.ts` с helpers для money tone, type tone, currency symbol и instrument display.
|
||||||
- [ ] Добавить `apps/frontend/src/widgets/broker-dashboard/lib/dashboardVisual.test.ts`.
|
- [x] Добавить `apps/frontend/src/widgets/broker-dashboard/lib/dashboardVisual.test.ts`.
|
||||||
- [ ] Расширить income helpers так, чтобы строки доходов могли отдавать main/subtitle инструмента без дублирования текста.
|
- [ ] Расширить income helpers так, чтобы строки доходов могли отдавать main/subtitle инструмента без дублирования текста.
|
||||||
- [ ] Обновить `BrokerDashboardHero`: semantic colors для доходности, дневного изменения и всего полученных доходов.
|
- [x] Обновить `BrokerDashboardHero`: semantic colors для доходности, дневного изменения и всего полученных доходов.
|
||||||
- [ ] Обновить `BrokerDashboardCard`: компактный card heading вместо крупного page-level heading.
|
- [x] Обновить `BrokerDashboardCard`: компактный card heading вместо крупного page-level heading.
|
||||||
- [ ] Обновить `BrokerDashboardDateFilter`: единый toolbar-паттерн и chevron-иконка вместо текстового `v`.
|
- [x] Обновить `BrokerDashboardDateFilter`: единый toolbar-паттерн и chevron-иконка вместо текстового `v`.
|
||||||
- [ ] Обновить `BrokerDashboardEventsCard`: `thead`, двухстрочный инструмент, type badges, semantic amount colors, status badges.
|
- [x] Обновить `BrokerDashboardEventsCard`: `thead`, двухстрочный инструмент, type badges, semantic amount colors, status badges.
|
||||||
- [ ] Обновить `BrokerDashboardIncomeCard`: `thead`, двухстрочный инструмент, type badges, semantic amount colors.
|
- [x] Обновить `BrokerDashboardIncomeCard`: `thead`, двухстрочный инструмент, type badges, semantic amount colors.
|
||||||
- [ ] Обновить `BrokerDashboardTableSkeleton`: общий skeleton-паттерн для событий и доходов с корректной геометрией колонок.
|
Бейдж `Купон` приведён к `info` (синий) согласно HTML-эталону `.type-badge.coupon` (`incomeTypeTone` в `dashboardVisual.ts`),
|
||||||
|
`Дивиденд (внешний)` отнесён к `success` (семейство дивидендов).
|
||||||
|
- [x] Обновить `BrokerDashboardTableSkeleton`: общий skeleton-паттерн для событий и доходов с корректной геометрией колонок.
|
||||||
- [x] Обновить `BrokerDashboardAnalyticsCard`: `₽` для RUB и positive/negative tone карточек.
|
- [x] Обновить `BrokerDashboardAnalyticsCard`: `₽` для RUB и positive/negative tone карточек.
|
||||||
- [x] Обновить component tests dashboard под HTML parity (analytics tones).
|
- [x] Обновить component tests dashboard под HTML parity (analytics tones).
|
||||||
- [ ] Проверить, что блок `Доходы` не расширяет backend/API и остаётся в рамках текущих income-типов.
|
- [x] Проверить, что блок `Доходы` не расширяет backend/API и остаётся в рамках текущих income-типов.
|
||||||
|
Исправлен пресет «Всё»: `applyDatePreset('all')` теперь отдаёт широкий диапазон `2000-01-01`–`2099-12-31`
|
||||||
|
вместо пустых `from`/`to`, которые backend (`BrokerEventsQueryDto @Matches`) отвергал с 400. В DateFilter
|
||||||
|
кнопка «Применить период» блокируется при инвертированном диапазоне (`to < from`), чтобы избежать
|
||||||
|
молчаливо пустого ответа (например, будущий `from` при дефолтном `to=сегодня`).
|
||||||
|
- [x] Исправить default-range `События`, чтобы dashboard по умолчанию и через пресеты мог загружать будущие события.
|
||||||
|
- [x] Подтянуть card headers, toolbar label `Тип`, count badges и footer summaries ближе к
|
||||||
|
`docs/research/2026-06-27-broker-account-redesign.html`.
|
||||||
- [ ] Проверить desktop layout `/broker/2084014113` против `docs/research/2026-06-27-broker-account-redesign.html`.
|
- [ ] Проверить desktop layout `/broker/2084014113` против `docs/research/2026-06-27-broker-account-redesign.html`.
|
||||||
- [ ] Проверить mobile layout `/broker/2084014113` на viewport `390x844` против `docs/research/2026-06-27-broker-account-redesign.html`.
|
- [ ] Проверить mobile layout `/broker/2084014113` на viewport `390x844` против `docs/research/2026-06-27-broker-account-redesign.html`.
|
||||||
|
|
||||||
@ -127,4 +137,3 @@ HTML-эталона (`docs/research/2026-06-27-broker-account-redesign.html:1145
|
|||||||
- Реальный viewport на `390x844` для подтверждения отсутствия общего горизонтального overflow.
|
- Реальный viewport на `390x844` для подтверждения отсутствия общего горизонтального overflow.
|
||||||
Геометрия таблиц уже переключена на внутренний `overflowX: 'auto'` (`BrokerDashboardEventsCard.tsx:176`,
|
Геометрия таблиц уже переключена на внутренний `overflowX: 'auto'` (`BrokerDashboardEventsCard.tsx:176`,
|
||||||
`BrokerDashboardIncomeCard.tsx`), но фактическая вёрстка в браузере не сверялась с эталоном.
|
`BrokerDashboardIncomeCard.tsx`), но фактическая вёрстка в браузере не сверялась с эталоном.
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user