f
This commit is contained in:
parent
33602d0fe9
commit
a0febf85f3
@ -14,6 +14,7 @@ import {
|
||||
import { AnalyticsService } from './analytics.service';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { CurrentUser, JwtPayload } from '../../common/decorators/user.decorator';
|
||||
import { TransactionType } from '../../common/constants/categories';
|
||||
|
||||
@ApiTags('Аналитика')
|
||||
@ApiBearerAuth()
|
||||
@ -49,19 +50,19 @@ export class AnalyticsController {
|
||||
@ApiOperation({ summary: 'Разбивка по категориям' })
|
||||
@ApiQuery({ name: 'startDate', required: true, example: '2024-01-01' })
|
||||
@ApiQuery({ name: 'endDate', required: true, example: '2024-01-31' })
|
||||
@ApiQuery({ name: 'type', enum: ['INCOME', 'EXPENSE'], required: false })
|
||||
@ApiQuery({ name: 'type', enum: TransactionType, required: false })
|
||||
@ApiResponse({ status: 200, description: 'Разбивка расходов по категориям' })
|
||||
async getCategoryBreakdown(
|
||||
@CurrentUser() user: JwtPayload,
|
||||
@Query('startDate') startDate: string,
|
||||
@Query('endDate') endDate: string,
|
||||
@Query('type') type?: 'INCOME' | 'EXPENSE',
|
||||
@Query('type') type?: TransactionType,
|
||||
) {
|
||||
return this.analyticsService.getCategoryBreakdown(
|
||||
user.sub,
|
||||
new Date(startDate),
|
||||
new Date(endDate),
|
||||
type || 'EXPENSE',
|
||||
type || TransactionType.EXPENSE,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import { Budget } from '../budgets/entities/budget.entity';
|
||||
import { Goal } from '../goals/entities/goal.entity';
|
||||
import { startOfMonth, endOfMonth, subMonths, format, startOfYear, endOfYear } from 'date-fns';
|
||||
import { formatRubles } from '../../common/utils/currency.utils';
|
||||
import { TransactionType } from '../../common/constants/categories';
|
||||
|
||||
export interface MonthlyOverview {
|
||||
month: string;
|
||||
@ -80,11 +81,11 @@ export class AnalyticsService {
|
||||
});
|
||||
|
||||
const totalIncome = transactions
|
||||
.filter(t => t.type === 'INCOME')
|
||||
.filter(t => t.type === TransactionType.INCOME)
|
||||
.reduce((sum, t) => sum + Number(t.amount), 0);
|
||||
|
||||
const totalExpenses = transactions
|
||||
.filter(t => t.type === 'EXPENSE')
|
||||
.filter(t => t.type === TransactionType.EXPENSE)
|
||||
.reduce((sum, t) => sum + Number(t.amount), 0);
|
||||
|
||||
const netSavings = totalIncome - totalExpenses;
|
||||
@ -93,7 +94,7 @@ export class AnalyticsService {
|
||||
// Calculate top spending categories
|
||||
const categorySpending: Record<string, { name: string; amount: number }> = {};
|
||||
transactions
|
||||
.filter(t => t.type === 'EXPENSE')
|
||||
.filter(t => t.type === TransactionType.EXPENSE)
|
||||
.forEach(t => {
|
||||
const catId = t.categoryId || 'other';
|
||||
const catName = t.category?.nameRu || 'Другое';
|
||||
@ -138,7 +139,7 @@ export class AnalyticsService {
|
||||
const transactions = await this.transactionRepository.find({
|
||||
where: {
|
||||
userId,
|
||||
type: 'EXPENSE',
|
||||
type: TransactionType.EXPENSE,
|
||||
transactionDate: Between(monthStart, monthEnd),
|
||||
},
|
||||
});
|
||||
@ -167,7 +168,7 @@ export class AnalyticsService {
|
||||
userId: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
type: 'INCOME' | 'EXPENSE' = 'EXPENSE',
|
||||
type: TransactionType = TransactionType.EXPENSE,
|
||||
): Promise<CategoryBreakdown[]> {
|
||||
const transactions = await this.transactionRepository.find({
|
||||
where: {
|
||||
@ -246,11 +247,11 @@ export class AnalyticsService {
|
||||
});
|
||||
|
||||
const income = transactions
|
||||
.filter(t => t.type === 'INCOME')
|
||||
.filter(t => t.type === TransactionType.INCOME)
|
||||
.reduce((sum, t) => sum + Number(t.amount), 0);
|
||||
|
||||
const expenses = transactions
|
||||
.filter(t => t.type === 'EXPENSE')
|
||||
.filter(t => t.type === TransactionType.EXPENSE)
|
||||
.reduce((sum, t) => sum + Number(t.amount), 0);
|
||||
|
||||
result.push({
|
||||
@ -344,7 +345,7 @@ export class AnalyticsService {
|
||||
|
||||
const monthTransactions = transactions.filter(t => {
|
||||
const tDate = new Date(t.transactionDate);
|
||||
return t.type === 'EXPENSE' && tDate >= monthStart && tDate <= monthEnd;
|
||||
return t.type === TransactionType.EXPENSE && tDate >= monthStart && tDate <= monthEnd;
|
||||
});
|
||||
|
||||
monthlyExpenses.push(monthTransactions.reduce((sum, t) => sum + Number(t.amount), 0));
|
||||
@ -404,11 +405,11 @@ export class AnalyticsService {
|
||||
});
|
||||
|
||||
const totalIncome = transactions
|
||||
.filter(t => t.type === 'INCOME')
|
||||
.filter(t => t.type === TransactionType.INCOME)
|
||||
.reduce((sum, t) => sum + Number(t.amount), 0);
|
||||
|
||||
const totalExpenses = transactions
|
||||
.filter(t => t.type === 'EXPENSE')
|
||||
.filter(t => t.type === TransactionType.EXPENSE)
|
||||
.reduce((sum, t) => sum + Number(t.amount), 0);
|
||||
|
||||
// Calculate monthly data
|
||||
@ -418,7 +419,7 @@ export class AnalyticsService {
|
||||
if (!monthlyData[month]) {
|
||||
monthlyData[month] = { income: 0, expenses: 0 };
|
||||
}
|
||||
if (t.type === 'INCOME') {
|
||||
if (t.type === TransactionType.INCOME) {
|
||||
monthlyData[month].income += Number(t.amount);
|
||||
} else {
|
||||
monthlyData[month].expenses += Number(t.amount);
|
||||
@ -445,7 +446,7 @@ export class AnalyticsService {
|
||||
// Top expense categories
|
||||
const categoryExpenses: Record<string, { name: string; amount: number }> = {};
|
||||
transactions
|
||||
.filter(t => t.type === 'EXPENSE')
|
||||
.filter(t => t.type === TransactionType.EXPENSE)
|
||||
.forEach(t => {
|
||||
const catName = t.category?.nameRu || 'Другое';
|
||||
if (!categoryExpenses[catName]) {
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
import { User } from '../../auth/entities/user.entity';
|
||||
import { BudgetGroupType, TransactionType } from '../../../common/constants/categories';
|
||||
|
||||
@Entity('categories')
|
||||
export class Category {
|
||||
@ -21,11 +22,11 @@ export class Category {
|
||||
nameEn: string;
|
||||
|
||||
@Index()
|
||||
@Column({ length: 10 })
|
||||
type: 'INCOME' | 'EXPENSE';
|
||||
@Column({ type: 'enum', enum: TransactionType })
|
||||
type: TransactionType;
|
||||
|
||||
@Column({ name: 'group_type', length: 20, nullable: true })
|
||||
groupType: 'ESSENTIAL' | 'PERSONAL' | 'SAVINGS' | null;
|
||||
@Column({ name: 'group_type', type: 'enum', enum: BudgetGroupType, nullable: true })
|
||||
groupType: BudgetGroupType | null;
|
||||
|
||||
@Column({ length: 50, nullable: true })
|
||||
icon: string;
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
MaxLength,
|
||||
Matches,
|
||||
} from 'class-validator';
|
||||
import { GoalPriority } from '../entities/goal.entity';
|
||||
import { GoalAutoSaveFrequency, GoalPriority } from '../entities/goal.entity';
|
||||
|
||||
export class CreateGoalDto {
|
||||
@ApiProperty({
|
||||
@ -101,10 +101,10 @@ export class CreateGoalDto {
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Частота автоматического накопления',
|
||||
enum: ['DAILY', 'WEEKLY', 'MONTHLY'],
|
||||
enum: GoalAutoSaveFrequency,
|
||||
example: 'MONTHLY',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEnum(['DAILY', 'WEEKLY', 'MONTHLY'])
|
||||
autoSaveFrequency?: 'DAILY' | 'WEEKLY' | 'MONTHLY';
|
||||
@IsEnum(GoalAutoSaveFrequency)
|
||||
autoSaveFrequency?: GoalAutoSaveFrequency;
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
MaxLength,
|
||||
Matches,
|
||||
} from 'class-validator';
|
||||
import { GoalPriority, GoalStatus } from '../entities/goal.entity';
|
||||
import { GoalAutoSaveFrequency, GoalPriority, GoalStatus } from '../entities/goal.entity';
|
||||
|
||||
export class UpdateGoalDto {
|
||||
@ApiPropertyOptional({
|
||||
@ -103,12 +103,12 @@ export class UpdateGoalDto {
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Частота автоматического накопления',
|
||||
enum: ['DAILY', 'WEEKLY', 'MONTHLY'],
|
||||
enum: GoalAutoSaveFrequency,
|
||||
example: 'MONTHLY',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEnum(['DAILY', 'WEEKLY', 'MONTHLY'])
|
||||
autoSaveFrequency?: 'DAILY' | 'WEEKLY' | 'MONTHLY';
|
||||
@IsEnum(GoalAutoSaveFrequency)
|
||||
autoSaveFrequency?: GoalAutoSaveFrequency;
|
||||
}
|
||||
|
||||
export class AddFundsDto {
|
||||
|
||||
@ -23,6 +23,12 @@ export enum GoalPriority {
|
||||
HIGH = 'HIGH',
|
||||
}
|
||||
|
||||
export enum GoalAutoSaveFrequency {
|
||||
DAILY = 'DAILY',
|
||||
WEEKLY = 'WEEKLY',
|
||||
MONTHLY = 'MONTHLY',
|
||||
}
|
||||
|
||||
@Entity('goals')
|
||||
export class Goal {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
@ -69,8 +75,8 @@ export class Goal {
|
||||
@Column({ name: 'auto_save_amount', type: 'decimal', precision: 15, scale: 2, nullable: true })
|
||||
autoSaveAmount: number;
|
||||
|
||||
@Column({ name: 'auto_save_frequency', length: 20, nullable: true })
|
||||
autoSaveFrequency: 'DAILY' | 'WEEKLY' | 'MONTHLY' | null;
|
||||
@Column({ name: 'auto_save_frequency', type: 'enum', enum: GoalAutoSaveFrequency, nullable: true })
|
||||
autoSaveFrequency: GoalAutoSaveFrequency | null;
|
||||
|
||||
// Timestamps
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
|
||||
@ -49,9 +49,19 @@ export class GoalsService {
|
||||
*/
|
||||
async create(userId: string, dto: CreateGoalDto): Promise<Goal> {
|
||||
const goal = this.goalRepository.create({
|
||||
...dto,
|
||||
userId,
|
||||
titleRu: dto.titleRu,
|
||||
descriptionRu: dto.descriptionRu,
|
||||
targetAmount: dto.targetAmount,
|
||||
currentAmount: dto.currentAmount ?? 0,
|
||||
currency: 'RUB',
|
||||
targetDate: dto.targetDate ? new Date(dto.targetDate) : undefined,
|
||||
priority: dto.priority,
|
||||
icon: dto.icon,
|
||||
color: dto.color,
|
||||
autoSaveEnabled: dto.autoSaveEnabled ?? false,
|
||||
autoSaveAmount: dto.autoSaveAmount,
|
||||
autoSaveFrequency: dto.autoSaveFrequency ?? undefined,
|
||||
});
|
||||
|
||||
return this.goalRepository.save(goal);
|
||||
@ -63,14 +73,17 @@ export class GoalsService {
|
||||
async update(id: string, userId: string, dto: UpdateGoalDto): Promise<Goal> {
|
||||
const goal = await this.findOne(id, userId);
|
||||
|
||||
if (dto.targetDate) {
|
||||
goal.targetDate = new Date(dto.targetDate);
|
||||
}
|
||||
|
||||
Object.assign(goal, {
|
||||
...dto,
|
||||
targetDate: dto.targetDate ? new Date(dto.targetDate) : goal.targetDate,
|
||||
});
|
||||
if (dto.titleRu !== undefined) goal.titleRu = dto.titleRu;
|
||||
if (dto.descriptionRu !== undefined) goal.descriptionRu = dto.descriptionRu;
|
||||
if (dto.targetAmount !== undefined) goal.targetAmount = dto.targetAmount;
|
||||
if (dto.targetDate !== undefined) goal.targetDate = dto.targetDate ? new Date(dto.targetDate) : null as any;
|
||||
if (dto.status !== undefined) goal.status = dto.status;
|
||||
if (dto.priority !== undefined) goal.priority = dto.priority;
|
||||
if (dto.icon !== undefined) goal.icon = dto.icon;
|
||||
if (dto.color !== undefined) goal.color = dto.color;
|
||||
if (dto.autoSaveEnabled !== undefined) goal.autoSaveEnabled = dto.autoSaveEnabled;
|
||||
if (dto.autoSaveAmount !== undefined) goal.autoSaveAmount = dto.autoSaveAmount;
|
||||
if (dto.autoSaveFrequency !== undefined) goal.autoSaveFrequency = dto.autoSaveFrequency;
|
||||
|
||||
// Check if goal is completed
|
||||
if (goal.isCompleted && goal.status === GoalStatus.ACTIVE) {
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
} from 'typeorm';
|
||||
import { User } from '../../auth/entities/user.entity';
|
||||
import { Category } from '../../categories/entities/category.entity';
|
||||
import { PaymentMethod, TransactionType } from '../../../common/constants/categories';
|
||||
|
||||
@Entity('transactions')
|
||||
@Check('"amount" > 0')
|
||||
@ -30,8 +31,8 @@ export class Transaction {
|
||||
currency: string;
|
||||
|
||||
@Index()
|
||||
@Column({ length: 10 })
|
||||
type: 'INCOME' | 'EXPENSE';
|
||||
@Column({ type: 'enum', enum: TransactionType })
|
||||
type: TransactionType;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'category_id', type: 'uuid', nullable: true })
|
||||
@ -44,8 +45,8 @@ export class Transaction {
|
||||
@Column({ name: 'transaction_date', type: 'date' })
|
||||
transactionDate: Date;
|
||||
|
||||
@Column({ name: 'payment_method', length: 20, nullable: true })
|
||||
paymentMethod: 'CASH' | 'CARD' | 'BANK_TRANSFER' | null;
|
||||
@Column({ name: 'payment_method', type: 'enum', enum: PaymentMethod, nullable: true })
|
||||
paymentMethod: PaymentMethod | null;
|
||||
|
||||
// Receipt tracking
|
||||
@Column({ name: 'receipt_url', length: 500, nullable: true })
|
||||
|
||||
Loading…
Reference in New Issue
Block a user