feat(vocab): enhance lesson progress tracking and review scheduling
All checks were successful
Deploy to production / deploy (push) Successful in 2m48s
All checks were successful
Deploy to production / deploy (push) Successful in 2m48s
- Updated VocabService to include lessonId and lessonNumber in progress data, improving tracking accuracy. - Modified getLessonProgress and lastProgressTouch methods to accept lesson parameters, enhancing flexibility in progress retrieval. - Implemented formatReviewBadgeSchedule method to manage review scheduling notifications, providing clearer user feedback. - Updated VocabCourseView to reflect changes in lesson progress handling, ensuring accurate display of review statuses and due dates. - Expanded localization for review scheduling messages across multiple languages, enhancing user experience.
This commit is contained in:
@@ -69,7 +69,7 @@
|
||||
@click="openLesson(lesson.id)"
|
||||
>
|
||||
<strong>{{ lesson.title }}</strong>
|
||||
<span>{{ formatReviewDue(getLessonProgress(lesson.id)?.reviewNextDueAt) }}</span>
|
||||
<span>{{ formatReviewDue(getLessonProgress(lesson.id, lesson)?.reviewNextDueAt) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-else class="course-flow-card__empty">{{ $t('socialnetwork.vocab.courses.courseFlowReviewEmpty') }}</p>
|
||||
@@ -164,21 +164,21 @@
|
||||
<div class="lesson-card__header">
|
||||
<span class="lesson-number">#{{ lesson.lessonNumber }}</span>
|
||||
<div class="lesson-status-content">
|
||||
<span v-if="getLessonProgress(lesson.id)?.completed" class="badge completed">
|
||||
<span v-if="getLessonProgress(lesson.id, lesson)?.completed" class="badge completed">
|
||||
{{ $t('socialnetwork.vocab.courses.completed') }}
|
||||
</span>
|
||||
<span v-else-if="getLessonProgress(lesson.id)?.score" class="score">
|
||||
{{ $t('socialnetwork.vocab.courses.score') }}: {{ getLessonProgress(lesson.id).score }}%
|
||||
<span v-else-if="getLessonProgress(lesson.id, lesson)?.score" class="score">
|
||||
{{ $t('socialnetwork.vocab.courses.score') }}: {{ getLessonProgress(lesson.id, lesson).score }}%
|
||||
</span>
|
||||
<span v-else class="status-new">
|
||||
{{ $t('socialnetwork.vocab.courses.notStarted') }}
|
||||
</span>
|
||||
<span
|
||||
v-if="getReviewBadgeLabel(getLessonProgress(lesson.id))"
|
||||
v-if="getReviewBadgeLabel(getLessonProgress(lesson.id, lesson))"
|
||||
class="review-badge"
|
||||
:class="getReviewBadgeClass(getLessonProgress(lesson.id))"
|
||||
:class="getReviewBadgeClass(getLessonProgress(lesson.id, lesson))"
|
||||
>
|
||||
{{ getReviewBadgeLabel(getLessonProgress(lesson.id)) }}
|
||||
{{ getReviewBadgeLabel(getLessonProgress(lesson.id, lesson)) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -199,10 +199,10 @@
|
||||
:disabled="!canStartLesson(lesson)"
|
||||
:title="!canStartLesson(lesson) ? $t('socialnetwork.vocab.courses.previousLessonRequired') : ''"
|
||||
>
|
||||
{{ getLessonProgress(lesson.id)?.completed ? $t('socialnetwork.vocab.courses.review') : $t('socialnetwork.vocab.courses.start') }}
|
||||
{{ getLessonProgress(lesson.id, lesson)?.completed ? $t('socialnetwork.vocab.courses.review') : $t('socialnetwork.vocab.courses.start') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="getLessonProgress(lesson.id)?.completed"
|
||||
v-if="canShowLessonTrainer(lesson)"
|
||||
@click="openLessonPractice(lesson)"
|
||||
class="btn-edit"
|
||||
>
|
||||
@@ -306,7 +306,7 @@ export default {
|
||||
|
||||
// Finde die erste nicht abgeschlossene Lektion
|
||||
for (const lesson of this.sortedLessons) {
|
||||
const progress = this.getLessonProgress(lesson.id);
|
||||
const progress = this.getLessonProgress(lesson.id, lesson);
|
||||
if (!progress || !progress.completed) {
|
||||
return lesson;
|
||||
}
|
||||
@@ -321,12 +321,12 @@ export default {
|
||||
dueReviewLessons() {
|
||||
return this.sortedLessons
|
||||
.filter((lesson) => {
|
||||
const progress = this.getLessonProgress(lesson.id);
|
||||
const progress = this.getLessonProgress(lesson.id, lesson);
|
||||
return Boolean(progress?.completed && progress?.reviewDue);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const left = this.getLessonProgress(a.id)?.reviewNextDueAt || '';
|
||||
const right = this.getLessonProgress(b.id)?.reviewNextDueAt || '';
|
||||
const left = this.getLessonProgress(a.id, a)?.reviewNextDueAt || '';
|
||||
const right = this.getLessonProgress(b.id, b)?.reviewNextDueAt || '';
|
||||
return left.localeCompare(right);
|
||||
})
|
||||
.slice(0, 4);
|
||||
@@ -340,14 +340,14 @@ export default {
|
||||
if (lessonBlock !== this.currentBlockNumber) {
|
||||
return false;
|
||||
}
|
||||
return !this.getLessonProgress(lesson.id)?.completed;
|
||||
return !this.getLessonProgress(lesson.id, lesson)?.completed;
|
||||
});
|
||||
},
|
||||
nextIntensiveReviewLesson() {
|
||||
return this.sortedLessons.find((lesson) => {
|
||||
const isIntensive = lesson.pedagogy?.didacticMode === 'intensive_review' || lesson.pedagogy?.isIntensiveReview;
|
||||
if (!isIntensive) return false;
|
||||
if (this.getLessonProgress(lesson.id)?.completed) return false;
|
||||
if (this.getLessonProgress(lesson.id, lesson)?.completed) return false;
|
||||
|
||||
const blockNumber = lesson.pedagogy?.blockNumber;
|
||||
const blockLessons = this.sortedLessons.filter((candidate) => {
|
||||
@@ -356,18 +356,18 @@ export default {
|
||||
return !candidateIsIntensive && candidate.lessonNumber < lesson.lessonNumber;
|
||||
});
|
||||
|
||||
return blockLessons.length > 0 && blockLessons.every((candidate) => this.getLessonProgress(candidate.id)?.completed);
|
||||
return blockLessons.length > 0 && blockLessons.every((candidate) => this.getLessonProgress(candidate.id, candidate)?.completed);
|
||||
}) || null;
|
||||
},
|
||||
freePracticeLessons() {
|
||||
return this.sortedLessons
|
||||
.filter((lesson) => {
|
||||
const progress = this.getLessonProgress(lesson.id);
|
||||
const progress = this.getLessonProgress(lesson.id, lesson);
|
||||
return Boolean(progress?.completed) && !progress?.reviewDue;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const left = this.lastProgressTouch(a.id) || '';
|
||||
const right = this.lastProgressTouch(b.id) || '';
|
||||
const left = this.lastProgressTouch(a.id, a) || '';
|
||||
const right = this.lastProgressTouch(b.id, b) || '';
|
||||
return right.localeCompare(left);
|
||||
})
|
||||
.slice(0, 4);
|
||||
@@ -439,11 +439,21 @@ export default {
|
||||
this.assistantSettings = null;
|
||||
}
|
||||
},
|
||||
getLessonProgress(lessonId) {
|
||||
return this.progress.find(p => p.lessonId === lessonId);
|
||||
getLessonProgress(lessonId, lesson = null) {
|
||||
const id = lessonId == null ? NaN : Number(lessonId);
|
||||
const byId = this.progress.find((p) => Number(p.lessonId) === id);
|
||||
if (byId) {
|
||||
return byId;
|
||||
}
|
||||
const num = lesson?.lessonNumber;
|
||||
if (num != null && Number.isFinite(Number(num))) {
|
||||
const n = Number(num);
|
||||
return this.progress.find((p) => Number(p.lessonNumber) === n) || null;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
lastProgressTouch(lessonId) {
|
||||
const progress = this.getLessonProgress(lessonId);
|
||||
lastProgressTouch(lessonId, lesson = null) {
|
||||
const progress = this.getLessonProgress(lessonId, lesson);
|
||||
return progress?.lastAccessedAt || progress?.completedAt || progress?.updatedAt || '';
|
||||
},
|
||||
daysSince(dateString) {
|
||||
@@ -492,6 +502,41 @@ export default {
|
||||
}
|
||||
return this.$t('socialnetwork.vocab.courses.reviewDueSinceDays', { count: diffDays });
|
||||
},
|
||||
formatReviewBadgeSchedule(reviewNextDueAt) {
|
||||
if (!reviewNextDueAt) {
|
||||
return '';
|
||||
}
|
||||
const dueTimestamp = new Date(reviewNextDueAt).getTime();
|
||||
if (!Number.isFinite(dueTimestamp)) {
|
||||
return '';
|
||||
}
|
||||
const diffMs = dueTimestamp - Date.now();
|
||||
if (diffMs > 0) {
|
||||
const untilDays = Math.ceil(diffMs / (24 * 60 * 60 * 1000));
|
||||
if (untilDays <= 1) {
|
||||
return this.$t('socialnetwork.vocab.courses.reviewBadgeScheduleTomorrow');
|
||||
}
|
||||
return this.$t('socialnetwork.vocab.courses.reviewBadgeScheduleInDays', { count: untilDays });
|
||||
}
|
||||
const diffDays = Math.floor((Date.now() - dueTimestamp) / (24 * 60 * 60 * 1000));
|
||||
if (diffDays <= 0) {
|
||||
return this.$t('socialnetwork.vocab.courses.reviewBadgeScheduleToday');
|
||||
}
|
||||
return this.$t('socialnetwork.vocab.courses.reviewBadgeScheduleOverdue', { count: diffDays });
|
||||
},
|
||||
canShowLessonTrainer(lesson) {
|
||||
const p = this.getLessonProgress(lesson.id, lesson);
|
||||
if (p?.completed) {
|
||||
return true;
|
||||
}
|
||||
if (this.canStartLesson(lesson)) {
|
||||
return true;
|
||||
}
|
||||
if (p && Number(p.score) > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
getReviewStageLabel(progress) {
|
||||
const stage = Number(progress?.reviewStage || 0);
|
||||
if (stage === 0) return this.$t('socialnetwork.vocab.courses.reviewStageDay1');
|
||||
@@ -511,8 +556,12 @@ export default {
|
||||
if (progress.reviewCompleted) {
|
||||
return stageLabel;
|
||||
}
|
||||
const dueLabel = this.formatReviewDue(progress.reviewNextDueAt);
|
||||
return `${stageLabel} · ${dueLabel}`;
|
||||
if (progress.reviewDue) {
|
||||
const dueLabel = this.formatReviewDue(progress.reviewNextDueAt);
|
||||
return `${stageLabel} · ${dueLabel}`;
|
||||
}
|
||||
const scheduleLabel = this.formatReviewBadgeSchedule(progress.reviewNextDueAt);
|
||||
return scheduleLabel ? `${stageLabel} · ${scheduleLabel}` : stageLabel;
|
||||
},
|
||||
getReviewBadgeClass(progress) {
|
||||
if (!progress?.completed) {
|
||||
@@ -542,7 +591,7 @@ export default {
|
||||
// Wenn es nicht die erste Lektion ist, prüfe ob die vorherige abgeschlossen wurde
|
||||
if (currentIndex > 0) {
|
||||
const previousLesson = this.sortedLessons[currentIndex - 1];
|
||||
const previousProgress = this.getLessonProgress(previousLesson.id);
|
||||
const previousProgress = this.getLessonProgress(previousLesson.id, previousLesson);
|
||||
return previousProgress && previousProgress.completed;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user