feat(course): reset student course progress (#671)
* feat(course): add reset student course progress action Give course admins and tutors a destructive reset-progress flow on the student people detail page. The backend clears learner progress for one (courseId, memberId) pair in a single transaction while keeping the groupmember enrolled, and logs an audit summary. The dashboard shows a confirmation dialog with impact counts from analytics before deleting. Co-authored-by: Rotimi Best <[email protected]> * fix(i18n): add localized reset progress strings for all locales Co-authored-by: Rotimi Best <[email protected]> * fix(course): scope reset progress to course data only Stop deleting org-level community questions and answers during course progress reset. Fix course newsfeed deletion order so comments on a student's posts are removed before the posts themselves. Restrict reset to student members, move analytics loading to the person detail layout, and rename impact copy to course newsfeed only. Co-authored-by: Rotimi Best <[email protected]> * fix(course): address Greptile review on reset progress Make progressImpact opt-in via includeProgressImpact query param so analytics requests avoid extra DB work unless the reset dialog needs it. Count only student-authored course newsfeed posts and comments in the impact summary to avoid double-counting replies on the student's posts. Co-authored-by: Rotimi Best <[email protected]> --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
co-authored by
Rotimi Best
Cursor Agent
parent
e43d8a2444
commit
a695c97eda
@@ -10,9 +10,13 @@
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
import { courseApi } from '$features/course/api';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import ResetProgressButton from '$features/course/components/people/reset-progress-button.svelte';
|
||||
import type { UserCourseAnalytics } from '$features/course/utils/types';
|
||||
|
||||
let { data = $bindable(), children } = $props();
|
||||
|
||||
let userCourseAnalytics = $derived(page.data.userCourseAnalytics as UserCourseAnalytics | null | undefined);
|
||||
|
||||
// Get back URL from query parameters
|
||||
let backUrl = $derived(page.url.searchParams.get('back'));
|
||||
|
||||
@@ -31,6 +35,10 @@
|
||||
async function refreshPeoplePage() {
|
||||
await Promise.all([courseApi.refreshCourse(data.courseId, $profile.id), invalidateAll()]);
|
||||
}
|
||||
|
||||
async function handleProgressReset() {
|
||||
await invalidateAll();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Page.Root class="mx-auto w-[90%] md:max-w-3xl">
|
||||
@@ -49,6 +57,16 @@
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if data.personId && userCourseAnalytics}
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<ResetProgressButton
|
||||
courseId={data.courseId}
|
||||
personId={data.personId}
|
||||
{userCourseAnalytics}
|
||||
onSuccess={handleProgressReset}
|
||||
/>
|
||||
</RoleBasedSecurity>
|
||||
{/if}
|
||||
{#if !data.personId}
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<Button onclick={handleClick}>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { classroomio, getApiHeaders } from '$lib/utils/services/api';
|
||||
import { safeServerApi } from '$lib/utils/services/api/server';
|
||||
|
||||
import type { GetUserCourseAnalyticsSuccess, UserCourseAnalytics } from '$features/course/utils/types';
|
||||
|
||||
export const load = async ({ params, cookies }) => {
|
||||
const courseId = params.id || '';
|
||||
const personId = params.personId || '';
|
||||
|
||||
if (!courseId || !personId) {
|
||||
return {
|
||||
userCourseAnalytics: null
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await safeServerApi<GetUserCourseAnalyticsSuccess>(() =>
|
||||
classroomio.course[':courseId']['members'][':userId']['analytics'].$get(
|
||||
{
|
||||
param: { courseId, userId: personId },
|
||||
query: { includeProgressImpact: 'true' }
|
||||
},
|
||||
getApiHeaders(cookies, '')
|
||||
)
|
||||
);
|
||||
const userCourseAnalytics: UserCourseAnalytics | null = result.ok && result.body.data ? result.body.data : null;
|
||||
|
||||
return {
|
||||
userCourseAnalytics
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user course analytics:', error);
|
||||
return {
|
||||
userCourseAnalytics: null
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,42 +1,9 @@
|
||||
import { classroomio, getApiHeaders } from '$lib/utils/services/api';
|
||||
import { safeServerApi } from '$lib/utils/services/api/server';
|
||||
export const load = async ({ parent }) => {
|
||||
const parentData = await parent();
|
||||
|
||||
import type { GetUserCourseAnalyticsSuccess, UserCourseAnalytics } from '$features/course/utils/types';
|
||||
|
||||
export const load = async ({ params, cookies }) => {
|
||||
const courseId = params.id || '';
|
||||
const userId = params.personId;
|
||||
|
||||
if (!courseId || !userId) {
|
||||
return {
|
||||
courseId,
|
||||
userId,
|
||||
userCourseAnalytics: null
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await safeServerApi<GetUserCourseAnalyticsSuccess>(() =>
|
||||
classroomio.course[':courseId']['members'][':userId']['analytics'].$get(
|
||||
{
|
||||
param: { courseId, userId }
|
||||
},
|
||||
getApiHeaders(cookies, '')
|
||||
)
|
||||
);
|
||||
const userCourseAnalytics: UserCourseAnalytics | null = result.ok && result.body.data ? result.body.data : null;
|
||||
|
||||
return {
|
||||
courseId,
|
||||
userId,
|
||||
userCourseAnalytics
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user course analytics:', error);
|
||||
return {
|
||||
courseId,
|
||||
userId,
|
||||
userCourseAnalytics: null
|
||||
};
|
||||
}
|
||||
return {
|
||||
courseId: parentData.courseId,
|
||||
userId: parentData.personId,
|
||||
userCourseAnalytics: parentData.userCourseAnalytics
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user