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
@@ -2,12 +2,19 @@ import {
|
||||
ZAddCourseMembers,
|
||||
ZCourseMembersMemberParam,
|
||||
ZCourseMembersParam,
|
||||
ZResetCourseMemberProgressParam,
|
||||
ZUpdateCourseMember
|
||||
} from '@cio/utils/validation/course/people';
|
||||
import { addMembers, deleteMember, listCourseMembers, updateMember } from '@api/services/course/people';
|
||||
import {
|
||||
addMembers,
|
||||
deleteMember,
|
||||
listCourseMembers,
|
||||
resetMemberCourseProgress,
|
||||
updateMember
|
||||
} from '@api/services/course/people';
|
||||
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import { ZCourseUserAnalyticsParam } from '@cio/utils/validation/course';
|
||||
import { ZCourseUserAnalyticsParam, ZCourseUserAnalyticsQuery } from '@cio/utils/validation/course';
|
||||
import { courseTeamMemberMiddleware } from '@api/middlewares/course-team-member';
|
||||
import { getUserCourseAnalytics } from '@cio/core/services/course/course';
|
||||
import { handleError } from '@api/utils/errors';
|
||||
@@ -115,24 +122,58 @@ export const membersRouter = new Hono()
|
||||
return handleError(c, error, 'Failed to delete course member');
|
||||
}
|
||||
})
|
||||
/**
|
||||
* POST /course/:courseId/members/:memberId/reset-progress
|
||||
* Clears all learner progress for a course member while keeping them enrolled.
|
||||
* Requires authentication and course team membership (admin/tutor role)
|
||||
*/
|
||||
.post(
|
||||
'/:memberId/reset-progress',
|
||||
courseTeamMemberMiddleware,
|
||||
zValidator('param', ZResetCourseMemberProgressParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const user = c.get('user')!;
|
||||
const { courseId, memberId } = c.req.valid('param');
|
||||
const summary = await resetMemberCourseProgress(courseId, memberId, user.id);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: summary
|
||||
},
|
||||
200
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to reset course member progress');
|
||||
}
|
||||
}
|
||||
)
|
||||
/**
|
||||
* GET /course/:courseId/members/:userId/analytics
|
||||
* Gets user course analytics for a specific course
|
||||
* Requires authentication and course membership
|
||||
*/
|
||||
.get('/:userId/analytics', courseTeamMemberMiddleware, zValidator('param', ZCourseUserAnalyticsParam), async (c) => {
|
||||
try {
|
||||
const { courseId, userId } = c.req.valid('param');
|
||||
const analytics = await getUserCourseAnalytics(courseId, userId);
|
||||
.get(
|
||||
'/:userId/analytics',
|
||||
courseTeamMemberMiddleware,
|
||||
zValidator('param', ZCourseUserAnalyticsParam),
|
||||
zValidator('query', ZCourseUserAnalyticsQuery),
|
||||
async (c) => {
|
||||
try {
|
||||
const { courseId, userId } = c.req.valid('param');
|
||||
const { includeProgressImpact } = c.req.valid('query');
|
||||
const analytics = await getUserCourseAnalytics(courseId, userId, { includeProgressImpact });
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: analytics
|
||||
},
|
||||
200
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to fetch user course analytics');
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: analytics
|
||||
},
|
||||
200
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to fetch user course analytics');
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user