feat: enhance API and middleware functionality: add public API routes, implement program and course member checks, and improve rate limiting for invite acceptance. Update OpenAPI spec generation to focus on public API endpoints and add new middleware for automation key scopes. Include new documentation for lesson AI features and public API tests.
This commit is contained in:
@@ -7,9 +7,9 @@ import {
|
||||
eq,
|
||||
group,
|
||||
groupmember,
|
||||
isNotNull,
|
||||
lesson,
|
||||
lessonCompletion,
|
||||
organization,
|
||||
profile,
|
||||
sql
|
||||
} from '@db/drizzle';
|
||||
@@ -24,7 +24,11 @@ export function getCourseStats(orgId: string) {
|
||||
completedLessons: sql<number>`
|
||||
COUNT(DISTINCT ${lessonCompletion.id})
|
||||
FILTER (WHERE ${lessonCompletion.isComplete} = true)
|
||||
`.as('completed_lessons')
|
||||
`.as('completed_lessons'),
|
||||
certificatesEarned: sql<number>`
|
||||
COUNT(DISTINCT ${groupmember.id})
|
||||
FILTER (WHERE ${groupmember.certificateEarnedAt} IS NOT NULL)
|
||||
`.as('certificates_earned')
|
||||
})
|
||||
.from(course)
|
||||
.innerJoin(group, eq(group.id, course.groupId))
|
||||
@@ -49,6 +53,12 @@ export function getCourseStats(orgId: string) {
|
||||
WHEN ${subquery.totalStudents} * ${subquery.totalLessons} = 0 THEN 0
|
||||
ELSE ROUND((${subquery.completedLessons}::numeric / (${subquery.totalStudents} * ${subquery.totalLessons})) * 100)::integer
|
||||
END
|
||||
`,
|
||||
certificationPercentage: sql<number>`
|
||||
CASE
|
||||
WHEN ${subquery.totalStudents} = 0 THEN 0
|
||||
ELSE ROUND((${subquery.certificatesEarned}::numeric / ${subquery.totalStudents}) * 100)::integer
|
||||
END
|
||||
`
|
||||
})
|
||||
.from(subquery)
|
||||
@@ -76,6 +86,52 @@ export function getRecentEnrollments(orgId: string) {
|
||||
.limit(5);
|
||||
}
|
||||
|
||||
/** Count of student enrollments that have earned a certificate (per org, active courses). */
|
||||
export function getTotalCertificatesIssued(orgId: string) {
|
||||
return db
|
||||
.select({
|
||||
count: sql<number>`count(${groupmember.id})::int`
|
||||
})
|
||||
.from(groupmember)
|
||||
.innerJoin(group, eq(groupmember.groupId, group.id))
|
||||
.innerJoin(course, eq(course.groupId, group.id))
|
||||
.where(
|
||||
and(
|
||||
eq(group.organizationId, orgId),
|
||||
eq(course.status, 'ACTIVE'),
|
||||
eq(groupmember.roleId, 3),
|
||||
isNotNull(groupmember.certificateEarnedAt)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** Most recent certificate earners for the org dashboard. */
|
||||
export function getRecentCertifications(orgId: string) {
|
||||
return db
|
||||
.select({
|
||||
profileId: profile.id,
|
||||
avatarUrl: profile.avatarUrl,
|
||||
fullname: profile.fullname,
|
||||
courseId: course.id,
|
||||
courseTitle: course.title,
|
||||
earnedAt: groupmember.certificateEarnedAt
|
||||
})
|
||||
.from(groupmember)
|
||||
.innerJoin(group, eq(groupmember.groupId, group.id))
|
||||
.innerJoin(course, eq(course.groupId, group.id))
|
||||
.innerJoin(profile, eq(profile.id, groupmember.profileId))
|
||||
.where(
|
||||
and(
|
||||
eq(group.organizationId, orgId),
|
||||
eq(course.status, 'ACTIVE'),
|
||||
eq(groupmember.roleId, 3),
|
||||
isNotNull(groupmember.certificateEarnedAt)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(groupmember.certificateEarnedAt))
|
||||
.limit(5);
|
||||
}
|
||||
|
||||
export function getDashOrgStats(orgId: string) {
|
||||
return db.select().from(dashOrgStats).where(eq(dashOrgStats.orgId, orgId));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface OrganisationAnalytics {
|
||||
revenue: number;
|
||||
totalCertificates: number;
|
||||
numberOfCourses: number;
|
||||
totalStudents: number;
|
||||
topCourses: {
|
||||
@@ -7,8 +7,9 @@ export interface OrganisationAnalytics {
|
||||
title: string;
|
||||
enrollments: number;
|
||||
completion: number;
|
||||
certification: number;
|
||||
}[];
|
||||
enrollments: {
|
||||
recentCertifications: {
|
||||
id: string;
|
||||
avatarUrl: string;
|
||||
name: string;
|
||||
|
||||
Reference in New Issue
Block a user