|
|
|
|
@@ -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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|