feat(frontend): add positions hook and skeleton loading to account detail page
This commit is contained in:
parent
2670097a5e
commit
28579321a9
@ -3,12 +3,13 @@ import { useParams } from 'react-router-dom';
|
||||
import type { BrokerMoney } from '../../api/responses';
|
||||
import { useBrokerOperations } from '../../hooks/useBrokerOperations';
|
||||
import { useBrokerPortfolio } from '../../hooks/useBrokerPortfolio';
|
||||
import { useBrokerPositions } from '../../hooks/useBrokerPositions';
|
||||
import { BrokerOperationsTable } from './BrokerOperationsTable';
|
||||
import { BrokerPositionsSection } from './BrokerPositionsSection';
|
||||
import { SkeletonBlock } from '../../components/SkeletonBlock';
|
||||
|
||||
function formatMoney(value: BrokerMoney | null | undefined) {
|
||||
if (!value) return '-';
|
||||
|
||||
return new Intl.NumberFormat('ru-RU', {
|
||||
style: 'currency',
|
||||
currency: value.currency || 'RUB',
|
||||
@ -20,10 +21,127 @@ export function BrokerAccountDetailPage() {
|
||||
const { accountId } = useParams();
|
||||
const [operationCursor, setOperationCursor] = useState<string | undefined>(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 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) {
|
||||
return <p style={{ color: 'var(--color-negative)' }}>Не удалось загрузить портфель</p>;
|
||||
}
|
||||
@ -31,20 +149,33 @@ export function BrokerAccountDetailPage() {
|
||||
function handleNextOperationsPage() {
|
||||
const nextCursor = operations.data?.nextCursor;
|
||||
if (!nextCursor || !operations.data?.hasNext) return;
|
||||
|
||||
setOperationCursorStack((previous) => [...previous, operationCursor]);
|
||||
setOperationCursor(nextCursor);
|
||||
}
|
||||
|
||||
function handlePreviousOperationsPage() {
|
||||
if (operationCursorStack.length === 0) return;
|
||||
|
||||
const nextStack = operationCursorStack.slice(0, -1);
|
||||
const previousCursor = operationCursorStack[operationCursorStack.length - 1];
|
||||
setOperationCursorStack(nextStack);
|
||||
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 (
|
||||
<div style={{ display: 'grid', gap: 24 }}>
|
||||
<header>
|
||||
@ -87,7 +218,15 @@ export function BrokerAccountDetailPage() {
|
||||
))}
|
||||
</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
|
||||
isLoading={operations.isLoading}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user