diff --git a/apps/api/src/__tests__/reset-member-course-progress.test.ts b/apps/api/src/__tests__/reset-member-course-progress.test.ts new file mode 100644 index 00000000..6126d3fa --- /dev/null +++ b/apps/api/src/__tests__/reset-member-course-progress.test.ts @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@api/services/course/compliance', () => ({ + ensureComplianceEnrollmentRecordsForProfiles: vi.fn() +})); + +vi.mock('@cio/db/queries/course/people', () => ({ + getCourseMember: vi.fn(), + getCourseMembers: vi.fn(), + addCourseMember: vi.fn(), + deleteCourseMember: vi.fn(), + updateCourseMember: vi.fn(), + getCourseTeachers: vi.fn() +})); + +vi.mock('@cio/db/queries/course/reset-progress', () => ({ + resetStudentCourseProgress: vi.fn() +})); + +import { AppError } from '@api/utils/errors'; +import { ROLE } from '@cio/utils/constants'; +import { getCourseMember } from '@cio/db/queries/course/people'; +import { resetStudentCourseProgress } from '@cio/db/queries/course/reset-progress'; +import { resetMemberCourseProgress } from '@api/services/course/people'; + +describe('resetMemberCourseProgress', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('returns 404 when the member is missing', async () => { + vi.mocked(getCourseMember).mockResolvedValue(null); + + await expect(resetMemberCourseProgress('course-1', 'member-1', 'actor-1')).rejects.toMatchObject({ + statusCode: 404 + }); + }); + + it('returns 404 when the member has no profileId', async () => { + vi.mocked(getCourseMember).mockResolvedValue({ + id: 'member-1', + profileId: null, + roleId: ROLE.STUDENT + } as Awaited>); + + await expect(resetMemberCourseProgress('course-1', 'member-1', 'actor-1')).rejects.toBeInstanceOf(AppError); + }); + + it('returns 400 when the member is not a student', async () => { + vi.mocked(getCourseMember).mockResolvedValue({ + id: 'member-1', + profileId: 'profile-1', + roleId: ROLE.TUTOR + } as Awaited>); + + await expect(resetMemberCourseProgress('course-1', 'member-1', 'actor-1')).rejects.toMatchObject({ + statusCode: 400 + }); + expect(resetStudentCourseProgress).not.toHaveBeenCalled(); + }); + + it('resets progress and returns the summary', async () => { + const summary = { + lessonCompletion: 2, + lessonVideoProgress: 1, + submissions: 3, + groupAttendance: 0, + lessonComments: 1, + courseNewsfeed: 0, + courseNewsfeedComments: 0, + courseCertificateIssues: 0, + courseCompletionRecords: 0, + aiChatConversations: 0 + }; + + vi.mocked(getCourseMember).mockResolvedValue({ + id: 'member-1', + profileId: 'profile-1', + roleId: ROLE.STUDENT + } as Awaited>); + vi.mocked(resetStudentCourseProgress).mockResolvedValue(summary); + + const result = await resetMemberCourseProgress('course-1', 'member-1', 'actor-1'); + + expect(resetStudentCourseProgress).toHaveBeenCalledWith({ + courseId: 'course-1', + groupMemberId: 'member-1', + profileId: 'profile-1' + }); + expect(result).toEqual(summary); + }); +}); diff --git a/apps/api/src/routes/course/people.ts b/apps/api/src/routes/course/people.ts index 85034240..c5e28f36 100644 --- a/apps/api/src/routes/course/people.ts +++ b/apps/api/src/routes/course/people.ts @@ -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'); + } } - }); + ); diff --git a/apps/api/src/services/course/people.ts b/apps/api/src/services/course/people.ts index 8c725788..100ef299 100644 --- a/apps/api/src/services/course/people.ts +++ b/apps/api/src/services/course/people.ts @@ -1,3 +1,4 @@ +import { ROLE } from '@cio/utils/constants'; import { AppError, ErrorCodes } from '@api/utils/errors'; import { addCourseMember, @@ -7,8 +8,8 @@ import { getCourseTeachers, updateCourseMember } from '@cio/db/queries/course/people'; +import { resetStudentCourseProgress } from '@cio/db/queries/course/reset-progress'; -import { ROLE } from '@cio/utils/constants'; import type { TAddCourseMembers } from '@cio/utils/validation/course/people'; import type { TGroupmember } from '@cio/db/types'; import { getDashboardBaseUrl } from '@cio/core/config/dashboard-url'; @@ -316,3 +317,43 @@ export async function deleteMember(courseId: string, memberId: string) { ); } } + +/** + * Resets all learner progress for a course member while keeping them enrolled. + */ +export async function resetMemberCourseProgress(courseId: string, memberId: string, actorProfileId: string) { + try { + const member = await getCourseMember(courseId, memberId); + if (!member || !member.profileId) { + throw new AppError('Course member not found', ErrorCodes.NOT_FOUND, 404); + } + + if (member.roleId !== ROLE.STUDENT) { + throw new AppError('Only student progress can be reset', ErrorCodes.VALIDATION_ERROR, 400); + } + + const summary = await resetStudentCourseProgress({ + courseId, + groupMemberId: memberId, + profileId: member.profileId + }); + + console.info('resetMemberCourseProgress', { + actorProfileId, + courseId, + memberId, + summary + }); + + return summary; + } catch (error) { + if (error instanceof AppError) { + throw error; + } + throw new AppError( + error instanceof Error ? error.message : 'Failed to reset course member progress', + ErrorCodes.INTERNAL_ERROR, + 500 + ); + } +} diff --git a/apps/dashboard/src/lib/features/course/api/people.svelte.ts b/apps/dashboard/src/lib/features/course/api/people.svelte.ts index c3238c2f..eee29a9a 100644 --- a/apps/dashboard/src/lib/features/course/api/people.svelte.ts +++ b/apps/dashboard/src/lib/features/course/api/people.svelte.ts @@ -6,6 +6,7 @@ import type { GetUserCourseAnalyticsRequest, ListStudentInvitesRequest, ListPeopleRequest, + ResetMemberCourseProgressRequest, RevokeStudentInviteRequest, UpdatePeopleRequest } from '../utils/types'; @@ -290,6 +291,29 @@ export class PeopleApi extends BaseApiWithErrors { } }); } + + /** + * Resets all learner progress for a course member while keeping them enrolled. + */ + async resetCourseProgress(courseId: string, memberId: string) { + await this.execute({ + requestFn: () => + classroomio.course[':courseId']['members'][':memberId']['reset-progress'].$post({ + param: { courseId, memberId } + }), + logContext: 'resetting course member progress', + onSuccess: () => { + snackbar.success('snackbar.course.people.progress_reset'); + this.success = true; + this.errors = {}; + }, + onError: (result) => { + if (typeof result === 'string') { + snackbar.error('snackbar.course.people.progress_reset_failed'); + } + } + }); + } } export const peopleApi = /* @__PURE__ */ new PeopleApi(); diff --git a/apps/dashboard/src/lib/features/course/components/people/reset-progress-button.svelte b/apps/dashboard/src/lib/features/course/components/people/reset-progress-button.svelte new file mode 100644 index 00000000..b8f83acd --- /dev/null +++ b/apps/dashboard/src/lib/features/course/components/people/reset-progress-button.svelte @@ -0,0 +1,33 @@ + + + + +{#if canReset} + +{/if} diff --git a/apps/dashboard/src/lib/features/course/components/people/reset-progress-dialog.svelte b/apps/dashboard/src/lib/features/course/components/people/reset-progress-dialog.svelte new file mode 100644 index 00000000..af07ae6e --- /dev/null +++ b/apps/dashboard/src/lib/features/course/components/people/reset-progress-dialog.svelte @@ -0,0 +1,124 @@ + + + + + + + {$t('course.navItem.people.reset_progress.dialog.title', { studentName })} + + + {$t('course.navItem.people.reset_progress.dialog.intro')} + + + +
    + {#each impactBullets as bullet (bullet.key)} +
  • {bullet.text}
  • + {/each} +
+ +

+ {$t('course.navItem.people.reset_progress.dialog.warning')} +

+ + + + + +
+
diff --git a/apps/dashboard/src/lib/features/course/utils/types.ts b/apps/dashboard/src/lib/features/course/utils/types.ts index 06f670e6..e17db6df 100644 --- a/apps/dashboard/src/lib/features/course/utils/types.ts +++ b/apps/dashboard/src/lib/features/course/utils/types.ts @@ -404,6 +404,12 @@ export type GetUserCourseAnalyticsResponse = InferResponseType; export type UserCourseAnalytics = GetUserCourseAnalyticsSuccess['data']; +export type ResetMemberCourseProgressRequest = + (typeof classroomio.course)[':courseId']['members'][':memberId']['reset-progress']['$post']; +export type ResetMemberCourseProgressResponse = InferResponseType; +export type ResetMemberCourseProgressSuccess = Extract; +export type ResetMemberCourseProgressSummary = ResetMemberCourseProgressSuccess['data']; + export type GetCourseAnalyticsRequest = (typeof classroomio.course)[':courseId']['analytics']['$get']; export type GetCourseAnalyticsResponse = InferResponseType; export type GetCourseAnalyticsSuccess = Extract; diff --git a/apps/dashboard/src/lib/utils/translations/da.json b/apps/dashboard/src/lib/utils/translations/da.json index bde2ec6b..9c25e300 100644 --- a/apps/dashboard/src/lib/utils/translations/da.json +++ b/apps/dashboard/src/lib/utils/translations/da.json @@ -242,6 +242,10 @@ "unpinned": "Frigjort", "successfully": "Med succes", "feed_delete_success": "Feedet blev slettet" + }, + "people": { + "progress_reset": "Fremskridt nulstillet", + "progress_reset_failed": "Kunne ikke nulstille fremskridt" } }, "landing_page_settings": { @@ -1100,6 +1104,26 @@ "no": "Nej", "yes": "Ja" }, + "reset_progress": { + "button": "Nulstil fremskridt", + "dialog": { + "title": "Nulstil fremskridt for {studentName}?", + "intro": "Dette sletter permanent denne elevs fremskridt i dette kursus. Eleven forbliver tilmeldt og kan starte forfra fra lektion 1.", + "bullets": { + "lessons": "Gennemførte lektioner — {completed} af {total}", + "exercises": "Afleverede øvelser — {count} afleveringer", + "comments": "Lektionskommentarer fra denne elev — {count}", + "newsfeed": "Kursusnyhedsfeed-opslag og svar fra denne elev — {count}", + "video_progress": "Videofremskridt — {count} lektioner sporet", + "attendance": "Fremmødeposter — {count}", + "certificate_yes": "Certificerings-/compliance-data — ja", + "certificate_no": "Certificerings-/compliance-data — nej" + }, + "warning": "Dette kan ikke fortrydes.", + "cancel": "Annuller", + "confirm": "Slet" + } + }, "invite_modal": { "title": "Inviter folk", "invite": "Inviter undervisere", diff --git a/apps/dashboard/src/lib/utils/translations/de.json b/apps/dashboard/src/lib/utils/translations/de.json index 05522897..2a6c9f46 100644 --- a/apps/dashboard/src/lib/utils/translations/de.json +++ b/apps/dashboard/src/lib/utils/translations/de.json @@ -242,6 +242,10 @@ "unpinned": "Nicht angepinnt", "successfully": "Erfolgreich", "feed_delete_success": "Feed erfolgreich gelöscht" + }, + "people": { + "progress_reset": "Fortschritt zurückgesetzt", + "progress_reset_failed": "Fortschritt konnte nicht zurückgesetzt werden" } }, "landing_page_settings": { @@ -1100,6 +1104,26 @@ "no": "Nein", "yes": "Ja" }, + "reset_progress": { + "button": "Fortschritt zurücksetzen", + "dialog": { + "title": "Fortschritt von {studentName} zurücksetzen?", + "intro": "Dadurch wird der Fortschritt dieses Teilnehmers in diesem Kurs dauerhaft gelöscht. Die Person bleibt eingeschrieben und kann bei Lektion 1 neu beginnen.", + "bullets": { + "lessons": "Abgeschlossene Lektionen — {completed} von {total}", + "exercises": "Eingereichte Übungen — {count} Abgaben", + "comments": "Lektionskommentare dieses Teilnehmers — {count}", + "newsfeed": "Kurs-Newsfeed-Beiträge und Antworten dieses Teilnehmers — {count}", + "video_progress": "Videofortschritt — {count} verfolgte Lektionen", + "attendance": "Anwesenheitseinträge — {count}", + "certificate_yes": "Zertifizierungs-/Compliance-Daten — ja", + "certificate_no": "Zertifizierungs-/Compliance-Daten — nein" + }, + "warning": "Dies kann nicht rückgängig gemacht werden.", + "cancel": "Abbrechen", + "confirm": "Löschen" + } + }, "invite_modal": { "title": "Leute einladen", "invite": "Laden Sie Tutoren ein", diff --git a/apps/dashboard/src/lib/utils/translations/en.json b/apps/dashboard/src/lib/utils/translations/en.json index 2473ed00..10882284 100644 --- a/apps/dashboard/src/lib/utils/translations/en.json +++ b/apps/dashboard/src/lib/utils/translations/en.json @@ -3308,6 +3308,26 @@ "no": "No", "yes": "Yes" }, + "reset_progress": { + "button": "Reset progress", + "dialog": { + "title": "Reset progress for {studentName}?", + "intro": "This permanently deletes this student's progress in this course. They will stay enrolled and can restart from lesson 1.", + "bullets": { + "lessons": "Lessons completed — {completed} of {total}", + "exercises": "Exercises submitted — {count} submissions", + "comments": "Lesson comments by this student — {count}", + "newsfeed": "Course newsfeed posts and replies by this student — {count}", + "video_progress": "Video watch progress — {count} lessons tracked", + "attendance": "Attendance entries — {count}", + "certificate_yes": "Certification / compliance records — yes", + "certificate_no": "Certification / compliance records — no" + }, + "warning": "This cannot be undone.", + "cancel": "Cancel", + "confirm": "Delete" + } + }, "roles": { "admin": "Admin", "tutor": "Tutor", @@ -4196,6 +4216,10 @@ "toggle_error": "Failed to toggle pinned feed", "news_feed_fail": "Failed to fetch news feeds", "feed_fetch_error": "An error occured while fetching feed" + }, + "people": { + "progress_reset": "Progress reset", + "progress_reset_failed": "Failed to reset progress" } }, "lms": { diff --git a/apps/dashboard/src/lib/utils/translations/es.json b/apps/dashboard/src/lib/utils/translations/es.json index fc8e67d0..3995d431 100644 --- a/apps/dashboard/src/lib/utils/translations/es.json +++ b/apps/dashboard/src/lib/utils/translations/es.json @@ -242,6 +242,10 @@ "unpinned": "Desanclar", "successfully": "Con éxito", "feed_delete_success": "Feed eliminado correctamente" + }, + "people": { + "progress_reset": "Progreso restablecido", + "progress_reset_failed": "No se pudo restablecer el progreso" } }, "landing_page_settings": { @@ -1100,6 +1104,26 @@ "no": "No", "yes": "si" }, + "reset_progress": { + "button": "Restablecer progreso", + "dialog": { + "title": "¿Restablecer el progreso de {studentName}?", + "intro": "Esto elimina permanentemente el progreso de este estudiante en este curso. Seguirá inscrito y podrá empezar de nuevo desde la lección 1.", + "bullets": { + "lessons": "Lecciones completadas — {completed} de {total}", + "exercises": "Ejercicios enviados — {count} entregas", + "comments": "Comentarios de lección de este estudiante — {count}", + "newsfeed": "Publicaciones y respuestas del feed del curso de este estudiante — {count}", + "video_progress": "Progreso de visualización de video — {count} lecciones registradas", + "attendance": "Registros de asistencia — {count}", + "certificate_yes": "Registros de certificación/cumplimiento — sí", + "certificate_no": "Registros de certificación/cumplimiento — no" + }, + "warning": "Esto no se puede deshacer.", + "cancel": "Cancelar", + "confirm": "Eliminar" + } + }, "invite_modal": { "title": "invitar gente", "invite": "Invitar tutores", diff --git a/apps/dashboard/src/lib/utils/translations/fr.json b/apps/dashboard/src/lib/utils/translations/fr.json index ebf30ec4..c2eaa8e6 100644 --- a/apps/dashboard/src/lib/utils/translations/fr.json +++ b/apps/dashboard/src/lib/utils/translations/fr.json @@ -242,6 +242,10 @@ "unpinned": "Désépinglé", "successfully": "Avec succès", "feed_delete_success": "Flux supprimé avec succès" + }, + "people": { + "progress_reset": "Progression réinitialisée", + "progress_reset_failed": "Échec de la réinitialisation de la progression" } }, "landing_page_settings": { @@ -1100,6 +1104,26 @@ "no": "Non", "yes": "Oui" }, + "reset_progress": { + "button": "Réinitialiser la progression", + "dialog": { + "title": "Réinitialiser la progression de {studentName} ?", + "intro": "Cela supprime définitivement la progression de cet apprenant dans ce cours. Il restera inscrit et pourra recommencer à la leçon 1.", + "bullets": { + "lessons": "Leçons terminées — {completed} sur {total}", + "exercises": "Exercices soumis — {count} soumissions", + "comments": "Commentaires de leçon de cet apprenant — {count}", + "newsfeed": "Publications et réponses du fil d’actualité du cours de cet apprenant — {count}", + "video_progress": "Progression de visionnage vidéo — {count} leçons suivies", + "attendance": "Entrées de présence — {count}", + "certificate_yes": "Dossiers de certification/conformité — oui", + "certificate_no": "Dossiers de certification/conformité — non" + }, + "warning": "Cette action est irréversible.", + "cancel": "Annuler", + "confirm": "Supprimer" + } + }, "invite_modal": { "title": "Inviter des personnes", "invite": "Inviter des tuteurs", diff --git a/apps/dashboard/src/lib/utils/translations/hi.json b/apps/dashboard/src/lib/utils/translations/hi.json index e1aa0e41..508fb422 100644 --- a/apps/dashboard/src/lib/utils/translations/hi.json +++ b/apps/dashboard/src/lib/utils/translations/hi.json @@ -242,6 +242,10 @@ "unpinned": "अनपिन किया गया", "successfully": "सफलतापूर्वक", "feed_delete_success": "फ़ीड सफलतापूर्वक हटा दी गई" + }, + "people": { + "progress_reset": "प्रगति रीसेट की गई", + "progress_reset_failed": "प्रगति रीसेट नहीं हो सकी" } }, "landing_page_settings": { @@ -1100,6 +1104,26 @@ "no": "नहीं", "yes": "हाँ" }, + "reset_progress": { + "button": "प्रगति रीसेट करें", + "dialog": { + "title": "{studentName} की प्रगति रीसेट करें?", + "intro": "यह इस कोर्स में इस छात्र की प्रगति को स्थायी रूप से हटा देगा। वे नामांकित रहेंगे और पाठ 1 से फिर से शुरू कर सकेंगे।", + "bullets": { + "lessons": "पूर्ण पाठ — {completed} में से {total}", + "exercises": "जमा किए गए अभ्यास — {count} सबमिशन", + "comments": "इस छात्र के पाठ टिप्पणियाँ — {count}", + "newsfeed": "इस छात्र की कोर्स न्यूज़फ़ीड पोस्ट और जवाब — {count}", + "video_progress": "वीडियो देखने की प्रगति — {count} ट्रैक किए गए पाठ", + "attendance": "उपस्थिति प्रविष्टियाँ — {count}", + "certificate_yes": "प्रमाणन/अनुपालन रिकॉर्ड — हाँ", + "certificate_no": "प्रमाणन/अनुपालन रिकॉर्ड — नहीं" + }, + "warning": "इसे पूर्ववत नहीं किया जा सकता।", + "cancel": "रद्द करें", + "confirm": "हटाएँ" + } + }, "invite_modal": { "title": "लोगों को आमंत्रित करें", "invite": "शिक्षकों को आमंत्रित करें", diff --git a/apps/dashboard/src/lib/utils/translations/pl.json b/apps/dashboard/src/lib/utils/translations/pl.json index ebc3ca4c..826f03dd 100644 --- a/apps/dashboard/src/lib/utils/translations/pl.json +++ b/apps/dashboard/src/lib/utils/translations/pl.json @@ -242,6 +242,10 @@ "unpinned": "Odpięty", "successfully": "Pomyślnie", "feed_delete_success": "Kanał został pomyślnie usunięty" + }, + "people": { + "progress_reset": "Postęp został zresetowany", + "progress_reset_failed": "Nie udało się zresetować postępu" } }, "landing_page_settings": { @@ -1100,6 +1104,26 @@ "no": "Nie", "yes": "Tak" }, + "reset_progress": { + "button": "Zresetuj postęp", + "dialog": { + "title": "Zresetować postęp użytkownika {studentName}?", + "intro": "Spowoduje to trwałe usunięcie postępu tego uczestnika w tym kursie. Pozostanie zapisany i będzie mógł zacząć od lekcji 1.", + "bullets": { + "lessons": "Ukończone lekcje — {completed} z {total}", + "exercises": "Przesłane ćwiczenia — {count} zgłoszeń", + "comments": "Komentarze do lekcji tego uczestnika — {count}", + "newsfeed": "Wpisy i odpowiedzi na kanale kursu tego uczestnika — {count}", + "video_progress": "Postęp oglądania wideo — {count} śledzonych lekcji", + "attendance": "Wpisy obecności — {count}", + "certificate_yes": "Rekordy certyfikacji/zgodności — tak", + "certificate_no": "Rekordy certyfikacji/zgodności — nie" + }, + "warning": "Tej operacji nie można cofnąć.", + "cancel": "Anuluj", + "confirm": "Usuń" + } + }, "invite_modal": { "title": "Zaproś ludzi", "invite": "Zaproś korepetytorów", diff --git a/apps/dashboard/src/lib/utils/translations/pt.json b/apps/dashboard/src/lib/utils/translations/pt.json index 955d91c1..926440c7 100644 --- a/apps/dashboard/src/lib/utils/translations/pt.json +++ b/apps/dashboard/src/lib/utils/translations/pt.json @@ -242,6 +242,10 @@ "unpinned": "Desafixado", "successfully": "Com sucesso", "feed_delete_success": "Feed excluído com sucesso" + }, + "people": { + "progress_reset": "Progresso redefinido", + "progress_reset_failed": "Falha ao redefinir o progresso" } }, "landing_page_settings": { @@ -1100,6 +1104,26 @@ "no": "Não", "yes": "Sim" }, + "reset_progress": { + "button": "Redefinir progresso", + "dialog": { + "title": "Redefinir o progresso de {studentName}?", + "intro": "Isso exclui permanentemente o progresso deste aluno neste curso. Ele permanecerá matriculado e poderá recomeçar da lição 1.", + "bullets": { + "lessons": "Lições concluídas — {completed} de {total}", + "exercises": "Exercícios enviados — {count} envios", + "comments": "Comentários de lição deste aluno — {count}", + "newsfeed": "Publicações e respostas no feed do curso deste aluno — {count}", + "video_progress": "Progresso de visualização de vídeo — {count} lições acompanhadas", + "attendance": "Registros de presença — {count}", + "certificate_yes": "Registros de certificação/conformidade — sim", + "certificate_no": "Registros de certificação/conformidade — não" + }, + "warning": "Isso não pode ser desfeito.", + "cancel": "Cancelar", + "confirm": "Excluir" + } + }, "invite_modal": { "title": "Convide pessoas", "invite": "Convidar tutores", diff --git a/apps/dashboard/src/lib/utils/translations/ru.json b/apps/dashboard/src/lib/utils/translations/ru.json index 7d4e243c..5f43c6ad 100644 --- a/apps/dashboard/src/lib/utils/translations/ru.json +++ b/apps/dashboard/src/lib/utils/translations/ru.json @@ -242,6 +242,10 @@ "unpinned": "Откреплено", "successfully": "Успешно", "feed_delete_success": "Фид успешно удален" + }, + "people": { + "progress_reset": "Прогресс сброшен", + "progress_reset_failed": "Не удалось сбросить прогресс" } }, "landing_page_settings": { @@ -1100,6 +1104,26 @@ "no": "Нет", "yes": "Да" }, + "reset_progress": { + "button": "Сбросить прогресс", + "dialog": { + "title": "Сбросить прогресс для {studentName}?", + "intro": "Это навсегда удалит прогресс этого студента в данном курсе. Он останется зачисленным и сможет начать заново с урока 1.", + "bullets": { + "lessons": "Завершённые уроки — {completed} из {total}", + "exercises": "Отправленные упражнения — {count} отправок", + "comments": "Комментарии к урокам этого студента — {count}", + "newsfeed": "Посты и ответы в ленте курса от этого студента — {count}", + "video_progress": "Прогресс просмотра видео — {count} отслеживаемых уроков", + "attendance": "Записи посещаемости — {count}", + "certificate_yes": "Записи сертификации/соответствия — да", + "certificate_no": "Записи сертификации/соответствия — нет" + }, + "warning": "Это действие нельзя отменить.", + "cancel": "Отмена", + "confirm": "Удалить" + } + }, "invite_modal": { "title": "Пригласить людей", "invite": "Пригласить репетиторов", diff --git a/apps/dashboard/src/lib/utils/translations/vi.json b/apps/dashboard/src/lib/utils/translations/vi.json index 2fc5401b..ed1e6bbd 100644 --- a/apps/dashboard/src/lib/utils/translations/vi.json +++ b/apps/dashboard/src/lib/utils/translations/vi.json @@ -242,6 +242,10 @@ "unpinned": "Đã bỏ ghim", "successfully": "thành công", "feed_delete_success": "Đã xóa nguồn cấp dữ liệu thành công" + }, + "people": { + "progress_reset": "Đã đặt lại tiến độ", + "progress_reset_failed": "Không thể đặt lại tiến độ" } }, "landing_page_settings": { @@ -1100,6 +1104,26 @@ "no": "Không", "yes": "Có" }, + "reset_progress": { + "button": "Đặt lại tiến độ", + "dialog": { + "title": "Đặt lại tiến độ cho {studentName}?", + "intro": "Thao tác này xóa vĩnh viễn tiến độ của học viên này trong khóa học. Họ vẫn được ghi danh và có thể bắt đầu lại từ bài 1.", + "bullets": { + "lessons": "Bài học đã hoàn thành — {completed}/{total}", + "exercises": "Bài tập đã nộp — {count} bài nộp", + "comments": "Bình luận bài học của học viên này — {count}", + "newsfeed": "Bài đăng và phản hồi bảng tin khóa học của học viên này — {count}", + "video_progress": "Tiến độ xem video — {count} bài học được theo dõi", + "attendance": "Mục điểm danh — {count}", + "certificate_yes": "Hồ sơ chứng nhận/tuân thủ — có", + "certificate_no": "Hồ sơ chứng nhận/tuân thủ — không" + }, + "warning": "Không thể hoàn tác.", + "cancel": "Hủy", + "confirm": "Xóa" + } + }, "invite_modal": { "title": "Mời mọi người", "invite": "Mời gia sư", diff --git a/apps/dashboard/src/routes/(app)/courses/[id]/people/+layout.svelte b/apps/dashboard/src/routes/(app)/courses/[id]/people/+layout.svelte index 70054d6b..00898200 100644 --- a/apps/dashboard/src/routes/(app)/courses/[id]/people/+layout.svelte +++ b/apps/dashboard/src/routes/(app)/courses/[id]/people/+layout.svelte @@ -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(); + } @@ -49,6 +57,16 @@
+ {#if data.personId && userCourseAnalytics} + + + + {/if} {#if !data.personId}