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:
@@ -291,6 +291,8 @@ export default class VocabService {
|
||||
|
||||
return {
|
||||
...plainProgress,
|
||||
lessonId: Number(plainProgress.lessonId),
|
||||
lessonNumber: lessonData?.lessonNumber ?? plainProgress.lesson?.lessonNumber ?? null,
|
||||
lessonState,
|
||||
targetScore,
|
||||
hasReachedTarget,
|
||||
|
||||
@@ -341,6 +341,10 @@
|
||||
"reviewDueToday": "angay karon",
|
||||
"reviewDueSinceOneDay": "angay na sukad 1 ka adlaw",
|
||||
"reviewDueSinceDays": "angay na sukad {count} ka adlaw",
|
||||
"reviewBadgeScheduleTomorrow": "sunod nga wave ugma",
|
||||
"reviewBadgeScheduleInDays": "sunod nga wave sulod sa {count} ka adlaw",
|
||||
"reviewBadgeScheduleToday": "gitakda ang wave karon",
|
||||
"reviewBadgeScheduleOverdue": "nilapas na ang wave sukad {count} ka adlaw",
|
||||
"reviewStageDay1": "Adlaw 1",
|
||||
"reviewStageDay3": "Adlaw 3",
|
||||
"reviewStageDay7": "Adlaw 7",
|
||||
|
||||
@@ -696,6 +696,10 @@
|
||||
"reviewDueToday": "heute fällig",
|
||||
"reviewDueSinceOneDay": "seit 1 Tag fällig",
|
||||
"reviewDueSinceDays": "seit {count} Tagen fällig",
|
||||
"reviewBadgeScheduleTomorrow": "nächste Welle morgen",
|
||||
"reviewBadgeScheduleInDays": "nächste Welle in {count} Tagen",
|
||||
"reviewBadgeScheduleToday": "Welle heute vorgesehen",
|
||||
"reviewBadgeScheduleOverdue": "Welle überfällig (seit {count} Tagen)",
|
||||
"reviewStageDay1": "Tag 1",
|
||||
"reviewStageDay3": "Tag 3",
|
||||
"reviewStageDay7": "Tag 7",
|
||||
|
||||
@@ -696,6 +696,10 @@
|
||||
"reviewDueToday": "due today",
|
||||
"reviewDueSinceOneDay": "due since 1 day",
|
||||
"reviewDueSinceDays": "due since {count} days",
|
||||
"reviewBadgeScheduleTomorrow": "next review wave tomorrow",
|
||||
"reviewBadgeScheduleInDays": "next wave in {count} days",
|
||||
"reviewBadgeScheduleToday": "wave slated for today",
|
||||
"reviewBadgeScheduleOverdue": "wave overdue ({count} days)",
|
||||
"reviewStageDay1": "Day 1",
|
||||
"reviewStageDay3": "Day 3",
|
||||
"reviewStageDay7": "Day 7",
|
||||
|
||||
@@ -694,6 +694,10 @@
|
||||
"reviewDueToday": "vence hoy",
|
||||
"reviewDueSinceOneDay": "vence desde hace 1 día",
|
||||
"reviewDueSinceDays": "vence desde hace {count} días",
|
||||
"reviewBadgeScheduleTomorrow": "siguiente ola mañana",
|
||||
"reviewBadgeScheduleInDays": "siguiente ola en {count} días",
|
||||
"reviewBadgeScheduleToday": "ola prevista hoy",
|
||||
"reviewBadgeScheduleOverdue": "ola atrasada ({count} días)",
|
||||
"reviewStageDay1": "Día 1",
|
||||
"reviewStageDay3": "Día 3",
|
||||
"reviewStageDay7": "Día 7",
|
||||
|
||||
@@ -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