feat(vocab): implement scheduled review management in VocabService and UI updates
All checks were successful
Deploy to production / deploy (push) Successful in 2m56s

- Added methods in VocabService to handle scheduled review states, including normalization of review dates and management of review stages.
- Enhanced lesson state management to support review scheduling, improving the learning process for users.
- Updated VocabCourseView and VocabLessonView to display review statuses and due dates, providing clearer feedback on lesson progress and review requirements.
- Introduced new UI elements to indicate review status, enhancing user engagement and understanding of lesson timelines.
This commit is contained in:
Torsten Schulz (local)
2026-04-01 16:03:42 +02:00
parent 3ff8e4fc40
commit ac5d436a36
3 changed files with 716 additions and 22 deletions

View File

@@ -54,6 +54,17 @@
<strong>{{ effectiveExercises?.length || 0 }} {{ $t('socialnetwork.vocab.courses.exercisesShort') }}</strong>
</div>
</div>
<div
v-if="lessonReviewBadgeLabel"
class="lesson-review-status"
:class="lessonReviewStatusClass"
>
<div class="lesson-review-status__top">
<span class="lesson-review-status__badge">{{ lessonReviewBadgeLabel }}</span>
<strong>{{ lessonReviewHeadline }}</strong>
</div>
<p>{{ lessonReviewHint }}</p>
</div>
<details
v-if="lessonPedagogy.phaseLabel || lessonPedagogy.newUnitTarget || lessonPedagogy.reviewWeight != null"
class="lesson-overview-more"
@@ -1212,6 +1223,57 @@ export default {
isIntensiveReview: false
};
},
lessonProgress() {
return this.lesson?.progress || null;
},
lessonReviewBadgeLabel() {
const progress = this.lessonProgress;
if (!progress?.completed) {
return '';
}
const stage = Number(progress.reviewStage || 0);
if (stage === 0) return 'Tag 1';
if (stage === 1) return 'Tag 3';
if (stage === 2) return 'Tag 7';
if (stage >= 3) return 'Review abgeschlossen';
return '';
},
lessonReviewStatusClass() {
const progress = this.lessonProgress;
if (!progress?.completed) {
return '';
}
if (progress.reviewCompleted) {
return 'lesson-review-status--done';
}
if (progress.reviewDue) {
return 'lesson-review-status--due';
}
return 'lesson-review-status--scheduled';
},
lessonReviewHeadline() {
const progress = this.lessonProgress;
if (!progress?.completed) {
return '';
}
if (progress.reviewCompleted) {
return 'Diese Lektion ist in der freien Vertiefung angekommen.';
}
if (progress.reviewDue) {
return 'Diese Review-Welle ist jetzt fällig.';
}
return 'Diese Lektion ist für die nächste Review-Welle vorgemerkt.';
},
lessonReviewHint() {
const progress = this.lessonProgress;
if (!progress?.completed) {
return '';
}
if (progress.reviewCompleted) {
return 'Die 1/3/7-Tage-Wiederholung ist abgeschlossen. Du kannst die Lektion jetzt flexibel weitertrainieren.';
}
return `Nächste Fälligkeit: ${this.formatLessonReviewDue(progress.reviewNextDueAt)}.`;
},
assistantAvailable() {
if (!this.assistantSettings) {
return false;
@@ -1993,6 +2055,31 @@ export default {
}
return this.$t('socialnetwork.vocab.courses.durationMinutes', { minutes });
},
formatLessonReviewDue(reviewNextDueAt) {
if (!reviewNextDueAt) {
return 'jetzt';
}
const dueTimestamp = new Date(reviewNextDueAt).getTime();
if (!Number.isFinite(dueTimestamp)) {
return 'jetzt';
}
const diffMs = dueTimestamp - Date.now();
if (diffMs > 0) {
const untilDays = Math.ceil(diffMs / (24 * 60 * 60 * 1000));
if (untilDays <= 1) {
return 'morgen';
}
return `in ${untilDays} Tagen`;
}
const overdueDays = Math.floor((Date.now() - dueTimestamp) / (24 * 60 * 60 * 1000));
if (overdueDays <= 0) {
return 'heute';
}
if (overdueDays === 1) {
return 'seit 1 Tag';
}
return `seit ${overdueDays} Tagen`;
},
getQuestionData(exercise) {
if (!exercise.questionData) return null;
return typeof exercise.questionData === 'string'
@@ -3040,6 +3127,61 @@ export default {
border-radius: 10px;
}
.lesson-review-status {
padding: 14px 16px;
border-radius: 12px;
border: 1px solid rgba(34, 96, 164, 0.18);
background: rgba(235, 244, 255, 0.86);
color: #21598f;
}
.lesson-review-status__top {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.lesson-review-status__badge {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 999px;
background: rgba(34, 96, 164, 0.14);
font-size: 0.78rem;
font-weight: 700;
}
.lesson-review-status strong,
.lesson-review-status p {
margin: 0;
}
.lesson-review-status p {
margin-top: 8px;
color: inherit;
}
.lesson-review-status--due {
border-color: rgba(185, 99, 24, 0.22);
background: rgba(255, 246, 226, 0.92);
color: #8d5412;
}
.lesson-review-status--due .lesson-review-status__badge {
background: rgba(185, 99, 24, 0.16);
}
.lesson-review-status--done {
border-color: rgba(68, 138, 86, 0.22);
background: rgba(239, 250, 242, 0.92);
color: #2f6b3d;
}
.lesson-review-status--done .lesson-review-status__badge {
background: rgba(68, 138, 86, 0.14);
}
.lesson-meta-label {
display: block;
margin-bottom: 6px;