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