feat: setup dashboard analytics api (#604)
* feat: setup dashboard analytics api * chore: requested pr fixes * fix: dash analytics server side type * refactor: update Avatar component usage across the dashboard - Replaced custom Avatar component with the new @cio/ui/base/avatar implementation for consistency. - Enhanced avatar rendering with fallback options and improved accessibility. - Cleaned up unused Avatar component files and related functions. - Updated various components to utilize the new Avatar structure, ensuring a uniform appearance and behavior across the application. * chore: remove console.log statements from analytics routes and API calls - Eliminated console.log statements for orgId and siteName in the dash analytics router and DashStatApi class to clean up the code and improve performance. --------- Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
co-authored by
Rotimi Best
parent
c7854efae3
commit
9a5aee1ee0
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
and,
|
||||
course,
|
||||
dashOrgStats,
|
||||
db,
|
||||
desc,
|
||||
eq,
|
||||
group,
|
||||
groupmember,
|
||||
lesson,
|
||||
lessonCompletion,
|
||||
organization,
|
||||
profile,
|
||||
sql
|
||||
} from '@db/drizzle';
|
||||
|
||||
export function getCourseStats(orgId: string) {
|
||||
const subquery = db
|
||||
.select({
|
||||
courseId: course.id,
|
||||
courseTitle: course.title,
|
||||
totalStudents: sql<number>`COUNT(DISTINCT ${groupmember.id})`.as('total_students'),
|
||||
totalLessons: sql<number>`COUNT(DISTINCT ${lesson.id})`.as('total_lessons'),
|
||||
completedLessons: sql<number>`
|
||||
COUNT(DISTINCT ${lessonCompletion.id})
|
||||
FILTER (WHERE ${lessonCompletion.isComplete} = true)
|
||||
`.as('completed_lessons')
|
||||
})
|
||||
.from(course)
|
||||
.innerJoin(group, eq(group.id, course.groupId))
|
||||
.leftJoin(groupmember, and(eq(groupmember.groupId, group.id), eq(groupmember.roleId, 3)))
|
||||
.leftJoin(lesson, eq(lesson.courseId, course.id))
|
||||
.leftJoin(
|
||||
lessonCompletion,
|
||||
and(eq(lessonCompletion.lessonId, lesson.id), eq(lessonCompletion.profileId, groupmember.profileId))
|
||||
)
|
||||
.where(and(eq(course.status, 'ACTIVE'), eq(group.organizationId, orgId)))
|
||||
.groupBy(course.id, course.title)
|
||||
.orderBy(sql`completed_lessons DESC`)
|
||||
.as('course_stats');
|
||||
|
||||
return db
|
||||
.select({
|
||||
courseId: subquery.courseId,
|
||||
courseTitle: subquery.courseTitle,
|
||||
totalStudents: sql<number>`${subquery.totalStudents}::integer`,
|
||||
completionPercentage: sql<number>`
|
||||
CASE
|
||||
WHEN ${subquery.totalStudents} * ${subquery.totalLessons} = 0 THEN 0
|
||||
ELSE ROUND((${subquery.completedLessons}::numeric / (${subquery.totalStudents} * ${subquery.totalLessons})) * 100)::integer
|
||||
END
|
||||
`
|
||||
})
|
||||
.from(subquery)
|
||||
.orderBy(subquery.completedLessons)
|
||||
.limit(5);
|
||||
}
|
||||
|
||||
export function getRecentEnrollments(orgId: string) {
|
||||
return db
|
||||
.select({
|
||||
profileId: profile.id,
|
||||
avatarUrl: profile.avatarUrl,
|
||||
fullname: profile.fullname,
|
||||
courseId: course.id,
|
||||
courseTitle: course.title,
|
||||
enrolledAt: groupmember.createdAt
|
||||
})
|
||||
.from(course)
|
||||
.innerJoin(group, eq(group.id, course.groupId))
|
||||
.leftJoin(groupmember, and(eq(groupmember.groupId, group.id), eq(groupmember.roleId, 3)))
|
||||
.innerJoin(profile, eq(profile.id, groupmember.profileId))
|
||||
.where(and(eq(course.status, 'ACTIVE'), eq(group.organizationId, orgId)))
|
||||
.groupBy(profile.id, course.id, course.title, groupmember.createdAt)
|
||||
.orderBy(desc(groupmember.createdAt))
|
||||
.limit(5);
|
||||
}
|
||||
|
||||
export function getDashOrgStats(orgId: string) {
|
||||
return db.select().from(dashOrgStats).where(eq(dashOrgStats.orgId, orgId));
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './dash';
|
||||
@@ -0,0 +1,19 @@
|
||||
export interface OrganisationAnalytics {
|
||||
revenue: number;
|
||||
numberOfCourses: number;
|
||||
totalStudents: number;
|
||||
topCourses: {
|
||||
id: string;
|
||||
title: string;
|
||||
enrollments: number;
|
||||
completion: number;
|
||||
}[];
|
||||
enrollments: {
|
||||
id: string;
|
||||
avatarUrl: string;
|
||||
name: string;
|
||||
courseId: string;
|
||||
course: string;
|
||||
date: string;
|
||||
}[];
|
||||
}
|
||||
@@ -13,6 +13,10 @@ import { and, eq, inArray, or, sql } from 'drizzle-orm';
|
||||
import { ROLE } from '@cio/utils/constants';
|
||||
import { db } from '@db/drizzle';
|
||||
|
||||
export function getOrgIdBySiteName(siteName: string) {
|
||||
return db.select().from(schema.organization).where(eq(schema.organization.siteName, siteName)).limit(1);
|
||||
}
|
||||
|
||||
export const getOrganizationByProfileId = async (profileId: string): Promise<OrganizationWithMemberAndPlans[]> => {
|
||||
const result = await db
|
||||
.select({
|
||||
|
||||
@@ -137,3 +137,5 @@ export type TCourseVersion = (typeof schema.courseVersion.enumValues)[number];
|
||||
export type TLocale = (typeof schema.locale.enumValues)[number];
|
||||
|
||||
export type TPlan = (typeof schema.plan.enumValues)[number];
|
||||
|
||||
export type TDashStats = typeof schema.dashOrgStats.$inferSelect;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './stats';
|
||||
@@ -0,0 +1,12 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
export const ZDashStats = z
|
||||
.object({
|
||||
orgId: z.string().optional(),
|
||||
siteName: z.string().optional()
|
||||
})
|
||||
.refine((v) => !!(v.orgId || v.siteName), {
|
||||
message: 'Either orgId or siteName is required'
|
||||
});
|
||||
|
||||
export type TDashStats = z.infer<typeof ZDashStats>;
|
||||
@@ -6,3 +6,4 @@ export * from './mail';
|
||||
export * from './media';
|
||||
export * from './onboarding';
|
||||
export * from './organization';
|
||||
export * from './dash';
|
||||
|
||||
Reference in New Issue
Block a user