feat: add A2-B1 course expansion features including listening activities and conversation sessions

- Updated VocabCourseView.vue to display CEFR level for courses.
- Enhanced VocabLessonView.vue with new sections for listening activities and conversation labs, including audio controls and transcript toggling.
- Created migration to add new columns to vocab_course and new tables for conversation sessions and listening activities.
- Implemented models for vocab_conversation_session and vocab_listening_activity.
- Developed scripts to create new A2 and B1 bridge courses with structured lesson plans.
- Documented the A2-B1 expansion plan detailing course structure, learning goals, and assessment criteria.
This commit is contained in:
Torsten Schulz (local)
2026-08-27 15:18:16 +02:00
parent d9397f741f
commit f9c884b3cf
16 changed files with 680 additions and 12 deletions

View File

@@ -0,0 +1,90 @@
#!/usr/bin/env node
/**
* Erstellt den eigenständigen Anschlusskurs „Bisaya A2 Alltag & Gespräch“.
* Verwendung: node backend/scripts/create-bisaya-a2-course.js <ownerHashedId>
*/
import crypto from 'crypto';
import { sequelize } from '../utils/sequelize.js';
import User from '../models/community/user.js';
import VocabCourse from '../models/community/vocab_course.js';
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
const BLOCKS = [
['Wohnen & Reparaturen', 'Wohnung beschreiben, Probleme melden und eine Reparatur organisieren.', 'Naay problema sa balay.', 'Es gibt ein Problem im Haus.'],
['Arbeit & Tagesorganisation', 'Aufgaben, Arbeitszeiten und Prioritäten nachvollziehbar erklären.', 'Humanon nako una kini.', 'Ich mache das zuerst fertig.'],
['Schule & digitale Kommunikation', 'Schule, Nachrichten und einfache Absprachen verbinden.', 'Magpadala ko ug mensahe unya.', 'Ich schicke später eine Nachricht.'],
['Familie & Beziehungen', 'Gefühle, Wünsche und Rücksicht in längeren Antworten ausdrücken.', 'Importante ka para nako.', 'Du bist mir wichtig.'],
['Meinung & Entscheidungen', 'Zustimmen, begründen und höflich eine andere Meinung sagen.', 'Para nako, mas maayo kung…', 'Für mich wäre es besser, wenn …'],
['Erlebnisse erzählen', 'Erlebtes in sinnvoller Reihenfolge erzählen und nachfragen.', 'Pagkahuman, miadto mi sa merkado.', 'Danach gingen wir zum Markt.'],
['Reisen & Unterkunft', 'Fahrt, Unterkunft und Änderungen eines Plans klären.', 'Pwede ba nato usbon ang plano?', 'Können wir den Plan ändern?'],
['Geld, Dokumente & Dienste', 'Zahlung, Dokumente und öffentliche Erledigungen bewältigen.', 'Kinahanglan nako ang resibo.', 'Ich brauche die Quittung.'],
['Gesundheit & unerwartete Probleme', 'Beschwerden genauer beschreiben und Hilfe organisieren.', 'Dugay na ni nagsakit.', 'Das tut schon länger weh.'],
['A2-Transfer: Alltag', 'Mehrere Alltagsthemen ohne feste Reihenfolge verbinden.', 'Una, kinahanglan nato…', 'Zuerst müssen wir …'],
['A2-Transfer: Gespräch', 'Ein längeres Gespräch beginnen, tragen und freundlich beenden.', 'Nasabtan nako imong giingon.', 'Ich verstehe, was du gesagt hast.'],
['A2-Abschluss & Weiterlernen', 'Eigene Stärken und nächste Lernziele benennen.', 'Gusto pa ko mopadayon ug praktis.', 'Ich möchte weiter üben.']
];
const TYPES = ['vocab', 'grammar', 'conversation', 'listening', 'conversation', 'review'];
const SUFFIXES = ['Wortschatz im Kontext', 'Muster & Grammatik', 'Geführtes Gespräch', 'Hörverstehen', 'Rollenspiel', 'Checkpoint'];
async function main() {
const ownerHashedId = String(process.argv[2] || '').trim();
if (!ownerHashedId) throw new Error('Bitte ownerHashedId angeben.');
await sequelize.authenticate();
const owner = await User.findOne({ where: { hashedId: ownerHashedId } });
if (!owner) throw new Error('Nutzer nicht gefunden.');
const [language] = await sequelize.query("SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1", { type: sequelize.QueryTypes.SELECT });
if (!language) throw new Error('Bisaya-Sprache nicht gefunden.');
const baseCourse = await VocabCourse.findOne({
where: { languageId: language.id, courseKind: 'core' }, order: [['id', 'ASC']]
});
const [course] = await VocabCourse.findOrCreate({
where: { title: 'Bisaya A2 Alltag & Gespräch', languageId: language.id },
defaults: {
ownerUserId: owner.id,
description: 'Anschlusskurs für variablere Alltagssprache, Hörverstehen und längere Gespräche.',
nativeLanguageId: null,
difficultyLevel: 2,
cefrLevel: 'A2',
courseKind: 'continuation',
prerequisiteCourseId: baseCourse?.id || null,
isPublic: true,
shareCode: crypto.randomBytes(8).toString('hex')
}
});
let created = 0;
for (const [blockIndex, block] of BLOCKS.entries()) {
const [topic, goal, target, gloss] = block;
for (let offset = 0; offset < 6; offset += 1) {
const lessonNumber = blockIndex * 6 + offset + 1;
const [lesson, made] = await VocabCourseLesson.findOrCreate({
where: { courseId: course.id, lessonNumber },
defaults: {
courseId: course.id, chapterId: null, lessonNumber,
title: `${topic}: ${SUFFIXES[offset]}`,
description: goal,
weekNumber: blockIndex + 1, dayNumber: offset + 1,
lessonType: TYPES[offset], didacticMode: TYPES[offset] === 'review' ? 'checkpoint' : TYPES[offset] === 'listening' ? 'listening' : 'real_life_scenario',
phaseLabel: 'a2', blockNumber: blockIndex + 1, difficultyWeight: 2,
newUnitTarget: offset === 5 ? 0 : 6, reviewWeight: offset === 5 ? 70 : 35,
isIntensiveReview: offset === 5, targetMinutes: offset === 3 ? 25 : 30,
targetScorePercent: 80, requiresReview: offset === 5,
learningGoals: [goal, 'Mindestens vier eigene Sätze zum Thema formulieren.', 'Frühere Muster passend wiederverwenden.'],
corePatterns: [
{ target, gloss },
{ target: 'Pwede nimo ko tabangan?', gloss: 'Kannst du mir helfen?' },
{ target: 'Sa akong hunahuna, maayo ni.', gloss: 'Ich finde, das ist gut.' },
{ target: 'Kung mahimo, buhaton nato unya.', gloss: 'Wenn möglich, machen wir es später.' }
],
grammarFocus: [{ title: 'Muster im Kontext', text: 'Nutze die neuen Formen, um einen vollständigen Gedanken zu verbinden.', example: target }],
speakingPrompts: [{ title: 'Eigene Antwort', prompt: `${goal} Antworte frei in vier bis sechs Sätzen.`, cue: target }],
practicalTasks: [{ title: 'Transfer', text: 'Nutze ein Kernmuster heute in einer echten oder simulierten Nachricht.' }]
}
});
if (made) created += 1;
}
}
console.log(`A2-Kurs ${course.id}: ${created} Lektionen angelegt.`);
}
main().then(() => sequelize.close()).catch((error) => { console.error(error); sequelize.close(); process.exitCode = 1; });