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)}% ); }