223 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react';
import { Link } from 'react-router-dom';
import type { PositionWithPrice } from '@/shared/api/responses';
interface Props {
position: PositionWithPrice;
onUpdate: (data: { quantity?: number; buyPrice?: number }) => void;
onDelete: () => void;
}
export function BondPositionRow({ position, onUpdate, onDelete }: Props) {
const [editingQty, setEditingQty] = useState(false);
const [editingPrice, setEditingPrice] = useState(false);
const [qty, setQty] = useState(String(position.quantity));
const [price, setPrice] = useState(String(position.buyPrice ?? ''));
function handleSaveQty() {
const num = parseInt(qty, 10);
if (!isNaN(num) && num >= 0 && num !== position.quantity) {
onUpdate({ quantity: num });
}
setEditingQty(false);
}
function handleSavePrice() {
const num = parseFloat(price);
if (!isNaN(num) && num >= 0 && num !== position.buyPrice) {
onUpdate({ buyPrice: num });
} else if (price === '' && position.buyPrice !== null) {
onUpdate({ buyPrice: undefined });
}
setEditingPrice(false);
}
function formatDate(dateStr: string | null | undefined): string {
if (!dateStr) return '—';
return new Date(dateStr).toLocaleDateString('ru-RU');
}
function formatPct(value: number | null | undefined): string {
return value != null ? `${value.toFixed(2)}%` : '—';
}
function formatRubles(value: number | null | undefined): string {
return value != null
? value.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
: '—';
}
const totalAccrued = position.accruedInt != null ? position.accruedInt * position.quantity : null;
return (
<tr style={{ borderBottom: '1px solid #f0f0f0' }}>
<td style={{ padding: '8px 12px', fontWeight: 600, fontFamily: 'monospace' }}>
<Link to={`/bonds/${position.secid}`} style={{ color: 'inherit', textDecoration: 'none' }}>
{position.secid}
</Link>
</td>
<td style={{ padding: '8px 12px', color: 'var(--color-text-secondary)' }}>
{position.shortName ?? '—'}
</td>
<td style={{ padding: '8px 12px', fontSize: 13, color: 'var(--color-text-secondary)' }}>
{position.bondType ? <span>{position.bondType}</span> : '—'}
</td>
<td style={{ padding: '8px 12px' }}>
{editingQty ? (
<input
type="number"
min={0}
value={qty}
onChange={(e) => setQty(e.target.value)}
onBlur={handleSaveQty}
onKeyDown={(e) => e.key === 'Enter' && handleSaveQty()}
autoFocus
style={{
width: 80,
padding: '4px 8px',
border: '1px solid var(--color-primary)',
borderRadius: 'var(--border-radius)',
fontSize: 14,
}}
/>
) : (
<span
onClick={() => {
setQty(String(position.quantity));
setEditingQty(true);
}}
style={{ cursor: 'pointer', padding: '4px 0', display: 'inline-block' }}
>
{position.quantity.toLocaleString('ru-RU')}
</span>
)}
</td>
<td style={{ padding: '8px 12px', textAlign: 'right' }}>
{editingPrice ? (
<input
type="number"
step="0.01"
value={price}
onChange={(e) => setPrice(e.target.value)}
onBlur={handleSavePrice}
onKeyDown={(e) => e.key === 'Enter' && handleSavePrice()}
autoFocus
style={{
width: 90,
padding: '4px 8px',
border: '1px solid var(--color-primary)',
borderRadius: 'var(--border-radius)',
fontSize: 14,
textAlign: 'right',
}}
/>
) : (
<span
onClick={() => {
setPrice(String(position.buyPrice ?? ''));
setEditingPrice(true);
}}
style={{
cursor: 'pointer',
padding: '4px 0',
display: 'inline-block',
color: position.buyPrice === null ? 'var(--color-text-secondary)' : 'inherit',
}}
>
{formatPct(position.buyPrice)}
</span>
)}
</td>
<td style={{ padding: '8px 12px', textAlign: 'right' }}>
{formatPct(position.currentPrice)}
</td>
<td style={{ padding: '8px 12px', textAlign: 'right' }}>{formatPct(position.bid)}</td>
<td style={{ padding: '8px 12px', textAlign: 'right' }}>{formatPct(position.offer)}</td>
<td style={{ padding: '8px 12px', textAlign: 'right' }}>
{formatPct(position.yieldToMaturity)}
</td>
<td style={{ padding: '8px 12px', textAlign: 'right' }}>
{position.duration != null ? `${position.duration.toFixed(2)}г` : '—'}
</td>
<td style={{ padding: '8px 12px', textAlign: 'right' }}>
{formatRubles(position.couponValue)}
</td>
<td style={{ padding: '8px 12px', textAlign: 'right' }}>
{formatPct(position.couponPercent)}
</td>
<td style={{ padding: '8px 12px', textAlign: 'right' }}>
{position.couponPeriod ? `${position.couponPeriod}д` : '—'}
</td>
<td style={{ padding: '8px 12px', textAlign: 'right' }}>
{totalAccrued != null
? totalAccrued.toLocaleString('ru-RU', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
: '—'}
</td>
<td style={{ padding: '8px 12px', textAlign: 'right' }}>
{position.totalCost !== null
? position.totalCost.toLocaleString('ru-RU', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
: '—'}
</td>
<td style={{ padding: '8px 12px', textAlign: 'right' }}>
{position.pnl !== null ? (
<span
style={{ color: position.pnl >= 0 ? 'var(--color-positive)' : 'var(--color-negative)' }}
>
{position.pnl >= 0 ? '+' : ''}
{position.pnl.toLocaleString('ru-RU', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</span>
) : (
'—'
)}
</td>
<td style={{ padding: '8px 12px', textAlign: 'right' }}>
{position.pnlPercent !== null ? (
<span
style={{
color: position.pnlPercent >= 0 ? 'var(--color-positive)' : 'var(--color-negative)',
}}
>
{position.pnlPercent >= 0 ? '+' : ''}
{position.pnlPercent.toFixed(2)}%
</span>
) : (
'—'
)}
</td>
<td style={{ padding: '8px 12px', textAlign: 'right' }}>
{formatDate(position.nextCouponDate)}
</td>
<td style={{ padding: '8px 12px', textAlign: 'right' }}>{formatDate(position.matDate)}</td>
<td style={{ padding: '8px 12px', textAlign: 'right' }}>{formatDate(position.offerDate)}</td>
<td style={{ padding: '8px 12px', textAlign: 'right' }}>
{position.weightPercent.toFixed(1)}%
</td>
<td style={{ padding: '8px 12px' }}>
<button
onClick={onDelete}
style={{
background: 'none',
border: 'none',
color: '#e53935',
cursor: 'pointer',
fontSize: 13,
padding: 4,
}}
title="Удалить позицию"
>
</button>
</td>
</tr>
);
}