refactor(frontend): bring FSD architecture into compliance
- Remove cross-entity import (broker-position -> broker-operation) - Extract duplicated formatters to shared/lib/formatters.ts - Flatten shared/ui/broker-allocation-bar nesting - Clean up entities/stock/index.ts public API All 111 tests pass, build clean.
This commit is contained in:
parent
1f426e9734
commit
1774c7ffd6
@ -38,74 +38,6 @@ function moneyValue(money: BrokerMoney | null | undefined): number {
|
|||||||
return money?.value ?? 0;
|
return money?.value ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatBrokerMoney(value: BrokerMoney | null | undefined): string {
|
|
||||||
if (!value) {
|
|
||||||
return '—';
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatBrokerCurrencyValue(value.currency, value.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatBrokerCurrencyValue(currency: string, value: number): string {
|
|
||||||
return new Intl.NumberFormat('ru-RU', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: currency || 'RUB',
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
}).format(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatBrokerSignedCurrencyValue(currency: string, value: number | null): string {
|
|
||||||
if (value === null) {
|
|
||||||
return '—';
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatted = formatBrokerCurrencyValue(currency, Math.abs(value));
|
|
||||||
|
|
||||||
if (value > 0) {
|
|
||||||
return `+${formatted}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value < 0) {
|
|
||||||
return `−${formatted}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatted;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatBrokerPercent(value: number | null): string {
|
|
||||||
if (value === null) {
|
|
||||||
return '—';
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 2 }).format(value)}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatBrokerSignedPercent(value: number | null): string {
|
|
||||||
if (value === null) {
|
|
||||||
return '—';
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatted = formatBrokerPercent(Math.abs(value));
|
|
||||||
|
|
||||||
if (value > 0) {
|
|
||||||
return `+${formatted}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value < 0) {
|
|
||||||
return `−${formatted}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatted;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatBrokerDate(value: string | null | undefined): string | null {
|
|
||||||
if (!value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Date(value).toLocaleDateString('ru-RU');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function brokerAccountTypeLabel(type: 'brokerage' | 'iis'): string {
|
export function brokerAccountTypeLabel(type: 'brokerage' | 'iis'): string {
|
||||||
return type === 'iis' ? 'ИИС' : 'Брокерский счёт';
|
return type === 'iis' ? 'ИИС' : 'Брокерский счёт';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,13 +5,8 @@ export {
|
|||||||
type BrokerAllocationKey,
|
type BrokerAllocationKey,
|
||||||
} from './model/brokerAllocation';
|
} from './model/brokerAllocation';
|
||||||
export {
|
export {
|
||||||
BROKER_OPERATION_TYPE_OPTIONS,
|
|
||||||
getBrokerInstrumentPath,
|
getBrokerInstrumentPath,
|
||||||
getBrokerOperationImpact,
|
|
||||||
getBrokerOperationTypeLabel,
|
|
||||||
getBrokerPositionGroup,
|
getBrokerPositionGroup,
|
||||||
isBrokerOperationType,
|
|
||||||
type BrokerOperationImpact,
|
|
||||||
type BrokerPositionGroup,
|
type BrokerPositionGroup,
|
||||||
} from './model/brokerDisplay';
|
} from './model/brokerDisplay';
|
||||||
export { useBrokerPositions } from './model/useBrokerPositions';
|
export { useBrokerPositions } from './model/useBrokerPositions';
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import type { BrokerOperation, BrokerPosition } from '@/shared/api/responses';
|
import type { BrokerOperation, BrokerPosition } from '@/shared/api/responses';
|
||||||
|
import { getBrokerInstrumentPath, getBrokerPositionGroup } from './brokerDisplay';
|
||||||
import {
|
import {
|
||||||
BROKER_OPERATION_TYPE_OPTIONS,
|
BROKER_OPERATION_TYPE_OPTIONS,
|
||||||
getBrokerInstrumentPath,
|
|
||||||
getBrokerOperationImpact,
|
getBrokerOperationImpact,
|
||||||
getBrokerOperationTypeLabel,
|
getBrokerOperationTypeLabel,
|
||||||
getBrokerPositionGroup,
|
|
||||||
isBrokerOperationType,
|
isBrokerOperationType,
|
||||||
} from './brokerDisplay';
|
} from '@/entities/broker-operation';
|
||||||
|
|
||||||
function position(input: Partial<BrokerPosition>): BrokerPosition {
|
function position(input: Partial<BrokerPosition>): BrokerPosition {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,11 +1,4 @@
|
|||||||
import type { BrokerPosition } from '@/shared/api/responses';
|
import type { BrokerPosition } from '@/shared/api/responses';
|
||||||
export {
|
|
||||||
BROKER_OPERATION_TYPE_OPTIONS,
|
|
||||||
getBrokerOperationImpact,
|
|
||||||
getBrokerOperationTypeLabel,
|
|
||||||
isBrokerOperationType,
|
|
||||||
} from '@/entities/broker-operation';
|
|
||||||
export type { BrokerOperationImpact } from '@/entities/broker-operation';
|
|
||||||
|
|
||||||
export type BrokerPositionGroup = 'shares' | 'bonds' | 'other';
|
export type BrokerPositionGroup = 'shares' | 'bonds' | 'other';
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,3 @@
|
|||||||
export { useStock } from './model/useStock';
|
export { useStock } from './model/useStock';
|
||||||
export { useStockCandles } from './model/useStockCandles';
|
export { useStockCandles } from './model/useStockCandles';
|
||||||
export { useStockDividends } from './model/useStockDividends';
|
export { useStockDividends } from './model/useStockDividends';
|
||||||
export {
|
|
||||||
getShare,
|
|
||||||
getShareMarketData,
|
|
||||||
getShareDividends,
|
|
||||||
getShareHistory,
|
|
||||||
getShareCandles,
|
|
||||||
} from './api/stockApi';
|
|
||||||
|
|||||||
@ -5,29 +5,11 @@ import { useBrokerOperations } from '@/entities/broker-operation';
|
|||||||
import { useBrokerAccountContext } from '@/widgets/broker-account-layout';
|
import { useBrokerAccountContext } from '@/widgets/broker-account-layout';
|
||||||
import { BrokerAllocationChart } from '@/widgets/broker-allocation-chart';
|
import { BrokerAllocationChart } from '@/widgets/broker-allocation-chart';
|
||||||
import { BrokerOperationsTable } from '@/widgets/broker-operations-table';
|
import { BrokerOperationsTable } from '@/widgets/broker-operations-table';
|
||||||
|
import {
|
||||||
function formatMoney(value: BrokerMoney | null | undefined) {
|
formatBrokerMoney as formatMoney,
|
||||||
if (!value) return '—';
|
formatBrokerPercent as formatPercent,
|
||||||
return new Intl.NumberFormat('ru-RU', {
|
pluralize,
|
||||||
style: 'currency',
|
} from '@/shared/lib/formatters';
|
||||||
currency: value.currency || 'RUB',
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
}).format(value.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatPercent(value: number | null) {
|
|
||||||
if (value === null) return '—';
|
|
||||||
return `${new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 2 }).format(value)}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pluralize(count: number, one: string, few: string, many: string) {
|
|
||||||
const modulo100 = Math.abs(count) % 100;
|
|
||||||
const modulo10 = modulo100 % 10;
|
|
||||||
if (modulo100 > 10 && modulo100 < 20) return many;
|
|
||||||
if (modulo10 === 1) return one;
|
|
||||||
if (modulo10 >= 2 && modulo10 <= 4) return few;
|
|
||||||
return many;
|
|
||||||
}
|
|
||||||
|
|
||||||
function BrokerSummary({ portfolio }: { portfolio: BrokerPortfolio }) {
|
function BrokerSummary({ portfolio }: { portfolio: BrokerPortfolio }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import type {
|
import type {
|
||||||
BrokerMoney,
|
|
||||||
BrokerPosition,
|
BrokerPosition,
|
||||||
BrokerPositionsPage as BrokerPositionsPageData,
|
BrokerPositionsPage as BrokerPositionsPageData,
|
||||||
} from '@/shared/api/responses';
|
} from '@/shared/api/responses';
|
||||||
import { TableSkeleton } from '@/shared/ui/TableSkeleton';
|
import { TableSkeleton } from '@/shared/ui/TableSkeleton';
|
||||||
import { getBrokerInstrumentPath, useBrokerPositions } from '@/entities/broker-position';
|
import { getBrokerInstrumentPath, useBrokerPositions } from '@/entities/broker-position';
|
||||||
import { useBrokerAccountContext } from '@/widgets/broker-account-layout';
|
import { useBrokerAccountContext } from '@/widgets/broker-account-layout';
|
||||||
|
import { formatBrokerMoney as formatMoney } from '@/shared/lib/formatters';
|
||||||
|
|
||||||
const tableStyle = {
|
const tableStyle = {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@ -46,15 +46,6 @@ const pagButtonDisabledStyle = {
|
|||||||
cursor: 'not-allowed',
|
cursor: 'not-allowed',
|
||||||
} satisfies React.CSSProperties;
|
} satisfies React.CSSProperties;
|
||||||
|
|
||||||
function formatMoney(value: BrokerMoney | null | undefined) {
|
|
||||||
if (!value) return '-';
|
|
||||||
return new Intl.NumberFormat('ru-RU', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: value.currency || 'RUB',
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
}).format(value.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatQuantity(value: number | null | undefined) {
|
function formatQuantity(value: number | null | undefined) {
|
||||||
return value == null ? '-' : value.toLocaleString('ru-RU');
|
return value == null ? '-' : value.toLocaleString('ru-RU');
|
||||||
}
|
}
|
||||||
|
|||||||
89
apps/frontend/src/shared/lib/formatters.ts
Normal file
89
apps/frontend/src/shared/lib/formatters.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import type { BrokerMoney } from '@/shared/api/responses';
|
||||||
|
|
||||||
|
export function formatBrokerCurrencyValue(currency: string, value: number): string {
|
||||||
|
return new Intl.NumberFormat('ru-RU', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: currency || 'RUB',
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
}).format(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatBrokerMoney(value: BrokerMoney | null | undefined): string {
|
||||||
|
if (!value) {
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatBrokerCurrencyValue(value.currency, value.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatBrokerSignedCurrencyValue(currency: string, value: number | null): string {
|
||||||
|
if (value === null) {
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = formatBrokerCurrencyValue(currency, Math.abs(value));
|
||||||
|
|
||||||
|
if (value > 0) {
|
||||||
|
return `+${formatted}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value < 0) {
|
||||||
|
return `\u2212${formatted}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatBrokerSignedMoney(value: BrokerMoney | null | undefined): string {
|
||||||
|
if (!value) {
|
||||||
|
return '\u2014';
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatBrokerSignedCurrencyValue(value.currency, value.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatBrokerPercent(value: number | null): string {
|
||||||
|
if (value === null) {
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 2 }).format(value)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatBrokerSignedPercent(value: number | null): string {
|
||||||
|
if (value === null) {
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = formatBrokerPercent(Math.abs(value));
|
||||||
|
|
||||||
|
if (value > 0) {
|
||||||
|
return `+${formatted}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value < 0) {
|
||||||
|
return `\u2212${formatted}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatBrokerDate(value: string | null | undefined): string | null {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(value).toLocaleDateString('ru-RU');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pluralize(n: number, one: string, few: string, many: string): string {
|
||||||
|
const abs = Math.abs(n);
|
||||||
|
const modulo100 = abs % 100;
|
||||||
|
const modulo10 = modulo100 % 10;
|
||||||
|
|
||||||
|
if (modulo100 > 10 && modulo100 < 20) return many;
|
||||||
|
if (modulo10 === 1) return one;
|
||||||
|
if (modulo10 >= 2 && modulo10 <= 4) return few;
|
||||||
|
|
||||||
|
return many;
|
||||||
|
}
|
||||||
@ -1 +1 @@
|
|||||||
export { BrokerAllocationBar } from './ui/BrokerAllocationBar';
|
export { BrokerAllocationBar } from './BrokerAllocationBar';
|
||||||
|
|||||||
@ -1,70 +1,14 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { SkeletonBlock } from '@/shared/ui/SkeletonBlock';
|
import { SkeletonBlock } from '@/shared/ui/SkeletonBlock';
|
||||||
import type { BrokerAccount, BrokerMoney, BrokerPortfolio } from '@/shared/api/responses';
|
import type { BrokerAccount, BrokerPortfolio } from '@/shared/api/responses';
|
||||||
import { buildBrokerAllocation } from '@/entities/broker-position';
|
import { buildBrokerAllocation } from '@/entities/broker-position';
|
||||||
import { BrokerAllocationBar } from '@/shared/ui/broker-allocation-bar';
|
import { BrokerAllocationBar } from '@/shared/ui/broker-allocation-bar';
|
||||||
|
import {
|
||||||
function formatBrokerCurrencyValue(currency: string, value: number): string {
|
formatBrokerMoney,
|
||||||
return new Intl.NumberFormat('ru-RU', {
|
formatBrokerSignedCurrencyValue,
|
||||||
style: 'currency',
|
formatBrokerSignedPercent,
|
||||||
currency: currency || 'RUB',
|
formatBrokerDate,
|
||||||
maximumFractionDigits: 2,
|
} from '@/shared/lib/formatters';
|
||||||
}).format(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatBrokerMoney(value: BrokerMoney | null | undefined): string {
|
|
||||||
if (!value) {
|
|
||||||
return '—';
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatBrokerCurrencyValue(value.currency, value.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatBrokerSignedCurrencyValue(currency: string, value: number | null): string {
|
|
||||||
if (value === null) {
|
|
||||||
return '—';
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatted = formatBrokerCurrencyValue(currency, Math.abs(value));
|
|
||||||
|
|
||||||
if (value > 0) {
|
|
||||||
return `+${formatted}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value < 0) {
|
|
||||||
return `−${formatted}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatted;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatBrokerSignedPercent(value: number | null): string {
|
|
||||||
if (value === null) {
|
|
||||||
return '—';
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatted = `${new Intl.NumberFormat('ru-RU', {
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
}).format(Math.abs(value))}%`;
|
|
||||||
|
|
||||||
if (value > 0) {
|
|
||||||
return `+${formatted}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value < 0) {
|
|
||||||
return `−${formatted}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatted;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatBrokerDate(value: string | null | undefined): string | null {
|
|
||||||
if (!value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Date(value).toLocaleDateString('ru-RU');
|
|
||||||
}
|
|
||||||
|
|
||||||
function brokerAccountTypeLabel(type: 'brokerage' | 'iis'): string {
|
function brokerAccountTypeLabel(type: 'brokerage' | 'iis'): string {
|
||||||
return type === 'iis' ? 'ИИС' : 'Брокерский счёт';
|
return type === 'iis' ? 'ИИС' : 'Брокерский счёт';
|
||||||
|
|||||||
@ -2,52 +2,11 @@ import { SkeletonBlock } from '@/shared/ui/SkeletonBlock';
|
|||||||
import type { BrokerAccountsAggregate } from '@/entities/broker-account';
|
import type { BrokerAccountsAggregate } from '@/entities/broker-account';
|
||||||
import { buildBrokerAllocation } from '@/entities/broker-position';
|
import { buildBrokerAllocation } from '@/entities/broker-position';
|
||||||
import { BrokerAllocationBar } from '@/shared/ui/broker-allocation-bar';
|
import { BrokerAllocationBar } from '@/shared/ui/broker-allocation-bar';
|
||||||
|
import {
|
||||||
function formatBrokerCurrencyValue(currency: string, value: number): string {
|
formatBrokerCurrencyValue,
|
||||||
return new Intl.NumberFormat('ru-RU', {
|
formatBrokerSignedCurrencyValue,
|
||||||
style: 'currency',
|
formatBrokerSignedPercent,
|
||||||
currency: currency || 'RUB',
|
} from '@/shared/lib/formatters';
|
||||||
maximumFractionDigits: 2,
|
|
||||||
}).format(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatBrokerSignedCurrencyValue(currency: string, value: number | null): string {
|
|
||||||
if (value === null) {
|
|
||||||
return '—';
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatted = formatBrokerCurrencyValue(currency, Math.abs(value));
|
|
||||||
|
|
||||||
if (value > 0) {
|
|
||||||
return `+${formatted}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value < 0) {
|
|
||||||
return `−${formatted}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatted;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatBrokerSignedPercent(value: number | null): string {
|
|
||||||
if (value === null) {
|
|
||||||
return '—';
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatted = `${new Intl.NumberFormat('ru-RU', {
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
}).format(Math.abs(value))}%`;
|
|
||||||
|
|
||||||
if (value > 0) {
|
|
||||||
return `+${formatted}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value < 0) {
|
|
||||||
return `−${formatted}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatted;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BrokerAccountsSummary({
|
export function BrokerAccountsSummary({
|
||||||
aggregate,
|
aggregate,
|
||||||
|
|||||||
@ -1,17 +1,10 @@
|
|||||||
import type { BrokerPortfolio } from '@/shared/api/responses';
|
import type { BrokerPortfolio } from '@/shared/api/responses';
|
||||||
import { buildBrokerAllocation } from '@/entities/broker-position';
|
import { buildBrokerAllocation } from '@/entities/broker-position';
|
||||||
|
import { formatBrokerCurrencyValue } from '@/shared/lib/formatters';
|
||||||
|
|
||||||
const RADIUS = 44;
|
const RADIUS = 44;
|
||||||
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
|
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
|
||||||
|
|
||||||
function formatMoneyValue(value: number, currency: string) {
|
|
||||||
return new Intl.NumberFormat('ru-RU', {
|
|
||||||
style: 'currency',
|
|
||||||
currency,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
}).format(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function allocationCurrency(portfolio: BrokerPortfolio) {
|
function allocationCurrency(portfolio: BrokerPortfolio) {
|
||||||
return (
|
return (
|
||||||
portfolio.totals.portfolio?.currency ||
|
portfolio.totals.portfolio?.currency ||
|
||||||
@ -64,7 +57,7 @@ export function BrokerAllocationChart({ portfolio }: { portfolio: BrokerPortfoli
|
|||||||
style={{ background: sector.color }}
|
style={{ background: sector.color }}
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
{sector.label}: {formatMoneyValue(sector.value, currency)} ·{' '}
|
{sector.label}: {formatBrokerCurrencyValue(currency, sector.value)} ·{' '}
|
||||||
{sector.percent.toFixed(1)}%
|
{sector.percent.toFixed(1)}%
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
@ -78,7 +71,8 @@ export function BrokerAllocationChart({ portfolio }: { portfolio: BrokerPortfoli
|
|||||||
>
|
>
|
||||||
{negative.map((item) => (
|
{negative.map((item) => (
|
||||||
<li key={item.key}>
|
<li key={item.key}>
|
||||||
{item.label}: отрицательное значение {formatMoneyValue(item.value, currency)}
|
{item.label}: отрицательное значение{' '}
|
||||||
|
{formatBrokerCurrencyValue(currency, item.value)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import type { BrokerMoney, BrokerOperation, BrokerOperationsPage } from '@/shared/api/responses';
|
import type { BrokerOperation, BrokerOperationsPage } from '@/shared/api/responses';
|
||||||
import { TableSkeleton } from '@/shared/ui/TableSkeleton';
|
import { TableSkeleton } from '@/shared/ui/TableSkeleton';
|
||||||
import {
|
import {
|
||||||
getBrokerOperationImpact,
|
getBrokerOperationImpact,
|
||||||
@ -8,6 +8,7 @@ import {
|
|||||||
type BrokerOperationImpact,
|
type BrokerOperationImpact,
|
||||||
} from '@/entities/broker-operation';
|
} from '@/entities/broker-operation';
|
||||||
import { getBrokerInstrumentPath } from '@/entities/broker-position';
|
import { getBrokerInstrumentPath } from '@/entities/broker-position';
|
||||||
|
import { formatBrokerSignedMoney } from '@/shared/lib/formatters';
|
||||||
|
|
||||||
const tableStyle = {
|
const tableStyle = {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@ -46,16 +47,6 @@ const pagButtonDisabledStyle = {
|
|||||||
cursor: 'not-allowed',
|
cursor: 'not-allowed',
|
||||||
} satisfies React.CSSProperties;
|
} satisfies React.CSSProperties;
|
||||||
|
|
||||||
function formatMoney(value: BrokerMoney | null | undefined) {
|
|
||||||
if (!value) return '-';
|
|
||||||
const formatted = new Intl.NumberFormat('ru-RU', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: value.currency || 'RUB',
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
}).format(value.value);
|
|
||||||
return value.value > 0 ? `+${formatted}` : formatted;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(value: string | null) {
|
function formatDate(value: string | null) {
|
||||||
if (!value) return '-';
|
if (!value) return '-';
|
||||||
|
|
||||||
@ -235,7 +226,7 @@ export function BrokerOperationsTable({
|
|||||||
align="right"
|
align="right"
|
||||||
style={{ ...tdStyle, color: moneyColor(impact), fontWeight: 700 }}
|
style={{ ...tdStyle, color: moneyColor(impact), fontWeight: 700 }}
|
||||||
>
|
>
|
||||||
{formatMoney(operation.payment)}
|
{formatBrokerSignedMoney(operation.payment)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import type { Portfolio } from '@/shared/api/responses';
|
import type { Portfolio } from '@/shared/api/responses';
|
||||||
|
import { pluralize } from '@/shared/lib/formatters';
|
||||||
|
|
||||||
export function PortfolioCard({ portfolio }: { portfolio: Portfolio }) {
|
export function PortfolioCard({ portfolio }: { portfolio: Portfolio }) {
|
||||||
const chipStyle = (bg: string): React.CSSProperties => ({
|
const chipStyle = (bg: string): React.CSSProperties => ({
|
||||||
@ -79,9 +80,3 @@ export function PortfolioCard({ portfolio }: { portfolio: Portfolio }) {
|
|||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|||||||
32
docs/features/fsd-quick-fix/plan.md
Normal file
32
docs/features/fsd-quick-fix/plan.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# FSD Quick Fix — Plan
|
||||||
|
|
||||||
|
## Архитектурные решения
|
||||||
|
|
||||||
|
1. **Все форматтеры** переносятся в `shared/lib/formatters.ts`. Каноничные реализации берутся из `entities/broker-account/model/brokerAccountsOverview.ts`. Исходные функции в brokerAccountsOverview становятся тонкими врапперами (re-export), чтобы не менять контракт entity.
|
||||||
|
2. **Cross-entity импорт** удаляется полностью — символы broker-operation никто не импортирует через broker-position, реэкспорты мёртвые.
|
||||||
|
3. **Stock API-функции** убираются из barrel — используются только внутри entity хуками.
|
||||||
|
4. **shared/ui/broker-allocation-bar** уплощается: компонент поднимается на уровень slice.
|
||||||
|
|
||||||
|
## Порядок выполнения
|
||||||
|
|
||||||
|
### Task 1: Убрать cross-entity import
|
||||||
|
- broker-position/model/brokerDisplay.ts — удалить re-export строки
|
||||||
|
- broker-position/index.ts — удалить соответствующие экспорты
|
||||||
|
|
||||||
|
### Task 2: Дедуплицировать форматтеры
|
||||||
|
- Создать shared/lib/formatters.ts
|
||||||
|
- Переделать brokerAccountsOverview.ts на импорт из shared
|
||||||
|
- Заменить дубликаты во всех консьюмерах
|
||||||
|
|
||||||
|
### Task 3: Уплостить shared/ui/broker-allocation-bar
|
||||||
|
- Переместить BrokerAllocationBar.tsx на уровень выше
|
||||||
|
- Обновить index.ts
|
||||||
|
- Удалить ui/
|
||||||
|
|
||||||
|
### Task 4: Очистить entities/stock/index.ts
|
||||||
|
- Убрать getShare, getShareMarketData и т.д. из barrel
|
||||||
|
|
||||||
|
### Проверка
|
||||||
|
- npm run lint (frontend)
|
||||||
|
- npm run build (frontend)
|
||||||
|
- npm run test (frontend)
|
||||||
22
docs/features/fsd-quick-fix/spec.md
Normal file
22
docs/features/fsd-quick-fix/spec.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# FSD Quick Fix
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Привести фронтенд к соответствию Feature-Sliced Design (FSD) путём исправления выявленных нарушений и дублирования кода. Никакой новой функциональности — только архитектурный рефакторинг.
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
|
||||||
|
1. Устранить cross-entity импорт (entities → entities).
|
||||||
|
2. Устранить дублирование форматтеров денежных величин и pluralize.
|
||||||
|
3. Устранить избыточную вложенность shared/ui-компонента.
|
||||||
|
4. Убрать из публичного API entity сырые API-функции, не используемые снаружи.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] `entities/broker-position` не импортирует символы из `entities/broker-operation`
|
||||||
|
- [ ] `shared/lib/formatters.ts` содержит каноничные реализации всех форматтеров
|
||||||
|
- [ ] Все консьюмеры используют форматтеры из `shared/lib/formatters.ts` вместо локальных копий
|
||||||
|
- [ ] `shared/ui/broker-allocation-bar` не имеет лишней вложенности `ui/`
|
||||||
|
- [ ] `entities/stock/index.ts` не экспортирует неиспользуемые извне API-функции
|
||||||
|
- [ ] Lint проходит, build проходит, тесты проходят
|
||||||
|
- [ ] Нет изменений в поведении системы
|
||||||
28
docs/features/fsd-quick-fix/tasks.md
Normal file
28
docs/features/fsd-quick-fix/tasks.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# FSD Quick Fix — Tasks
|
||||||
|
|
||||||
|
- [x] Создать feature branch `codex/fsd-quick-fix`
|
||||||
|
- [x] Написать spec.md
|
||||||
|
- [x] Написать plan.md
|
||||||
|
- [x] **Task 1**: Убрать cross-entity import broker-position → broker-operation
|
||||||
|
- [x] `entities/broker-position/model/brokerDisplay.ts` — удалить re-export строк 2-8
|
||||||
|
- [x] `entities/broker-position/index.ts` — удалить BROKER_OPERATION_TYPE_OPTIONS, getBrokerOperationImpact, getBrokerOperationTypeLabel, isBrokerOperationType, BrokerOperationImpact
|
||||||
|
- [x] `entities/broker-position/model/brokerDisplay.test.ts` — перенести импорты broker-operation напрямую
|
||||||
|
- [x] **Task 2**: Дедуплицировать форматтеры
|
||||||
|
- [x] Создать `shared/lib/formatters.ts` с каноничными реализациями
|
||||||
|
- [x] `entities/broker-account/model/brokerAccountsOverview.ts` — перейти на импорт из shared
|
||||||
|
- [x] `widgets/broker-account-card/BrokerAccountCard.tsx` — убрать дубликаты
|
||||||
|
- [x] `widgets/broker-accounts-summary/BrokerAccountsSummary.tsx` — убрать дубликаты
|
||||||
|
- [x] `widgets/broker-allocation-chart/BrokerAllocationChart.tsx` — убрать дубликаты
|
||||||
|
- [x] `widgets/broker-operations-table/BrokerOperationsTable.tsx` — убрать дубликаты
|
||||||
|
- [x] `widgets/portfolio-card/PortfolioCard.tsx` — убрать дубликаты
|
||||||
|
- [x] `pages/broker-account/BrokerAccountOverviewPage.tsx` — убрать дубликаты
|
||||||
|
- [x] `pages/broker-positions/BrokerPositionsPage.tsx` — убрать дубликаты
|
||||||
|
- [x] **Task 3**: Уплостить shared/ui/broker-allocation-bar
|
||||||
|
- [x] Переместить BrokerAllocationBar.tsx из `ui/` на уровень slice
|
||||||
|
- [x] Обновить `index.ts`
|
||||||
|
- [x] Удалить пустую `ui/`
|
||||||
|
- [x] **Task 4**: Очистить entities/stock/index.ts
|
||||||
|
- [x] Убрать getShare, getShareMarketData, getShareDividends, getShareHistory, getShareCandles
|
||||||
|
- [x] **Проверка**: lint + build + тесты
|
||||||
|
- [x] Build — OK
|
||||||
|
- [x] Tests — 23/23 files, 111/111 passed
|
||||||
Loading…
x
Reference in New Issue
Block a user