From 44da49060a5e2d853dd72dcc67fd0966975bc878 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Tue, 25 Aug 2026 12:15:46 +0200 Subject: [PATCH] feat(vocab): add intensive review status dialog and related functionality --- .../src/i18n/locales/de/socialnetwork.json | 6 + .../src/i18n/locales/en/socialnetwork.json | 6 + frontend/src/views/social/VocabCourseView.vue | 110 +++++++++++++++++- 3 files changed, 119 insertions(+), 3 deletions(-) diff --git a/frontend/src/i18n/locales/de/socialnetwork.json b/frontend/src/i18n/locales/de/socialnetwork.json index b4a330e..d75bce6 100755 --- a/frontend/src/i18n/locales/de/socialnetwork.json +++ b/frontend/src/i18n/locales/de/socialnetwork.json @@ -774,6 +774,12 @@ "courseFlowIntensiveTitle": "Fällige Intensivphase", "courseFlowIntensiveDescription": "Verdichtete Wiederholung, sobald der Block davor weitgehend sitzt.", "courseFlowIntensiveEmpty": "Aktuell ist keine neue Intensivphase freigeschaltet.", + "courseFlowIntensiveStatusAction": "Freischaltung prüfen", + "courseFlowIntensiveStatusTitle": "Status der Intensivphase", + "courseFlowIntensiveStatusAllDone": "Alle Intensivphasen dieses Kurses sind bereits abgeschlossen.", + "courseFlowIntensiveStatusReady": "„{lesson}“ ist bereit und kann jetzt geöffnet werden.", + "courseFlowIntensiveStatusProgress": "Für „{lesson}“ sind {done} von {total} vorausgehenden Lektionen im selben Block abgeschlossen.", + "courseFlowIntensiveStatusNoDate": "Die Intensivphase wird nicht nach einer Wartezeit freigeschaltet, sondern sofort nach Abschluss dieser Lektionen.", "courseFlowPracticeTitle": "Freie Vertiefung", "courseFlowPracticeDescription": "Abgeschlossene Lektionen für lockeres Nachtrainieren außerhalb des Pflichtpfads.", "courseFlowPracticeEmpty": "Sobald du erste Lektionen abgeschlossen hast, erscheinen sie hier für freies Nachtrainieren.", diff --git a/frontend/src/i18n/locales/en/socialnetwork.json b/frontend/src/i18n/locales/en/socialnetwork.json index 1773e25..04c0c0e 100755 --- a/frontend/src/i18n/locales/en/socialnetwork.json +++ b/frontend/src/i18n/locales/en/socialnetwork.json @@ -774,6 +774,12 @@ "courseFlowIntensiveTitle": "Due intensive review", "courseFlowIntensiveDescription": "Condensed review once the block before it is mostly stable.", "courseFlowIntensiveEmpty": "No new intensive review is unlocked right now.", + "courseFlowIntensiveStatusAction": "Check availability", + "courseFlowIntensiveStatusTitle": "Intensive review status", + "courseFlowIntensiveStatusAllDone": "All intensive review phases in this course are already complete.", + "courseFlowIntensiveStatusReady": "“{lesson}” is ready and can be opened now.", + "courseFlowIntensiveStatusProgress": "For “{lesson}”, {done} of {total} preceding lessons in the same block are complete.", + "courseFlowIntensiveStatusNoDate": "The intensive review unlocks immediately after these lessons are complete; it does not have a waiting period.", "courseFlowPracticeTitle": "Free practice", "courseFlowPracticeDescription": "Completed lessons for relaxed extra practice outside the required path.", "courseFlowPracticeEmpty": "As soon as you complete your first lessons, they will appear here for free practice.", diff --git a/frontend/src/views/social/VocabCourseView.vue b/frontend/src/views/social/VocabCourseView.vue index 6094335..caa73d6 100755 --- a/frontend/src/views/social/VocabCourseView.vue +++ b/frontend/src/views/social/VocabCourseView.vue @@ -206,7 +206,12 @@ {{ $t('socialnetwork.vocab.courses.lessonBlockLabel', { number: nextIntensiveReviewLesson.pedagogy?.blockNumber || '—' }) }} -

{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveEmpty') }}

+
@@ -312,6 +317,37 @@ +
+ +
+
@@ -376,6 +412,7 @@ export default { srsDailyLimit: 50, srsTodayRemaining: null, srsLoading: false, + showIntensiveStatusDialog: false, showAddLessonDialog: false, assistantSettings: null, hardVocabList: [], @@ -460,20 +497,46 @@ export default { }, nextIntensiveReviewLesson() { return this.sortedLessons.find((lesson) => { - const isIntensive = lesson.pedagogy?.didacticMode === 'intensive_review' || lesson.pedagogy?.isIntensiveReview; + const isIntensive = this.isIntensiveReviewLesson(lesson); if (!isIntensive) return false; if (this.getLessonProgress(lesson.id, lesson)?.completed) return false; const blockNumber = lesson.pedagogy?.blockNumber; const blockLessons = this.sortedLessons.filter((candidate) => { if ((candidate.pedagogy?.blockNumber || null) !== blockNumber) return false; - const candidateIsIntensive = candidate.pedagogy?.didacticMode === 'intensive_review' || candidate.pedagogy?.isIntensiveReview; + const candidateIsIntensive = this.isIntensiveReviewLesson(candidate); return !candidateIsIntensive && candidate.lessonNumber < lesson.lessonNumber; }); return blockLessons.length > 0 && blockLessons.every((candidate) => this.getLessonProgress(candidate.id, candidate)?.completed); }) || null; }, + intensiveReviewStatus() { + const lesson = this.sortedLessons.find((candidate) => ( + this.isIntensiveReviewLesson(candidate) + && !this.getLessonProgress(candidate.id, candidate)?.completed + )); + if (!lesson) { + return { allCompleted: true, ready: false, prerequisiteLessons: [], missingPrerequisiteLessons: [], completedPrerequisiteCount: 0 }; + } + + const prerequisiteLessons = this.sortedLessons.filter((candidate) => ( + !this.isIntensiveReviewLesson(candidate) + && candidate.pedagogy?.blockNumber === lesson.pedagogy?.blockNumber + && candidate.lessonNumber < lesson.lessonNumber + )); + const missingPrerequisiteLessons = prerequisiteLessons.filter((candidate) => ( + !this.getLessonProgress(candidate.id, candidate)?.completed + )); + return { + allCompleted: false, + lesson, + ready: prerequisiteLessons.length > 0 && missingPrerequisiteLessons.length === 0, + prerequisiteLessons, + missingPrerequisiteLessons, + completedPrerequisiteCount: prerequisiteLessons.length - missingPrerequisiteLessons.length + }; + }, freePracticeLessons() { return this.sortedLessons .filter((lesson) => { @@ -566,6 +629,9 @@ export default { } }, methods: { + isIntensiveReviewLesson(lesson) { + return lesson?.pedagogy?.didacticMode === 'intensive_review' || Boolean(lesson?.pedagogy?.isIntensiveReview); + }, displayCourseTitle(course) { return localizeVocabCourseTitle(course?.title, this.$i18n?.locale) || ''; }, @@ -935,6 +1001,10 @@ export default { openLesson(lessonId) { this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}/lessons/${lessonId}`); }, + openIntensivePrerequisite(lessonId) { + this.showIntensiveStatusDialog = false; + this.openLesson(lessonId); + }, todayPlanStepLabel(type) { const key = { review_due: 'courseTodayPlanStepReviewDue', @@ -1382,6 +1452,40 @@ export default { font-size: 0.92rem; } +.course-flow-card__status { + margin-top: 10px; + padding: 0; + border: 0; + background: transparent; + color: #2f6b3d; + font-weight: 650; + text-decoration: underline; + cursor: pointer; +} + +.intensive-status-dialog__hint { + color: var(--color-text-secondary); + line-height: 1.5; +} + +.intensive-status-dialog__list { + display: grid; + gap: 8px; + margin: 16px 0; + padding: 0; + list-style: none; +} + +.intensive-status-dialog__list li { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); +} + .share-code { font-family: monospace; }