Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions backend/src/progress/controllers/progress.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {
Controller,
Get,
Param,
Query,
UseGuards,
BadRequestException,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { AuthGuard } from '@nestjs/passport';
import { paginationQueryDto } from '../../common/pagination/paginationQueryDto';
import { GetProgressHistoryProvider } from '../providers/get-progress-history.provider';
import { GetCategoryStatsProvider } from '../providers/get-category-stats.provider';
import { GetOverallStatsProvider } from '../providers/get-overall-stats.provider';
import { PaginatedProgressDto } from '../dtos/paginated-progress.dto';
import { CategoryStatsDto } from '../dtos/category-stats.dto';
import { OverallStatsDto } from '../dtos/overall-stats.dto';
import { ActiveUser } from '../../auth/decorators/activeUser.decorator';
import { ActiveUserData } from '../../auth/interfaces/activeInterface';

@Controller('progress')
@ApiTags('Progress')
@UseGuards(AuthGuard('jwt'))
@ApiBearerAuth()
export class ProgressController {
constructor(
private readonly getProgressHistoryProvider: GetProgressHistoryProvider,
private readonly getCategoryStatsProvider: GetCategoryStatsProvider,
private readonly getOverallStatsProvider: GetOverallStatsProvider,
) {}

@Get()
@ApiOperation({
summary: 'Get paginated progress history',
description:
'Retrieve user answer history ordered by most recent attempts first',
})
@ApiResponse({
status: 200,
description: 'Paginated progress history',
type: PaginatedProgressDto,
})
async getProgressHistory(
@ActiveUser() user: ActiveUserData,
@Query() paginationDto: paginationQueryDto,
) {
if (!user || !user.sub) {
throw new BadRequestException('User not found');
}

const { limit = 10, page = 1 } = paginationDto;
return this.getProgressHistoryProvider.getProgressHistory(
user.sub,
page,
limit,
);
}

@Get('stats')
@ApiOperation({
summary: 'Get overall user statistics',
description:
'Retrieve total attempts, correct answers, accuracy, points earned, and time spent',
})
@ApiResponse({
status: 200,
description: 'Overall user statistics',
type: OverallStatsDto,
})
async getOverallStats(@ActiveUser() user: ActiveUserData) {
if (!user || !user.sub) {
throw new BadRequestException('User not found');
}

return this.getOverallStatsProvider.getOverallStats(user.sub);
}

@Get('category/:id')
@ApiOperation({
summary: 'Get category-specific statistics',
description:
'Retrieve total attempts, correct answers, and accuracy for a specific category',
})
@ApiResponse({
status: 200,
description: 'Category statistics',
type: CategoryStatsDto,
})
async getCategoryStats(
@ActiveUser() user: ActiveUserData,
@Param('id') categoryId: string,
) {
if (!user || !user.sub) {
throw new BadRequestException('User not found');
}

if (!categoryId) {
throw new BadRequestException('Category ID is required');
}

return this.getCategoryStatsProvider.getCategoryStats(user.sub, categoryId);
}
}
33 changes: 33 additions & 0 deletions backend/src/progress/dtos/category-stats.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ApiProperty } from '@nestjs/swagger';

export class CategoryStatsDto {
@ApiProperty({
example: '123e4567-e89b-12d3-a456-426614174000',
description: 'Category ID',
})
categoryId: string;

@ApiProperty({
example: 'Algorithms',
description: 'Category name',
})
categoryName: string;

@ApiProperty({
example: 25,
description: 'Total number of attempts in this category',
})
totalAttempts: number;

@ApiProperty({
example: 20,
description: 'Number of correct answers',
})
correctAnswers: number;

@ApiProperty({
example: 80,
description: 'Accuracy percentage',
})
accuracy: number;
}
33 changes: 33 additions & 0 deletions backend/src/progress/dtos/overall-stats.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ApiProperty } from '@nestjs/swagger';

export class OverallStatsDto {
@ApiProperty({
example: 150,
description: 'Total number of attempts across all categories',
})
totalAttempts: number;

@ApiProperty({
example: 120,
description: 'Total number of correct answers',
})
totalCorrect: number;

@ApiProperty({
example: 80,
description: 'Overall accuracy percentage',
})
accuracy: number;

@ApiProperty({
example: 1500,
description: 'Total points earned across all puzzles',
})
totalPointsEarned: number;

@ApiProperty({
example: 3600,
description: 'Total time spent on all puzzles in seconds',
})
totalTimeSpent: number;
}
44 changes: 44 additions & 0 deletions backend/src/progress/dtos/paginated-progress.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ApiProperty } from '@nestjs/swagger';
import { ProgressHistoryDto } from './progress-history.dto';

export class PaginatedProgressDto {
@ApiProperty({
type: [ProgressHistoryDto],
description: 'Array of progress history records',
})
data: ProgressHistoryDto[];

@ApiProperty({
example: {
itemsPerPage: 10,
totalItems: 150,
currentPage: 1,
totalPages: 15,
},
description: 'Pagination metadata',
})
meta: {
itemsPerPage: number;
totalItems: number;
currentPage: number;
totalPages: number;
};

@ApiProperty({
example: {
first: 'http://localhost:3000/progress?limit=10&page=1',
last: 'http://localhost:3000/progress?limit=10&page=15',
current: 'http://localhost:3000/progress?limit=10&page=1',
previous: 'http://localhost:3000/progress?limit=10&page=1',
next: 'http://localhost:3000/progress?limit=10&page=2',
},
description: 'Navigation links for pagination',
})
links: {
first: string;
last: string;
current: string;
previous: string;
next: string;
};
}
57 changes: 57 additions & 0 deletions backend/src/progress/dtos/progress-history.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ApiProperty } from '@nestjs/swagger';

export class ProgressHistoryDto {
@ApiProperty({
example: '123e4567-e89b-12d3-a456-426614174000',
description: 'Progress record ID',
})
id: string;

@ApiProperty({
example: '123e4567-e89b-12d3-a456-426614174001',
description: 'Puzzle ID',
})
puzzleId: string;

@ApiProperty({
example: 'What is the time complexity of binary search?',
description: 'The puzzle question',
})
question: string;

@ApiProperty({
example: 'O(log n)',
description: 'User answer',
})
userAnswer: string;

@ApiProperty({
example: true,
description: 'Whether the answer was correct',
})
isCorrect: boolean;

@ApiProperty({
example: 10,
description: 'Points earned for this attempt',
})
pointsEarned: number;

@ApiProperty({
example: 45,
description: 'Time spent on this puzzle in seconds',
})
timeSpent: number;

@ApiProperty({
example: '2024-01-28T10:30:00Z',
description: 'When the puzzle was attempted',
})
attemptedAt: Date;

@ApiProperty({
example: '123e4567-e89b-12d3-a456-426614174002',
description: 'Category ID',
})
categoryId: string;
}
2 changes: 1 addition & 1 deletion backend/src/progress/progress.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ import { ProgressCalculationProvider } from './providers/progress-calculation.pr
providers: [ProgressService, ProgressCalculationProvider],
exports: [ProgressService, ProgressCalculationProvider, TypeOrmModule],
})
export class ProgressModule {}
export class ProgressModule {}
57 changes: 57 additions & 0 deletions backend/src/progress/providers/get-category-stats.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserProgress } from '../entities/progress.entity';
import { CategoryStatsDto } from '../dtos/category-stats.dto';

@Injectable()
export class GetCategoryStatsProvider {
constructor(
@InjectRepository(UserProgress)
private readonly progressRepo: Repository<UserProgress>,
) {}

async getCategoryStats(userId: string, categoryId: string) {
const result = await this.progressRepo
.createQueryBuilder('progress')
.select('progress.categoryId', 'categoryId')
.addSelect('COUNT(*)', 'totalAttempts')
.addSelect('SUM(CASE WHEN progress.isCorrect = true THEN 1 ELSE 0 END)', 'correctAnswers')
.where('progress.userId = :userId', { userId })
.andWhere('progress.categoryId = :categoryId', { categoryId })
.groupBy('progress.categoryId')
.getRawOne();

if (!result) {
return {
categoryId,
categoryName: '',
totalAttempts: 0,
correctAnswers: 0,
accuracy: 0,
};
}

const totalAttempts = parseInt(result.totalAttempts, 10) || 0;
const correctAnswers = parseInt(result.correctAnswers, 10) || 0;
const accuracy =
totalAttempts > 0 ? Math.round((correctAnswers / totalAttempts) * 100) : 0;

// Get category name
const category = await this.progressRepo
.createQueryBuilder('progress')
.leftJoinAndSelect('progress.category', 'category')
.where('progress.categoryId = :categoryId', { categoryId })
.select('category.name')
.limit(1)
.getRawOne();

return {
categoryId,
categoryName: category?.category_name || '',
totalAttempts,
correctAnswers,
accuracy,
};
}
}
49 changes: 49 additions & 0 deletions backend/src/progress/providers/get-overall-stats.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserProgress } from '../entities/progress.entity';
import { OverallStatsDto } from '../dtos/overall-stats.dto';

@Injectable()
export class GetOverallStatsProvider {
constructor(
@InjectRepository(UserProgress)
private readonly progressRepo: Repository<UserProgress>,
) {}

async getOverallStats(userId: string): Promise<OverallStatsDto> {
const result = await this.progressRepo
.createQueryBuilder('progress')
.select('COUNT(*)', 'totalAttempts')
.addSelect('SUM(CASE WHEN progress.isCorrect = true THEN 1 ELSE 0 END)', 'totalCorrect')
.addSelect('SUM(progress.pointsEarned)', 'totalPointsEarned')
.addSelect('SUM(progress.timeSpent)', 'totalTimeSpent')
.where('progress.userId = :userId', { userId })
.getRawOne();

if (!result) {
return {
totalAttempts: 0,
totalCorrect: 0,
accuracy: 0,
totalPointsEarned: 0,
totalTimeSpent: 0,
};
}

const totalAttempts = parseInt(result.totalAttempts, 10) || 0;
const totalCorrect = parseInt(result.totalCorrect, 10) || 0;
const accuracy =
totalAttempts > 0
? Math.round((totalCorrect / totalAttempts) * 100)
: 0;

return {
totalAttempts,
totalCorrect,
accuracy,
totalPointsEarned: parseInt(result.totalPointsEarned, 10) || 0,
totalTimeSpent: parseInt(result.totalTimeSpent, 10) || 0,
};
}
}
Loading