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

@@ -1974,6 +1974,28 @@ class AdminService {
}
}
async adminMarkUserVocabLessonsCompleteThrough(requesterHashedId, targetHashedId, courseId, throughLessonNumber) {
if (!(await this.hasUserAccess(requesterHashedId, 'useradministration'))) {
throw new Error('noaccess');
}
const vocab = new VocabService();
try {
return await vocab.adminMarkLessonsCompleteThrough(
targetHashedId,
Number(courseId),
Number(throughLessonNumber)
);
} catch (e) {
if (e.status === 403) {
throw new Error('notenrolled');
}
if (e.status === 400) {
throw new Error('badrequest');
}
throw e;
}
}
async adminListUserEnrolledVocabCourses(requesterHashedId, targetHashedId) {
if (!(await this.hasUserAccess(requesterHashedId, 'useradministration'))) {
throw new Error('noaccess');

View File

@@ -2609,6 +2609,101 @@ export default class VocabService {
return this._purgeLessonProgressForUser(user.id, lesson.id);
}
/**
* Admin: Alle Lektionen eines Kurses bis einschließlich lesson_number als abgeschlossen setzen
* (nur Zeilen, die noch nicht completed sind). Nur bei eingeschriebenem Nutzer.
*/
async adminMarkLessonsCompleteThrough(targetHashedUserId, courseId, throughLessonNumber) {
const user = await this._getUserByHashedId(targetHashedUserId);
const cid = Number(courseId);
const maxNum = Number(throughLessonNumber);
if (!Number.isFinite(cid) || cid < 1) {
const err = new Error('Invalid courseId');
err.status = 400;
throw err;
}
if (!Number.isFinite(maxNum) || maxNum < 1) {
const err = new Error('Invalid throughLessonNumber');
err.status = 400;
throw err;
}
const enrollment = await VocabCourseEnrollment.findOne({
where: { userId: user.id, courseId: cid }
});
if (!enrollment) {
const err = new Error('Not enrolled in this course');
err.status = 403;
throw err;
}
const lessons = await VocabCourseLesson.findAll({
where: { courseId: cid, lessonNumber: { [Op.lte]: maxNum } },
order: [['lessonNumber', 'ASC']]
});
const now = new Date();
const details = [];
for (const lesson of lessons) {
const lessonData = lesson.get({ plain: true });
const targetScore = lessonData.targetScorePercent || 80;
const [progress] = await VocabCourseProgress.findOrCreate({
where: { userId: user.id, lessonId: lesson.id },
defaults: {
userId: user.id,
courseId: cid,
lessonId: lesson.id,
completed: false,
score: 0,
lessonState: {}
}
});
if (progress.completed) {
details.push({
lessonNumber: lesson.lessonNumber,
lessonId: lesson.id,
status: 'unchanged'
});
continue;
}
const mergedState = this._applyScheduledReviewState(
this._sanitizeLessonState(progress.lessonState),
{
previousCompleted: false,
nextCompleted: true,
shouldAdvanceReview: true,
lessonData,
now
}
);
await progress.update({
completed: true,
completedAt: now,
score: Math.max(Number(progress.score) || 0, targetScore),
lastAccessedAt: now,
lessonState: mergedState
});
details.push({
lessonNumber: lesson.lessonNumber,
lessonId: lesson.id,
status: 'marked_complete'
});
}
return {
courseId: cid,
throughLessonNumber: maxNum,
lessonsConsidered: lessons.length,
details
};
}
// ========== GRAMMAR EXERCISE METHODS ==========
async getExerciseTypes() {