653 lines
19 KiB
Markdown
653 lines
19 KiB
Markdown
# Аналитика прибыльности брокерского счёта — Implementation Plan
|
||
|
||
> **For agentic workers:** Use subagent-driven-development or executing-plans to implement this plan task-by-task.
|
||
|
||
**Goal:** Add an analytics tab to the broker account page showing net invested vs received (dividends + coupons).
|
||
|
||
**Architecture:** New backend service aggregates BrokerOperation records by type via Prisma, returns DTO. New frontend tab page displays invested/received blocks.
|
||
|
||
**Tech Stack:** NestJS + Prisma (SQLite), React + TanStack Query + react-router
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
### Backend (new/modified)
|
||
- Create: `apps/backend/src/modules/tbank/dto/broker-analytics-response.dto.ts`
|
||
- Create: `apps/backend/src/modules/tbank/services/broker-analytics.service.ts`
|
||
- Modify: `apps/backend/src/modules/tbank/tbank.controller.ts` — add `GET /analytics` endpoint
|
||
- Modify: `apps/backend/src/modules/tbank/tbank.module.ts` — register service
|
||
|
||
### Frontend (new/modified)
|
||
- Create: `apps/frontend/src/entities/broker-analytics/api/brokerAnalyticsApi.ts`
|
||
- Create: `apps/frontend/src/entities/broker-analytics/model/useBrokerAnalytics.ts`
|
||
- Create: `apps/frontend/src/entities/broker-analytics/index.ts`
|
||
- Create: `apps/frontend/src/pages/broker-analytics/ui/BrokerAnalyticsPage.tsx`
|
||
- Create: `apps/frontend/src/pages/broker-analytics/index.ts`
|
||
- Modify: `apps/frontend/src/widgets/broker-account-layout/ui/BrokerAccountLayout.tsx`
|
||
- Modify: `apps/frontend/src/app/routing/routeTree.tsx`
|
||
- Modify: `apps/frontend/src/shared/api/types.ts`
|
||
|
||
---
|
||
|
||
### Task 1: Backend DTO и сервис аналитики
|
||
|
||
**Files:**
|
||
- Create: `apps/backend/src/modules/tbank/dto/broker-analytics-response.dto.ts`
|
||
- Create: `apps/backend/src/modules/tbank/services/broker-analytics.service.ts`
|
||
|
||
#### Шаг 1.1: Создать DTO
|
||
|
||
```typescript
|
||
import { ApiProperty } from '@nestjs/swagger';
|
||
|
||
export class BrokerAnalyticsDto {
|
||
@ApiProperty()
|
||
totalDeposits!: number;
|
||
|
||
@ApiProperty()
|
||
totalWithdrawn!: number;
|
||
|
||
@ApiProperty()
|
||
netInvested!: number;
|
||
|
||
@ApiProperty()
|
||
totalDividends!: number;
|
||
|
||
@ApiProperty()
|
||
totalCoupons!: number;
|
||
|
||
@ApiProperty()
|
||
totalReceived!: number;
|
||
|
||
@ApiProperty({ type: Number, nullable: true })
|
||
totalReturnPercent!: number | null;
|
||
|
||
@ApiProperty()
|
||
currency!: string;
|
||
}
|
||
```
|
||
|
||
#### Шаг 1.2: Создать сервис `BrokerAnalyticsService`
|
||
|
||
```typescript
|
||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||
import { PrismaService } from '../../prisma/prisma.service';
|
||
import { CacheService } from '../../cache/cache.service';
|
||
import { BrokerAnalyticsDto } from '../dto/broker-analytics-response.dto';
|
||
import { BrokerAccountsService } from './broker-accounts.service';
|
||
import { TBANK_CACHE_KEYS } from '../tbank.config';
|
||
|
||
const DEPOSIT_TYPES = new Set([
|
||
'OPERATION_TYPE_INPUT',
|
||
'OPERATION_TYPE_INPUT_SWIFT',
|
||
'OPERATION_TYPE_INPUT_ACQUIRING',
|
||
'OPERATION_TYPE_INP_MULTI',
|
||
'OPERATION_TYPE_OVER_PLACEMENT',
|
||
'OPERATION_TYPE_TRANS_IIS_BS',
|
||
'OPERATION_TYPE_TRANS_BS_BS',
|
||
]);
|
||
|
||
const WITHDRAWAL_TYPES = new Set([
|
||
'OPERATION_TYPE_OUTPUT',
|
||
'OPERATION_TYPE_OUTPUT_SWIFT',
|
||
'OPERATION_TYPE_OUTPUT_ACQUIRING',
|
||
'OPERATION_TYPE_OUT_MULTI',
|
||
]);
|
||
|
||
const DIVIDEND_TYPES = new Set(['OPERATION_TYPE_DIVIDEND', 'OPERATION_TYPE_DIV_EXT']);
|
||
|
||
const COUPON_TYPES = new Set(['OPERATION_TYPE_COUPON']);
|
||
|
||
const ANALYTICS_TYPES = new Set([
|
||
...DEPOSIT_TYPES,
|
||
...WITHDRAWAL_TYPES,
|
||
...DIVIDEND_TYPES,
|
||
...COUPON_TYPES,
|
||
]);
|
||
|
||
@Injectable()
|
||
export class BrokerAnalyticsService {
|
||
constructor(
|
||
private readonly prisma: PrismaService,
|
||
private readonly accountsService: BrokerAccountsService,
|
||
private readonly cacheService: CacheService,
|
||
) {}
|
||
|
||
async getAnalytics(accountId: string): Promise<{
|
||
data: BrokerAnalyticsDto;
|
||
meta: { fromCache: boolean; cachedAt: string | null };
|
||
}> {
|
||
const account = await this.accountsService.findById(accountId);
|
||
if (!account) throw new NotFoundException('Broker account not found');
|
||
|
||
return this.cacheService.getOrFetch(
|
||
TBANK_CACHE_KEYS.analytics,
|
||
[accountId],
|
||
() => this.computeAnalytics(accountId),
|
||
'tbankAnalyticsTtl',
|
||
);
|
||
}
|
||
|
||
private async computeAnalytics(accountId: string): Promise<BrokerAnalyticsDto> {
|
||
const operations = await this.prisma.brokerOperation.findMany({
|
||
where: {
|
||
accountId,
|
||
type: { in: Array.from(ANALYTICS_TYPES) },
|
||
payment: { not: null },
|
||
},
|
||
select: { type: true, payment: true },
|
||
});
|
||
|
||
let totalDeposits = 0;
|
||
let totalWithdrawn = 0;
|
||
let totalDividends = 0;
|
||
let totalCoupons = 0;
|
||
|
||
for (const op of operations) {
|
||
const payment = JSON.parse(op.payment!);
|
||
const value = payment.value ?? 0;
|
||
|
||
if (DEPOSIT_TYPES.has(op.type)) {
|
||
totalDeposits += value;
|
||
} else if (WITHDRAWAL_TYPES.has(op.type)) {
|
||
totalWithdrawn += Math.abs(value);
|
||
} else if (DIVIDEND_TYPES.has(op.type)) {
|
||
totalDividends += value;
|
||
} else if (COUPON_TYPES.has(op.type)) {
|
||
totalCoupons += value;
|
||
}
|
||
}
|
||
|
||
const netInvested = totalDeposits - totalWithdrawn;
|
||
const totalReceived = totalDividends + totalCoupons;
|
||
const totalReturnPercent =
|
||
netInvested > 0 ? Math.round((totalReceived / netInvested) * 10000) / 100 : null;
|
||
|
||
return {
|
||
totalDeposits: Math.round(totalDeposits * 100) / 100,
|
||
totalWithdrawn: Math.round(totalWithdrawn * 100) / 100,
|
||
netInvested: Math.round(netInvested * 100) / 100,
|
||
totalDividends: Math.round(totalDividends * 100) / 100,
|
||
totalCoupons: Math.round(totalCoupons * 100) / 100,
|
||
totalReceived: Math.round(totalReceived * 100) / 100,
|
||
totalReturnPercent,
|
||
currency: 'RUB',
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Шаг 1.3: Проверить сборку
|
||
|
||
```bash
|
||
npm run build -w apps/backend
|
||
```
|
||
|
||
#### Шаг 1.4: Закоммитить
|
||
|
||
```bash
|
||
git add apps/backend/src/modules/tbank/dto/broker-analytics-response.dto.ts
|
||
git add apps/backend/src/modules/tbank/services/broker-analytics.service.ts
|
||
git commit -m "feat(backend): add broker analytics DTO and service"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: Backend controller, module registration и конфиг кеша
|
||
|
||
**Files:**
|
||
- Modify: `apps/backend/src/modules/tbank/tbank.controller.ts`
|
||
- Modify: `apps/backend/src/modules/tbank/tbank.module.ts`
|
||
- Modify: `apps/backend/src/modules/tbank/tbank.config.ts`
|
||
- Modify: `apps/backend/src/modules/tbank/dto/broker-envelope.dto.ts`
|
||
- Modify: `apps/backend/src/config/configuration.ts`
|
||
|
||
#### Шаг 2.1: Добавить cache key
|
||
|
||
В `apps/backend/src/modules/tbank/tbank.config.ts`:
|
||
|
||
```typescript
|
||
export const TBANK_CACHE_KEYS = {
|
||
// ... existing keys
|
||
analytics: 'tbank:analytics',
|
||
} as const;
|
||
```
|
||
|
||
#### Шаг 2.2: Добавить TTL config
|
||
|
||
В `apps/backend/src/config/configuration.ts`, в секцию `cache`:
|
||
|
||
```typescript
|
||
tbankAnalyticsTtl: parseInt(process.env.CACHE_TBANK_ANALYTICS_TTL || '300', 10),
|
||
```
|
||
|
||
#### Шаг 2.3: Зарегистрировать сервис в `TbankModule`
|
||
|
||
В `apps/backend/src/modules/tbank/tbank.module.ts`:
|
||
- Добавить `BrokerAnalyticsService` в `providers`
|
||
|
||
```typescript
|
||
import { BrokerAnalyticsService } from './services/broker-analytics.service';
|
||
|
||
@Module({
|
||
providers: [
|
||
// ... existing services
|
||
BrokerAnalyticsService,
|
||
],
|
||
})
|
||
```
|
||
|
||
#### Шаг 2.4: Добавить envelope DTO
|
||
|
||
В `apps/backend/src/modules/tbank/dto/broker-envelope.dto.ts`:
|
||
|
||
```typescript
|
||
import { BrokerAnalyticsDto } from './broker-analytics-response.dto';
|
||
|
||
export class BrokerAnalyticsEnvelopeDto {
|
||
@ApiProperty({ type: BrokerAnalyticsDto })
|
||
data!: BrokerAnalyticsDto;
|
||
|
||
@ApiProperty({ type: BrokerResponseMetaDto })
|
||
meta!: BrokerResponseMetaDto;
|
||
}
|
||
```
|
||
|
||
#### Шаг 2.5: Добавить endpoint в `TbankController`
|
||
|
||
```typescript
|
||
import { BrokerAnalyticsService } from './services/broker-analytics.service';
|
||
import { BrokerAnalyticsEnvelopeDto } from './dto/broker-envelope.dto';
|
||
|
||
@Get('accounts/:accountId/analytics')
|
||
@ApiOperation({ summary: 'Get broker account profitability analytics' })
|
||
@ApiOkResponse({ type: BrokerAnalyticsEnvelopeDto })
|
||
async getAnalytics(@Param('accountId') accountId: string) {
|
||
const result = await this.brokerAnalyticsService.getAnalytics(accountId);
|
||
return new ApiResponse(result.data, result.meta.fromCache, result.meta.cachedAt);
|
||
}
|
||
```
|
||
|
||
Добавить `private readonly brokerAnalyticsService: BrokerAnalyticsService` в конструктор.
|
||
|
||
#### Шаг 2.6: Проверить сборку
|
||
|
||
```bash
|
||
npm run build -w apps/backend
|
||
```
|
||
|
||
#### Шаг 2.7: Закоммитить
|
||
|
||
```bash
|
||
git add apps/backend/src/modules/tbank/
|
||
git add apps/backend/src/config/configuration.ts
|
||
git commit -m "feat(backend): add broker analytics endpoint with caching"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: Frontend — shared types barrel, entity API и хук
|
||
|
||
**Files:**
|
||
- Modify: `apps/frontend/src/shared/api/index.ts`
|
||
- Modify: `apps/frontend/src/shared/api/types.ts` (codegen)
|
||
- Create: `apps/frontend/src/entities/broker-analytics/api/brokerAnalyticsApi.ts`
|
||
- Create: `apps/frontend/src/entities/broker-analytics/model/useBrokerAnalytics.ts`
|
||
- Create: `apps/frontend/src/entities/broker-analytics/index.ts`
|
||
|
||
#### Шаг 3.1: Добавить тип `BrokerAnalyticsDto` в `types.ts`
|
||
|
||
Добавить в `components['schemas']` секцию `shared/api/types.ts`:
|
||
|
||
```typescript
|
||
BrokerAnalyticsDto: {
|
||
totalDeposits: number
|
||
totalWithdrawn: number
|
||
netInvested: number
|
||
totalDividends: number
|
||
totalCoupons: number
|
||
totalReceived: number
|
||
totalReturnPercent: number | null
|
||
currency: string
|
||
}
|
||
```
|
||
|
||
#### Шаг 3.2: Добавить брокерский тип в barrel export
|
||
|
||
В `apps/frontend/src/shared/api/index.ts`:
|
||
|
||
```typescript
|
||
// Broker analytics
|
||
export type BrokerAnalytics = components['schemas']['BrokerAnalyticsDto']
|
||
```
|
||
|
||
#### Шаг 3.3: Создать API функцию
|
||
|
||
```typescript
|
||
// apps/frontend/src/entities/broker-analytics/api/brokerAnalyticsApi.ts
|
||
import type { ApiResponseMeta, BrokerAnalytics } from '@/shared/api'
|
||
import { request } from '@/shared/api/kyClient'
|
||
|
||
export function getBrokerAnalytics(
|
||
accountId: string,
|
||
): Promise<{ data: BrokerAnalytics; meta: ApiResponseMeta }> {
|
||
return request<BrokerAnalytics>(
|
||
`/api/v1/broker/accounts/${encodeURIComponent(accountId)}/analytics`,
|
||
)
|
||
}
|
||
```
|
||
|
||
#### Шаг 3.4: Создать хук
|
||
|
||
```typescript
|
||
// apps/frontend/src/entities/broker-analytics/model/useBrokerAnalytics.ts
|
||
import { useQuery } from '@tanstack/react-query'
|
||
import type { BrokerAnalytics } from '@/shared/api'
|
||
import { getBrokerAnalytics } from '../api/brokerAnalyticsApi'
|
||
|
||
export function useBrokerAnalytics(accountId: string | undefined) {
|
||
return useQuery<BrokerAnalytics>({
|
||
queryKey: ['broker', 'analytics', accountId],
|
||
enabled: Boolean(accountId),
|
||
queryFn: async () => (await getBrokerAnalytics(accountId!)).data,
|
||
staleTime: 300_000,
|
||
retry: 2,
|
||
refetchOnWindowFocus: false,
|
||
})
|
||
}
|
||
```
|
||
|
||
#### Шаг 3.5: Создать barrel export
|
||
|
||
```typescript
|
||
// apps/frontend/src/entities/broker-analytics/index.ts
|
||
export { getBrokerAnalytics } from './api/brokerAnalyticsApi'
|
||
export { useBrokerAnalytics } from './model/useBrokerAnalytics'
|
||
```
|
||
|
||
#### Шаг 3.6: Проверить сборку
|
||
|
||
```bash
|
||
npm run build -w apps/frontend
|
||
```
|
||
|
||
#### Шаг 3.7: Закоммитить
|
||
|
||
```bash
|
||
git add apps/frontend/src/entities/broker-analytics/
|
||
git add apps/frontend/src/shared/api/
|
||
git commit -m "feat(frontend): add broker analytics data layer"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: Frontend — страница аналитики
|
||
|
||
**Files:**
|
||
- Create: `apps/frontend/src/pages/broker-analytics/ui/BrokerAnalyticsPage.tsx`
|
||
- Create: `apps/frontend/src/pages/broker-analytics/index.ts`
|
||
|
||
#### Шаг 4.1: Создать страницу
|
||
|
||
```tsx
|
||
// apps/frontend/src/pages/broker-analytics/ui/BrokerAnalyticsPage.tsx
|
||
import { useBrokerAccountContext } from '@/widgets/broker-account-layout';
|
||
import { useBrokerAnalytics } from '@/entities/broker-analytics';
|
||
|
||
function formatAmount(value: number, currency: string): string {
|
||
return `${value.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ${currency}`;
|
||
}
|
||
|
||
export function BrokerAnalyticsPage() {
|
||
const { accountId } = useBrokerAccountContext();
|
||
const { data, isLoading, isError } = useBrokerAnalytics(accountId);
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<section>
|
||
<div style={{ padding: '24px', color: 'var(--color-text-secondary)' }}>Загрузка...</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
if (isError) {
|
||
return (
|
||
<section>
|
||
<div style={{ padding: '24px', color: 'var(--color-text-negative)' }}>
|
||
Не удалось загрузить аналитику
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
if (!data || (data.totalDeposits === 0 && data.totalReceived === 0)) {
|
||
return (
|
||
<section>
|
||
<div style={{ padding: '24px', color: 'var(--color-text-secondary)' }}>
|
||
Нет данных для аналитики
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<section>
|
||
<div style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: '1fr 1fr',
|
||
gap: '16px',
|
||
marginBottom: '24px',
|
||
}}>
|
||
{/* Вложено */}
|
||
<div style={{
|
||
background: 'var(--color-surface-card)',
|
||
borderRadius: '12px',
|
||
padding: '20px',
|
||
}}>
|
||
<h3 style={{ margin: '0 0 16px', fontSize: '16px', fontWeight: 600 }}>Вложено</h3>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||
<Row label="Пополнения" value={formatAmount(data.totalDeposits, data.currency)} />
|
||
<Row label="Выводы" value={`−${formatAmount(data.totalWithdrawn, data.currency)}`} negative />
|
||
<Divider />
|
||
<Row label="Нетто" value={formatAmount(data.netInvested, data.currency)} bold />
|
||
</div>
|
||
</div>
|
||
|
||
{/* Получено */}
|
||
<div style={{
|
||
background: 'var(--color-surface-card)',
|
||
borderRadius: '12px',
|
||
padding: '20px',
|
||
}}>
|
||
<h3 style={{ margin: '0 0 16px', fontSize: '16px', fontWeight: 600 }}>Получено</h3>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||
<Row label="Дивиденды" value={formatAmount(data.totalDividends, data.currency)} positive />
|
||
<Row label="Купоны" value={formatAmount(data.totalCoupons, data.currency)} positive />
|
||
<Divider />
|
||
<Row label="Итого" value={formatAmount(data.totalReceived, data.currency)} bold positive />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Сводка */}
|
||
<div style={{
|
||
background: 'var(--color-surface-card)',
|
||
borderRadius: '12px',
|
||
padding: '20px',
|
||
}}>
|
||
<h3 style={{ margin: '0 0 16px', fontSize: '16px', fontWeight: 600 }}>Сводка</h3>
|
||
<div style={{ display: 'flex', gap: '48px' }}>
|
||
<SummaryItem label="Вложено нетто" value={formatAmount(data.netInvested, data.currency)} />
|
||
<SummaryItem label="Получено" value={formatAmount(data.totalReceived, data.currency)} />
|
||
{data.totalReturnPercent !== null && (
|
||
<SummaryItem
|
||
label="Доходность"
|
||
value={`${data.totalReturnPercent.toFixed(2)}%`}
|
||
positive
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function Row({ label, value, bold, positive, negative }: {
|
||
label: string;
|
||
value: string;
|
||
bold?: boolean;
|
||
positive?: boolean;
|
||
negative?: boolean;
|
||
}) {
|
||
const color = positive
|
||
? 'var(--color-text-positive, #22c55e)'
|
||
: negative
|
||
? 'var(--color-text-negative, #ef4444)'
|
||
: 'var(--color-text-primary)';
|
||
return (
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
<span style={{ color: 'var(--color-text-secondary)', fontSize: '14px' }}>{label}</span>
|
||
<span style={{ fontWeight: bold ? 600 : 400, color, fontSize: '15px' }}>{value}</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function Divider() {
|
||
return (
|
||
<div style={{
|
||
height: '1px',
|
||
background: 'var(--color-border)',
|
||
margin: '4px 0',
|
||
}} />
|
||
);
|
||
}
|
||
|
||
function SummaryItem({ label, value, positive }: {
|
||
label: string;
|
||
value: string;
|
||
positive?: boolean;
|
||
}) {
|
||
return (
|
||
<div>
|
||
<div style={{ color: 'var(--color-text-secondary)', fontSize: '13px', marginBottom: '4px' }}>
|
||
{label}
|
||
</div>
|
||
<div style={{
|
||
fontSize: '20px',
|
||
fontWeight: 600,
|
||
color: positive ? 'var(--color-text-positive, #22c55e)' : 'var(--color-text-primary)',
|
||
}}>
|
||
{value}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
#### Шаг 4.2: Создать barrel
|
||
|
||
```typescript
|
||
// apps/frontend/src/pages/broker-analytics/index.ts
|
||
export { BrokerAnalyticsPage } from './ui/BrokerAnalyticsPage'
|
||
```
|
||
|
||
#### Шаг 4.3: Проверить сборку
|
||
|
||
```bash
|
||
npm run build -w apps/frontend
|
||
```
|
||
|
||
#### Шаг 4.4: Закоммитить
|
||
|
||
```bash
|
||
git add apps/frontend/src/pages/broker-analytics/
|
||
git commit -m "feat(frontend): add broker analytics page"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: Frontend — роут и таб навигации
|
||
|
||
**Files:**
|
||
- Modify: `apps/frontend/src/app/routing/routeTree.tsx`
|
||
- Modify: `apps/frontend/src/widgets/broker-account-layout/ui/BrokerAccountLayout.tsx`
|
||
|
||
#### Шаг 5.1: Добавить роут
|
||
|
||
В `apps/frontend/src/app/routing/routeTree.tsx`:
|
||
|
||
```typescript
|
||
import { BrokerAnalyticsPage } from '@/pages/broker-analytics'
|
||
|
||
const brokerAnalyticsRoute = createRoute({
|
||
getParentRoute: () => brokerAccountRoot,
|
||
path: '/analytics',
|
||
component: BrokerAnalyticsPage,
|
||
})
|
||
|
||
// Добавить в brokerAccountRoot.addChildren([...])
|
||
brokerAccountRoot.addChildren([
|
||
brokerAccountIndexRoute,
|
||
brokerSharesRoute,
|
||
brokerBondsRoute,
|
||
brokerOperationsRoute,
|
||
brokerEventsRoute,
|
||
brokerAnalyticsRoute, // <-- добавить
|
||
])
|
||
```
|
||
|
||
#### Шаг 5.2: Добавить таб в навигацию
|
||
|
||
В `apps/frontend/src/widgets/broker-account-layout/ui/BrokerAccountLayout.tsx`:
|
||
|
||
```typescript
|
||
const links = [
|
||
{ to: '', label: 'Обзор' },
|
||
{ to: '/shares', label: 'Акции' },
|
||
{ to: '/bonds', label: 'Облигации' },
|
||
{ to: '/operations', label: 'Операции' },
|
||
{ to: '/events', label: 'События' },
|
||
{ to: '/analytics', label: 'Аналитика' },
|
||
]
|
||
```
|
||
|
||
#### Шаг 5.3: Проверить сборку
|
||
|
||
```bash
|
||
npm run build -w apps/frontend
|
||
```
|
||
|
||
#### Шаг 5.4: Закоммитить
|
||
|
||
```bash
|
||
git add apps/frontend/src/app/routing/routeTree.tsx
|
||
git add apps/frontend/src/widgets/broker-account-layout/ui/BrokerAccountLayout.tsx
|
||
git commit -m "feat(frontend): add analytics route and tab"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 6: Проверка линта и тестов
|
||
|
||
#### Шаг 6.1: Запустить линт и сборку
|
||
|
||
```bash
|
||
npm run lint -w apps/backend && npm run build -w apps/backend
|
||
npm run lint -w apps/frontend && npm run build -w apps/frontend
|
||
```
|
||
|
||
#### Шаг 6.2: Запустить тесты
|
||
|
||
```bash
|
||
npm test -w apps/backend -- --run
|
||
npm test -w apps/frontend -- --run
|
||
```
|
||
|
||
#### Шаг 6.3: Если всё ок — закоммитить финальные правки и запушить
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "chore: fix lint and tests"
|
||
```
|