feat(admin): add user vocab course management functionality
All checks were successful
Deploy to production / deploy (push) Successful in 2m59s
All checks were successful
Deploy to production / deploy (push) Successful in 2m59s
- Implemented `getUserVocabCourses` and `getVocabCourseForAdmin` methods in `AdminController` to allow admins to retrieve enrolled vocab courses for users and specific course details, respectively. - Updated `adminRouter` to include new routes for accessing user vocab courses and course details. - Enhanced `AdminService` with methods to list user-enrolled vocab courses and retrieve course information with lessons, ensuring proper access control. - Improved `VocabService` to support the new functionalities, including attaching language names to course data. - Updated UI components in `UsersView` to reflect changes, including error handling and loading states for course retrieval, along with localization updates for new features.
This commit is contained in:
@@ -23,8 +23,8 @@
|
||||
},
|
||||
"vocabLessonReset": {
|
||||
"title": "Kurso sa pinulongan: pag-uswag sa leksiyon",
|
||||
"intro": "Tangtanga ang pag-uswag, mga resulta sa ehersisyo ug natipig nga kahimtang sa usa ka leksiyon lamang (dili ang tibuok kurso). Makita ra ang mga kurso nga makita sa imong admin account (publiko o imoha).",
|
||||
"loadCourses": "Ikarga ang mga kurso",
|
||||
"intro": "Tangtanga ang pag-uswag, mga resulta sa ehersisyo ug natipig nga kahimtang sa usa ka leksiyon lamang (dili ang tibuok kurso). Makita ra ang mga kurso nga na-enroll niini nga tiggamit.",
|
||||
"loadCourses": "Ikarga ang na-enroll nga mga kurso",
|
||||
"selectCourse": "Kurso",
|
||||
"selectLesson": "Leksiyon",
|
||||
"reset": "I-reset ang leksiyon niining user",
|
||||
@@ -32,7 +32,8 @@
|
||||
"success": "Na-reset na ang pag-uswag sa leksiyon.",
|
||||
"error": "Dili ma-reset.",
|
||||
"pickUserFirst": "Una pagpili ug user.",
|
||||
"noCourses": "Walay nakarga nga kurso o walay makita nga kurso.",
|
||||
"noEnrolledCourses": "Kini nga tiggamit wala na-enroll sa bisan unsang kurso sa pinulongan.",
|
||||
"loadCoursesError": "Dili makarga ang lista sa mga kurso.",
|
||||
"loadingLessons": "Nagkarga sa mga leksiyon …"
|
||||
},
|
||||
"rights": {
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
},
|
||||
"vocabLessonReset": {
|
||||
"title": "Sprachkurs: Lektionsfortschritt",
|
||||
"intro": "Fortschritt, Übungsergebnisse und gespeicherter Lektionszustand für eine einzelne Lektion löschen (nicht der ganze Kurs). Es werden nur Kurse gelistet, die du als Admin sehen kannst (öffentlich oder eigene).",
|
||||
"loadCourses": "Kurse laden",
|
||||
"intro": "Fortschritt, Übungsergebnisse und gespeicherter Lektionszustand für eine einzelne Lektion löschen (nicht der ganze Kurs). Es werden nur Sprachkurse gelistet, in die dieser Benutzer eingeschrieben ist.",
|
||||
"loadCourses": "Eingeschriebene Kurse laden",
|
||||
"selectCourse": "Kurs",
|
||||
"selectLesson": "Lektion",
|
||||
"reset": "Lektion für diesen Nutzer zurücksetzen",
|
||||
@@ -41,7 +41,8 @@
|
||||
"success": "Lektionsfortschritt wurde zurückgesetzt.",
|
||||
"error": "Zurücksetzen fehlgeschlagen.",
|
||||
"pickUserFirst": "Zuerst einen Benutzer auswählen.",
|
||||
"noCourses": "Keine Kurse geladen oder keine sichtbaren Kurse.",
|
||||
"noEnrolledCourses": "Dieser Benutzer ist in keinem Sprachkurs eingeschrieben.",
|
||||
"loadCoursesError": "Die Kursliste konnte nicht geladen werden.",
|
||||
"loadingLessons": "Lektionen werden geladen …"
|
||||
},
|
||||
"adultVerification": {
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
},
|
||||
"vocabLessonReset": {
|
||||
"title": "Language course: lesson progress",
|
||||
"intro": "Delete progress, exercise results and saved lesson state for a single lesson (not the whole course). Only courses you can see as this admin account are listed (public or your own).",
|
||||
"loadCourses": "Load courses",
|
||||
"intro": "Delete progress, exercise results and saved lesson state for a single lesson (not the whole course). Only language courses this user is enrolled in are listed.",
|
||||
"loadCourses": "Load enrolled courses",
|
||||
"selectCourse": "Course",
|
||||
"selectLesson": "Lesson",
|
||||
"reset": "Reset lesson for this user",
|
||||
@@ -41,7 +41,8 @@
|
||||
"success": "Lesson progress was reset.",
|
||||
"error": "Reset failed.",
|
||||
"pickUserFirst": "Select a user first.",
|
||||
"noCourses": "No courses loaded or no visible courses.",
|
||||
"noEnrolledCourses": "This user is not enrolled in any language course.",
|
||||
"loadCoursesError": "Could not load the course list.",
|
||||
"loadingLessons": "Loading lessons…"
|
||||
},
|
||||
"adultVerification": {
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
},
|
||||
"vocabLessonReset": {
|
||||
"title": "Curso de idiomas: progreso de lección",
|
||||
"intro": "Elimina el progreso, los resultados de ejercicios y el estado guardado de una sola lección (no todo el curso). Solo se listan cursos visibles para tu cuenta de administración (públicos o propios).",
|
||||
"loadCourses": "Cargar cursos",
|
||||
"intro": "Elimina el progreso, los resultados de ejercicios y el estado guardado de una sola lección (no todo el curso). Solo aparecen los cursos de idiomas en los que está inscrito este usuario.",
|
||||
"loadCourses": "Cargar cursos inscritos",
|
||||
"selectCourse": "Curso",
|
||||
"selectLesson": "Lección",
|
||||
"reset": "Restablecer lección para este usuario",
|
||||
@@ -41,7 +41,8 @@
|
||||
"success": "Se restableció el progreso de la lección.",
|
||||
"error": "No se pudo restablecer.",
|
||||
"pickUserFirst": "Primero elige un usuario.",
|
||||
"noCourses": "No hay cursos cargados o no hay cursos visibles.",
|
||||
"noEnrolledCourses": "Este usuario no está inscrito en ningún curso de idiomas.",
|
||||
"loadCoursesError": "No se pudo cargar la lista de cursos.",
|
||||
"loadingLessons": "Cargando lecciones…"
|
||||
},
|
||||
"adultVerification": {
|
||||
|
||||
@@ -50,12 +50,15 @@
|
||||
<span>{{ $t('admin.vocabLessonReset.selectCourse') }}</span>
|
||||
<select v-model="selectedVocabCourseId" @change="onVocabCourseChange">
|
||||
<option value="">{{ $t('admin.vocabLessonReset.selectCourse') }}</option>
|
||||
<option v-for="c in vocabCourses" :key="c.id" :value="String(c.id)">{{ c.title }}</option>
|
||||
<option v-for="c in vocabCoursesForSelect" :key="c.id" :value="String(c.id)">{{ c.selectLabel }}</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<p v-else-if="vocabCoursesAttempted && !loadingVocabCourses && vocabLoadError" class="vocab-reset__hint">
|
||||
{{ $t('admin.vocabLessonReset.loadCoursesError') }}
|
||||
</p>
|
||||
<p v-else-if="vocabCoursesAttempted && !loadingVocabCourses" class="vocab-reset__hint">
|
||||
{{ $t('admin.vocabLessonReset.noCourses') }}
|
||||
{{ $t('admin.vocabLessonReset.noEnrolledCourses') }}
|
||||
</p>
|
||||
|
||||
<label v-if="vocabCourseLessons.length" class="edit__field">
|
||||
@@ -101,13 +104,34 @@ export default {
|
||||
selectedVocabLessonId: '',
|
||||
loadingVocabCourses: false,
|
||||
loadingVocabCourseDetail: false,
|
||||
vocabResetSubmitting: false
|
||||
vocabResetSubmitting: false,
|
||||
vocabLoadError: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/** Gleicher Titel bei mehreren Kurs-IDs → Kurs-ID anhängen (z. B. geklonte Kurse). */
|
||||
vocabCoursesForSelect() {
|
||||
const list = this.vocabCourses;
|
||||
const counts = {};
|
||||
list.forEach((c) => {
|
||||
const t = (c.title || '').trim() || '—';
|
||||
counts[t] = (counts[t] || 0) + 1;
|
||||
});
|
||||
return list.map((c) => {
|
||||
const t = (c.title || '').trim() || '—';
|
||||
const dup = counts[t] > 1;
|
||||
return {
|
||||
...c,
|
||||
selectLabel: dup ? `${c.title} (#${c.id})` : (c.title || `#${c.id}`)
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clearVocabResetUi() {
|
||||
this.vocabCourses = [];
|
||||
this.vocabCoursesAttempted = false;
|
||||
this.vocabLoadError = false;
|
||||
this.selectedVocabCourseId = '';
|
||||
this.vocabCourseLessons = [];
|
||||
this.selectedVocabLessonId = '';
|
||||
@@ -137,8 +161,9 @@ export default {
|
||||
}
|
||||
this.loadingVocabCourses = true;
|
||||
this.vocabCoursesAttempted = true;
|
||||
this.vocabLoadError = false;
|
||||
try {
|
||||
const { data } = await apiClient.get('/api/vocab/courses');
|
||||
const { data } = await apiClient.get(`/api/admin/users/${this.selected.id}/vocab-courses`);
|
||||
this.vocabCourses = Array.isArray(data) ? data : [];
|
||||
this.selectedVocabCourseId = '';
|
||||
this.vocabCourseLessons = [];
|
||||
@@ -146,6 +171,7 @@ export default {
|
||||
} catch (e) {
|
||||
console.error('[UsersView] vocab courses:', e);
|
||||
this.vocabCourses = [];
|
||||
this.vocabLoadError = true;
|
||||
this.$root?.$refs?.messageDialog?.open?.(this.$t('admin.vocabLessonReset.error'));
|
||||
} finally {
|
||||
this.loadingVocabCourses = false;
|
||||
@@ -159,7 +185,7 @@ export default {
|
||||
}
|
||||
this.loadingVocabCourseDetail = true;
|
||||
try {
|
||||
const { data } = await apiClient.get(`/api/vocab/courses/${this.selectedVocabCourseId}`);
|
||||
const { data } = await apiClient.get(`/api/admin/vocab/courses/${this.selectedVocabCourseId}`);
|
||||
const lessons = data?.lessons || [];
|
||||
this.vocabCourseLessons = [...lessons].sort((a, b) => {
|
||||
if (a.weekNumber !== b.weekNumber) {
|
||||
|
||||
Reference in New Issue
Block a user