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
44 changes: 19 additions & 25 deletions backend/src/progress/providers/get-category-stats.provider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { FindOptionsWhere, Repository } from 'typeorm';
import { UserProgress } from '../entities/progress.entity';

interface CategoryStatsRaw {
Expand All @@ -21,20 +21,17 @@ export class GetCategoryStatsProvider {
) {}

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<CategoryStatsRaw>();
const where: FindOptionsWhere<UserProgress> = {
userId,
categoryId,
};

if (!result) {
const progressRecords = await this.progressRepo.find({
where,
relations: ['category'],
});

if (progressRecords.length === 0) {
return {
categoryId,
categoryName: '',
Expand All @@ -44,25 +41,22 @@ export class GetCategoryStatsProvider {
};
}

const totalAttempts = parseInt(result.totalAttempts, 10) || 0;
const correctAnswers = parseInt(result.correctAnswers, 10) || 0;
const totalAttempts = progressRecords.length;
const correctAnswers = progressRecords.reduce(
(sum, record) => sum + (record.isCorrect ? 1 : 0),
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<CategoryNameRaw>();
const categoryName = progressRecords[0]?.category?.name || '';

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

Expand All @@ -19,19 +19,13 @@ export class GetOverallStatsProvider {
) {}

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<OverallStatsRaw>();

if (!result) {
const where: FindOptionsWhere<UserProgress> = {
userId,
};

const progressRecords = await this.progressRepo.find({ where });

if (progressRecords.length === 0) {
return {
totalAttempts: 0,
totalCorrect: 0,
Expand All @@ -41,17 +35,29 @@ export class GetOverallStatsProvider {
};
}

const totalAttempts = parseInt(result.totalAttempts, 10) || 0;
const totalCorrect = parseInt(result.totalCorrect, 10) || 0;
const totalAttempts = progressRecords.length;
const totalCorrect = progressRecords.reduce(
(sum, record) => sum + (record.isCorrect ? 1 : 0),
0,
);
const totalPointsEarned = progressRecords.reduce(
(sum, record) => sum + record.pointsEarned,
0,
);
const totalTimeSpent = progressRecords.reduce(
(sum, record) => sum + record.timeSpent,
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,
totalPointsEarned,
totalTimeSpent,
};
}
}
28 changes: 20 additions & 8 deletions backend/src/progress/providers/get-progress-history.provider.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { FindOptionsWhere, Repository } from 'typeorm';
import { UserProgress } from '../entities/progress.entity';
import { paginate } from '../../common/pagination/paginate';

@Injectable()
export class GetProgressHistoryProvider {
Expand All @@ -16,12 +15,25 @@ export class GetProgressHistoryProvider {
page: number = 1,
limit: number = 10,
) {
const qb = this.progressRepo
.createQueryBuilder('progress')
.leftJoinAndSelect('progress.puzzle', 'puzzle')
.where('progress.userId = :userId', { userId })
.orderBy('progress.attemptedAt', 'DESC');
const where: FindOptionsWhere<UserProgress> = {
userId,
};

return paginate(qb, page, limit);
const [data, total] = await this.progressRepo.findAndCount({
where,
relations: ['puzzle'],
order: { attemptedAt: 'DESC' },
skip: (page - 1) * limit,
take: limit,
});

return {
data,
meta: {
page,
limit,
total,
},
};
}
}
58 changes: 33 additions & 25 deletions backend/src/progress/providers/progress-calculation.provider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { MoreThan, Repository } from 'typeorm';
import { FindOptionsWhere, MoreThan, Repository } from 'typeorm';
import { Puzzle } from '../../puzzles/entities/puzzle.entity';
import { UserProgress } from '../entities/progress.entity';
import { SubmitAnswerDto } from '../dtos/submit-answer.dto';
Expand Down Expand Up @@ -245,20 +245,14 @@ export class ProgressCalculationProvider {
* Gets user progress statistics for a category
*/
async getUserProgressStats(userId: string, categoryId: string) {
const stats = await this.userProgressRepository
.createQueryBuilder('progress')
.select('COUNT(*)', 'totalAttempts')
.addSelect(
'SUM(CASE WHEN progress.isCorrect = true THEN 1 ELSE 0 END)',
'correctAttempts',
)
.addSelect('SUM(progress.pointsEarned)', 'totalPoints')
.addSelect('AVG(progress.timeSpent)', 'averageTimeSpent')
.where('progress.userId = :userId', { userId })
.andWhere('progress.categoryId = :categoryId', { categoryId })
.getRawOne<ProgressStatsRaw>();

if (!stats) {
const where: FindOptionsWhere<UserProgress> = {
userId,
categoryId,
};

const progressRecords = await this.userProgressRepository.find({ where });

if (progressRecords.length === 0) {
return {
totalAttempts: 0,
correctAttempts: 0,
Expand All @@ -268,17 +262,31 @@ export class ProgressCalculationProvider {
};
}

const totalAttempts = progressRecords.length;
const correctAttempts = progressRecords.reduce(
(sum, record) => sum + (record.isCorrect ? 1 : 0),
0,
);
const totalPoints = progressRecords.reduce(
(sum, record) => sum + record.pointsEarned,
0,
);
const totalTimeSpent = progressRecords.reduce(
(sum, record) => sum + record.timeSpent,
0,
);
const averageTimeSpent =
totalAttempts > 0 ? totalTimeSpent / totalAttempts : 0;

const accuracy =
totalAttempts > 0 ? (correctAttempts / totalAttempts) * 100 : 0;

return {
totalAttempts: Number(stats?.totalAttempts) || 0,
correctAttempts: parseInt(stats?.correctAttempts || '0', 10),
totalPoints: parseInt(stats?.totalPoints || '0', 10),
averageTimeSpent: parseFloat(stats?.averageTimeSpent || '0'),
accuracy:
stats && Number(stats.totalAttempts) > 0
? (parseInt(stats.correctAttempts, 10) /
Number(stats.totalAttempts)) *
100
: 0,
totalAttempts,
correctAttempts,
totalPoints,
averageTimeSpent,
accuracy,
};
}
}
28 changes: 20 additions & 8 deletions backend/src/puzzles/providers/getAll-puzzle.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { Puzzle } from '../entities/puzzle.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm/repository/Repository';
import { paginate } from '../../common/pagination/paginate';
import { FindOptionsWhere } from 'typeorm';
import { PuzzleQueryDto } from '../dtos/puzzle-query.dto';

@Injectable()
Expand All @@ -15,19 +15,31 @@ export class GetAllPuzzlesProvider {
public async findAll(query: PuzzleQueryDto) {
const { categoryId, difficulty, page = 1, limit = 10 } = query;

const qb = this.puzzleRepo
.createQueryBuilder('puzzle')
.leftJoinAndSelect('puzzle.category', 'category')
.orderBy('puzzle.createdAt', 'DESC');
const where: FindOptionsWhere<Puzzle> = {};

if (categoryId) {
qb.andWhere('puzzle.categoryId = :categoryId', { categoryId });
where.categoryId = categoryId;
}

if (difficulty) {
qb.andWhere('puzzle.difficulty = :difficulty', { difficulty });
where.difficulty = difficulty;
}

return paginate(qb, page, limit);
const [data, total] = await this.puzzleRepo.findAndCount({
where,
relations: ['category'],
order: { createdAt: 'DESC' },
skip: (page - 1) * limit,
take: limit,
});

return {
data,
meta: {
page,
limit,
total,
},
};
}
}
25 changes: 16 additions & 9 deletions backend/src/puzzles/providers/puzzles.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,22 @@ export class PuzzlesService {
// GET /puzzles/daily-quest
// -------------------------------
async getDailyQuestPuzzles(): Promise<Puzzle[]> {
const puzzles = await this.puzzleRepo
.createQueryBuilder('puzzle')
.innerJoinAndSelect('puzzle.category', 'category')
.where('category.isActive = true')
.orderBy('RANDOM()') // PostgreSQL-safe randomness
.limit(5)
.getMany();

return puzzles;
const puzzles = await this.puzzleRepo.find({
where: {
category: {
isActive: true,
},
},
relations: ['category'],
});

// Fisher-Yates shuffle for randomized order similar to ORDER BY RANDOM()
for (let i = puzzles.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[puzzles[i], puzzles[j]] = [puzzles[j], puzzles[i]];
}

return puzzles.slice(0, 5);
}

public async findAll(query: PuzzleQueryDto) {
Expand Down
27 changes: 17 additions & 10 deletions backend/src/quests/providers/getTodaysDailyQuest.provider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { FindOptionsWhere, In, Repository } from 'typeorm';
import { DailyQuest } from '../entities/daily-quest.entity';
import { DailyQuestPuzzle } from '../entities/daily-quest-puzzle.entity';
import { Puzzle } from '../../puzzles/entities/puzzle.entity';
Expand Down Expand Up @@ -153,15 +153,22 @@ export class GetTodaysDailyQuestProvider {
categoryIds: string[],
count: number,
): Promise<Puzzle[]> {
const puzzles = await this.puzzleRepository
.createQueryBuilder('puzzle')
.where('puzzle.difficulty = :difficulty', { difficulty })
.andWhere('puzzle.categoryId IN (:...categoryIds)', { categoryIds })
.orderBy('RANDOM()')
.limit(count)
.getMany();

return puzzles;
const where: FindOptionsWhere<Puzzle> = {
difficulty,
categoryId: In(categoryIds),
};

const puzzles = await this.puzzleRepository.find({
where,
});

// Fisher-Yates shuffle for randomized order similar to ORDER BY RANDOM()
for (let i = puzzles.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[puzzles[i], puzzles[j]] = [puzzles[j], puzzles[i]];
}

return puzzles.slice(0, count);
}

private async buildQuestResponse(
Expand Down