feat(admin): add user vocab course management functionality
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:
Torsten Schulz (local)
2026-04-02 09:21:52 +02:00
parent b3c8e8e210
commit 2272db7f91
9 changed files with 217 additions and 67 deletions

View File

@@ -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) {