feat(frontend): add positions hook and skeleton loading to account detail page

This commit is contained in:
Sergey Krylov 2026-06-17 14:46:48 +03:00
parent 2670097a5e
commit 28579321a9

View File

@ -3,12 +3,13 @@ import { useParams } from 'react-router-dom';
import type { BrokerMoney } from '../../api/responses'; import type { BrokerMoney } from '../../api/responses';
import { useBrokerOperations } from '../../hooks/useBrokerOperations'; import { useBrokerOperations } from '../../hooks/useBrokerOperations';
import { useBrokerPortfolio } from '../../hooks/useBrokerPortfolio'; import { useBrokerPortfolio } from '../../hooks/useBrokerPortfolio';
import { useBrokerPositions } from '../../hooks/useBrokerPositions';
import { BrokerOperationsTable } from './BrokerOperationsTable'; import { BrokerOperationsTable } from './BrokerOperationsTable';
import { BrokerPositionsSection } from './BrokerPositionsSection'; import { BrokerPositionsSection } from './BrokerPositionsSection';
import { SkeletonBlock } from '../../components/SkeletonBlock';
function formatMoney(value: BrokerMoney | null | undefined) { function formatMoney(value: BrokerMoney | null | undefined) {
if (!value) return '-'; if (!value) return '-';
return new Intl.NumberFormat('ru-RU', { return new Intl.NumberFormat('ru-RU', {
style: 'currency', style: 'currency',
currency: value.currency || 'RUB', currency: value.currency || 'RUB',
@ -20,10 +21,127 @@ export function BrokerAccountDetailPage() {
const { accountId } = useParams(); const { accountId } = useParams();
const [operationCursor, setOperationCursor] = useState<string | undefined>(undefined); const [operationCursor, setOperationCursor] = useState<string | undefined>(undefined);
const [operationCursorStack, setOperationCursorStack] = useState<Array<string | undefined>>([]); const [operationCursorStack, setOperationCursorStack] = useState<Array<string | undefined>>([]);
const [positionCursor, setPositionCursor] = useState<string | undefined>(undefined);
const [positionCursorStack, setPositionCursorStack] = useState<Array<string | undefined>>([]);
const portfolio = useBrokerPortfolio(accountId); const portfolio = useBrokerPortfolio(accountId);
const operations = useBrokerOperations(accountId, { limit: 10, cursor: operationCursor }); const operations = useBrokerOperations(accountId, { limit: 10, cursor: operationCursor });
const positions = useBrokerPositions(accountId, { limit: 10, cursor: positionCursor });
if (portfolio.isLoading) {
return (
<div style={{ display: 'grid', gap: 24 }}>
<div style={{ display: 'grid', gap: 12 }}>
<SkeletonBlock height={32} width="60%" />
<SkeletonBlock height={24} width="40%" />
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))',
gap: 12,
}}
>
{[1, 2, 3].map((i) => (
<div
key={i}
style={{
padding: 16,
background: 'var(--color-surface)',
border: '1px solid #e0e0e0',
borderRadius: 8,
}}
>
<SkeletonBlock height={14} width="40%" />
<div style={{ height: 8 }} />
<SkeletonBlock height={20} width="60%" />
</div>
))}
</div>
<div style={{ overflowX: 'auto', background: 'var(--color-surface)' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
<thead>
<tr>
<th
align="left"
style={{
borderBottom: '1px solid #e0e0e0',
color: 'var(--color-text-secondary)',
fontWeight: 600,
padding: '10px 8px',
}}
>
Тикер
</th>
<th
align="left"
style={{
borderBottom: '1px solid #e0e0e0',
color: 'var(--color-text-secondary)',
fontWeight: 600,
padding: '10px 8px',
}}
>
Название
</th>
<th
align="right"
style={{
borderBottom: '1px solid #e0e0e0',
color: 'var(--color-text-secondary)',
fontWeight: 600,
padding: '10px 8px',
}}
>
Количество
</th>
<th
align="right"
style={{
borderBottom: '1px solid #e0e0e0',
color: 'var(--color-text-secondary)',
fontWeight: 600,
padding: '10px 8px',
}}
>
Цена
</th>
<th
align="right"
style={{
borderBottom: '1px solid #e0e0e0',
color: 'var(--color-text-secondary)',
fontWeight: 600,
padding: '10px 8px',
}}
>
Стоимость
</th>
</tr>
</thead>
<tbody>
{Array.from({ length: 4 }).map((_, i) => (
<tr key={i}>
{Array.from({ length: 5 }).map((_, j) => (
<td
key={j}
style={{
borderBottom: '1px solid #eeeeee',
padding: '10px 8px',
verticalAlign: 'top',
}}
>
<SkeletonBlock height={12} width={`${30 + j * 10}%`} />
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
if (portfolio.isLoading) return <p>Загрузка портфеля...</p>;
if (portfolio.error || !portfolio.data) { if (portfolio.error || !portfolio.data) {
return <p style={{ color: 'var(--color-negative)' }}>Не удалось загрузить портфель</p>; return <p style={{ color: 'var(--color-negative)' }}>Не удалось загрузить портфель</p>;
} }
@ -31,20 +149,33 @@ export function BrokerAccountDetailPage() {
function handleNextOperationsPage() { function handleNextOperationsPage() {
const nextCursor = operations.data?.nextCursor; const nextCursor = operations.data?.nextCursor;
if (!nextCursor || !operations.data?.hasNext) return; if (!nextCursor || !operations.data?.hasNext) return;
setOperationCursorStack((previous) => [...previous, operationCursor]); setOperationCursorStack((previous) => [...previous, operationCursor]);
setOperationCursor(nextCursor); setOperationCursor(nextCursor);
} }
function handlePreviousOperationsPage() { function handlePreviousOperationsPage() {
if (operationCursorStack.length === 0) return; if (operationCursorStack.length === 0) return;
const nextStack = operationCursorStack.slice(0, -1); const nextStack = operationCursorStack.slice(0, -1);
const previousCursor = operationCursorStack[operationCursorStack.length - 1]; const previousCursor = operationCursorStack[operationCursorStack.length - 1];
setOperationCursorStack(nextStack); setOperationCursorStack(nextStack);
setOperationCursor(previousCursor); setOperationCursor(previousCursor);
} }
function handleNextPositionsPage() {
const nextCursor = positions.data?.nextCursor;
if (!nextCursor || !positions.data?.hasNext) return;
setPositionCursorStack((previous) => [...previous, positionCursor]);
setPositionCursor(nextCursor);
}
function handlePreviousPositionsPage() {
if (positionCursorStack.length === 0) return;
const nextStack = positionCursorStack.slice(0, -1);
const previousCursor = positionCursorStack[positionCursorStack.length - 1];
setPositionCursorStack(nextStack);
setPositionCursor(previousCursor);
}
return ( return (
<div style={{ display: 'grid', gap: 24 }}> <div style={{ display: 'grid', gap: 24 }}>
<header> <header>
@ -87,7 +218,15 @@ export function BrokerAccountDetailPage() {
))} ))}
</section> </section>
<BrokerPositionsSection positions={portfolio.data.positions} /> <BrokerPositionsSection
page={positions.data}
isLoading={positions.isLoading}
pageNumber={positionCursorStack.length + 1}
canGoBack={positionCursorStack.length > 0}
canGoForward={Boolean(positions.data?.hasNext && positions.data.nextCursor)}
onPrevious={handlePreviousPositionsPage}
onNext={handleNextPositionsPage}
/>
<BrokerOperationsTable <BrokerOperationsTable
isLoading={operations.isLoading} isLoading={operations.isLoading}