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.
101 lines
3.3 KiB
TypeScript
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);
|
|
}
|
|
})();
|
|
}
|