diff --git a/apps/frontend/src/components/portfolios/AllocationChart.tsx b/apps/frontend/src/components/portfolios/AllocationChart.tsx
deleted file mode 100644
index 275b9ed..0000000
--- a/apps/frontend/src/components/portfolios/AllocationChart.tsx
+++ /dev/null
@@ -1,162 +0,0 @@
-import type { PositionWithPrice } from '@/shared/api/responses';
-
-interface AllocationChartProps {
- positions: PositionWithPrice[];
- totalValue: number;
-}
-
-interface SectorData {
- type: 'share' | 'bond';
- label: string;
- value: number;
- count: number;
- color: string;
-}
-
-const SECTOR_COLORS = {
- share: 'var(--color-primary, #1976d2)',
- bond: '#f57c00',
-} as const;
-
-const SECTOR_LABELS = {
- share: 'Акции',
- bond: 'Облигации',
-} as const;
-
-function computeSectors(positions: PositionWithPrice[]): SectorData[] {
- const sectors: SectorData[] = [
- { type: 'share', label: SECTOR_LABELS.share, value: 0, count: 0, color: SECTOR_COLORS.share },
- { type: 'bond', label: SECTOR_LABELS.bond, value: 0, count: 0, color: SECTOR_COLORS.bond },
- ];
-
- for (const p of positions) {
- const sector = sectors.find((s) => s.type === p.type);
- if (sector) {
- sector.value += p.currentValue ?? 0;
- sector.count += 1;
- }
- }
-
- return sectors;
-}
-
-export function AllocationChart({ positions, totalValue }: AllocationChartProps) {
- const sectors = computeSectors(positions);
- const nonZero = sectors.filter((s) => s.value > 0);
- const hasData = nonZero.length > 0;
-
- const cx = 60;
- const cy = 60;
- const r = 44;
- const strokeWidth = 10;
- const circumference = 2 * Math.PI * r;
- const viewBoxSize = 120;
-
- function renderArcs() {
- if (!hasData) {
- return (
-
- );
- }
-
- if (nonZero.length === 1) {
- const sector = nonZero[0];
- return (
-
- );
- }
-
- return sectors.map((sector, i) => {
- const ratio = totalValue > 0 ? sector.value / totalValue : 0;
- const dashLen = ratio * circumference;
- const gapLen = circumference - dashLen;
- let rotation = -90;
-
- for (let j = 0; j < i; j++) {
- const prevRatio = totalValue > 0 ? sectors[j].value / totalValue : 0;
- rotation += prevRatio * 360;
- }
-
- return (
-
- );
- });
- }
-
- return (
-
-
-
- {sectors.map((s) => {
- const ratio = totalValue > 0 ? (s.value / totalValue) * 100 : 0;
- return (
-
-
-
- {s.label}: {s.count} / {ratio.toFixed(1)}%
-
-
- );
- })}
-
-
- );
-}
diff --git a/apps/frontend/src/components/portfolios/AnalyticsSummary.tsx b/apps/frontend/src/components/portfolios/AnalyticsSummary.tsx
deleted file mode 100644
index b2139ba..0000000
--- a/apps/frontend/src/components/portfolios/AnalyticsSummary.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import type { PortfolioSummary } from '@/shared/api/responses';
-
-export function AnalyticsSummary({ summary }: { summary: PortfolioSummary }) {
- const formatRub = (val: number | null) =>
- val != null
- ? val.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
- : '—';
-
- const formatPct = (val: number | null) => (val != null ? `${val.toFixed(2)}%` : '—');
-
- const pnlColor =
- summary.totalPnl > 0
- ? 'var(--color-positive)'
- : summary.totalPnl < 0
- ? 'var(--color-negative)'
- : 'inherit';
-
- return (
-
-
-
- Инвестировано
-
-
{formatRub(summary.totalInvested)}
-
-
-
-
- Текущая стоимость
-
-
{formatRub(summary.totalValue)}
-
-
-
-
- Прибыль/Убыток
-
-
- {summary.totalPnl > 0 ? '+' : ''}
- {formatRub(summary.totalPnl)}
-
- ({formatPct(summary.totalPnlPercent)})
-
-
-
-
-
-
- Доходность (weighted)
-
-
- {formatPct(summary.weightedYield)}
-
-
-
- );
-}
diff --git a/apps/frontend/src/components/portfolios/BondPositionRow.tsx b/apps/frontend/src/components/portfolios/BondPositionRow.tsx
deleted file mode 100644
index e1ae9b6..0000000
--- a/apps/frontend/src/components/portfolios/BondPositionRow.tsx
+++ /dev/null
@@ -1,222 +0,0 @@
-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 (
-
- |
-
- {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)}%
- |
-
-
- |
-
- );
-}
diff --git a/apps/frontend/src/components/portfolios/BondPositionTable.tsx b/apps/frontend/src/components/portfolios/BondPositionTable.tsx
deleted file mode 100644
index c1dfc2b..0000000
--- a/apps/frontend/src/components/portfolios/BondPositionTable.tsx
+++ /dev/null
@@ -1,273 +0,0 @@
-import { BondPositionRow } from './BondPositionRow';
-import type { PositionWithPrice } from '@/shared/api/responses';
-
-interface Props {
- positions: PositionWithPrice[];
- onUpdatePosition: (
- positionId: number,
- data: { quantity?: number; buyPrice?: number; buyDate?: string },
- ) => void;
- onDeletePosition: (positionId: number) => void;
-}
-
-export function BondPositionTable({ positions, onUpdatePosition, onDeletePosition }: Props) {
- if (positions.length === 0) return null;
-
- return (
-
-
- Облигации
-
-
-
-
-
- |
- Тикер
- |
-
- Название
- |
-
- Тип
- |
-
- Количество
- |
-
- Цена пок.
- |
-
- Цена
- |
-
- Бид
- |
-
- Оффер
- |
-
- Доходность
- |
-
- Дюрация
- |
-
- Купон
- |
-
- Куп. %
- |
-
- Период
- |
-
- НКД
- |
-
- Затраты
- |
-
- P&L
- |
-
- P&L %
- |
-
- След. купон
- |
-
- Погашение
- |
-
- Оферта
- |
-
- Доля
- |
- |
-
-
-
- {positions.map((pos) => (
- onUpdatePosition(pos.id, data)}
- onDelete={() => onDeletePosition(pos.id)}
- />
- ))}
-
-
-
-
- );
-}
diff --git a/apps/frontend/src/components/portfolios/PortfolioCard.tsx b/apps/frontend/src/components/portfolios/PortfolioCard.tsx
deleted file mode 100644
index 9c36320..0000000
--- a/apps/frontend/src/components/portfolios/PortfolioCard.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import { Link } from 'react-router-dom';
-import type { Portfolio } from '@/shared/api/responses';
-
-export function PortfolioCard({ portfolio }: { portfolio: Portfolio }) {
- const chipStyle = (bg: string): React.CSSProperties => ({
- background: bg,
- padding: '4px 10px',
- borderRadius: 6,
- fontSize: 12,
- color: '#fff',
- fontWeight: 500,
- });
-
- return (
- (e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.08)')}
- onMouseLeave={(e) => (e.currentTarget.style.boxShadow = 'none')}
- >
-
-
{portfolio.name}
-
-
- {portfolio.totalValue?.toLocaleString('ru-RU', {
- minimumFractionDigits: 2,
- maximumFractionDigits: 2,
- })}
-
-
- {portfolio.currency}
-
-
-
-
- {portfolio.description && (
-
- {portfolio.description}
-
- )}
-
-
- {portfolio.shareCount > 0 && (
-
- {portfolio.shareCount} {pluralize(portfolio.shareCount, 'акция', 'акции', 'акций')}
-
- )}
- {portfolio.bondCount > 0 && (
-
- {portfolio.bondCount}{' '}
- {pluralize(portfolio.bondCount, 'облигация', 'облигации', 'облигаций')}
-
- )}
-
- {portfolio.positionCount}{' '}
- {pluralize(portfolio.positionCount, 'позиция', 'позиции', 'позиций')}
-
-
-
-
- обновлён {new Date(portfolio.updatedAt).toLocaleDateString('ru-RU')}
-
-
- );
-}
-
-function pluralize(n: number, one: string, few: string, many: string): string {
- if (n % 10 === 1 && n % 100 !== 11) return one;
- if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) return few;
- return many;
-}
diff --git a/apps/frontend/src/components/portfolios/PortfolioForm.tsx b/apps/frontend/src/components/portfolios/PortfolioForm.tsx
deleted file mode 100644
index 870f288..0000000
--- a/apps/frontend/src/components/portfolios/PortfolioForm.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-import { useState } from 'react';
-import type { Portfolio } from '@/shared/api/responses';
-
-interface Props {
- initial?: Portfolio;
- onSave: (data: { name: string; description?: string; currency?: string }) => void;
- onCancel: () => void;
- isLoading?: boolean;
-}
-
-const CURRENCIES = ['RUB', 'USD', 'EUR', 'CNY', 'KZT', 'BYN'];
-
-export function PortfolioForm({ initial, onSave, onCancel, isLoading }: Props) {
- const [name, setName] = useState(initial?.name || '');
- const [description, setDescription] = useState(initial?.description || '');
- const [currency, setCurrency] = useState(initial?.currency || 'RUB');
-
- function handleSubmit(e: React.FormEvent) {
- e.preventDefault();
- if (!name.trim()) return;
- onSave({ name: name.trim(), description: description.trim() || undefined, currency });
- }
-
- return (
-
- );
-}
diff --git a/apps/frontend/src/components/portfolios/PortfolioSummary.tsx b/apps/frontend/src/components/portfolios/PortfolioSummary.tsx
deleted file mode 100644
index 98dfabf..0000000
--- a/apps/frontend/src/components/portfolios/PortfolioSummary.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { AllocationChart } from './AllocationChart';
-import type { PortfolioDetail } from '@/shared/api/responses';
-
-export function PortfolioSummary({ portfolio }: { portfolio: PortfolioDetail }) {
- return (
-
-
-
-
- Общая стоимость
-
-
- {portfolio.totalValue?.toLocaleString('ru-RU', {
- minimumFractionDigits: 2,
- maximumFractionDigits: 2,
- })}
-
- {portfolio.currency}
-
-
-
-
-
- Позиций
-
-
{portfolio.positions.length}
-
-
- );
-}
diff --git a/apps/frontend/src/components/portfolios/SharePositionRow.tsx b/apps/frontend/src/components/portfolios/SharePositionRow.tsx
deleted file mode 100644
index 4aca8e7..0000000
--- a/apps/frontend/src/components/portfolios/SharePositionRow.tsx
+++ /dev/null
@@ -1,203 +0,0 @@
-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 SharePositionRow({ 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 }); // Or handle null if backend supports it
- }
- setEditingPrice(false);
- }
-
- return (
-
- |
-
- {position.secid}
-
- |
-
- {position.shortName ?? '—'}
- |
-
- {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}
-
- )}
- |
-
- {editingPrice ? (
- setPrice(e.target.value)}
- onBlur={handleSavePrice}
- onKeyDown={(e) => e.key === 'Enter' && handleSavePrice()}
- autoFocus
- style={{
- width: 100,
- 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',
- }}
- >
- {position.buyPrice !== null
- ? position.buyPrice.toLocaleString('ru-RU', {
- minimumFractionDigits: 2,
- maximumFractionDigits: 2,
- })
- : '—'}
-
- )}
- |
-
- {position.currentPrice !== null
- ? position.currentPrice.toLocaleString('ru-RU', {
- minimumFractionDigits: 2,
- maximumFractionDigits: 2,
- })
- : '—'}
- |
-
- {position.change != null && position.change !== 0 ? (
- 0 ? '#43a047' : '#e53935' }}>
- {position.change > 0 ? '+' : ''}
- {position.change.toLocaleString('ru-RU', {
- minimumFractionDigits: 2,
- maximumFractionDigits: 2,
- })}
-
- ) : (
- '—'
- )}
- |
-
- {position.currentValue !== null
- ? position.currentValue.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)}%
-
- ) : (
- '—'
- )}
- |
-
- {position.weightPercent.toFixed(1)}%
- |
-
-
- |
-
- );
-}
diff --git a/apps/frontend/src/components/portfolios/SharePositionTable.tsx b/apps/frontend/src/components/portfolios/SharePositionTable.tsx
deleted file mode 100644
index 0463244..0000000
--- a/apps/frontend/src/components/portfolios/SharePositionTable.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-import { SharePositionRow } from './SharePositionRow';
-import type { PositionWithPrice } from '@/shared/api/responses';
-
-interface Props {
- positions: PositionWithPrice[];
- onUpdatePosition: (
- positionId: number,
- data: { quantity?: number; buyPrice?: number; buyDate?: string },
- ) => void;
- onDeletePosition: (positionId: number) => void;
-}
-
-export function SharePositionTable({ positions, onUpdatePosition, onDeletePosition }: Props) {
- if (positions.length === 0) return null;
-
- return (
-
-
- Акции
-
-
-
-
-
- |
- Тикер
- |
-
- Название
- |
-
- Количество
- |
-
- Цена пок.
- |
-
- Цена
- |
-
- Изм.
- |
-
- Стоимость
- |
-
- Затраты
- |
-
- P&L
- |
-
- P&L %
- |
-
- Доля
- |
- |
-
-
-
- {positions.map((pos) => (
- onUpdatePosition(pos.id, data)}
- onDelete={() => onDeletePosition(pos.id)}
- />
- ))}
-
-
-
-
- );
-}