fix(api): allow inviting students to compliance courses without a certificate deadline (#840)
* Allow teachers invite students * Greptile comment fix * coderabbit comment fix
This commit is contained in:
@@ -135,16 +135,17 @@ async function getTargetStudentMembers(courseId: string, profileIds?: string[])
|
|||||||
return students;
|
return students;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveComplianceInitialDueDate(course: Awaited<ReturnType<typeof getComplianceCourseOrThrow>>) {
|
/**
|
||||||
|
* Resolves the initial compliance cycle due date from the course certificate
|
||||||
|
* deadline. Returns `null` when no deadline is set — callers must then skip
|
||||||
|
* record creation (enrollment itself is still allowed). Throws only when a
|
||||||
|
* deadline is present but malformed.
|
||||||
|
*/
|
||||||
|
function getComplianceInitialDueDate(course: Awaited<ReturnType<typeof getComplianceCourseOrThrow>>): string | null {
|
||||||
const dueDate = course.certificate?.deadline ?? null;
|
const dueDate = course.certificate?.deadline ?? null;
|
||||||
|
|
||||||
if (!dueDate) {
|
if (!dueDate) {
|
||||||
throw new AppError(
|
return null;
|
||||||
'Compliance courses require a certificate deadline before learners can be enrolled',
|
|
||||||
ErrorCodes.VALIDATION_ERROR,
|
|
||||||
400,
|
|
||||||
'certificate.deadline'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedDueDate = new Date(dueDate);
|
const parsedDueDate = new Date(dueDate);
|
||||||
@@ -211,7 +212,12 @@ async function getComplianceCompletionSnapshot(params: {
|
|||||||
|
|
||||||
async function ensureComplianceEnrollmentRecordForLearner(courseId: string, groupMemberId: string, profileId: string) {
|
async function ensureComplianceEnrollmentRecordForLearner(courseId: string, groupMemberId: string, profileId: string) {
|
||||||
const course = await getComplianceCourseOrThrow(courseId);
|
const course = await getComplianceCourseOrThrow(courseId);
|
||||||
const dueDate = resolveComplianceInitialDueDate(course);
|
const dueDate = getComplianceInitialDueDate(course);
|
||||||
|
|
||||||
|
if (!dueDate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const [existingRecord] = await getLatestComplianceRecordsByProfiles(courseId, [profileId]);
|
const [existingRecord] = await getLatestComplianceRecordsByProfiles(courseId, [profileId]);
|
||||||
|
|
||||||
if (existingRecord) {
|
if (existingRecord) {
|
||||||
@@ -242,11 +248,14 @@ export async function ensureComplianceEnrollmentRecordsForProfiles(courseIds: st
|
|||||||
for (const courseId of courseIds) {
|
for (const courseId of courseIds) {
|
||||||
const [course] = await getCourseById(courseId);
|
const [course] = await getCourseById(courseId);
|
||||||
|
|
||||||
if (!course || course.type !== 'COMPLIANCE') {
|
if (!course || course.type !== 'COMPLIANCE' || !course.compliance) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveComplianceInitialDueDate(await getComplianceCourseOrThrow(courseId));
|
const initialDueDate = getComplianceInitialDueDate(course);
|
||||||
|
if (!initialDueDate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const learners = await getStudentCourseMembersForCompliance(courseId, profileIds);
|
const learners = await getStudentCourseMembersForCompliance(courseId, profileIds);
|
||||||
for (const learner of learners) {
|
for (const learner of learners) {
|
||||||
@@ -283,6 +292,14 @@ export async function syncComplianceProgressFromSubmission(courseId: string, gro
|
|||||||
}
|
}
|
||||||
|
|
||||||
const activeRecord = await ensureComplianceEnrollmentRecordForLearner(courseId, groupMemberId, profile.id);
|
const activeRecord = await ensureComplianceEnrollmentRecordForLearner(courseId, groupMemberId, profile.id);
|
||||||
|
|
||||||
|
if (!activeRecord) {
|
||||||
|
return {
|
||||||
|
status: 'not_started',
|
||||||
|
completed: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!activeRecord.completedAt && activeRecord.status === 'not_started') {
|
if (!activeRecord.completedAt && activeRecord.status === 'not_started') {
|
||||||
await updateCourseCompletionRecord(activeRecord.id, {
|
await updateCourseCompletionRecord(activeRecord.id, {
|
||||||
status: 'in_progress',
|
status: 'in_progress',
|
||||||
@@ -299,6 +316,14 @@ export async function syncComplianceProgressFromSubmission(courseId: string, gro
|
|||||||
}
|
}
|
||||||
|
|
||||||
const latestRecord = await ensureComplianceEnrollmentRecordForLearner(courseId, groupMemberId, profile.id);
|
const latestRecord = await ensureComplianceEnrollmentRecordForLearner(courseId, groupMemberId, profile.id);
|
||||||
|
|
||||||
|
if (!latestRecord) {
|
||||||
|
return {
|
||||||
|
status: 'not_started',
|
||||||
|
completed: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (latestRecord.completedAt) {
|
if (latestRecord.completedAt) {
|
||||||
return {
|
return {
|
||||||
status: latestRecord.status,
|
status: latestRecord.status,
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ export class ComplianceApi extends BaseApiWithErrors {
|
|||||||
return result?.data;
|
return result?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureOverview(courseId: string) {
|
async ensureOverview(courseId: string, refresh = false) {
|
||||||
if (this.overviewKey === courseId && this.overview) {
|
if (!refresh && this.overviewKey === courseId && this.overview) {
|
||||||
return this.overview;
|
return this.overview;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,9 +80,9 @@ export class ComplianceApi extends BaseApiWithErrors {
|
|||||||
return result?.data;
|
return result?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureLearnerHistory(courseId: string, profileId: string) {
|
async ensureLearnerHistory(courseId: string, profileId: string, refresh = false) {
|
||||||
const requestKey = `${courseId}:${profileId}`;
|
const requestKey = `${courseId}:${profileId}`;
|
||||||
if (this.learnerHistoryKey === requestKey && this.learnerHistory) {
|
if (!refresh && this.learnerHistoryKey === requestKey && this.learnerHistory) {
|
||||||
return this.learnerHistory;
|
return this.learnerHistory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { untrack } from 'svelte';
|
||||||
import { Badge } from '@cio/ui/base/badge';
|
import { Badge } from '@cio/ui/base/badge';
|
||||||
import { Button } from '@cio/ui/base/button';
|
import { Button } from '@cio/ui/base/button';
|
||||||
import { Checkbox } from '@cio/ui/base/checkbox';
|
import { Checkbox } from '@cio/ui/base/checkbox';
|
||||||
@@ -21,8 +22,8 @@
|
|||||||
type ComplianceStatus = CourseComplianceLearner['status'] | LearnerComplianceRecord['status'] | 'no_record';
|
type ComplianceStatus = CourseComplianceLearner['status'] | LearnerComplianceRecord['status'] | 'no_record';
|
||||||
type StaffAction = 'reset' | 'extend' | 'waive' | null;
|
type StaffAction = 'reset' | 'extend' | 'waive' | null;
|
||||||
|
|
||||||
let lastRequestKey = $state<string | null>(null);
|
|
||||||
let staffTab = $state<'overview' | 'learners'>('overview');
|
let staffTab = $state<'overview' | 'learners'>('overview');
|
||||||
|
let loadedCourseId = $state<string | null>(null);
|
||||||
let activeAction = $state<StaffAction>(null);
|
let activeAction = $state<StaffAction>(null);
|
||||||
let bulkActionType = $state<Exclude<StaffAction, null> | null>(null);
|
let bulkActionType = $state<Exclude<StaffAction, null> | null>(null);
|
||||||
let isBulkActionModalOpen = $state(false);
|
let isBulkActionModalOpen = $state(false);
|
||||||
@@ -76,29 +77,20 @@
|
|||||||
|
|
||||||
if (!isComplianceCourse) {
|
if (!isComplianceCourse) {
|
||||||
complianceApi.reset();
|
complianceApi.reset();
|
||||||
lastRequestKey = null;
|
loadedCourseId = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const courseId = courseApi.course.id;
|
const courseId = courseApi.course.id;
|
||||||
|
const needsRefresh = untrack(() => loadedCourseId) !== courseId;
|
||||||
|
loadedCourseId = courseId;
|
||||||
|
|
||||||
if (isStudent && currentMember?.profileId) {
|
if (isStudent && currentMember?.profileId) {
|
||||||
const requestKey = `${courseId}:${currentMember.profileId}:learner`;
|
void complianceApi.ensureLearnerHistory(courseId, currentMember.profileId, needsRefresh);
|
||||||
if (lastRequestKey === requestKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastRequestKey = requestKey;
|
|
||||||
void complianceApi.ensureLearnerHistory(courseId, currentMember.profileId);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestKey = `${courseId}:overview`;
|
void complianceApi.ensureOverview(courseId, needsRefresh);
|
||||||
if (lastRequestKey === requestKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastRequestKey = requestKey;
|
|
||||||
void complianceApi.ensureOverview(courseId);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user