feat: improove design
This commit is contained in:
parent
d804d43616
commit
0116c34a9f
@ -11,6 +11,7 @@ export type DashboardFilterState<T extends string> = {
|
|||||||
from: string
|
from: string
|
||||||
to: string
|
to: string
|
||||||
types: T[]
|
types: T[]
|
||||||
|
preset: DashboardDatePreset
|
||||||
}
|
}
|
||||||
|
|
||||||
export function defaultEventsFilters(): DashboardFilterState<DashboardEventType> {
|
export function defaultEventsFilters(): DashboardFilterState<DashboardEventType> {
|
||||||
@ -19,6 +20,7 @@ export function defaultEventsFilters(): DashboardFilterState<DashboardEventType>
|
|||||||
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.add(7, 'day').format('YYYY-MM-DD'),
|
||||||
types: [...DASHBOARD_EVENT_TYPES],
|
types: [...DASHBOARD_EVENT_TYPES],
|
||||||
|
preset: '7d',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,6 +30,7 @@ export function defaultIncomeFilters(): DashboardFilterState<DashboardIncomeType
|
|||||||
from: now.startOf('year').format('YYYY-MM-DD'),
|
from: now.startOf('year').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',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,11 +39,12 @@ export function applyDatePreset<T extends string>(
|
|||||||
preset: DashboardDatePreset,
|
preset: DashboardDatePreset,
|
||||||
): DashboardFilterState<T> {
|
): DashboardFilterState<T> {
|
||||||
const now = dayjs()
|
const now = dayjs()
|
||||||
if (preset === 'all') return { ...filters, from: '', to: now.format('YYYY-MM-DD') }
|
const next = { ...filters, preset }
|
||||||
|
if (preset === 'all') return { ...next, from: '', to: '' }
|
||||||
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 {
|
||||||
...filters,
|
...next,
|
||||||
from: now.subtract(amount, unit).format('YYYY-MM-DD'),
|
from: now.subtract(amount, unit).format('YYYY-MM-DD'),
|
||||||
to: now.format('YYYY-MM-DD'),
|
to: now.format('YYYY-MM-DD'),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,10 +40,6 @@ vi.mock('@/entities/broker-operation', () => ({
|
|||||||
useBrokerOperations: hookMocks.useBrokerOperations,
|
useBrokerOperations: hookMocks.useBrokerOperations,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/widgets/broker-allocation-chart', () => ({
|
|
||||||
BrokerAllocationChart: () => <div>allocation chart</div>,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const portfolio: BrokerPortfolio = {
|
const portfolio: BrokerPortfolio = {
|
||||||
account: {
|
account: {
|
||||||
id: 'acc-1',
|
id: 'acc-1',
|
||||||
@ -99,14 +95,14 @@ describe('BrokerDashboard', () => {
|
|||||||
expect(screen.getByText('Доходы')).toBeInTheDocument()
|
expect(screen.getByText('Доходы')).toBeInTheDocument()
|
||||||
expect(screen.getByText('Аналитика доходности')).toBeInTheDocument()
|
expect(screen.getByText('Аналитика доходности')).toBeInTheDocument()
|
||||||
expect(screen.getByText('Аллокация')).toBeInTheDocument()
|
expect(screen.getByText('Аллокация')).toBeInTheDocument()
|
||||||
expect(screen.getByText('allocation chart')).toBeInTheDocument()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses event chips as request filters', 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} />)
|
render(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
|
||||||
|
|
||||||
await user.click(screen.getAllByRole('button', { name: 'Купоны' })[0])
|
const couponChips = screen.getAllByRole('button', { name: 'Купоны' })
|
||||||
|
await user.click(couponChips[0])
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(hookMocks.useBrokerEvents).toHaveBeenLastCalledWith(
|
expect(hookMocks.useBrokerEvents).toHaveBeenLastCalledWith(
|
||||||
@ -117,11 +113,12 @@ describe('BrokerDashboard', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses income chips as request filters', 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} />)
|
render(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
|
||||||
|
|
||||||
await user.click(screen.getAllByRole('button', { name: 'Купоны' })[1])
|
const couponChips = screen.getAllByRole('button', { name: 'Купоны' })
|
||||||
|
await user.click(couponChips[1])
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(hookMocks.useBrokerOperations).toHaveBeenLastCalledWith(
|
expect(hookMocks.useBrokerOperations).toHaveBeenLastCalledWith(
|
||||||
@ -133,4 +130,21 @@ describe('BrokerDashboard', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('shows date filter toggle button and expandable panel', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
|
||||||
|
|
||||||
|
const filterButtons = screen.getAllByRole('button', { name: /Фильтр дат/ })
|
||||||
|
expect(filterButtons).toHaveLength(2)
|
||||||
|
|
||||||
|
await user.click(filterButtons[0])
|
||||||
|
|
||||||
|
expect(screen.getByText('7д')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('30д')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('90д')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('1г')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Всё')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Сбросить')).toBeInTheDocument()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
import { Box } from '@mui/material'
|
import { Box } from '@mui/material'
|
||||||
import { useState } from 'react'
|
import dayjs from 'dayjs'
|
||||||
|
import { useCallback, useState } from 'react'
|
||||||
import { useBrokerAnalytics } from '@/entities/broker-analytics'
|
import { useBrokerAnalytics } from '@/entities/broker-analytics'
|
||||||
import { useBrokerEvents } from '@/entities/broker-event'
|
import { useBrokerEvents } from '@/entities/broker-event'
|
||||||
import { useBrokerOperations } from '@/entities/broker-operation'
|
import { useBrokerOperations } from '@/entities/broker-operation'
|
||||||
import type { BrokerPortfolio } from '@/shared/api'
|
import type { BrokerPortfolio } from '@/shared/api'
|
||||||
import { useCursorPagination } from '@/shared/lib/useCursorPagination'
|
import { useCursorPagination } from '@/shared/lib/useCursorPagination'
|
||||||
import {
|
import {
|
||||||
|
applyDatePreset,
|
||||||
|
type DashboardDatePreset,
|
||||||
type DashboardEventType,
|
type DashboardEventType,
|
||||||
type DashboardIncomeType,
|
type DashboardIncomeType,
|
||||||
defaultEventsFilters,
|
defaultEventsFilters,
|
||||||
@ -18,6 +21,15 @@ import { BrokerDashboardEventsCard } from './BrokerDashboardEventsCard'
|
|||||||
import { BrokerDashboardHero } from './BrokerDashboardHero'
|
import { BrokerDashboardHero } from './BrokerDashboardHero'
|
||||||
import { BrokerDashboardIncomeCard } from './BrokerDashboardIncomeCard'
|
import { BrokerDashboardIncomeCard } from './BrokerDashboardIncomeCard'
|
||||||
|
|
||||||
|
function formatDateLabel(from: string, to: string): string | null {
|
||||||
|
if (!from && !to) return 'За всё время'
|
||||||
|
const fmt = (d: string) => dayjs(d).format('D MMM')
|
||||||
|
if (from && to) return `${fmt(from)} – ${fmt(to)}`
|
||||||
|
if (from) return `с ${fmt(from)}`
|
||||||
|
if (to) return `до ${fmt(to)}`
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
export function BrokerDashboard({
|
export function BrokerDashboard({
|
||||||
accountId,
|
accountId,
|
||||||
portfolio,
|
portfolio,
|
||||||
@ -25,34 +37,39 @@ export function BrokerDashboard({
|
|||||||
accountId: string
|
accountId: string
|
||||||
portfolio: BrokerPortfolio
|
portfolio: BrokerPortfolio
|
||||||
}) {
|
}) {
|
||||||
const [eventFilters, setEventFilters] = useState(defaultEventsFilters)
|
const [appliedEventFilters, setAppliedEventFilters] = useState(defaultEventsFilters)
|
||||||
|
const [draftEventFilters, setDraftEventFilters] = useState(defaultEventsFilters)
|
||||||
|
const [eventFilterPanelOpen, setEventFilterPanelOpen] = useState(false)
|
||||||
const [eventPage, setEventPage] = useState(1)
|
const [eventPage, setEventPage] = useState(1)
|
||||||
const [incomeFilters, setIncomeFilters] = useState(defaultIncomeFilters)
|
|
||||||
|
const [appliedIncomeFilters, setAppliedIncomeFilters] = 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)
|
||||||
const hasEventTypes = eventFilters.types.length > 0
|
const hasEventTypes = appliedEventFilters.types.length > 0
|
||||||
const hasIncomeTypes = incomeFilters.types.length > 0
|
const hasIncomeTypes = appliedIncomeFilters.types.length > 0
|
||||||
|
const hasDraftEventTypes = draftEventFilters.types.length > 0
|
||||||
|
const hasDraftIncomeTypes = draftIncomeFilters.types.length > 0
|
||||||
|
const eventPageSize = 10
|
||||||
|
|
||||||
const events = useBrokerEvents(
|
const events = useBrokerEvents(
|
||||||
accountId,
|
accountId,
|
||||||
{
|
{
|
||||||
from: eventFilters.from,
|
from: appliedEventFilters.from,
|
||||||
to: eventFilters.to,
|
to: appliedEventFilters.to,
|
||||||
types: eventFilters.types.join(','),
|
types: appliedEventFilters.types.join(','),
|
||||||
},
|
},
|
||||||
{ enabled: hasEventTypes },
|
{ enabled: hasEventTypes },
|
||||||
)
|
)
|
||||||
const eventItems = events.data?.items ?? []
|
|
||||||
const eventPageSize = 10
|
|
||||||
const eventPageItems = eventItems.slice(
|
|
||||||
(eventPage - 1) * eventPageSize,
|
|
||||||
eventPage * eventPageSize,
|
|
||||||
)
|
|
||||||
const operations = useBrokerOperations(
|
const operations = useBrokerOperations(
|
||||||
accountId,
|
accountId,
|
||||||
{
|
{
|
||||||
from: incomeFilters.from,
|
from: appliedIncomeFilters.from,
|
||||||
to: incomeFilters.to,
|
to: appliedIncomeFilters.to,
|
||||||
operationTypes: incomeTypesToOperationTypes(incomeFilters.types),
|
operationTypes: incomeTypesToOperationTypes(appliedIncomeFilters.types),
|
||||||
cursor: incomePagination.cursor,
|
cursor: incomePagination.cursor,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
},
|
},
|
||||||
@ -63,26 +80,106 @@ export function BrokerDashboard({
|
|||||||
? (operations.data.nextCursor as unknown as string)
|
? (operations.data.nextCursor as unknown as string)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
|
const eventItems = events.data?.items ?? []
|
||||||
|
const eventPageItems = eventItems.slice(
|
||||||
|
(eventPage - 1) * eventPageSize,
|
||||||
|
eventPage * eventPageSize,
|
||||||
|
)
|
||||||
|
|
||||||
function toggleEventType(type: DashboardEventType) {
|
function toggleEventType(type: DashboardEventType) {
|
||||||
setEventFilters((filters) => ({
|
setAppliedEventFilters((filters) => {
|
||||||
...filters,
|
const nextTypes = filters.types.includes(type)
|
||||||
types: filters.types.includes(type)
|
|
||||||
? filters.types.filter((item) => item !== type)
|
? filters.types.filter((item) => item !== type)
|
||||||
: [...filters.types, type],
|
: [...filters.types, type]
|
||||||
}))
|
return { ...filters, types: nextTypes }
|
||||||
|
})
|
||||||
|
setDraftEventFilters((filters) => {
|
||||||
|
const nextTypes = filters.types.includes(type)
|
||||||
|
? filters.types.filter((item) => item !== type)
|
||||||
|
: [...filters.types, type]
|
||||||
|
return { ...filters, types: nextTypes }
|
||||||
|
})
|
||||||
setEventPage(1)
|
setEventPage(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleIncomeType(type: DashboardIncomeType) {
|
function toggleIncomeType(type: DashboardIncomeType) {
|
||||||
setIncomeFilters((filters) => ({
|
setAppliedIncomeFilters((filters) => {
|
||||||
...filters,
|
const nextTypes = filters.types.includes(type)
|
||||||
types: filters.types.includes(type)
|
|
||||||
? filters.types.filter((item) => item !== type)
|
? filters.types.filter((item) => item !== type)
|
||||||
: [...filters.types, type],
|
: [...filters.types, type]
|
||||||
}))
|
return { ...filters, types: nextTypes }
|
||||||
|
})
|
||||||
|
setDraftIncomeFilters((filters) => {
|
||||||
|
const nextTypes = filters.types.includes(type)
|
||||||
|
? filters.types.filter((item) => item !== type)
|
||||||
|
: [...filters.types, type]
|
||||||
|
return { ...filters, types: nextTypes }
|
||||||
|
})
|
||||||
incomePagination.reset()
|
incomePagination.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const applyEventFilters = useCallback(() => {
|
||||||
|
setAppliedEventFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
from: draftEventFilters.from,
|
||||||
|
to: draftEventFilters.to,
|
||||||
|
preset: draftEventFilters.preset,
|
||||||
|
}))
|
||||||
|
setEventFilterPanelOpen(false)
|
||||||
|
setEventPage(1)
|
||||||
|
}, [draftEventFilters.from, draftEventFilters.to, draftEventFilters.preset])
|
||||||
|
|
||||||
|
const resetEventFilters = useCallback(() => {
|
||||||
|
const defaults = defaultEventsFilters()
|
||||||
|
setAppliedEventFilters(defaults)
|
||||||
|
setDraftEventFilters(defaults)
|
||||||
|
setEventFilterPanelOpen(false)
|
||||||
|
setEventPage(1)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const applyIncomeFilters = useCallback(() => {
|
||||||
|
setAppliedIncomeFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
from: draftIncomeFilters.from,
|
||||||
|
to: draftIncomeFilters.to,
|
||||||
|
preset: draftIncomeFilters.preset,
|
||||||
|
}))
|
||||||
|
setIncomeFilterPanelOpen(false)
|
||||||
|
incomePagination.reset()
|
||||||
|
}, [draftIncomeFilters.from, draftIncomeFilters.to, draftIncomeFilters.preset, incomePagination])
|
||||||
|
|
||||||
|
const resetIncomeFilters = useCallback(() => {
|
||||||
|
const defaults = defaultIncomeFilters()
|
||||||
|
setAppliedIncomeFilters(defaults)
|
||||||
|
setDraftIncomeFilters(defaults)
|
||||||
|
setIncomeFilterPanelOpen(false)
|
||||||
|
incomePagination.reset()
|
||||||
|
}, [incomePagination])
|
||||||
|
|
||||||
|
function handleDraftEventPresetChange(preset: DashboardDatePreset) {
|
||||||
|
setDraftEventFilters((filters) => applyDatePreset(filters, preset))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDraftEventFromChange(value: string) {
|
||||||
|
setDraftEventFilters((filters) => ({ ...filters, from: value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDraftEventToChange(value: string) {
|
||||||
|
setDraftEventFilters((filters) => ({ ...filters, to: value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDraftIncomePresetChange(preset: DashboardDatePreset) {
|
||||||
|
setDraftIncomeFilters((filters) => applyDatePreset(filters, preset))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDraftIncomeFromChange(value: string) {
|
||||||
|
setDraftIncomeFilters((filters) => ({ ...filters, from: value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDraftIncomeToChange(value: string) {
|
||||||
|
setDraftIncomeFilters((filters) => ({ ...filters, to: value }))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'grid', gap: 3 }}>
|
<Box sx={{ display: 'grid', gap: 3 }}>
|
||||||
<BrokerDashboardHero portfolio={portfolio} analytics={analytics.data} />
|
<BrokerDashboardHero portfolio={portfolio} analytics={analytics.data} />
|
||||||
@ -91,8 +188,20 @@ export function BrokerDashboard({
|
|||||||
data={events.data ? { ...events.data, items: eventPageItems } : undefined}
|
data={events.data ? { ...events.data, items: eventPageItems } : undefined}
|
||||||
isLoading={events.isLoading}
|
isLoading={events.isLoading}
|
||||||
isError={events.isError}
|
isError={events.isError}
|
||||||
selectedTypes={eventFilters.types}
|
selectedTypes={draftEventFilters.types}
|
||||||
onToggleType={toggleEventType}
|
onToggleType={toggleEventType}
|
||||||
|
appliedDateLabel={formatDateLabel(appliedEventFilters.from, appliedEventFilters.to)}
|
||||||
|
draftPreset={draftEventFilters.preset}
|
||||||
|
draftFrom={draftEventFilters.from}
|
||||||
|
draftTo={draftEventFilters.to}
|
||||||
|
onDraftPresetChange={handleDraftEventPresetChange}
|
||||||
|
onDraftFromChange={handleDraftEventFromChange}
|
||||||
|
onDraftToChange={handleDraftEventToChange}
|
||||||
|
onApplyFilters={applyEventFilters}
|
||||||
|
onResetFilters={resetEventFilters}
|
||||||
|
dateFilterOpen={eventFilterPanelOpen}
|
||||||
|
onToggleDateFilter={() => setEventFilterPanelOpen((v) => !v)}
|
||||||
|
hasDraftTypes={hasDraftEventTypes}
|
||||||
page={eventPage}
|
page={eventPage}
|
||||||
canGoBack={eventPage > 1}
|
canGoBack={eventPage > 1}
|
||||||
canGoForward={eventPage * eventPageSize < eventItems.length}
|
canGoForward={eventPage * eventPageSize < eventItems.length}
|
||||||
@ -104,8 +213,20 @@ export function BrokerDashboard({
|
|||||||
page={operations.data}
|
page={operations.data}
|
||||||
isLoading={operations.isLoading}
|
isLoading={operations.isLoading}
|
||||||
isError={operations.isError}
|
isError={operations.isError}
|
||||||
selectedTypes={incomeFilters.types}
|
selectedTypes={draftIncomeFilters.types}
|
||||||
onToggleType={toggleIncomeType}
|
onToggleType={toggleIncomeType}
|
||||||
|
appliedDateLabel={formatDateLabel(appliedIncomeFilters.from, appliedIncomeFilters.to)}
|
||||||
|
draftPreset={draftIncomeFilters.preset}
|
||||||
|
draftFrom={draftIncomeFilters.from}
|
||||||
|
draftTo={draftIncomeFilters.to}
|
||||||
|
onDraftPresetChange={handleDraftIncomePresetChange}
|
||||||
|
onDraftFromChange={handleDraftIncomeFromChange}
|
||||||
|
onDraftToChange={handleDraftIncomeToChange}
|
||||||
|
onApplyFilters={applyIncomeFilters}
|
||||||
|
onResetFilters={resetIncomeFilters}
|
||||||
|
dateFilterOpen={incomeFilterPanelOpen}
|
||||||
|
onToggleDateFilter={() => setIncomeFilterPanelOpen((v) => !v)}
|
||||||
|
hasDraftTypes={hasDraftIncomeTypes}
|
||||||
pageNumber={incomePagination.pageNumber}
|
pageNumber={incomePagination.pageNumber}
|
||||||
canGoBack={incomePagination.pageNumber > 1}
|
canGoBack={incomePagination.pageNumber > 1}
|
||||||
canGoForward={operations.data?.hasNext ?? false}
|
canGoForward={operations.data?.hasNext ?? false}
|
||||||
|
|||||||
@ -1,19 +1,78 @@
|
|||||||
import { Text } from '@moex-vibe/design-system'
|
import { Text } from '@moex-vibe/design-system'
|
||||||
import { Box } from '@mui/material'
|
import { Box } from '@mui/material'
|
||||||
|
import { buildBrokerAllocation } from '@/entities/broker-position'
|
||||||
import type { BrokerPortfolio } from '@/shared/api'
|
import type { BrokerPortfolio } from '@/shared/api'
|
||||||
import { formatBrokerMoney } from '@/shared/lib/formatters'
|
import { formatBrokerCurrencyValue, formatBrokerMoney } from '@/shared/lib/formatters'
|
||||||
import { BrokerAllocationChart } from '@/widgets/broker-allocation-chart'
|
|
||||||
import { BrokerDashboardCard } from './BrokerDashboardCard'
|
import { BrokerDashboardCard } from './BrokerDashboardCard'
|
||||||
|
|
||||||
|
const ALLOCATION_COLORS: Record<string, string> = {
|
||||||
|
shares: '#4969f5',
|
||||||
|
bonds: '#e5a33c',
|
||||||
|
etf: '#62b889',
|
||||||
|
cash: '#7b63cf',
|
||||||
|
other: '#aeb6c5',
|
||||||
|
}
|
||||||
|
|
||||||
export function BrokerDashboardAllocationCard({ portfolio }: { portfolio: BrokerPortfolio }) {
|
export function BrokerDashboardAllocationCard({ portfolio }: { portfolio: BrokerPortfolio }) {
|
||||||
|
const { total, sectors, negative } = buildBrokerAllocation(portfolio)
|
||||||
|
const currency =
|
||||||
|
total >= 0
|
||||||
|
? (portfolio.totals.portfolio?.currency ??
|
||||||
|
Object.values(portfolio.totals).find((t) => t?.currency)?.currency ??
|
||||||
|
'RUB')
|
||||||
|
: 'RUB'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrokerDashboardCard
|
<BrokerDashboardCard
|
||||||
title="Аллокация"
|
title="Аллокация"
|
||||||
action={<Text variant="numeric">{formatBrokerMoney(portfolio.totals.portfolio)}</Text>}
|
action={<Text variant="numeric">{formatBrokerMoney(portfolio.totals.portfolio)}</Text>}
|
||||||
>
|
>
|
||||||
<Box sx={{ minHeight: 220, display: 'flex', alignItems: 'center' }}>
|
{sectors.length === 0 && negative.length === 0 ? (
|
||||||
<BrokerAllocationChart portfolio={portfolio} />
|
<Text tone="muted">Нет данных для распределения</Text>
|
||||||
</Box>
|
) : (
|
||||||
|
<Box sx={{ display: 'grid', gap: 1.5 }}>
|
||||||
|
{sectors.map((sector) => (
|
||||||
|
<Box key={sector.key}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
||||||
|
<Text variant="body">{sector.label}</Text>
|
||||||
|
<Text variant="body" tone="secondary">
|
||||||
|
{formatBrokerCurrencyValue(currency, sector.value)} · {sector.percent.toFixed(1)}%
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: 10,
|
||||||
|
borderRadius: 5,
|
||||||
|
bgcolor: 'grey.200',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: `${sector.percent}%`,
|
||||||
|
height: '100%',
|
||||||
|
bgcolor: ALLOCATION_COLORS[sector.key] ?? '#aeb6c5',
|
||||||
|
borderRadius: 5,
|
||||||
|
transition: 'width 0.3s',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
{negative.length > 0 && (
|
||||||
|
<Box sx={{ mt: 1 }}>
|
||||||
|
{negative.map((item) => (
|
||||||
|
<Box key={item.key} sx={{ display: 'flex', gap: 1 }}>
|
||||||
|
<Text variant="body">{item.label}:</Text>
|
||||||
|
<Text variant="body" tone="negative">
|
||||||
|
{formatBrokerCurrencyValue(currency, item.value)}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</BrokerDashboardCard>
|
</BrokerDashboardCard>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,156 @@
|
|||||||
|
import { Button, Chip, Text } from '@moex-vibe/design-system'
|
||||||
|
import { Box } from '@mui/material'
|
||||||
|
import type { DashboardDatePreset } from '../lib/dashboardFilters'
|
||||||
|
|
||||||
|
const PRESETS: { key: DashboardDatePreset; label: string }[] = [
|
||||||
|
{ key: '7d', label: '7д' },
|
||||||
|
{ key: '30d', label: '30д' },
|
||||||
|
{ key: '90d', label: '90д' },
|
||||||
|
{ key: '1y', label: '1г' },
|
||||||
|
{ key: 'all', label: 'Всё' },
|
||||||
|
]
|
||||||
|
|
||||||
|
type BrokerDashboardDateFilterProps = {
|
||||||
|
appliedLabel: string | null
|
||||||
|
preset: DashboardDatePreset
|
||||||
|
draftFrom: string
|
||||||
|
draftTo: string
|
||||||
|
hasDraftTypes: boolean
|
||||||
|
onPresetChange: (preset: DashboardDatePreset) => void
|
||||||
|
onFromChange: (value: string) => void
|
||||||
|
onToChange: (value: string) => void
|
||||||
|
onReset: () => void
|
||||||
|
onApply: () => void
|
||||||
|
isOpen: boolean
|
||||||
|
onToggle: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BrokerDashboardDateFilter({
|
||||||
|
appliedLabel,
|
||||||
|
preset,
|
||||||
|
draftFrom,
|
||||||
|
draftTo,
|
||||||
|
hasDraftTypes,
|
||||||
|
onPresetChange,
|
||||||
|
onFromChange,
|
||||||
|
onToChange,
|
||||||
|
onReset,
|
||||||
|
onApply,
|
||||||
|
isOpen,
|
||||||
|
onToggle,
|
||||||
|
}: BrokerDashboardDateFilterProps) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
|
||||||
|
<Box
|
||||||
|
component="button"
|
||||||
|
type="button"
|
||||||
|
onClick={onToggle}
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
bgcolor: 'grey.800',
|
||||||
|
color: 'common.white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 1.5,
|
||||||
|
px: 1.5,
|
||||||
|
py: 0.75,
|
||||||
|
fontSize: 13,
|
||||||
|
cursor: 'pointer',
|
||||||
|
'&:hover': { bgcolor: 'grey.700' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
📅 Фильтр дат
|
||||||
|
{appliedLabel && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'grey.600',
|
||||||
|
borderRadius: 10,
|
||||||
|
px: 0.75,
|
||||||
|
py: 0.125,
|
||||||
|
fontSize: 11,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{appliedLabel}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Button variant="primary" size="small" onClick={onApply} disabled={!hasDraftTypes}>
|
||||||
|
Показать
|
||||||
|
</Button>
|
||||||
|
{isOpen && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
mt: 0.5,
|
||||||
|
p: 1.5,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
borderRadius: 2,
|
||||||
|
bgcolor: 'grey.50',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mb: 1 }}>
|
||||||
|
{PRESETS.map((p) => (
|
||||||
|
<Chip
|
||||||
|
key={p.key}
|
||||||
|
label={p.label}
|
||||||
|
tone={p.key === preset ? 'primary' : 'neutral'}
|
||||||
|
selected={p.key === preset}
|
||||||
|
onClick={() => onPresetChange(p.key)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||||
|
<input
|
||||||
|
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>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={draftTo}
|
||||||
|
onChange={(e) => onToChange(e.target.value)}
|
||||||
|
style={{
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: 6,
|
||||||
|
padding: '4px 8px',
|
||||||
|
fontSize: 13,
|
||||||
|
flex: '0 1 auto',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
component="button"
|
||||||
|
type="button"
|
||||||
|
onClick={onReset}
|
||||||
|
sx={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
color: 'text.secondary',
|
||||||
|
fontSize: 12,
|
||||||
|
cursor: 'pointer',
|
||||||
|
textDecoration: 'underline',
|
||||||
|
textUnderlineOffset: 2,
|
||||||
|
p: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Сбросить
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -3,9 +3,10 @@ import { Box } from '@mui/material'
|
|||||||
import { Link } from '@tanstack/react-router'
|
import { Link } from '@tanstack/react-router'
|
||||||
import type { BrokerEventItem, BrokerEventsData } from '@/shared/api'
|
import type { BrokerEventItem, BrokerEventsData } from '@/shared/api'
|
||||||
import { formatBrokerCurrencyValue, formatBrokerDate } from '@/shared/lib/formatters'
|
import { formatBrokerCurrencyValue, formatBrokerDate } from '@/shared/lib/formatters'
|
||||||
import type { DashboardEventType } from '../lib/dashboardFilters'
|
import type { DashboardDatePreset, DashboardEventType } from '../lib/dashboardFilters'
|
||||||
import { eventStatusLabel, eventTypeLabel } from '../lib/dashboardFormatters'
|
import { eventStatusLabel, eventTypeLabel } from '../lib/dashboardFormatters'
|
||||||
import { BrokerDashboardCard } from './BrokerDashboardCard'
|
import { BrokerDashboardCard } from './BrokerDashboardCard'
|
||||||
|
import { BrokerDashboardDateFilter } from './BrokerDashboardDateFilter'
|
||||||
|
|
||||||
const EVENT_FILTERS: Array<{
|
const EVENT_FILTERS: Array<{
|
||||||
type: DashboardEventType
|
type: DashboardEventType
|
||||||
@ -25,6 +26,18 @@ type BrokerDashboardEventsCardProps = {
|
|||||||
isError: boolean
|
isError: boolean
|
||||||
selectedTypes: DashboardEventType[]
|
selectedTypes: DashboardEventType[]
|
||||||
onToggleType: (type: DashboardEventType) => void
|
onToggleType: (type: DashboardEventType) => void
|
||||||
|
appliedDateLabel: string | null
|
||||||
|
draftPreset: DashboardDatePreset
|
||||||
|
draftFrom: string
|
||||||
|
draftTo: string
|
||||||
|
onDraftPresetChange: (preset: DashboardDatePreset) => void
|
||||||
|
onDraftFromChange: (value: string) => void
|
||||||
|
onDraftToChange: (value: string) => void
|
||||||
|
onApplyFilters: () => void
|
||||||
|
onResetFilters: () => void
|
||||||
|
dateFilterOpen: boolean
|
||||||
|
onToggleDateFilter: () => void
|
||||||
|
hasDraftTypes: boolean
|
||||||
page: number
|
page: number
|
||||||
onPreviousPage: () => void
|
onPreviousPage: () => void
|
||||||
onNextPage: () => void
|
onNextPage: () => void
|
||||||
@ -46,6 +59,18 @@ export function BrokerDashboardEventsCard({
|
|||||||
isError,
|
isError,
|
||||||
selectedTypes,
|
selectedTypes,
|
||||||
onToggleType,
|
onToggleType,
|
||||||
|
appliedDateLabel,
|
||||||
|
draftPreset,
|
||||||
|
draftFrom,
|
||||||
|
draftTo,
|
||||||
|
onDraftPresetChange,
|
||||||
|
onDraftFromChange,
|
||||||
|
onDraftToChange,
|
||||||
|
onApplyFilters,
|
||||||
|
onResetFilters,
|
||||||
|
dateFilterOpen,
|
||||||
|
onToggleDateFilter,
|
||||||
|
hasDraftTypes,
|
||||||
page,
|
page,
|
||||||
onPreviousPage,
|
onPreviousPage,
|
||||||
onNextPage,
|
onNextPage,
|
||||||
@ -58,18 +83,36 @@ export function BrokerDashboardEventsCard({
|
|||||||
<BrokerDashboardCard
|
<BrokerDashboardCard
|
||||||
title="События"
|
title="События"
|
||||||
action={<Link to={`/broker/${encodeURIComponent(accountId)}/events`}>Все события</Link>}
|
action={<Link to={`/broker/${encodeURIComponent(accountId)}/events`}>Все события</Link>}
|
||||||
>
|
filters={
|
||||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 1.5 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
{EVENT_FILTERS.map((filter) => (
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||||
<Chip
|
{EVENT_FILTERS.map((filter) => (
|
||||||
key={filter.type}
|
<Chip
|
||||||
label={filter.label}
|
key={filter.type}
|
||||||
tone={filter.tone}
|
label={filter.label}
|
||||||
selected={selectedTypes.includes(filter.type)}
|
tone={filter.tone}
|
||||||
onClick={() => onToggleType(filter.type)}
|
selected={selectedTypes.includes(filter.type)}
|
||||||
|
onClick={() => onToggleType(filter.type)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
<BrokerDashboardDateFilter
|
||||||
|
appliedLabel={appliedDateLabel}
|
||||||
|
preset={draftPreset}
|
||||||
|
draftFrom={draftFrom}
|
||||||
|
draftTo={draftTo}
|
||||||
|
hasDraftTypes={hasDraftTypes}
|
||||||
|
onPresetChange={onDraftPresetChange}
|
||||||
|
onFromChange={onDraftFromChange}
|
||||||
|
onToChange={onDraftToChange}
|
||||||
|
onReset={onResetFilters}
|
||||||
|
onApply={onApplyFilters}
|
||||||
|
isOpen={dateFilterOpen}
|
||||||
|
onToggle={onToggleDateFilter}
|
||||||
/>
|
/>
|
||||||
))}
|
</Box>
|
||||||
</Box>
|
}
|
||||||
|
>
|
||||||
{selectedTypes.length === 0 ? (
|
{selectedTypes.length === 0 ? (
|
||||||
<Text tone="negative">Выберите хотя бы один тип событий</Text>
|
<Text tone="negative">Выберите хотя бы один тип событий</Text>
|
||||||
) : isError ? (
|
) : isError ? (
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export function BrokerDashboardHero({ portfolio, analytics }: BrokerDashboardHer
|
|||||||
display: 'grid',
|
display: 'grid',
|
||||||
gap: 2,
|
gap: 2,
|
||||||
gridTemplateColumns: { xs: '1fr', md: 'minmax(240px, 1fr) repeat(3, auto)' },
|
gridTemplateColumns: { xs: '1fr', md: 'minmax(240px, 1fr) repeat(3, auto)' },
|
||||||
alignItems: 'center',
|
alignItems: 'stretch',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>
|
<Box>
|
||||||
@ -43,13 +43,19 @@ export function BrokerDashboardHero({ portfolio, analytics }: BrokerDashboardHer
|
|||||||
{accountName}
|
{accountName}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Metric label="Стоимость портфеля" value={formatBrokerMoney(portfolio.totals.portfolio)} />
|
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||||
<Metric
|
<Metric label="Стоимость портфеля" value={formatBrokerMoney(portfolio.totals.portfolio)} />
|
||||||
label="Доходность"
|
</Box>
|
||||||
value={percentValue(returnPercent)}
|
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||||
supportingText={`За день: ${formatBrokerMoney(portfolio.yields.daily)}`}
|
<Metric
|
||||||
/>
|
label="Доходность"
|
||||||
<Metric label="Всего доходов" value={totalReceived} />
|
value={percentValue(returnPercent)}
|
||||||
|
supportingText={`За день: ${formatBrokerMoney(portfolio.yields.daily)}`}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||||
|
<Metric label="Всего доходов" value={totalReceived} />
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,10 @@ import { Box } from '@mui/material'
|
|||||||
import { Link } from '@tanstack/react-router'
|
import { Link } from '@tanstack/react-router'
|
||||||
import type { BrokerOperationsPage } from '@/shared/api'
|
import type { BrokerOperationsPage } from '@/shared/api'
|
||||||
import { formatBrokerCurrencyValue, formatBrokerDate } from '@/shared/lib/formatters'
|
import { formatBrokerCurrencyValue, formatBrokerDate } from '@/shared/lib/formatters'
|
||||||
import type { DashboardIncomeType } from '../lib/dashboardFilters'
|
import type { DashboardDatePreset, DashboardIncomeType } from '../lib/dashboardFilters'
|
||||||
import { getDashboardIncomeRows, sumDashboardIncome } from '../lib/dashboardIncome'
|
import { getDashboardIncomeRows, sumDashboardIncome } from '../lib/dashboardIncome'
|
||||||
import { BrokerDashboardCard } from './BrokerDashboardCard'
|
import { BrokerDashboardCard } from './BrokerDashboardCard'
|
||||||
|
import { BrokerDashboardDateFilter } from './BrokerDashboardDateFilter'
|
||||||
|
|
||||||
const INCOME_FILTERS: Array<{
|
const INCOME_FILTERS: Array<{
|
||||||
type: DashboardIncomeType
|
type: DashboardIncomeType
|
||||||
@ -23,6 +24,18 @@ type BrokerDashboardIncomeCardProps = {
|
|||||||
isError: boolean
|
isError: boolean
|
||||||
selectedTypes: DashboardIncomeType[]
|
selectedTypes: DashboardIncomeType[]
|
||||||
onToggleType: (type: DashboardIncomeType) => void
|
onToggleType: (type: DashboardIncomeType) => void
|
||||||
|
appliedDateLabel: string | null
|
||||||
|
draftPreset: DashboardDatePreset
|
||||||
|
draftFrom: string
|
||||||
|
draftTo: string
|
||||||
|
onDraftPresetChange: (preset: DashboardDatePreset) => void
|
||||||
|
onDraftFromChange: (value: string) => void
|
||||||
|
onDraftToChange: (value: string) => void
|
||||||
|
onApplyFilters: () => void
|
||||||
|
onResetFilters: () => void
|
||||||
|
dateFilterOpen: boolean
|
||||||
|
onToggleDateFilter: () => void
|
||||||
|
hasDraftTypes: boolean
|
||||||
pageNumber: number
|
pageNumber: number
|
||||||
canGoBack: boolean
|
canGoBack: boolean
|
||||||
canGoForward: boolean
|
canGoForward: boolean
|
||||||
@ -37,6 +50,18 @@ export function BrokerDashboardIncomeCard({
|
|||||||
isError,
|
isError,
|
||||||
selectedTypes,
|
selectedTypes,
|
||||||
onToggleType,
|
onToggleType,
|
||||||
|
appliedDateLabel,
|
||||||
|
draftPreset,
|
||||||
|
draftFrom,
|
||||||
|
draftTo,
|
||||||
|
onDraftPresetChange,
|
||||||
|
onDraftFromChange,
|
||||||
|
onDraftToChange,
|
||||||
|
onApplyFilters,
|
||||||
|
onResetFilters,
|
||||||
|
dateFilterOpen,
|
||||||
|
onToggleDateFilter,
|
||||||
|
hasDraftTypes,
|
||||||
pageNumber,
|
pageNumber,
|
||||||
canGoBack,
|
canGoBack,
|
||||||
canGoForward,
|
canGoForward,
|
||||||
@ -50,18 +75,36 @@ export function BrokerDashboardIncomeCard({
|
|||||||
<BrokerDashboardCard
|
<BrokerDashboardCard
|
||||||
title="Доходы"
|
title="Доходы"
|
||||||
action={<Link to={`/broker/${encodeURIComponent(accountId)}/operations`}>Все операции</Link>}
|
action={<Link to={`/broker/${encodeURIComponent(accountId)}/operations`}>Все операции</Link>}
|
||||||
>
|
filters={
|
||||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 1.5 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
{INCOME_FILTERS.map((filter) => (
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||||
<Chip
|
{INCOME_FILTERS.map((filter) => (
|
||||||
key={filter.type}
|
<Chip
|
||||||
label={filter.label}
|
key={filter.type}
|
||||||
tone={filter.tone}
|
label={filter.label}
|
||||||
selected={selectedTypes.includes(filter.type)}
|
tone={filter.tone}
|
||||||
onClick={() => onToggleType(filter.type)}
|
selected={selectedTypes.includes(filter.type)}
|
||||||
|
onClick={() => onToggleType(filter.type)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
<BrokerDashboardDateFilter
|
||||||
|
appliedLabel={appliedDateLabel}
|
||||||
|
preset={draftPreset}
|
||||||
|
draftFrom={draftFrom}
|
||||||
|
draftTo={draftTo}
|
||||||
|
hasDraftTypes={hasDraftTypes}
|
||||||
|
onPresetChange={onDraftPresetChange}
|
||||||
|
onFromChange={onDraftFromChange}
|
||||||
|
onToChange={onDraftToChange}
|
||||||
|
onReset={onResetFilters}
|
||||||
|
onApply={onApplyFilters}
|
||||||
|
isOpen={dateFilterOpen}
|
||||||
|
onToggle={onToggleDateFilter}
|
||||||
/>
|
/>
|
||||||
))}
|
</Box>
|
||||||
</Box>
|
}
|
||||||
|
>
|
||||||
{selectedTypes.length === 0 ? (
|
{selectedTypes.length === 0 ? (
|
||||||
<Text tone="negative">Выберите хотя бы один тип доходов</Text>
|
<Text tone="negative">Выберите хотя бы один тип доходов</Text>
|
||||||
) : isError ? (
|
) : isError ? (
|
||||||
|
|||||||
@ -26,11 +26,11 @@
|
|||||||
- [x] Перенести навигацию счёта из левой колонки в горизонтальные вкладки над контентом.
|
- [x] Перенести навигацию счёта из левой колонки в горизонтальные вкладки над контентом.
|
||||||
- [x] Перестроить dashboard на один блок на строке для `События`, `Доходы`, `Аналитика доходности`, `Аллокация`.
|
- [x] Перестроить dashboard на один блок на строке для `События`, `Доходы`, `Аналитика доходности`, `Аллокация`.
|
||||||
- [x] Добавить кликабельные chip-фильтры типов для `События`.
|
- [x] Добавить кликабельные chip-фильтры типов для `События`.
|
||||||
- [ ] Добавить `BrokerDashboardDateFilter` — переиспользуемый expandable-компонент фильтра дат (пресеты 7д/30д/90д/1г/Всё, from/to поля, Сбросить/Показать).
|
- [x] Добавить `BrokerDashboardDateFilter` — переиспользуемый expandable-компонент фильтра дат (пресеты 7д/30д/90д/1г/Всё, from/to поля, Сбросить/Показать).
|
||||||
- [ ] Подключить `BrokerDashboardDateFilter` в `События` с draft/applied состоянием.
|
- [x] Подключить `BrokerDashboardDateFilter` в `События` с draft/applied состоянием.
|
||||||
- [x] Добавить локальную пагинацию по 10 событий в `События`.
|
- [x] Добавить локальную пагинацию по 10 событий в `События`.
|
||||||
- [x] Добавить кликабельные chip-фильтры типов для `Доходы`.
|
- [x] Добавить кликабельные chip-фильтры типов для `Доходы`.
|
||||||
- [ ] Подключить `BrokerDashboardDateFilter` в `Доходы` с draft/applied состоянием.
|
- [x] Подключить `BrokerDashboardDateFilter` в `Доходы` с draft/applied состоянием.
|
||||||
- [x] Добавить cursor-пагинацию по 10 операций в `Доходы`.
|
- [x] Добавить cursor-пагинацию по 10 операций в `Доходы`.
|
||||||
- [x] Добавить `BrokerDashboardAnalyticsCard` на основе `useBrokerAnalytics`.
|
- [x] Добавить `BrokerDashboardAnalyticsCard` на основе `useBrokerAnalytics`.
|
||||||
- [x] Добавить `BrokerDashboardAllocationCard` на основе существующей аллокации.
|
- [x] Добавить `BrokerDashboardAllocationCard` на основе существующей аллокации.
|
||||||
@ -38,8 +38,8 @@
|
|||||||
- [x] Добавить `BrokerDashboard` как top-level composition widget.
|
- [x] Добавить `BrokerDashboard` как top-level composition widget.
|
||||||
- [x] Заменить текущий вертикальный обзор в `BrokerAccountOverviewPage` на `BrokerDashboard`.
|
- [x] Заменить текущий вертикальный обзор в `BrokerAccountOverviewPage` на `BrokerDashboard`.
|
||||||
- [x] Добавить unit/component tests для helpers и базовой dashboard composition.
|
- [x] Добавить unit/component tests для helpers и базовой dashboard composition.
|
||||||
- [ ] Выровнять hero KPI: все Metric одной высоты, supportingText не раздвигает "Доходность" выше соседей.
|
- [x] Выровнять hero KPI: все Metric одной высоты, supportingText не раздвигает "Доходность" выше соседей.
|
||||||
- [ ] Заменить donut-диаграмму аллокации на горизонтальные бары в `BrokerDashboardAllocationCard`.
|
- [x] Заменить donut-диаграмму аллокации на горизонтальные бары в `BrokerDashboardAllocationCard`.
|
||||||
- [ ] Проверить desktop layout `/broker/2084014113`.
|
- [ ] Проверить desktop layout `/broker/2084014113`.
|
||||||
- [ ] Проверить mobile layout `/broker/2084014113`.
|
- [ ] Проверить mobile layout `/broker/2084014113`.
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user