extended admin tool for finished lessons
All checks were successful
Deploy to production / deploy (push) Successful in 2m54s

This commit is contained in:
Torsten Schulz (local)
2026-04-02 13:32:13 +02:00
parent 9d663e4f2b
commit edbf22ac5b
9 changed files with 281 additions and 0 deletions

View File

@@ -82,6 +82,31 @@
{{ vocabResetSubmitting ? $t('general.loading') : $t('admin.vocabLessonReset.reset') }}
</button>
</div>
<template v-if="vocabCourseLessons.length">
<p class="vocab-reset__divider">{{ $t('admin.vocabLessonMarkComplete.divider') }}</p>
<label class="edit__field">
<span>{{ $t('admin.vocabLessonMarkComplete.throughLabel') }}</span>
<input
v-model.number="vocabMarkThroughNumber"
type="number"
min="1"
:max="vocabMaxLessonNumber"
:disabled="loadingVocabCourseDetail || vocabMarkSubmitting"
class="vocab-reset__number"
/>
</label>
<p class="vocab-reset__hint">{{ $t('admin.vocabLessonMarkComplete.hint') }}</p>
<div class="vocab-reset__row">
<button
type="button"
:disabled="!canMarkVocabThrough || vocabMarkSubmitting || loadingVocabCourseDetail"
@click="adminMarkVocabLessonsThrough"
>
{{ vocabMarkSubmitting ? $t('general.loading') : $t('admin.vocabLessonMarkComplete.submit') }}
</button>
</div>
</template>
</section>
</div>
</template>
@@ -105,6 +130,8 @@ export default {
loadingVocabCourses: false,
loadingVocabCourseDetail: false,
vocabResetSubmitting: false,
vocabMarkThroughNumber: null,
vocabMarkSubmitting: false,
vocabLoadError: false
};
},
@@ -125,6 +152,15 @@ export default {
selectLabel: dup ? `${c.title} (#${c.id})` : (c.title || `#${c.id}`)
};
});
},
vocabMaxLessonNumber() {
const list = this.vocabCourseLessons;
if (!list.length) return 1;
return Math.max(...list.map((l) => Number(l.lessonNumber) || 0));
},
canMarkVocabThrough() {
const n = Number(this.vocabMarkThroughNumber);
return Number.isFinite(n) && n >= 1 && n <= this.vocabMaxLessonNumber;
}
},
methods: {
@@ -135,6 +171,7 @@ export default {
this.selectedVocabCourseId = '';
this.vocabCourseLessons = [];
this.selectedVocabLessonId = '';
this.vocabMarkThroughNumber = null;
},
async select(u) {
const res = await apiClient.get(`/api/admin/users/${u.id}`);
@@ -168,6 +205,7 @@ export default {
this.selectedVocabCourseId = '';
this.vocabCourseLessons = [];
this.selectedVocabLessonId = '';
this.vocabMarkThroughNumber = null;
} catch (e) {
console.error('[UsersView] vocab courses:', e);
this.vocabCourses = [];
@@ -179,6 +217,7 @@ export default {
},
async onVocabCourseChange() {
this.selectedVocabLessonId = '';
this.vocabMarkThroughNumber = null;
this.vocabCourseLessons = [];
if (!this.selectedVocabCourseId) {
return;
@@ -232,6 +271,45 @@ export default {
} finally {
this.vocabResetSubmitting = false;
}
},
adminMarkVocabLessonsThrough() {
if (!this.selected || !this.selectedVocabCourseId || !this.canMarkVocabThrough || this.vocabMarkSubmitting) {
return;
}
const n = Number(this.vocabMarkThroughNumber);
const msg = this.$t('admin.vocabLessonMarkComplete.confirm', {
n,
username: this.selected.username
});
if (!window.confirm(msg)) {
return;
}
this.runAdminVocabMarkThrough();
},
async runAdminVocabMarkThrough() {
this.vocabMarkSubmitting = true;
try {
const { data } = await apiClient.post(
`/api/admin/users/${this.selected.id}/vocab-lesson-progress/mark-complete-through`,
{
courseId: Number(this.selectedVocabCourseId),
throughLessonNumber: Number(this.vocabMarkThroughNumber)
}
);
const details = Array.isArray(data?.details) ? data.details : [];
const marked = details.filter((d) => d.status === 'marked_complete').length;
const unchanged = details.filter((d) => d.status === 'unchanged').length;
const msgKey =
marked === 0 && unchanged > 0
? 'admin.vocabLessonMarkComplete.successNone'
: 'admin.vocabLessonMarkComplete.success';
this.$root?.$refs?.messageDialog?.open?.(this.$t(msgKey, { marked, unchanged }));
} catch (e) {
console.error('[UsersView] admin vocab mark complete:', e);
this.$root?.$refs?.messageDialog?.open?.(this.$t('admin.vocabLessonMarkComplete.error'));
} finally {
this.vocabMarkSubmitting = false;
}
}
}
};
@@ -303,6 +381,18 @@ export default {
line-height: 1.45;
}
.vocab-reset__divider {
margin: 8px 0 0;
padding-top: 14px;
border-top: 1px dashed var(--color-border);
color: var(--color-text-secondary);
font-size: 0.88rem;
}
.vocab-reset__number {
max-width: 120px;
}
.vocab-reset__row {
display: flex;
flex-wrap: wrap;