extended admin tool for finished lessons
All checks were successful
Deploy to production / deploy (push) Successful in 2m54s
All checks were successful
Deploy to production / deploy (push) Successful in 2m54s
This commit is contained in:
@@ -34,6 +34,7 @@ class AdminController {
|
|||||||
this.getUsers = this.getUsers.bind(this);
|
this.getUsers = this.getUsers.bind(this);
|
||||||
this.updateUser = this.updateUser.bind(this);
|
this.updateUser = this.updateUser.bind(this);
|
||||||
this.resetUserVocabLessonProgress = this.resetUserVocabLessonProgress.bind(this);
|
this.resetUserVocabLessonProgress = this.resetUserVocabLessonProgress.bind(this);
|
||||||
|
this.markUserVocabLessonsCompleteThrough = this.markUserVocabLessonsCompleteThrough.bind(this);
|
||||||
this.getUserVocabCourses = this.getUserVocabCourses.bind(this);
|
this.getUserVocabCourses = this.getUserVocabCourses.bind(this);
|
||||||
this.getVocabCourseForAdmin = this.getVocabCourseForAdmin.bind(this);
|
this.getVocabCourseForAdmin = this.getVocabCourseForAdmin.bind(this);
|
||||||
this.getAdultVerificationRequests = this.getAdultVerificationRequests.bind(this);
|
this.getAdultVerificationRequests = this.getAdultVerificationRequests.bind(this);
|
||||||
@@ -151,6 +152,34 @@ class AdminController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async markUserVocabLessonsCompleteThrough(req, res) {
|
||||||
|
const schema = Joi.object({
|
||||||
|
courseId: Joi.number().integer().positive().required(),
|
||||||
|
throughLessonNumber: Joi.number().integer().positive().required()
|
||||||
|
});
|
||||||
|
const { error, value } = schema.validate(req.body || {});
|
||||||
|
if (error) {
|
||||||
|
return res.status(400).json({ error: error.details[0].message });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { userid: requester } = req.headers;
|
||||||
|
const { id } = req.params;
|
||||||
|
const result = await AdminService.adminMarkUserVocabLessonsCompleteThrough(
|
||||||
|
requester,
|
||||||
|
id,
|
||||||
|
value.courseId,
|
||||||
|
value.throughLessonNumber
|
||||||
|
);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (err) {
|
||||||
|
let status = 500;
|
||||||
|
if (err.message === 'noaccess') status = 403;
|
||||||
|
else if (err.message === 'notenrolled') status = 403;
|
||||||
|
else if (err.message === 'badrequest') status = 400;
|
||||||
|
res.status(status).json({ error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getUserVocabCourses(req, res) {
|
async getUserVocabCourses(req, res) {
|
||||||
try {
|
try {
|
||||||
const { userid: requester } = req.headers;
|
const { userid: requester } = req.headers;
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ router.get('/users/erotic-moderation/preview/:type/:targetId', authenticate, adm
|
|||||||
router.put('/users/erotic-moderation/:id', authenticate, adminController.applyEroticModerationAction);
|
router.put('/users/erotic-moderation/:id', authenticate, adminController.applyEroticModerationAction);
|
||||||
router.get('/users/:id/vocab-courses', authenticate, adminController.getUserVocabCourses);
|
router.get('/users/:id/vocab-courses', authenticate, adminController.getUserVocabCourses);
|
||||||
router.post('/users/:id/vocab-lesson-progress/reset', authenticate, adminController.resetUserVocabLessonProgress);
|
router.post('/users/:id/vocab-lesson-progress/reset', authenticate, adminController.resetUserVocabLessonProgress);
|
||||||
|
router.post(
|
||||||
|
'/users/:id/vocab-lesson-progress/mark-complete-through',
|
||||||
|
authenticate,
|
||||||
|
adminController.markUserVocabLessonsCompleteThrough
|
||||||
|
);
|
||||||
router.get('/vocab/courses/:courseId', authenticate, adminController.getVocabCourseForAdmin);
|
router.get('/vocab/courses/:courseId', authenticate, adminController.getVocabCourseForAdmin);
|
||||||
router.get('/users/:id', authenticate, adminController.getUser);
|
router.get('/users/:id', authenticate, adminController.getUser);
|
||||||
router.put('/users/:id', authenticate, adminController.updateUser);
|
router.put('/users/:id', authenticate, adminController.updateUser);
|
||||||
|
|||||||
@@ -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) {
|
async adminListUserEnrolledVocabCourses(requesterHashedId, targetHashedId) {
|
||||||
if (!(await this.hasUserAccess(requesterHashedId, 'useradministration'))) {
|
if (!(await this.hasUserAccess(requesterHashedId, 'useradministration'))) {
|
||||||
throw new Error('noaccess');
|
throw new Error('noaccess');
|
||||||
|
|||||||
@@ -2609,6 +2609,101 @@ export default class VocabService {
|
|||||||
return this._purgeLessonProgressForUser(user.id, lesson.id);
|
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 ==========
|
// ========== GRAMMAR EXERCISE METHODS ==========
|
||||||
|
|
||||||
async getExerciseTypes() {
|
async getExerciseTypes() {
|
||||||
|
|||||||
@@ -36,6 +36,16 @@
|
|||||||
"loadCoursesError": "Dili makarga ang lista sa mga kurso.",
|
"loadCoursesError": "Dili makarga ang lista sa mga kurso.",
|
||||||
"loadingLessons": "Nagkarga sa mga leksiyon …"
|
"loadingLessons": "Nagkarga sa mga leksiyon …"
|
||||||
},
|
},
|
||||||
|
"vocabLessonMarkComplete": {
|
||||||
|
"divider": "Ayuhon ang pag-uswag (dili paghimo og peke nga resulta sa ehersisyo)",
|
||||||
|
"throughLabel": "Tanang leksiyon hangtod sa numero (lakip)",
|
||||||
|
"hint": "I-mark ang kulang o abli nga mga row nga nahuman, lakip ang target score ug unang review wave. Ang nahuman na dili usbon.",
|
||||||
|
"submit": "I-mark nga nahuman hangtod dinhi",
|
||||||
|
"confirm": "I-mark nga nahuman ang tanang leksiyon nga numero ≤ {n} ni {username} niining kurso?",
|
||||||
|
"success": "{marked} ka leksiyon nga bag-ong gi-mark nga nahuman ({unchanged} klaro nang nahuman).",
|
||||||
|
"successNone": "Walay pagbag-o: tanang leksiyon nga naapektuhan ({unchanged}) klaro nang nahuman.",
|
||||||
|
"error": "Dili ma-mark nga nahuman."
|
||||||
|
},
|
||||||
"rights": {
|
"rights": {
|
||||||
"add": "Idugang ang katungod",
|
"add": "Idugang ang katungod",
|
||||||
"select": "Palihog pagpili",
|
"select": "Palihog pagpili",
|
||||||
|
|||||||
@@ -45,6 +45,16 @@
|
|||||||
"loadCoursesError": "Die Kursliste konnte nicht geladen werden.",
|
"loadCoursesError": "Die Kursliste konnte nicht geladen werden.",
|
||||||
"loadingLessons": "Lektionen werden geladen …"
|
"loadingLessons": "Lektionen werden geladen …"
|
||||||
},
|
},
|
||||||
|
"vocabLessonMarkComplete": {
|
||||||
|
"divider": "Fortschritt reparieren (ohne Übungsergebnisse zu fälschen)",
|
||||||
|
"throughLabel": "Alle Lektionen bis Lektionsnummer (einschließlich)",
|
||||||
|
"hint": "Setzt fehlende oder offene Einträge auf „abgeschlossen“, inkl. Ziel-Score und erster Review-Welle. Bereits abgeschlossene Lektionen bleiben unverändert.",
|
||||||
|
"submit": "Bis hier als abgeschlossen markieren",
|
||||||
|
"confirm": "Alle Lektionen mit Nummer ≤ {n} für {username} in diesem Kurs als abgeschlossen markieren?",
|
||||||
|
"success": "{marked} Lektion(en) neu als abgeschlossen gesetzt ({unchanged} waren bereits erledigt).",
|
||||||
|
"successNone": "Keine Änderung: alle betroffenen Lektionen ({unchanged}) waren bereits abgeschlossen.",
|
||||||
|
"error": "Markieren fehlgeschlagen."
|
||||||
|
},
|
||||||
"adultVerification": {
|
"adultVerification": {
|
||||||
"title": "[Admin] - Erotik-Freigaben",
|
"title": "[Admin] - Erotik-Freigaben",
|
||||||
"intro": "Volljährige Nutzer können den Erotikbereich beantragen. Hier werden Anfragen geprüft und freigegeben oder abgelehnt.",
|
"intro": "Volljährige Nutzer können den Erotikbereich beantragen. Hier werden Anfragen geprüft und freigegeben oder abgelehnt.",
|
||||||
|
|||||||
@@ -45,6 +45,16 @@
|
|||||||
"loadCoursesError": "Could not load the course list.",
|
"loadCoursesError": "Could not load the course list.",
|
||||||
"loadingLessons": "Loading lessons…"
|
"loadingLessons": "Loading lessons…"
|
||||||
},
|
},
|
||||||
|
"vocabLessonMarkComplete": {
|
||||||
|
"divider": "Repair progress (does not fabricate exercise answers)",
|
||||||
|
"throughLabel": "All lessons up to and including lesson number",
|
||||||
|
"hint": "Marks missing or open rows as completed, including target score and first review wave. Already completed lessons are left unchanged.",
|
||||||
|
"submit": "Mark through here as completed",
|
||||||
|
"confirm": "Mark every lesson with number ≤ {n} for {username} in this course as completed?",
|
||||||
|
"success": "{marked} lesson(s) newly marked complete ({unchanged} were already done).",
|
||||||
|
"successNone": "No change: all affected lessons ({unchanged}) were already completed.",
|
||||||
|
"error": "Could not mark lessons complete."
|
||||||
|
},
|
||||||
"adultVerification": {
|
"adultVerification": {
|
||||||
"title": "[Admin] - Erotic approvals",
|
"title": "[Admin] - Erotic approvals",
|
||||||
"intro": "Adult users can request access to the erotic area. Requests can be reviewed, approved or rejected here.",
|
"intro": "Adult users can request access to the erotic area. Requests can be reviewed, approved or rejected here.",
|
||||||
|
|||||||
@@ -45,6 +45,16 @@
|
|||||||
"loadCoursesError": "No se pudo cargar la lista de cursos.",
|
"loadCoursesError": "No se pudo cargar la lista de cursos.",
|
||||||
"loadingLessons": "Cargando lecciones…"
|
"loadingLessons": "Cargando lecciones…"
|
||||||
},
|
},
|
||||||
|
"vocabLessonMarkComplete": {
|
||||||
|
"divider": "Reparar progreso (no inventa resultados de ejercicios)",
|
||||||
|
"throughLabel": "Todas las lecciones hasta el número (incluido)",
|
||||||
|
"hint": "Marca filas faltantes o abiertas como completadas, con puntuación objetivo y primera ola de repaso. Las ya completadas no se cambian.",
|
||||||
|
"submit": "Marcar hasta aquí como completadas",
|
||||||
|
"confirm": "¿Marcar todas las lecciones con número ≤ {n} para {username} en este curso como completadas?",
|
||||||
|
"success": "{marked} lección(es) marcadas como completadas ({unchanged} ya estaban hechas).",
|
||||||
|
"successNone": "Sin cambios: todas las lecciones afectadas ({unchanged}) ya estaban completadas.",
|
||||||
|
"error": "No se pudo marcar como completadas."
|
||||||
|
},
|
||||||
"adultVerification": {
|
"adultVerification": {
|
||||||
"title": "[Admin] - Aprobaciones eróticas",
|
"title": "[Admin] - Aprobaciones eróticas",
|
||||||
"intro": "Los usuarios adultos pueden solicitar acceso al área erótica. Aquí se revisan, aprueban o rechazan las solicitudes.",
|
"intro": "Los usuarios adultos pueden solicitar acceso al área erótica. Aquí se revisan, aprueban o rechazan las solicitudes.",
|
||||||
|
|||||||
@@ -82,6 +82,31 @@
|
|||||||
{{ vocabResetSubmitting ? $t('general.loading') : $t('admin.vocabLessonReset.reset') }}
|
{{ vocabResetSubmitting ? $t('general.loading') : $t('admin.vocabLessonReset.reset') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -105,6 +130,8 @@ export default {
|
|||||||
loadingVocabCourses: false,
|
loadingVocabCourses: false,
|
||||||
loadingVocabCourseDetail: false,
|
loadingVocabCourseDetail: false,
|
||||||
vocabResetSubmitting: false,
|
vocabResetSubmitting: false,
|
||||||
|
vocabMarkThroughNumber: null,
|
||||||
|
vocabMarkSubmitting: false,
|
||||||
vocabLoadError: false
|
vocabLoadError: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -125,6 +152,15 @@ export default {
|
|||||||
selectLabel: dup ? `${c.title} (#${c.id})` : (c.title || `#${c.id}`)
|
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: {
|
methods: {
|
||||||
@@ -135,6 +171,7 @@ export default {
|
|||||||
this.selectedVocabCourseId = '';
|
this.selectedVocabCourseId = '';
|
||||||
this.vocabCourseLessons = [];
|
this.vocabCourseLessons = [];
|
||||||
this.selectedVocabLessonId = '';
|
this.selectedVocabLessonId = '';
|
||||||
|
this.vocabMarkThroughNumber = null;
|
||||||
},
|
},
|
||||||
async select(u) {
|
async select(u) {
|
||||||
const res = await apiClient.get(`/api/admin/users/${u.id}`);
|
const res = await apiClient.get(`/api/admin/users/${u.id}`);
|
||||||
@@ -168,6 +205,7 @@ export default {
|
|||||||
this.selectedVocabCourseId = '';
|
this.selectedVocabCourseId = '';
|
||||||
this.vocabCourseLessons = [];
|
this.vocabCourseLessons = [];
|
||||||
this.selectedVocabLessonId = '';
|
this.selectedVocabLessonId = '';
|
||||||
|
this.vocabMarkThroughNumber = null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[UsersView] vocab courses:', e);
|
console.error('[UsersView] vocab courses:', e);
|
||||||
this.vocabCourses = [];
|
this.vocabCourses = [];
|
||||||
@@ -179,6 +217,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async onVocabCourseChange() {
|
async onVocabCourseChange() {
|
||||||
this.selectedVocabLessonId = '';
|
this.selectedVocabLessonId = '';
|
||||||
|
this.vocabMarkThroughNumber = null;
|
||||||
this.vocabCourseLessons = [];
|
this.vocabCourseLessons = [];
|
||||||
if (!this.selectedVocabCourseId) {
|
if (!this.selectedVocabCourseId) {
|
||||||
return;
|
return;
|
||||||
@@ -232,6 +271,45 @@ export default {
|
|||||||
} finally {
|
} finally {
|
||||||
this.vocabResetSubmitting = false;
|
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;
|
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 {
|
.vocab-reset__row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
Reference in New Issue
Block a user