diff --git a/apps/dashboard/src/lib/features/course/components/card.svelte b/apps/dashboard/src/lib/features/course/components/card.svelte
index 15ada2eb..2d423ac6 100644
--- a/apps/dashboard/src/lib/features/course/components/card.svelte
+++ b/apps/dashboard/src/lib/features/course/components/card.svelte
@@ -23,7 +23,8 @@
import {
getStudentCourseComplianceDate,
getStudentCourseComplianceStatusKey,
- getStudentCourseComplianceStatusVariant
+ getStudentCourseComplianceStatusVariant,
+ shouldShowStudentCourseComplianceStatusBadge
} from '$features/course/utils/compliance-utils';
import CardDropdown from './card-dropdown.svelte';
import CoursePublishBadge from './course-publish-badge.svelte';
@@ -195,6 +196,11 @@
? getStudentCourseComplianceStatusKey(course as UserEnrolledCourses[number])
: null
);
+ const showComplianceStatusBadge = $derived(
+ isLMS && type === 'COMPLIANCE' && !isExplore
+ ? shouldShowStudentCourseComplianceStatusBadge(course as UserEnrolledCourses[number])
+ : false
+ );
const complianceStatusVariant = $derived(
isLMS && type === 'COMPLIANCE' && !isExplore
? getStudentCourseComplianceStatusVariant(course as UserEnrolledCourses[number])
@@ -300,7 +306,7 @@
{#if type === 'COMPLIANCE'}
- {#if complianceStatusKey}
+ {#if showComplianceStatusBadge && complianceStatusKey}
{$t(complianceStatusKey)}
diff --git a/apps/dashboard/src/lib/features/course/components/certificate-deadline-required-dialog.svelte b/apps/dashboard/src/lib/features/course/components/certificate-deadline-required-dialog.svelte
new file mode 100644
index 00000000..f7f5f386
--- /dev/null
+++ b/apps/dashboard/src/lib/features/course/components/certificate-deadline-required-dialog.svelte
@@ -0,0 +1,31 @@
+
+
+
+ e.preventDefault()}>
+
+ {$t('course.navItem.settings.certificate_deadline_required')}
+
+ {$t('course.navItem.settings.certificate_deadline_required_description')}
+
+
+
+
+
+
+
+
diff --git a/apps/dashboard/src/lib/features/course/components/view-course-site-unpublished-modal.svelte b/apps/dashboard/src/lib/features/course/components/view-course-site-unpublished-modal.svelte
index 81b45306..6832090f 100644
--- a/apps/dashboard/src/lib/features/course/components/view-course-site-unpublished-modal.svelte
+++ b/apps/dashboard/src/lib/features/course/components/view-course-site-unpublished-modal.svelte
@@ -1,10 +1,13 @@
-
+ e.preventDefault()}>
{$t('course.view_course_site.unpublished_title')}
{$t('course.view_course_site.unpublished_description')}
@@ -64,3 +87,5 @@
+
+
diff --git a/apps/dashboard/src/lib/features/course/pages/settings.svelte b/apps/dashboard/src/lib/features/course/pages/settings.svelte
index 953e6510..ebf2720f 100644
--- a/apps/dashboard/src/lib/features/course/pages/settings.svelte
+++ b/apps/dashboard/src/lib/features/course/pages/settings.svelte
@@ -6,12 +6,12 @@
import { Switch } from '@cio/ui/base/switch';
import * as RadioGroup from '@cio/ui/base/radio-group';
import * as Select from '@cio/ui/base/select';
- import * as Dialog from '@cio/ui/base/dialog';
import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw';
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
import XIcon from '@lucide/svelte/icons/x';
import ReorderMaterialTabs from '$features/course/components/reorder-material-tabs.svelte';
+ import CertificateDeadlineRequiredDialog from '$features/course/components/certificate-deadline-required-dialog.svelte';
import { CourseTagPicker } from '$features/course/components';
import { IconButton } from '@cio/ui/custom/icon-button';
import { TextareaField } from '@cio/ui/custom/textarea-field';
@@ -43,6 +43,7 @@
import { handleOpenWidget } from '$features/ui/course-landing-page/store';
import { currentOrgDomain, currentOrgPath, isFreePlan } from '$lib/utils/store/org';
import { page } from '$app/stores';
+ import { ROUTE_NAME, ROUTE_SECTIONS } from '$lib/routing/routes';
interface Props {
hasUnsavedChanges?: boolean;
@@ -519,24 +520,7 @@
-
-
-
- {$t('course.navItem.settings.certificate_deadline_required')}
-
- {$t('course.navItem.settings.certificate_deadline_required_description')}
-
-
-
-
-
-
-
-
+
@@ -705,7 +689,10 @@
{/if}
-
+
{$t('course.navItem.settings.completion_deadline_label')}
diff --git a/apps/dashboard/src/lib/features/course/utils/compliance-deadline.ts b/apps/dashboard/src/lib/features/course/utils/compliance-deadline.ts
new file mode 100644
index 00000000..8277f9e3
--- /dev/null
+++ b/apps/dashboard/src/lib/features/course/utils/compliance-deadline.ts
@@ -0,0 +1,22 @@
+import { isPublishedComplianceMissingDeadline } from '@cio/utils/functions';
+import { goAndHighlight } from '$lib/routing/go-and-highlight';
+import { ROUTE_NAME, ROUTE_SECTIONS } from '$lib/routing/routes';
+import type { Course } from './types';
+
+type CourseDeadlineSource = Pick & {
+ certificate?: { deadline?: string | null } | null;
+};
+
+export function isCourseMissingComplianceDeadline(course: CourseDeadlineSource): boolean {
+ return isPublishedComplianceMissingDeadline({
+ type: course.type,
+ isPublished: true,
+ deadline: course.certificate?.deadline
+ });
+}
+
+export function goToCourseCompletionDeadline(courseId: string) {
+ goAndHighlight(ROUTE_NAME.COURSE_SETTINGS, ROUTE_SECTIONS[ROUTE_NAME.COURSE_SETTINGS].COMPLETION_DEADLINE, {
+ id: courseId
+ });
+}
diff --git a/apps/dashboard/src/lib/features/course/utils/compliance-utils.test.ts b/apps/dashboard/src/lib/features/course/utils/compliance-utils.test.ts
new file mode 100644
index 00000000..a4fef9b0
--- /dev/null
+++ b/apps/dashboard/src/lib/features/course/utils/compliance-utils.test.ts
@@ -0,0 +1,28 @@
+import { shouldShowStudentCourseComplianceStatusBadge, getStudentCourseComplianceStatusKey } from './compliance-utils';
+
+function makeCourse(overrides: { type?: string; complianceStatus?: string | null }) {
+ return {
+ type: overrides.type ?? 'COMPLIANCE',
+ complianceStatus: overrides.complianceStatus
+ } as Parameters[0];
+}
+
+describe('shouldShowStudentCourseComplianceStatusBadge', () => {
+ it('hides the in-progress badge on learner course cards', () => {
+ const course = makeCourse({ complianceStatus: 'in_progress' });
+
+ expect(shouldShowStudentCourseComplianceStatusBadge(course)).toBe(false);
+ expect(getStudentCourseComplianceStatusKey(course)).toBe('course.navItem.compliance.status.in_progress');
+ });
+
+ it('still shows overdue and expiring statuses', () => {
+ expect(shouldShowStudentCourseComplianceStatusBadge(makeCourse({ complianceStatus: 'non_compliant' }))).toBe(true);
+ expect(shouldShowStudentCourseComplianceStatusBadge(makeCourse({ complianceStatus: 'expiring_soon' }))).toBe(true);
+ });
+
+ it('is hidden for non-compliance courses', () => {
+ expect(
+ shouldShowStudentCourseComplianceStatusBadge(makeCourse({ type: 'SELF_PACED', complianceStatus: 'in_progress' }))
+ ).toBe(false);
+ });
+});
diff --git a/apps/dashboard/src/lib/features/course/utils/compliance-utils.ts b/apps/dashboard/src/lib/features/course/utils/compliance-utils.ts
index 4b0bb3bf..afcc5d37 100644
--- a/apps/dashboard/src/lib/features/course/utils/compliance-utils.ts
+++ b/apps/dashboard/src/lib/features/course/utils/compliance-utils.ts
@@ -98,6 +98,12 @@ export function getStudentCourseComplianceStatusKey(course: StudentEnrolledCours
return complianceStatus ? `course.navItem.compliance.status.${complianceStatus}` : null;
}
+export function shouldShowStudentCourseComplianceStatusBadge(course: StudentEnrolledCourse) {
+ const complianceStatus = getStudentCourseComplianceStatus(course);
+
+ return !!complianceStatus && complianceStatus !== 'in_progress';
+}
+
export function getStudentCourseComplianceStatusVariant(course: StudentEnrolledCourse) {
const complianceStatus = getStudentCourseComplianceStatus(course);
if (!complianceStatus) {
diff --git a/apps/dashboard/src/lib/features/course/utils/publish-course.ts b/apps/dashboard/src/lib/features/course/utils/publish-course.ts
index aba34016..689fc3c4 100644
--- a/apps/dashboard/src/lib/features/course/utils/publish-course.ts
+++ b/apps/dashboard/src/lib/features/course/utils/publish-course.ts
@@ -2,10 +2,17 @@ import { generateSlug } from '@cio/utils/functions';
import { isObject } from '$lib/utils/functions/isObject';
import { courseApi } from '$features/course/api';
import type { Course } from '$features/course/utils/types';
+import { isCourseMissingComplianceDeadline } from './compliance-deadline';
-export async function publishCourse(course: Course): Promise {
+export type PublishCourseResult = { ok: true } | { ok: false; reason: 'missing_deadline' | 'failed' };
+
+export async function publishCourse(course: Course): Promise {
if (!course?.id) {
- return false;
+ return { ok: false, reason: 'failed' };
+ }
+
+ if (isCourseMissingComplianceDeadline(course)) {
+ return { ok: false, reason: 'missing_deadline' };
}
let slug = course.slug?.trim() ? course.slug : undefined;
@@ -28,5 +35,13 @@ export async function publishCourse(course: Course): Promise {
{ showSuccessToast: true }
);
- return !!result;
+ if (!result) {
+ if (courseApi.errors['certificate.deadline']) {
+ return { ok: false, reason: 'missing_deadline' };
+ }
+
+ return { ok: false, reason: 'failed' };
+ }
+
+ return { ok: true };
}
diff --git a/apps/dashboard/src/lib/features/lms/components/learning.svelte b/apps/dashboard/src/lib/features/lms/components/learning.svelte
index 88a17bd6..ad4d2697 100644
--- a/apps/dashboard/src/lib/features/lms/components/learning.svelte
+++ b/apps/dashboard/src/lib/features/lms/components/learning.svelte
@@ -11,7 +11,8 @@
getStudentCourseComplianceDate,
getStudentCourseComplianceStatusKey,
getStudentCourseComplianceStatusVariant,
- getStudentCourseProgressPercent
+ getStudentCourseProgressPercent,
+ shouldShowStudentCourseComplianceStatusBadge
} from '$features/course/utils/compliance-utils';
import { getStudentCourseContinuePath } from '$features/course/utils/student-course-navigation';
@@ -60,7 +61,7 @@
>
{#if highlightedCourses.length > 0}
- {#each highlightedCourses as course}
+ {#each highlightedCourses as course (course.id)}
- {#if complianceStatusKey}
+ {#if shouldShowStudentCourseComplianceStatusBadge(course) && complianceStatusKey}
{$t(complianceStatusKey)}
diff --git a/apps/dashboard/src/lib/features/ui/attention-highlight.svelte b/apps/dashboard/src/lib/features/ui/attention-highlight.svelte
index 2bf92bed..4aaa445e 100644
--- a/apps/dashboard/src/lib/features/ui/attention-highlight.svelte
+++ b/apps/dashboard/src/lib/features/ui/attention-highlight.svelte
@@ -1,8 +1,8 @@
-
+
{#if children}
{@render children()}
{/if}
diff --git a/apps/dashboard/src/lib/routing/go-and-highlight.ts b/apps/dashboard/src/lib/routing/go-and-highlight.ts
index f97d5e3c..85aa0277 100644
--- a/apps/dashboard/src/lib/routing/go-and-highlight.ts
+++ b/apps/dashboard/src/lib/routing/go-and-highlight.ts
@@ -21,5 +21,5 @@ export function goAndHighlight(
}
query.set('highlight', sectionName);
- goto(resolve(`${path}?${query.toString()}`, {}));
+ goto(resolve(`${path}?${query.toString()}`, {}), { noScroll: true, keepFocus: true });
}
diff --git a/apps/dashboard/src/lib/routing/routes.ts b/apps/dashboard/src/lib/routing/routes.ts
index 0890d4e5..bb396a1c 100644
--- a/apps/dashboard/src/lib/routing/routes.ts
+++ b/apps/dashboard/src/lib/routing/routes.ts
@@ -14,7 +14,8 @@ export const ROUTE_PATHS = {
export const ROUTE_SECTIONS = {
[ROUTE_NAME.COURSE_SETTINGS]: {
- PUBLISH: 'publish'
+ PUBLISH: 'publish',
+ COMPLETION_DEADLINE: 'course-completion-deadline'
},
[ROUTE_NAME.COURSE_CERTIFICATE]: {