feat(frontend): add pagination and skeleton to BrokerPositionsSection
This commit is contained in:
parent
d7b5a376a0
commit
e0c9d97ded
@ -1,6 +1,7 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import type { BrokerMoney, BrokerPosition } from '../../api/responses';
|
import type { BrokerMoney, BrokerPosition } from '../../api/responses';
|
||||||
import { getBrokerInstrumentPath, getBrokerPositionGroup } from './brokerDisplay';
|
import { getBrokerInstrumentPath, getBrokerPositionGroup } from './brokerDisplay';
|
||||||
|
import { TableSkeleton } from '../../components/TableSkeleton';
|
||||||
|
|
||||||
type BrokerPositionGroupConfig = {
|
type BrokerPositionGroupConfig = {
|
||||||
key: 'shares' | 'bonds' | 'other';
|
key: 'shares' | 'bonds' | 'other';
|
||||||
@ -32,9 +33,26 @@ const tdStyle = {
|
|||||||
verticalAlign: 'top',
|
verticalAlign: 'top',
|
||||||
} satisfies React.CSSProperties;
|
} 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 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',
|
||||||
@ -123,29 +141,161 @@ function PositionTable({ title, positions }: { title: string; positions: BrokerP
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BrokerPositionsSection({ positions }: { positions: BrokerPosition[] }) {
|
type BrokerPositionsSectionProps = {
|
||||||
|
page: { items: BrokerPosition[] } | undefined;
|
||||||
|
isLoading: boolean;
|
||||||
|
pageNumber: number;
|
||||||
|
canGoBack: boolean;
|
||||||
|
canGoForward: boolean;
|
||||||
|
onPrevious: () => void;
|
||||||
|
onNext: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function BrokerPositionsSection({
|
||||||
|
page,
|
||||||
|
isLoading,
|
||||||
|
pageNumber,
|
||||||
|
canGoBack,
|
||||||
|
canGoForward,
|
||||||
|
onPrevious,
|
||||||
|
onNext,
|
||||||
|
}: BrokerPositionsSectionProps) {
|
||||||
|
const positions = page?.items ?? [];
|
||||||
|
|
||||||
const grouped = GROUPS.map((group) => ({
|
const grouped = GROUPS.map((group) => ({
|
||||||
...group,
|
...group,
|
||||||
positions: positions.filter((position) => getBrokerPositionGroup(position) === group.key),
|
positions: positions.filter((position) => getBrokerPositionGroup(position) === group.key),
|
||||||
})).filter((group) => group.positions.length > 0);
|
})).filter((group) => group.positions.length > 0);
|
||||||
|
|
||||||
if (grouped.length === 0) {
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h2 style={{ fontSize: 20, marginBottom: 12 }}>Позиции</h2>
|
<div
|
||||||
<p style={{ color: 'var(--color-text-secondary)' }}>В портфеле нет позиций</p>
|
style={{
|
||||||
</section>
|
display: 'flex',
|
||||||
);
|
alignItems: 'center',
|
||||||
}
|
gap: 12,
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 style={{ fontSize: 20, margin: 0 }}>Позиции</h2>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onPrevious}
|
||||||
|
disabled={!canGoBack}
|
||||||
|
style={canGoBack ? pagButtonStyle : pagButtonDisabledStyle}
|
||||||
|
>
|
||||||
|
←
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
minWidth: 20,
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'var(--color-text-secondary)',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pageNumber}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onNext}
|
||||||
|
disabled={!canGoForward}
|
||||||
|
style={canGoForward ? pagButtonStyle : pagButtonDisabledStyle}
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
return (
|
{isLoading && grouped.length === 0 ? (
|
||||||
<section>
|
<div style={{ overflowX: 'auto', background: 'var(--color-surface)' }}>
|
||||||
<h2 style={{ fontSize: 20, marginBottom: 12 }}>Позиции</h2>
|
<table style={tableStyle}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="left" style={thStyle}>
|
||||||
|
Тикер
|
||||||
|
</th>
|
||||||
|
<th align="left" style={thStyle}>
|
||||||
|
Название
|
||||||
|
</th>
|
||||||
|
<th align="right" style={thStyle}>
|
||||||
|
Количество
|
||||||
|
</th>
|
||||||
|
<th align="right" style={thStyle}>
|
||||||
|
Цена
|
||||||
|
</th>
|
||||||
|
<th align="right" style={thStyle}>
|
||||||
|
Стоимость
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<TableSkeleton
|
||||||
|
rows={4}
|
||||||
|
columns={[
|
||||||
|
{ width: '30%' },
|
||||||
|
{ width: '50%' },
|
||||||
|
{ width: '20%' },
|
||||||
|
{ width: '25%' },
|
||||||
|
{ width: '25%' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : grouped.length === 0 ? (
|
||||||
|
<p style={{ color: 'var(--color-text-secondary)' }}>В портфеле нет позиций</p>
|
||||||
|
) : isLoading ? (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'grid', gap: 20 }}>
|
||||||
|
{grouped.map((group) => (
|
||||||
|
<div key={group.key}>
|
||||||
|
<h3 style={{ fontSize: 18, marginBottom: 10 }}>{group.title}</h3>
|
||||||
|
<div style={{ overflowX: 'auto', background: 'var(--color-surface)' }}>
|
||||||
|
<table style={tableStyle}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="left" style={thStyle}>
|
||||||
|
Тикер
|
||||||
|
</th>
|
||||||
|
<th align="left" style={thStyle}>
|
||||||
|
Название
|
||||||
|
</th>
|
||||||
|
<th align="right" style={thStyle}>
|
||||||
|
Количество
|
||||||
|
</th>
|
||||||
|
<th align="right" style={thStyle}>
|
||||||
|
Цена
|
||||||
|
</th>
|
||||||
|
<th align="right" style={thStyle}>
|
||||||
|
Стоимость
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<TableSkeleton
|
||||||
|
rows={2}
|
||||||
|
columns={[
|
||||||
|
{ width: '30%' },
|
||||||
|
{ width: '50%' },
|
||||||
|
{ width: '20%' },
|
||||||
|
{ width: '25%' },
|
||||||
|
{ width: '25%' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div style={{ display: 'grid', gap: 20 }}>
|
<div style={{ display: 'grid', gap: 20 }}>
|
||||||
{grouped.map((group) => (
|
{grouped.map((group) => (
|
||||||
<PositionTable key={group.key} title={group.title} positions={group.positions} />
|
<PositionTable key={group.key} title={group.title} positions={group.positions} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user