- Add state filter (EXECUTED/null) to analytics query (spec compliance) - Add service unit tests (11 tests) and controller test - Add sync button to operations page with mutation hook - Regenerate frontend types via codegen - Update tasks.md marking all items complete Backend: 114 tests, Frontend: 116 tests — all pass
114 lines
4.0 KiB
TypeScript
114 lines
4.0 KiB
TypeScript
import { Button, Heading, Text } from '@moex-vibe/design-system'
|
||
import { Box } from '@mui/material'
|
||
import { useEffect } from 'react'
|
||
import {
|
||
BROKER_OPERATION_TYPE_OPTIONS,
|
||
isBrokerOperationType,
|
||
useBrokerOperations,
|
||
useSyncBrokerOperations,
|
||
} from '@/entities/broker-operation'
|
||
import { useSearchParamsCompat } from '@/shared/lib/router/useSearchParams'
|
||
import { useCursorPagination } from '@/shared/lib/useCursorPagination'
|
||
import { useBrokerAccountContext } from '@/widgets/broker-account-layout'
|
||
import { BrokerOperationsTable } from '@/widgets/broker-operations-table'
|
||
|
||
function getDefaultSyncRange() {
|
||
const now = new Date()
|
||
const start = new Date(Date.UTC(now.getUTCFullYear(), 0, 1))
|
||
return {
|
||
from: start.toISOString(),
|
||
to: now.toISOString(),
|
||
}
|
||
}
|
||
|
||
export function BrokerOperationsPage() {
|
||
const { accountId } = useBrokerAccountContext()
|
||
const sync = useSyncBrokerOperations(accountId)
|
||
const [searchParams, setSearchParams] = useSearchParamsCompat()
|
||
const urlType = searchParams.get('type')
|
||
const selectedType = isBrokerOperationType(urlType) ? urlType : ''
|
||
const pagination = useCursorPagination()
|
||
|
||
const operations = useBrokerOperations(accountId, {
|
||
limit: 10,
|
||
cursor: pagination.cursor,
|
||
operationTypes: selectedType || undefined,
|
||
})
|
||
|
||
useEffect(() => {
|
||
pagination.reset()
|
||
}, [pagination.reset])
|
||
|
||
function handleTypeChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||
const nextType = event.target.value
|
||
setSearchParams(nextType ? { type: nextType } : {}, { replace: true })
|
||
}
|
||
|
||
const history = operations.error ? (
|
||
<Text component="p" tone="negative" role="alert">
|
||
Не удалось загрузить историю операций
|
||
</Text>
|
||
) : (
|
||
<BrokerOperationsTable
|
||
title="История операций"
|
||
emptyMessage={
|
||
selectedType ? 'Операций выбранного типа нет' : 'Операций с начала текущего года нет'
|
||
}
|
||
isLoading={operations.isLoading}
|
||
isFetching={operations.isFetching}
|
||
page={operations.data}
|
||
pagination={{
|
||
pageNumber: pagination.pageNumber,
|
||
canGoBack: pagination.pageNumber > 1,
|
||
canGoForward: Boolean(operations.data?.hasNext && operations.data.nextCursor),
|
||
onPrevious: pagination.handlePrevious,
|
||
onNext: () => pagination.handleNext(operations.data?.nextCursor),
|
||
}}
|
||
/>
|
||
)
|
||
|
||
function handleSync() {
|
||
const range = getDefaultSyncRange()
|
||
sync.mutate(range)
|
||
}
|
||
|
||
return (
|
||
<Box component="section" aria-labelledby="broker-operations-heading">
|
||
<Box
|
||
sx={{ display: 'flex', alignItems: 'end', justifyContent: 'space-between', gap: 2, mb: 2 }}
|
||
>
|
||
<Box sx={{ display: 'flex', alignItems: 'end', gap: 2 }}>
|
||
<Heading level={2} id="broker-operations-heading">
|
||
Операции
|
||
</Heading>
|
||
<Button variant="secondary" size="small" loading={sync.isPending} onClick={handleSync}>
|
||
Синхронизировать
|
||
</Button>
|
||
</Box>
|
||
<Box component="label" sx={{ display: 'grid', gap: 0.5, color: 'text.secondary' }}>
|
||
<Text variant="label">Тип операции</Text>
|
||
<select value={selectedType} onChange={handleTypeChange}>
|
||
<option value="">Все операции</option>
|
||
{BROKER_OPERATION_TYPE_OPTIONS.map((option) => (
|
||
<option key={option.value} value={option.value}>
|
||
{option.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</Box>
|
||
</Box>
|
||
{sync.isSuccess && (
|
||
<Text component="p" tone="positive" style={{ marginBottom: '12px' }}>
|
||
Синхронизировано: {sync.data?.upserted} операций
|
||
</Text>
|
||
)}
|
||
{sync.isError && (
|
||
<Text component="p" tone="negative" style={{ marginBottom: '12px' }}>
|
||
Ошибка синхронизации
|
||
</Text>
|
||
)}
|
||
{history}
|
||
</Box>
|
||
)
|
||
}
|