Files
classroomio/apps/api/src/utils/course-completion.ts
T
Rotimi Best 81294a47f9 feat: live class sessions, CE workflow, and docs (#683)
Adds live-class session scheduling on lessons (join link, date/time,
timezone) with automatic 24h and 1h reminder emails plus a calendar
invite (.ics), a time-change notification with an updated invite, a
teacher-authored welcome email and session invite on enrollment, and a
"Notify students" quiz nudge.

LMS gets an upcoming live-sessions card and Join buttons, plus a
reusable live-session card component (3 states) in @cio/ui with a
Storybook story. The sidebar content tree now reflects sequential
progression locking like the content list, and the course-completion
confetti regression is fixed.

Org-based emails no longer reference ClassroomIO by name. Docs add
live-class, welcome-email, and course-progression guides, expand the
certificates guide (design, signatures, corrected labels), move
plan-required callouts to the top, flatten how-to-guides to the docs
root with redirects, and add screenshots. Includes an add-docs-image
skill.
2026-06-22 05:43:48 +01:00

101 lines
3.3 KiB
TypeScript

import { getDashboardBaseUrl } from '@cio/core/config/dashboard-url';
import {
getCourseProgress as getCourseProgressQuery,
type TCourseCertificationRow
} from '@cio/db/queries/course/course';
import { setMemberCertificateEarned, setMemberCertificationEmailSent } from '@cio/db/queries/course/people';
import { getProfileById } from '@cio/db/queries/auth';
import { buildEmailFromName, buildEmailBranding } from '@cio/email';
import { enqueueTransactionalEmail } from '@api/services/jobs';
export function calcCourseProgressPercent(params: {
lessonsCompleted: number;
totalLessons: number;
exercisesCompleted: number;
exercisesCount: number;
}): number {
const totalItems = params.totalLessons + params.exercisesCount;
if (totalItems === 0) return 0;
const completedItems = params.lessonsCompleted + params.exercisesCompleted;
return Math.round((completedItems / totalItems) * 100);
}
export function isBeforeOrEqualDeadline(deadlineIso: string | null | undefined, now: Date): boolean {
if (!deadlineIso) return true;
const deadlineDate = new Date(deadlineIso);
if (Number.isNaN(deadlineDate.getTime())) return true;
return now.getTime() <= deadlineDate.getTime();
}
/**
* Persists certificate earned time, then sends the completion email and records
* {@link setMemberCertificationEmailSent}, without blocking the HTTP handler.
* Re-fetches progress so concurrent jobs can skip work already done.
*/
export function scheduleCertificationCompletionWork(params: {
courseId: string;
profileId: string;
groupMemberId: string;
courseRow: TCourseCertificationRow;
earnedAt: string;
}): void {
const { courseId, profileId, groupMemberId, courseRow, earnedAt } = params;
void (async () => {
try {
let latest = await getCourseProgressQuery(courseId, profileId);
if (!latest.certificateEarnedAt) {
await setMemberCertificateEarned(groupMemberId, earnedAt);
latest = await getCourseProgressQuery(courseId, profileId);
}
if (latest.certificationEmailSentAt) {
return;
}
const profile = await getProfileById(profileId);
const studentEmail = profile?.email;
if (!studentEmail) {
return;
}
const base = getDashboardBaseUrl({
siteName: courseRow.orgSiteName,
customDomain: courseRow.orgCustomDomain,
isCustomDomainVerified: courseRow.orgIsCustomDomainVerified
});
const certificateUrl = `${base}/courses/${courseId}/certificates`;
await enqueueTransactionalEmail('studentCourseCompletion', {
to: studentEmail,
fields: {
orgName: courseRow.orgName,
courseName: courseRow.title,
studentName: profile?.fullname || studentEmail,
certificateUrl,
customMessage: courseRow.certificate?.emailMessage ?? null,
branding: buildEmailBranding({
name: courseRow.orgName,
avatarUrl: courseRow.orgAvatarUrl,
theme: courseRow.orgTheme
})
},
from: buildEmailFromName(`${courseRow.orgName} (via ClassroomIO.com)`),
idempotencyKey: `course-completion:${groupMemberId}`
});
await setMemberCertificationEmailSent(groupMemberId, new Date().toISOString());
} catch (error) {
console.error('scheduleCertificationCompletionWork: failed:', error);
}
})();
}