From 8e70691bde7cbc10a4b94b6c5e01d4fd08cec1dd Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sat, 20 Jun 2026 23:12:28 +0300 Subject: [PATCH] refactor: extract BrokerPositionTable to widgets layer --- .../ui/BrokerPositionsPage.tsx | 259 +----------------- .../widgets/broker-positions-table/index.ts | 1 + .../ui/BrokerPositionTable.tsx | 234 ++++++++++++++++ .../ui/PositionTicker.tsx | 22 ++ 4 files changed, 259 insertions(+), 257 deletions(-) create mode 100644 apps/frontend/src/widgets/broker-positions-table/index.ts create mode 100644 apps/frontend/src/widgets/broker-positions-table/ui/BrokerPositionTable.tsx create mode 100644 apps/frontend/src/widgets/broker-positions-table/ui/PositionTicker.tsx diff --git a/apps/frontend/src/pages/broker-positions/ui/BrokerPositionsPage.tsx b/apps/frontend/src/pages/broker-positions/ui/BrokerPositionsPage.tsx index aab8d4d..3ecc332 100644 --- a/apps/frontend/src/pages/broker-positions/ui/BrokerPositionsPage.tsx +++ b/apps/frontend/src/pages/broker-positions/ui/BrokerPositionsPage.tsx @@ -1,262 +1,7 @@ import { useState } from 'react'; -import { Link } from 'react-router-dom'; -import type { - BrokerPosition, - BrokerPositionsPage as BrokerPositionsPageData, -} from '@/shared/api/responses'; -import { TableSkeleton } from '@/shared/ui/TableSkeleton'; -import { getBrokerInstrumentPath, useBrokerPositions } from '@/entities/broker-position'; +import { useBrokerPositions } from '@/entities/broker-position'; import { useBrokerAccountContext } from '@/widgets/broker-account-layout'; -import { formatBrokerMoney as formatMoney } from '@/shared/lib/formatters'; - -const tableStyle = { - width: '100%', - borderCollapse: 'collapse', - fontSize: 14, -} satisfies React.CSSProperties; - -const thStyle = { - borderBottom: '1px solid #e0e0e0', - color: 'var(--color-text-secondary)', - fontWeight: 600, - padding: '10px 8px', -} satisfies React.CSSProperties; - -const tdStyle = { - borderBottom: '1px solid #eeeeee', - padding: '10px 8px', - verticalAlign: 'top', -} satisfies React.CSSProperties; - -const pagButtonStyle = { - padding: '6px 14px', - borderRadius: 6, - border: '1px solid #e0e0e0', - background: 'var(--color-surface)', - color: 'var(--color-text)', - fontSize: 14, - fontWeight: 600, - cursor: 'pointer', - lineHeight: 1.4, -} satisfies React.CSSProperties; - -const pagButtonDisabledStyle = { - ...pagButtonStyle, - opacity: 0.35, - cursor: 'not-allowed', -} satisfies React.CSSProperties; - -function formatQuantity(value: number | null | undefined) { - return value == null ? '-' : value.toLocaleString('ru-RU'); -} - -function PositionTicker({ position }: { position: BrokerPosition }) { - const label = position.ticker || position.figi || '-'; - const path = getBrokerInstrumentPath({ - ticker: position.ticker, - instrumentType: position.instrumentType, - classCode: position.classCode, - }); - - if (!path || label === '-') { - return {label}; - } - - return ( - - {label} - - ); -} - -function BrokerPositionTable({ - title, - page, - isLoading, - isFetching, - emptyMessage, - pageNumber, - onNext, - onPrevious, -}: { - title: string; - page: BrokerPositionsPageData | undefined; - isLoading: boolean; - isFetching: boolean; - emptyMessage: string; - pageNumber: number; - onNext: () => void; - onPrevious: () => void; -}) { - const positions = page?.items ?? []; - const canGoBack = pageNumber > 1; - const canGoForward = Boolean(page?.hasNext && page.nextCursor); - - return ( -
-
-

- {title} -

-
- - - {pageNumber} - - -
-
- - {isLoading ? ( -
- - - - - - - - - - - -
- Тикер - - Название - - Количество - - Цена - - Стоимость -
-
- ) : positions.length === 0 && !isFetching ? ( -

{emptyMessage}

- ) : ( -
-
- - - - - - - - - - - - {positions.map((position) => ( - - - - - - - - ))} - -
- Тикер - - Название - - Количество - - Цена - - Стоимость -
- - - - {position.name || '-'} - - - {formatQuantity(position.quantity)} - - {formatMoney(position.currentPrice)} - - {formatMoney(position.currentValue)} -
-
- {isFetching && ( -
-
- - Загрузка страницы {pageNumber}… - -
- )} -
- )} -
- ); -} +import { BrokerPositionTable } from '@/widgets/broker-positions-table'; type BrokerPositionsPageProps = { type: 'share' | 'bond'; diff --git a/apps/frontend/src/widgets/broker-positions-table/index.ts b/apps/frontend/src/widgets/broker-positions-table/index.ts new file mode 100644 index 0000000..eef2f56 --- /dev/null +++ b/apps/frontend/src/widgets/broker-positions-table/index.ts @@ -0,0 +1 @@ +export { BrokerPositionTable } from './ui/BrokerPositionTable'; diff --git a/apps/frontend/src/widgets/broker-positions-table/ui/BrokerPositionTable.tsx b/apps/frontend/src/widgets/broker-positions-table/ui/BrokerPositionTable.tsx new file mode 100644 index 0000000..881c37f --- /dev/null +++ b/apps/frontend/src/widgets/broker-positions-table/ui/BrokerPositionTable.tsx @@ -0,0 +1,234 @@ +import type { BrokerPositionsPage as BrokerPositionsPageData } from '@/shared/api/responses'; +import { TableSkeleton } from '@/shared/ui/TableSkeleton'; +import { formatBrokerMoney as formatMoney } from '@/shared/lib/formatters'; +import { PositionTicker } from './PositionTicker'; + +const tableStyle = { + width: '100%', + borderCollapse: 'collapse', + fontSize: 14, +} satisfies React.CSSProperties; + +const thStyle = { + borderBottom: '1px solid #e0e0e0', + color: 'var(--color-text-secondary)', + fontWeight: 600, + padding: '10px 8px', +} satisfies React.CSSProperties; + +const tdStyle = { + borderBottom: '1px solid #eeeeee', + padding: '10px 8px', + verticalAlign: 'top', +} satisfies React.CSSProperties; + +const pagButtonStyle = { + padding: '6px 14px', + borderRadius: 6, + border: '1px solid #e0e0e0', + background: 'var(--color-surface)', + color: 'var(--color-text)', + fontSize: 14, + fontWeight: 600, + cursor: 'pointer', + lineHeight: 1.4, +} satisfies React.CSSProperties; + +const pagButtonDisabledStyle = { + ...pagButtonStyle, + opacity: 0.35, + cursor: 'not-allowed', +} satisfies React.CSSProperties; + +function formatQuantity(value: number | null | undefined) { + return value == null ? '-' : value.toLocaleString('ru-RU'); +} + +export function BrokerPositionTable({ + title, + page, + isLoading, + isFetching, + emptyMessage, + pageNumber, + onNext, + onPrevious, +}: { + title: string; + page: BrokerPositionsPageData | undefined; + isLoading: boolean; + isFetching: boolean; + emptyMessage: string; + pageNumber: number; + onNext: () => void; + onPrevious: () => void; +}) { + const positions = page?.items ?? []; + const canGoBack = pageNumber > 1; + const canGoForward = Boolean(page?.hasNext && page.nextCursor); + + return ( +
+
+

+ {title} +

+
+ + + {pageNumber} + + +
+
+ + {isLoading ? ( +
+ + + + + + + + + + + +
+ Тикер + + Название + + Количество + + Цена + + Стоимость +
+
+ ) : positions.length === 0 && !isFetching ? ( +

{emptyMessage}

+ ) : ( +
+
+ + + + + + + + + + + + {positions.map((position) => ( + + + + + + + + ))} + +
+ Тикер + + Название + + Количество + + Цена + + Стоимость +
+ + + + {position.name || '-'} + + + {formatQuantity(position.quantity)} + + {formatMoney(position.currentPrice)} + + {formatMoney(position.currentValue)} +
+
+ {isFetching && ( +
+
+ + Загрузка страницы {pageNumber}… + +
+ )} +
+ )} +
+ ); +} diff --git a/apps/frontend/src/widgets/broker-positions-table/ui/PositionTicker.tsx b/apps/frontend/src/widgets/broker-positions-table/ui/PositionTicker.tsx new file mode 100644 index 0000000..16a61fe --- /dev/null +++ b/apps/frontend/src/widgets/broker-positions-table/ui/PositionTicker.tsx @@ -0,0 +1,22 @@ +import { Link } from 'react-router-dom'; +import type { BrokerPosition } from '@/shared/api/responses'; +import { getBrokerInstrumentPath } from '@/entities/broker-position'; + +export function PositionTicker({ position }: { position: BrokerPosition }) { + const label = position.ticker || position.figi || '-'; + const path = getBrokerInstrumentPath({ + ticker: position.ticker, + instrumentType: position.instrumentType, + classCode: position.classCode, + }); + + if (!path || label === '-') { + return {label}; + } + + return ( + + {label} + + ); +}