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 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}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user