feat(dashboard): moved course completion deadline to course settings page (#842)

This commit is contained in:
Victor Jack
2026-08-15 18:55:51 +01:00
committed by GitHub
parent b267db6319
commit ad5c8db5a9
22 changed files with 528 additions and 244 deletions
+27 -4
View File
@@ -6,7 +6,7 @@ import {
createCourseSections,
deleteCourse as deleteCourseQuery,
getCourseProgress as getCourseProgressQuery,
getCourseTypeById,
getCourseById,
getCourseWithRelations,
updateCourse as updateCourseQuery,
updateLessonsSectionId
@@ -28,6 +28,7 @@ import { getStudentCourseProgressImpactCounts } from '@cio/db/queries/course/res
import { getCourseGroupId } from '@cio/db/queries/course/people';
import { ContentType, ROLE } from '@cio/utils/constants';
import { isPublishedComplianceMissingDeadline, resolveCourseCertificateDeadline } from '@cio/utils/functions';
import type { TCourse } from '@cio/db/types';
import type { TCourseCreate } from '@cio/utils/validation/course';
import { db } from '@cio/db/drizzle';
@@ -338,16 +339,38 @@ export async function updateCourse(courseId: string, data: Partial<TCourse>) {
slug: data.slug
};
if (data.type !== undefined) {
const currentType = await getCourseTypeById(courseId);
const [currentCourse] = await getCourseById(courseId);
if (!currentCourse) {
throw new AppError('Course not found', ErrorCodes.COURSE_NOT_FOUND, 404);
}
if (data.type !== undefined) {
await guardCourseTypeTransition({
courseId,
currentType: currentType ?? null,
currentType: currentCourse.type ?? null,
nextType: data.type
});
}
const nextType = data.type ?? currentCourse.type;
const nextIsPublished = data.isPublished ?? currentCourse.isPublished;
const nextDeadline = resolveCourseCertificateDeadline(currentCourse.certificate?.deadline, data.certificate);
if (
isPublishedComplianceMissingDeadline({
type: nextType,
isPublished: nextIsPublished,
deadline: nextDeadline
})
) {
throw new AppError(
'Compliance courses require a completion deadline before they can be published',
ErrorCodes.COMPLIANCE_DEADLINE_REQUIRED,
400,
'certificate.deadline'
);
}
if (data.certificate?.requiredExerciseId) {
const ok = await exerciseBelongsToCourse(data.certificate.requiredExerciseId, courseId);
if (!ok) {
@@ -3,7 +3,7 @@ import { getCourseContentItems, type CourseContentItemRow } from '@cio/db/querie
import { getCourseOrganizationId } from '@cio/db/queries/tag';
import { getOrganizationById } from '@cio/db/queries/organization';
import type { TCourse, TOrganization } from '@cio/db/types';
import { generateSlug } from '@cio/utils/functions';
import { generateSlug, isPublishedComplianceMissingDeadline } from '@cio/utils/functions';
import { ContentType } from '@cio/utils/constants';
import type { TCourseLandingPageUpdate } from '@cio/utils/validation/course';
@@ -32,7 +32,17 @@ export type CourseGoLiveReadiness = {
type CourseReadinessInput = {
course: Pick<
TCourse,
'id' | 'title' | 'description' | 'overview' | 'slug' | 'logo' | 'bannerImage' | 'metadata' | 'type' | 'cost'
| 'id'
| 'title'
| 'description'
| 'overview'
| 'slug'
| 'logo'
| 'bannerImage'
| 'metadata'
| 'type'
| 'cost'
| 'certificate'
>;
contentItems: CourseContentItemRow[];
suggestedSlug?: string;
@@ -190,6 +200,22 @@ export function evaluateCourseGoLiveReadiness(input: CourseReadinessInput): Cour
}
if (course.type === 'COMPLIANCE') {
if (
isPublishedComplianceMissingDeadline({
type: course.type,
isPublished: true,
deadline: course.certificate?.deadline
})
) {
blockers.push(
buildIssue(
'COMPLIANCE_DEADLINE_MISSING',
'Compliance courses require a completion deadline before they can be published.',
'course.certificate.deadline'
)
);
}
warnings.push(
buildIssue('COMPLIANCE_REVIEW_RECOMMENDED', 'Review compliance settings before publishing.', 'course.compliance')
);