Sergey Krylov 9932278b64 feat: complete Phase 3 API type unification, delete stale test file
- Delete shared/api/responses.ts, move type re-exports to index.ts
- Update all 55+ imports from shared/api/responses to shared/api
- Delete stale client.test.ts (tested deleted client.ts)
- Run biome checks and fix import ordering
- Update tasks.md and plan.md to reflect actual approach
2026-06-23 20:24:16 +03:00

272 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Button, Heading, Skeleton, Text } from '@moex-vibe/design-system'
import { Box } from '@mui/material'
import { Link } from '@tanstack/react-router'
import type { ReactNode } from 'react'
import {
type BrokerOperationImpact,
getBrokerOperationImpact,
getBrokerOperationTypeLabel,
} from '@/entities/broker-operation'
import { getBrokerInstrumentPath } from '@/entities/broker-position'
import type { BrokerOperation, BrokerOperationsPage } from '@/shared/api'
import { formatBrokerSignedMoney } from '@/shared/lib/formatters'
import { TableSkeleton } from '@/shared/ui/TableSkeleton'
const tableSx = {
width: '100%',
borderCollapse: 'collapse',
fontSize: 14,
} as const
const thSx = {
borderBottom: '1px solid',
borderColor: 'divider',
color: 'text.secondary',
fontWeight: 600,
p: 1,
textAlign: 'left',
} as const
const tdSx = {
borderBottom: '1px solid',
borderColor: 'divider',
p: 1,
verticalAlign: 'top',
textAlign: 'left',
} as const
const tdSxRight = {
...tdSx,
textAlign: 'right',
} as const
function formatDate(value: string | null) {
if (!value) return '-'
return new Date(value).toLocaleString('ru-RU')
}
function operationTone(impact: BrokerOperationImpact) {
if (impact === 'adds') return 'positive' as const
if (impact === 'reduces') return 'negative' as const
return 'primary' as const
}
function OperationInstrument({ operation }: { operation: BrokerOperation }) {
const ticker = operation.ticker || operation.description || '-'
const path = getBrokerInstrumentPath({
ticker: operation.ticker,
instrumentType: operation.instrumentType,
classCode: operation.classCode,
})
const name = operation.name || operation.description
if (!path && !name) return <Text>-</Text>
if (!path) return <Text>{name}</Text>
if (!ticker || ticker === '-') return <Link to={path}>{name}</Link>
return (
<Box sx={{ display: 'grid', gap: 0.5 }}>
<Link to={path}>
<Box component="span" sx={{ fontWeight: 700 }}>
{ticker}
</Box>
</Link>
{name && name !== ticker && (
<Text variant="caption" tone="secondary">
{name}
</Text>
)}
</Box>
)
}
export function BrokerOperationsTable({
title,
headerAction,
emptyMessage,
isLoading,
isFetching,
page,
pagination,
}: BrokerOperationsTableProps) {
const pageNumber = pagination?.pageNumber
const canGoBack = pagination?.canGoBack ?? false
const canGoForward = pagination?.canGoForward ?? false
const onPrevious = pagination?.onPrevious
const onNext = pagination?.onNext
const operations = page?.items ?? []
return (
<section aria-busy={isFetching}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1.5,
justifyContent: 'space-between',
mb: 1.5,
}}
>
<Heading level={2}>{title}</Heading>
{headerAction}
{pagination && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Button
variant="secondary"
size="small"
aria-label="Предыдущая страница"
onClick={onPrevious}
disabled={!canGoBack || isFetching}
>
{isFetching ? <Skeleton shape="circular" width={14} height={14} /> : '←'}
</Button>
<Box
sx={{
minWidth: 20,
textAlign: 'center',
color: 'text.secondary',
fontSize: 14,
fontWeight: 600,
}}
>
{pageNumber}
</Box>
<Button
variant="secondary"
size="small"
aria-label="Следующая страница"
onClick={onNext}
disabled={!canGoForward || isFetching}
>
{isFetching ? <Skeleton shape="circular" width={14} height={14} /> : '→'}
</Button>
</Box>
)}
</Box>
{isLoading ? (
<Box sx={{ overflowX: 'auto', bgcolor: 'surface.default' }}>
<Box component="table" sx={tableSx}>
<Box component="thead">
<Box component="tr">
<Box component="th" sx={thSx}>
Дата
</Box>
<Box component="th" sx={thSx}>
Тип
</Box>
<Box component="th" sx={thSx}>
Инструмент
</Box>
<Box component="th" sx={{ ...thSx, textAlign: 'right' }}>
Сумма
</Box>
</Box>
</Box>
<TableSkeleton
rows={5}
columns={[{ width: '35%' }, { width: '30%' }, { width: '40%' }, { width: '25%' }]}
/>
</Box>
</Box>
) : operations.length === 0 && !isFetching ? (
<Text component="p" tone="muted">
{emptyMessage}
</Text>
) : (
<Box sx={{ position: 'relative' }}>
<Box sx={{ overflowX: 'auto', bgcolor: 'surface.default' }}>
<Box component="table" sx={tableSx}>
<Box component="thead">
<Box component="tr">
<Box component="th" sx={thSx}>
Дата
</Box>
<Box component="th" sx={thSx}>
Тип
</Box>
<Box component="th" sx={thSx}>
Инструмент
</Box>
<Box component="th" sx={{ ...thSx, textAlign: 'right' }}>
Сумма
</Box>
</Box>
</Box>
<Box component="tbody">
{operations.map((operation) => {
const impact = getBrokerOperationImpact(operation)
return (
<Box component="tr" key={operation.cursor || operation.id}>
<Box component="td" sx={tdSx}>
{formatDate(operation.date)}
</Box>
<Box component="td" sx={tdSx}>
<Text>{getBrokerOperationTypeLabel(operation)}</Text>
</Box>
<Box component="td" sx={tdSx}>
<OperationInstrument operation={operation} />
</Box>
<Box
component="td"
sx={{
...tdSxRight,
color:
operationTone(impact) === 'positive'
? 'success.main'
: operationTone(impact) === 'negative'
? 'error.main'
: 'text.primary',
fontWeight: 700,
}}
align="right"
>
{formatBrokerSignedMoney(operation.payment)}
</Box>
</Box>
)
})}
</Box>
</Box>
</Box>
{isFetching && (
<Box
sx={{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 1,
bgcolor: 'rgba(255,255,255,0.7)',
}}
>
<Box className="loading-spinner" />
<Text variant="caption" tone="secondary">
{pagination ? `Загрузка страницы ${pageNumber}` : 'Обновление операций…'}
</Text>
</Box>
)}
</Box>
)}
</section>
)
}
type BrokerOperationsTableProps = {
title: string
headerAction?: ReactNode
emptyMessage: string
isLoading: boolean
isFetching: boolean
page: BrokerOperationsPage | undefined
pagination?: {
pageNumber: number
canGoBack: boolean
canGoForward: boolean
onPrevious: () => void
onNext: () => void
}
}