import { useState } from 'react';
import { Link } from 'react-router-dom';
import type { PositionWithPrice } from '../../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 (
|
{position.secid}
|
{position.shortName ?? '—'}
|
{position.bondType ? {position.bondType} : '—'}
|
{editingQty ? (
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,
}}
/>
) : (
{
setQty(String(position.quantity));
setEditingQty(true);
}}
style={{ cursor: 'pointer', padding: '4px 0', display: 'inline-block' }}
>
{position.quantity.toLocaleString('ru-RU')}
)}
|
{editingPrice ? (
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',
}}
/>
) : (
{
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)}
)}
|
{formatPct(position.currentPrice)}
|
{formatPct(position.bid)} |
{formatPct(position.offer)} |
{formatPct(position.yieldToMaturity)}
|
{position.duration != null ? `${position.duration.toFixed(2)}г` : '—'}
|
{formatRubles(position.couponValue)}
|
{formatPct(position.couponPercent)}
|
{position.couponPeriod ? `${position.couponPeriod}д` : '—'}
|
{totalAccrued != null
? totalAccrued.toLocaleString('ru-RU', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
: '—'}
|
{position.totalCost !== null
? position.totalCost.toLocaleString('ru-RU', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
: '—'}
|
{position.pnl !== null ? (
= 0 ? 'var(--color-positive)' : 'var(--color-negative)' }}
>
{position.pnl >= 0 ? '+' : ''}
{position.pnl.toLocaleString('ru-RU', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
) : (
'—'
)}
|
{position.pnlPercent !== null ? (
= 0 ? 'var(--color-positive)' : 'var(--color-negative)',
}}
>
{position.pnlPercent >= 0 ? '+' : ''}
{position.pnlPercent.toFixed(2)}%
) : (
'—'
)}
|
{formatDate(position.nextCouponDate)}
|
{formatDate(position.matDate)} |
{formatDate(position.offerDate)} |
{position.weightPercent.toFixed(1)}%
|
|
);
}