Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
piotr-pajak committed Nov 21, 2024
1 parent 8e36faf commit a8e6305
Show file tree
Hide file tree
Showing 15 changed files with 612 additions and 352 deletions.
2 changes: 2 additions & 0 deletions apps/api/src/lessons/schemas/lesson.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ export const LessonProgress = {
inProgress: "in_progress",
completed: "completed",
} as const;

export type LessonProgressType = (typeof LessonProgress)[keyof typeof LessonProgress];
15 changes: 3 additions & 12 deletions apps/api/src/statistics/api/statistics.controller.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,16 @@
import { Controller, Get } from "@nestjs/common";
import { Validate } from "nestjs-typebox";

import { baseResponse, BaseResponse, UUIDType } from "src/common";
import { BaseResponse, UUIDType } from "src/common";
import { CurrentUser } from "src/common/decorators/user.decorator";

import { CurrentUser } from "../../common/decorators/user.decorator";
import { UserStatsSchema } from "../schemas/userStats.schema";
import { StatisticsService } from "../statistics.service";

import type { UserStats } from "../schemas/userStats.schema";

@Controller("statistics")
export class StatisticsController {
constructor(private statisticsService: StatisticsService) {}

@Get()
@Validate({
response: baseResponse(UserStatsSchema),
})
async getUserStatistics(
@CurrentUser("userId") currentUserId: UUIDType,
): Promise<BaseResponse<UserStats>> {
async getUserStatistics(@CurrentUser("userId") currentUserId: UUIDType) {
return new BaseResponse(await this.statisticsService.getUserStats(currentUserId));
}
}
156 changes: 149 additions & 7 deletions apps/api/src/statistics/repositories/statistics.repository.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { Inject, Injectable } from "@nestjs/common";
import { startOfDay, differenceInDays, eachDayOfInterval, format } from "date-fns";
import { and, eq, sql } from "drizzle-orm";
import { differenceInDays, eachDayOfInterval, format, startOfDay } from "date-fns";
import { and, desc, eq, sql } from "drizzle-orm";

import { DatabasePg } from "src/common";

import { LessonsRepository } from "src/lessons/repositories/lessons.repository";
import { LessonProgress } from "src/lessons/schemas/lesson.types";
import {
lessonItems,
lessons,
quizAttempts,
studentCompletedLessonItems,
studentCourses,
studentLessonsProgress,
userStatistics,
} from "../../storage/schema";
} from "src/storage/schema";

import type { LessonProgressType } from "src/lessons/schemas/lesson.types";

type Stats = {
month: string;
Expand All @@ -20,7 +26,10 @@ type Stats = {

@Injectable()
export class StatisticsRepository {
constructor(@Inject("DB") private readonly db: DatabasePg) {}
constructor(
@Inject("DB") private readonly db: DatabasePg,
private readonly lessonsRepository: LessonsRepository,
) {}

async getUserStats(userId: string) {
const [quizStatsResult] = await this.db
Expand Down Expand Up @@ -71,6 +80,29 @@ export class StatisticsRepository {
.groupBy(sql<string>`date_trunc('month', ${studentCourses.createdAt})`)
.orderBy(sql<string>`date_trunc('month', ${studentCourses.createdAt})`);

const [courseStats] = await this.db
.select({
started: sql<number>`count(*)::INTEGER`,
completed: sql<number>`count(case when ${studentCourses.state} = 'completed' then 1 end)::INTEGER`,
completionRate: sql<number>`
coalesce(
round(
(count(case when ${studentCourses.state} = 'completed' then 1 end)::numeric /
nullif(count(*)::numeric, 0)) * 100,
2
),
0
)::INTEGER
`,
})
.from(studentCourses)
.where(
and(
eq(studentCourses.studentId, userId),
sql`${studentCourses.createdAt} >= date_trunc('month', current_date) - interval '11 months'`,
),
);

const [activityStats] = await this.db
.select()
.from(userStatistics)
Expand Down Expand Up @@ -99,10 +131,112 @@ export class StatisticsRepository {
sql`${studentLessonsProgress.createdAt} >= date_trunc('month', current_date) - interval '11 months'`,
),
)

.groupBy(sql<string>`date_trunc('month', ${studentLessonsProgress.createdAt})`)
.orderBy(sql<string>`date_trunc('month', ${studentLessonsProgress.createdAt})`);

const [lessonStats] = await this.db
.select({
started: sql<number>`count(distinct ${studentLessonsProgress.lessonId})::INTEGER`,
completed: sql<number>`count(case when ${studentLessonsProgress.completedLessonItemCount} = ${studentLessonsProgress.lessonItemCount} then 1 end)::INTEGER`,
completionRate: sql<number>`
coalesce(
round(
(count(case when ${studentLessonsProgress.completedLessonItemCount} = ${studentLessonsProgress.lessonItemCount} then 1 end)::numeric /
nullif(count(distinct ${studentLessonsProgress.lessonId})::numeric, 0)) * 100,
2
),
0
)::INTEGER
`,
})
.from(studentLessonsProgress)
.where(
and(
eq(studentLessonsProgress.studentId, userId),
sql`${studentLessonsProgress.createdAt} >= date_trunc('month', current_date) - interval '11 months'`,
),
);

const [lastLessonItem] = await this.db
.select()
.from(studentCompletedLessonItems)
.where(and(eq(studentCompletedLessonItems.studentId, userId)))
.orderBy(desc(studentCompletedLessonItems.updatedAt))
.limit(1);

const lastLessonDetails = await this.lessonsRepository.getLessonForUser(
lastLessonItem.courseId,
lastLessonItem.lessonId,
userId,
);

const [lastLesson] = await this.db
.select({
// TODO: Code below needs https://github.com/wielopolski love
lessonProgress: sql<LessonProgressType>`
(CASE
WHEN (
SELECT COUNT(*)
FROM ${lessonItems}
WHERE ${lessonItems.lessonId} = ${lastLessonItem.lessonId}
AND ${lessonItems.lessonItemType} != 'text_block'
) = (
SELECT COUNT(*)
FROM ${studentCompletedLessonItems}
WHERE ${studentCompletedLessonItems.lessonId} = ${lastLessonItem.lessonId}
AND ${studentCompletedLessonItems.courseId} = ${lastLessonItem.courseId}
AND ${studentCompletedLessonItems.studentId} = ${userId}
) AND (
SELECT COUNT(*)
FROM ${lessonItems}
WHERE ${lessonItems.lessonId} = ${lastLessonItem.lessonId}
AND ${lessonItems.lessonItemType} != 'text_block'
) > 0
THEN ${LessonProgress.completed}
WHEN (
SELECT COUNT(*)
FROM ${studentCompletedLessonItems}
WHERE ${studentCompletedLessonItems.lessonId} = ${lastLessonItem.lessonId}
AND ${studentCompletedLessonItems.courseId} = ${lastLessonItem.courseId}
AND ${studentCompletedLessonItems.studentId} = ${userId}
) > 0
THEN ${LessonProgress.inProgress}
ELSE ${LessonProgress.notStarted}
END)
`,
itemsCount: sql<number>`
(SELECT COUNT(*)
FROM ${lessonItems}
WHERE ${lessonItems.lessonId} = ${lastLessonItem.lessonId}
AND ${lessonItems.lessonItemType} != 'text_block')::INTEGER`,
itemsCompletedCount: sql<number>`
(SELECT COUNT(*)
FROM ${studentCompletedLessonItems}
WHERE ${studentCompletedLessonItems.lessonId} = ${lastLessonItem.lessonId}
AND ${studentCompletedLessonItems.courseId} = ${lastLessonItem.courseId}
AND ${studentCompletedLessonItems.studentId} = ${userId})::INTEGER
`,
})
.from(lessons)
.where(and(eq(lessons.id, lastLessonItem.lessonId)))
.leftJoin(
studentLessonsProgress,
and(
eq(studentLessonsProgress.studentId, userId),
eq(studentLessonsProgress.lessonId, lastLessonItem.lessonId),
eq(studentLessonsProgress.courseId, lastLessonItem.courseId),
),
)
.leftJoin(lessonItems, eq(studentLessonsProgress.lessonId, lessonItems.lessonId))
.leftJoin(
studentCompletedLessonItems,
and(
eq(studentLessonsProgress.lessonId, studentCompletedLessonItems.lessonId),
eq(studentCompletedLessonItems.courseId, studentLessonsProgress.courseId),
),
);

console.log(lastLesson);
return {
quizzes: {
totalAttempts: Number(quizStats.totalAttempts),
Expand All @@ -119,6 +253,14 @@ export class StatisticsRepository {
activityHistory: activityStats?.activityHistory || {},
},
lessons: this.formatLessonStats(lessonStatsResult),
averageStats: {
lessonStats,
courseStats,
},
lastLesson: {
...lastLessonDetails,
...lastLesson,
},
};
}

Expand All @@ -130,7 +272,7 @@ export class StatisticsRepository {
wrongAnswers: number;
score: number;
}) {
return await this.db.insert(quizAttempts).values(data);
return this.db.insert(quizAttempts).values(data);
}

async updateUserActivity(userId: string) {
Expand Down
4 changes: 3 additions & 1 deletion apps/api/src/statistics/statistics.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Module } from "@nestjs/common";
import { CqrsModule } from "@nestjs/cqrs";

import { LessonsRepository } from "src/lessons/repositories/lessons.repository";

import { StatisticsController } from "./api/statistics.controller";
import { StatisticsHandler } from "./handlers/statistics.handler";
import { StatisticsRepository } from "./repositories/statistics.repository";
Expand All @@ -9,7 +11,7 @@ import { StatisticsService } from "./statistics.service";
@Module({
imports: [CqrsModule],
controllers: [StatisticsController],
providers: [StatisticsHandler, StatisticsRepository, StatisticsService],
providers: [StatisticsHandler, StatisticsRepository, StatisticsService, LessonsRepository],
exports: [StatisticsRepository],
})
export class StatisticsModule {}
2 changes: 1 addition & 1 deletion apps/api/src/statistics/statistics.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export class StatisticsService {
constructor(private statisticsRepository: StatisticsRepository) {}

async getUserStats(userId: string) {
return this.statisticsRepository.getUserStats(userId);
return await this.statisticsRepository.getUserStats(userId);
}
}
Loading

0 comments on commit a8e6305

Please sign in to comment.