moex-vibe/apps/frontend/src/pages/portfolios/PortfolioDetailPage.tsx

295 lines
8.9 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 { useParams, Link } from 'react-router-dom';
import {
usePortfolio,
usePortfolioMutations,
usePositionMutations,
} from '../../entities/portfolio';
import { PortfolioForm } from '../../widgets/portfolio-form';
import { PortfolioSummary } from '../../widgets/portfolio-summary';
import { AnalyticsSummary } from '../../widgets/portfolio-analytics';
import { SharePositionTable } from '../../widgets/share-positions-table';
import { BondPositionTable } from '../../widgets/bond-positions-table';
export function PortfolioDetailPage() {
const { id } = useParams<{ id: string }>();
const portfolioId = parseInt(id!, 10);
const { data: portfolio, isLoading, error } = usePortfolio(portfolioId);
const { update, remove } = usePortfolioMutations();
const {
update: updatePosition,
remove: removePosition,
add: addPosition,
} = usePositionMutations(portfolioId);
const [editing, setEditing] = useState(false);
const [showAddForm, setShowAddForm] = useState(false);
const [newSecid, setNewSecid] = useState('');
const [newQty, setNewQty] = useState('1');
const [newPrice, setNewPrice] = useState('');
const [newDate, setNewDate] = useState(new Date().toISOString().split('T')[0]);
if (isLoading) {
return (
<div style={{ padding: 40, textAlign: 'center', color: 'var(--color-text-secondary)' }}>
Загрузка...
</div>
);
}
if (error || !portfolio) {
return (
<div style={{ padding: 40, textAlign: 'center', color: '#e53935' }}>
Ошибка загрузки портфеля
</div>
);
}
async function handleDelete() {
if (window.confirm('Удалить портфель и все позиции?')) {
remove.mutate(portfolioId);
}
}
function handleAddPosition() {
if (!newSecid.trim() || !parseInt(newQty, 10)) return;
addPosition.mutate(
{
secid: newSecid.trim().toUpperCase(),
quantity: parseInt(newQty, 10),
buyPrice: newPrice ? parseFloat(newPrice) : undefined,
buyDate: newDate || undefined,
},
{
onSuccess: () => {
setShowAddForm(false);
setNewSecid('');
setNewQty('1');
setNewPrice('');
},
},
);
}
return (
<div>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
marginBottom: 24,
}}
>
<Link
to="/portfolios"
style={{ color: 'var(--color-text-secondary)', textDecoration: 'none', fontSize: 14 }}
>
К списку
</Link>
<h1 style={{ margin: 0, fontSize: 24, fontWeight: 700 }}>{portfolio.name}</h1>
<button
onClick={() => setEditing(!editing)}
style={{
marginLeft: 'auto',
padding: '6px 16px',
background: 'transparent',
border: '1px solid #e0e0e0',
borderRadius: 'var(--border-radius)',
fontSize: 13,
cursor: 'pointer',
}}
>
{editing ? 'Закрыть' : 'Редактировать'}
</button>
<button
onClick={handleDelete}
style={{
padding: '6px 16px',
background: 'transparent',
border: '1px solid #e53935',
color: '#e53935',
borderRadius: 'var(--border-radius)',
fontSize: 13,
cursor: 'pointer',
}}
>
Удалить
</button>
</div>
{editing && (
<div
style={{
marginBottom: 24,
padding: 20,
background: 'var(--color-surface)',
border: '1px solid #e0e0e0',
borderRadius: 'var(--border-radius)',
}}
>
<h3 style={{ margin: '0 0 16px', fontSize: 16, fontWeight: 600 }}>
Редактировать портфель
</h3>
<PortfolioForm
initial={portfolio}
onSave={(d) => update.mutate({ id: portfolioId, data: d })}
onCancel={() => setEditing(false)}
isLoading={update.isPending}
/>
</div>
)}
<PortfolioSummary portfolio={portfolio} />
{portfolio.analytics && <AnalyticsSummary summary={portfolio.analytics} />}
<div
style={{
marginTop: 24,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>Позиции</h2>
<button
onClick={() => setShowAddForm(!showAddForm)}
style={{
padding: '6px 16px',
background: 'var(--color-primary)',
color: '#fff',
border: 'none',
borderRadius: 'var(--border-radius)',
fontSize: 13,
fontWeight: 600,
cursor: 'pointer',
}}
>
+ Добавить
</button>
</div>
{showAddForm && (
<div
style={{
marginTop: 12,
padding: 16,
background: 'var(--color-surface)',
border: '1px solid #e0e0e0',
borderRadius: 'var(--border-radius)',
display: 'flex',
gap: 12,
alignItems: 'flex-end',
}}
>
<div>
<label style={{ display: 'block', fontSize: 12, fontWeight: 600, marginBottom: 4 }}>
Тикер
</label>
<input
value={newSecid}
onChange={(e) => setNewSecid(e.target.value)}
placeholder="SBER"
style={{
padding: '8px 12px',
border: '1px solid #e0e0e0',
borderRadius: 'var(--border-radius)',
fontSize: 14,
width: 120,
}}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: 12, fontWeight: 600, marginBottom: 4 }}>
Количество
</label>
<input
type="number"
min={1}
value={newQty}
onChange={(e) => setNewQty(e.target.value)}
style={{
padding: '8px 12px',
border: '1px solid #e0e0e0',
borderRadius: 'var(--border-radius)',
fontSize: 14,
width: 100,
}}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: 12, fontWeight: 600, marginBottom: 4 }}>
Цена покупки
</label>
<input
type="number"
step="0.01"
value={newPrice}
onChange={(e) => setNewPrice(e.target.value)}
placeholder="0.00"
style={{
padding: '8px 12px',
border: '1px solid #e0e0e0',
borderRadius: 'var(--border-radius)',
fontSize: 14,
width: 120,
}}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: 12, fontWeight: 600, marginBottom: 4 }}>
Дата покупки
</label>
<input
type="date"
value={newDate}
onChange={(e) => setNewDate(e.target.value)}
style={{
padding: '8px 12px',
border: '1px solid #e0e0e0',
borderRadius: 'var(--border-radius)',
fontSize: 14,
width: 150,
}}
/>
</div>
<button
onClick={handleAddPosition}
disabled={addPosition.isPending}
style={{
padding: '8px 16px',
background: 'var(--color-primary)',
color: '#fff',
border: 'none',
borderRadius: 'var(--border-radius)',
fontSize: 13,
fontWeight: 600,
cursor: 'pointer',
}}
>
Добавить
</button>
</div>
)}
<SharePositionTable
positions={portfolio.positions.filter((p) => p.type === 'share')}
onUpdatePosition={(positionId, data) => updatePosition.mutate({ positionId, data })}
onDeletePosition={(positionId) => {
if (window.confirm('Удалить позицию?')) removePosition.mutate(positionId);
}}
/>
<BondPositionTable
positions={portfolio.positions.filter((p) => p.type === 'bond')}
onUpdatePosition={(positionId, data) => updatePosition.mutate({ positionId, data })}
onDeletePosition={(positionId) => {
if (window.confirm('Удалить позицию?')) removePosition.mutate(positionId);
}}
/>
</div>
);
}