Files
yourpart3/backend/scripts/create-german-for-bisaya-course-content.js
Torsten Schulz (local) 0d625f1727
All checks were successful
Deploy to production / deploy (push) Successful in 3m7s
feat(falukant): add age information to lovers in family view
- Updated FalukantService to include age details for partners in relationships.
- Added translations for 'age' in English, German, and Spanish localization files.
- Enhanced FamilyView component to display age information for lovers and candidates, improving user experience.
2026-03-31 11:36:12 +02:00

736 lines
24 KiB
JavaScript

#!/usr/bin/env node
/**
* Script zum Erstellen von Übungen für Deutschkurse aus Sicht von Bisaya-Lernenden.
*
* Verwendung:
* node backend/scripts/create-german-for-bisaya-course-content.js
*/
import { sequelize } from '../utils/sequelize.js';
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
import User from '../models/community/user.js';
import { GERMAN_FOR_BISAYA_PHASE1_DIDACTICS } from './german-for-bisaya-phase1.js';
import { GERMAN_FOR_BISAYA_PHASE3_DIDACTICS } from './german-for-bisaya-phase3-extension.js';
import { GERMAN_FOR_BISAYA_PHASE4_DIDACTICS } from './german-for-bisaya-phase4-extension.js';
import { GERMAN_FOR_BISAYA_PHASE5_DIDACTICS } from './german-for-bisaya-phase5-extension.js';
function withTypeName(exerciseTypeName, exercise) {
return {
...exercise,
exerciseTypeName
};
}
const GERMAN_DIDACTICS = {
...GERMAN_FOR_BISAYA_PHASE1_DIDACTICS,
...GERMAN_FOR_BISAYA_PHASE3_DIDACTICS,
...GERMAN_FOR_BISAYA_PHASE4_DIDACTICS,
...GERMAN_FOR_BISAYA_PHASE5_DIDACTICS
};
const GENERIC_DISTRACTOR_PATTERNS = Array.from(new Set(
Object.values(GERMAN_DIDACTICS)
.flatMap((entry) => Array.isArray(entry?.corePatterns) ? entry.corePatterns : [])
.map((pattern) => String(pattern || '').trim())
.filter(Boolean)
)).slice(0, 300);
function normalizeText(value) {
return String(value || '')
.trim()
.replace(/\s+/g, ' ');
}
function simpleHash(value) {
return Array.from(String(value || '')).reduce((sum, char) => sum + char.charCodeAt(0), 0);
}
function rotateArray(values, offset) {
if (!Array.isArray(values) || values.length === 0) return [];
const normalizedOffset = ((offset % values.length) + values.length) % values.length;
return values.slice(normalizedOffset).concat(values.slice(0, normalizedOffset));
}
function getLessonDidactics(lesson) {
const staticDidactics = GERMAN_DIDACTICS[lesson.title] || {};
return {
learningGoals: Array.isArray(lesson.learningGoals) ? lesson.learningGoals : (staticDidactics.learningGoals || []),
corePatterns: (Array.isArray(lesson.corePatterns) ? lesson.corePatterns : (staticDidactics.corePatterns || []))
.map((entry) => normalizeText(entry))
.filter(Boolean),
grammarFocus: Array.isArray(lesson.grammarFocus) ? lesson.grammarFocus : (staticDidactics.grammarFocus || []),
speakingPrompts: Array.isArray(lesson.speakingPrompts) ? lesson.speakingPrompts : (staticDidactics.speakingPrompts || []),
practicalTasks: Array.isArray(lesson.practicalTasks) ? lesson.practicalTasks : (staticDidactics.practicalTasks || [])
};
}
function getScenarioPrompt(lesson, didactics) {
const speakingPrompt = Array.isArray(didactics.speakingPrompts) ? didactics.speakingPrompts[0] : null;
const practicalTask = Array.isArray(didactics.practicalTasks) ? didactics.practicalTasks[0] : null;
if (speakingPrompt?.prompt) return speakingPrompt.prompt;
if (practicalTask?.text) return practicalTask.text;
if (lesson.description) return lesson.description;
return `Reagiere passend in einer Alltagssituation aus der Lektion "${lesson.title}".`;
}
function getChoiceQuestion(lesson, didactics) {
const scenarioPrompt = getScenarioPrompt(lesson, didactics);
switch (lesson.lessonType) {
case 'conversation':
return `${scenarioPrompt} Welche deutsche Formulierung passt am besten?`;
case 'grammar':
return `Welche deutsche Struktur passt am besten zum Schwerpunkt "${lesson.title}"?`;
case 'review':
return `Welche Formulierung solltest du aus "${lesson.title}" sicher wiedererkennen?`;
case 'culture':
return `Welche Formulierung passt besonders gut zum kulturellen Schwerpunkt "${lesson.title}"?`;
case 'vocab':
default:
return `Welche Formulierung gehört thematisch zu "${lesson.title}"?`;
}
}
function pickDistractors(pattern, allPatterns, count) {
return allPatterns
.filter((entry) => entry !== pattern)
.slice(0, count);
}
function buildChoiceExercise(lesson, didactics, pattern, allPatterns, variant = 0) {
const distractors = pickDistractors(pattern, allPatterns, 3);
if (distractors.length < 3) return null;
const options = rotateArray([pattern, ...distractors], simpleHash(`${lesson.title}:${pattern}:${variant}`) % 4);
return {
exerciseTypeId: 2,
title: `${lesson.title}: Passende Formulierung wählen`,
instruction: 'Wähle die natürlichste deutsche Formulierung für die Situation oder den Schwerpunkt der Lektion.',
questionData: {
type: 'multiple_choice',
question: getChoiceQuestion(lesson, didactics),
options
},
answerData: {
type: 'multiple_choice',
correctAnswer: options.indexOf(pattern)
},
explanation: `"${pattern}" ist ein zentrales deutsches Muster dieser Lektion.`
};
}
function pickGapTarget(pattern) {
const tokens = normalizeText(pattern)
.split(' ')
.map((token) => token.trim())
.filter(Boolean);
const candidates = tokens
.map((token, index) => ({ token, index, score: token.replace(/[.,?!]/g, '').length }))
.filter(({ token }) => token.replace(/[.,?!]/g, '').length >= 3);
if (candidates.length === 0) return null;
candidates.sort((left, right) => right.score - left.score);
return candidates[0];
}
function buildGapExercise(lessonTitle, pattern) {
const gapTarget = pickGapTarget(pattern);
if (!gapTarget) return null;
const tokens = normalizeText(pattern).split(' ');
tokens[gapTarget.index] = '{gap}';
return {
exerciseTypeId: 1,
title: `${lessonTitle}: Muster vervollständigen`,
instruction: 'Fülle die Lücke mit dem passenden deutschen Ausdruck.',
questionData: {
type: 'gap_fill',
text: tokens.join(' '),
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: [gapTarget.token.replace(/[.,?!]/g, '')]
},
explanation: `Das vollständige deutsche Kernmuster lautet: "${pattern}".`
};
}
function buildSentenceExercise(lessonTitle, pattern) {
const tokens = normalizeText(pattern)
.replace(/[?!]/g, '')
.split(' ')
.filter(Boolean);
if (tokens.length < 2) return null;
return {
exerciseTypeId: 3,
title: `${lessonTitle}: Satz bauen`,
instruction: 'Ordne die Wörter zu einem korrekten deutschen Satz.',
questionData: {
type: 'sentence_building',
question: `Baue das Kernmuster aus der Lektion "${lessonTitle}".`,
tokens
},
answerData: {
correct: [normalizeText(pattern)]
},
explanation: `Dieses Kernmuster gehört zur Lektion "${lessonTitle}".`
};
}
function buildSpeakingExercise(lessonTitle, didactics, fallbackPattern) {
const speakingPrompt = Array.isArray(didactics.speakingPrompts) ? didactics.speakingPrompts[0] : null;
const expectedText = normalizeText(speakingPrompt?.cue || fallbackPattern);
if (!expectedText) return null;
const keywords = expectedText
.toLowerCase()
.replace(/[.,?!]/g, '')
.split(' ')
.filter((token) => token.length >= 3)
.slice(0, 5);
return {
exerciseTypeId: 8,
title: `${lessonTitle}: Frei sprechen`,
instruction: 'Sprich das zentrale deutsche Muster frei nach.',
questionData: {
type: 'speaking_from_memory',
question: speakingPrompt?.prompt || `Sprich ein zentrales Muster aus der Lektion "${lessonTitle}".`,
expectedText,
keywords
},
answerData: {
type: 'speaking_from_memory'
},
explanation: 'Wichtig sind ein flüssiger Abruf und die zentralen deutschen Schlüsselwörter.'
};
}
function buildSituationalExercise(lessonTitle, didactics, fallbackPattern) {
const speakingPrompt = Array.isArray(didactics.speakingPrompts) ? didactics.speakingPrompts[0] : null;
const modelAnswer = normalizeText(speakingPrompt?.cue || fallbackPattern);
if (!modelAnswer) return null;
const keywords = modelAnswer
.toLowerCase()
.replace(/[.,?!]/g, '')
.split(' ')
.filter((token) => token.length >= 3)
.slice(0, 5);
return withTypeName('situational_response', {
title: `${lessonTitle}: Situativ reagieren`,
instruction: 'Antworte kurz und passend auf Deutsch.',
questionData: {
type: 'situational_response',
question: speakingPrompt?.prompt || `Reagiere passend mit einem Ausdruck aus der Lektion "${lessonTitle}".`,
keywords
},
answerData: {
modelAnswer,
keywords
},
explanation: `Das Kernmuster "${modelAnswer}" passt natürlich zu dieser Situation.`
});
}
function buildCultureExercise(lesson, pattern, allPatterns) {
const distractors = pickDistractors(pattern, allPatterns, 3);
if (distractors.length < 3) return null;
const options = rotateArray([pattern, ...distractors], simpleHash(`${lesson.title}:culture`) % 4);
const culturalNote = normalizeText(lesson.culturalNotes || '');
return {
exerciseTypeId: 2,
title: `${lesson.title}: Kulturell einordnen`,
instruction: 'Ordne den Ausdruck dem kulturellen Schwerpunkt der Lektion zu.',
questionData: {
type: 'multiple_choice',
question: culturalNote
? `${culturalNote} Welche Formulierung passt dazu besonders gut?`
: `Welche Formulierung passt besonders gut zum Schwerpunkt "${lesson.title}"?`,
options
},
answerData: {
type: 'multiple_choice',
correctAnswer: options.indexOf(pattern)
},
explanation: `"${pattern}" ist eng mit dem kulturellen Schwerpunkt dieser Lektion verbunden.`
};
}
function generateExercisesFromDidactics(lesson) {
const didactics = getLessonDidactics(lesson);
const corePatterns = didactics.corePatterns;
if (corePatterns.length === 0) return [];
const patternA = corePatterns[0];
const patternB = corePatterns[1] || corePatterns[0];
const pool = Array.from(new Set([...corePatterns, ...GENERIC_DISTRACTOR_PATTERNS]));
if (lesson.lessonType === 'conversation') {
return [
buildChoiceExercise(lesson, didactics, patternA, pool, 0),
buildGapExercise(lesson.title, patternA),
buildSentenceExercise(lesson.title, patternB),
buildSituationalExercise(lesson.title, didactics, patternA),
buildSpeakingExercise(lesson.title, didactics, patternB)
].filter(Boolean);
}
if (lesson.lessonType === 'grammar') {
return [
buildChoiceExercise(lesson, didactics, patternA, pool, 0),
buildChoiceExercise(lesson, didactics, patternB, pool, 1),
buildGapExercise(lesson.title, patternA),
buildSentenceExercise(lesson.title, patternB),
buildSpeakingExercise(lesson.title, didactics, patternA)
].filter(Boolean);
}
if (lesson.lessonType === 'review' || lesson.didacticMode === 'intensive_review') {
return [
buildChoiceExercise(lesson, didactics, patternA, pool, 0),
buildChoiceExercise(lesson, didactics, patternB, pool, 1),
buildGapExercise(lesson.title, patternA),
buildSentenceExercise(lesson.title, patternB),
buildSituationalExercise(lesson.title, didactics, patternA),
buildSpeakingExercise(lesson.title, didactics, patternB)
].filter(Boolean);
}
if (lesson.lessonType === 'culture') {
return [
buildCultureExercise(lesson, patternA, pool),
buildGapExercise(lesson.title, patternA),
buildSpeakingExercise(lesson.title, didactics, patternB)
].filter(Boolean);
}
return [
buildChoiceExercise(lesson, didactics, patternA, pool, 0),
buildChoiceExercise(lesson, didactics, patternB, pool, 1),
buildGapExercise(lesson.title, patternA),
buildSentenceExercise(lesson.title, patternB)
].filter(Boolean);
}
const GERMAN_EXERCISES = {
'Begrüßung & Vorstellung': [
{
exerciseTypeId: 2,
title: 'Vorstellung erkennen',
instruction: 'Wähle die passendste deutsche Vorstellung.',
questionData: {
type: 'multiple_choice',
question: 'Du triffst jemanden zum ersten Mal. Welche Formulierung passt?',
options: ['Hallo, ich heiße Maria.', 'Ich habe Maria.', 'Wo Maria?', 'Nicht Maria.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Hallo, ich heiße Maria." ist eine natürliche deutsche Selbstvorstellung.'
},
{
exerciseTypeId: 1,
title: 'Herkunft ergänzen',
instruction: 'Fülle die Lücke mit dem passenden Ausdruck.',
questionData: {
type: 'gap_fill',
text: 'Ich komme {gap} Cebu.',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['aus']
},
explanation: 'Im Deutschen sagt man "Ich komme aus Cebu."'
},
withTypeName('situational_response', {
title: 'Kurz reagieren bei der Begrüßung',
instruction: 'Antworte kurz und passend auf Deutsch.',
questionData: {
type: 'situational_response',
question: 'Jemand sagt: "Hallo, ich heiße Anna." Wie reagierst du mit deiner eigenen Vorstellung?',
keywords: ['hallo', 'heiße', 'ich']
},
answerData: {
modelAnswer: 'Hallo, ich heiße Maria.',
keywords: ['hallo', 'heiße', 'ich']
},
explanation: 'Die sicherste frühe Reaktion ist eine eigene kurze Vorstellung mit "ich heiße".'
})
],
'der / die / das - Einstieg': [
{
exerciseTypeId: 2,
title: 'Artikel als Chunk erkennen',
instruction: 'Wähle die Form mit dem richtigen Artikel.',
questionData: {
type: 'multiple_choice',
question: 'Welche Form ist als Lern-Chunk richtig?',
options: ['der Tisch', 'die Tisch', 'das Tasche', 'der Kind']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: 'Nomen sollen möglichst zusammen mit dem Artikel gelernt werden.'
},
{
exerciseTypeId: 1,
title: 'Artikel ergänzen',
instruction: 'Fülle den passenden Artikel ein.',
questionData: {
type: 'gap_fill',
text: '{gap} Tasche',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['die']
},
explanation: 'Es heißt "die Tasche".'
},
{
exerciseTypeId: 3,
title: 'Chunk richtig bauen',
instruction: 'Ordne die Wörter zu einem korrekten Lern-Chunk.',
questionData: {
type: 'sentence_building',
question: 'Baue den richtigen Chunk für "child".',
tokens: ['das', 'Kind']
},
answerData: {
correct: ['das Kind']
},
explanation: 'Auch sehr kurze Strukturen sollen als vollständiger Chunk gelernt werden.'
}
],
'nicht / kein - Einstieg': [
{
exerciseTypeId: 2,
title: 'Negation unterscheiden',
instruction: 'Wähle die passende deutsche Negation.',
questionData: {
type: 'multiple_choice',
question: 'Welche Formulierung ist richtig?',
options: ['Ich habe kein Geld.', 'Ich habe nicht Geld.', 'Ich bin kein müde.', 'Ich kein habe Geld.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Kein" steht hier vor dem Nomen, deshalb ist "Ich habe kein Geld." richtig.'
},
{
exerciseTypeId: 2,
title: 'Adjektiv negieren',
instruction: 'Wähle die richtige Form für eine Aussage mit Adjektiv.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagst du korrekt: "Ich bin nicht müde"?',
options: ['Ich bin nicht müde.', 'Ich bin kein müde.', 'Ich nicht bin müde.', 'Ich bin müde kein.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: 'Adjektive und Aussagen werden im Deutschen oft mit "nicht" negiert.'
},
{
exerciseTypeId: 1,
title: 'Kein ergänzen',
instruction: 'Ergänze die passende Negation.',
questionData: {
type: 'gap_fill',
text: 'Ich habe {gap} Zeit.',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['keine']
},
explanation: 'Bei "Zeit" braucht man hier "keine".'
}
],
'wo / wohin - Kontrast': [
{
exerciseTypeId: 2,
title: 'Ort oder Richtung?',
instruction: 'Wähle die Frage nach einem Ziel.',
questionData: {
type: 'multiple_choice',
question: 'Welche Frage passt, wenn du nach dem Ziel fragst?',
options: ['Wohin gehst du?', 'Wo gehst du?', 'Wie gehst du?', 'Wann gehst du?']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Wohin" fragt nach einer Richtung oder einem Ziel.'
},
{
exerciseTypeId: 1,
title: 'Fragewort ergänzen',
instruction: 'Setze das passende Fragewort ein.',
questionData: {
type: 'gap_fill',
text: '{gap} bist du? Ich bin zu Hause.',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['Wo']
},
explanation: 'Bei einem Ort fragt man im Deutschen mit "Wo".'
}
],
'du / Sie - Einstieg': [
{
exerciseTypeId: 2,
title: 'Höfliche Anrede erkennen',
instruction: 'Wähle die höfliche deutsche Form.',
questionData: {
type: 'multiple_choice',
question: 'Du sprichst eine fremde erwachsene Person höflich an. Welche Frage passt?',
options: ['Wie heißen Sie?', 'Wie heißt du?', 'Wo bist du?', 'Wie bist du?']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Sie" markiert im Deutschen die höfliche Anrede.'
},
{
exerciseTypeId: 1,
title: 'Anredeform ergänzen',
instruction: 'Fülle die passende Anredeform ein.',
questionData: {
type: 'gap_fill',
text: 'Können {gap} mir helfen?',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['Sie']
},
explanation: 'In einer höflichen Bitte heißt es "Können Sie mir helfen?".'
}
],
'Perfekt - Einstieg': [
{
exerciseTypeId: 2,
title: 'Perfektform erkennen',
instruction: 'Wähle die passende deutsche Vergangenheitsform.',
questionData: {
type: 'multiple_choice',
question: 'Welche Form ist ein korrektes frühes Perfekt?',
options: ['Ich habe gearbeitet.', 'Ich habe arbeiten.', 'Ich bin gearbeitet.', 'Ich arbeitete habe.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: 'Viele frühe Alltagsverben bilden das Perfekt mit "haben" + Partizip II.'
},
{
exerciseTypeId: 1,
title: 'Hilfsverb ergänzen',
instruction: 'Ergänze das passende Hilfsverb.',
questionData: {
type: 'gap_fill',
text: 'Ich {gap} gestern gearbeitet.',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['habe']
},
explanation: 'Im Perfekt braucht man hier das Hilfsverb "habe".'
}
]
};
async function resolveExerciseTypeId(exercise) {
if (exercise.exerciseTypeId) {
return exercise.exerciseTypeId;
}
const trimmedName =
exercise.exerciseTypeName != null && exercise.exerciseTypeName !== ''
? String(exercise.exerciseTypeName).trim()
: '';
if (!trimmedName) {
throw new Error(`Kein exerciseTypeId oder exerciseTypeName für Übung "${exercise.title || 'unbenannt'}" definiert`);
}
const [type] = await sequelize.query(
`SELECT id FROM community.vocab_grammar_exercise_type WHERE name = :name LIMIT 1`,
{
replacements: { name: trimmedName },
type: sequelize.QueryTypes.SELECT
}
);
if (!type) {
throw new Error(`Übungstyp "${trimmedName}" nicht gefunden`);
}
return Number(type.id);
}
async function findOrCreateSystemUser() {
let systemUser = await User.findOne({ where: { username: 'system' } });
if (!systemUser) {
systemUser = await User.findOne({ where: { username: 'admin' } });
}
if (!systemUser) {
throw new Error('System user not found');
}
return systemUser;
}
function getExercisesForLesson(lesson) {
if (GERMAN_EXERCISES[lesson.title]) {
return GERMAN_EXERCISES[lesson.title];
}
for (const [key, exercises] of Object.entries(GERMAN_EXERCISES)) {
if (lesson.title.includes(key) || key.includes(lesson.title)) {
return exercises;
}
}
return generateExercisesFromDidactics(lesson);
}
async function createGermanForBisayaCourseContent() {
await sequelize.authenticate();
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
const systemUser = await findOrCreateSystemUser();
console.log(`Verwende System-Benutzer: ${systemUser.username} (ID: ${systemUser.id})\n`);
const [germanLanguage] = await sequelize.query(
`SELECT id FROM community.vocab_language WHERE name = 'Deutsch' LIMIT 1`,
{
type: sequelize.QueryTypes.SELECT
}
);
const [bisayaLanguage] = await sequelize.query(
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
{
type: sequelize.QueryTypes.SELECT
}
);
if (!germanLanguage || !bisayaLanguage) {
throw new Error('Deutsch oder Bisaya als Sprache nicht gefunden');
}
const courses = await sequelize.query(
`SELECT id, title, owner_user_id AS "ownerUserId"
FROM community.vocab_course
WHERE language_id = :languageId
AND native_language_id = :nativeLanguageId
AND title LIKE 'Deutsch für Bisaya-Lernende%'`,
{
replacements: {
languageId: germanLanguage.id,
nativeLanguageId: bisayaLanguage.id
},
type: sequelize.QueryTypes.SELECT
}
);
console.log(`Gefunden: ${courses.length} Deutsch-für-Bisaya-Kurse\n`);
let totalExercisesAdded = 0;
let totalLessonsProcessed = 0;
const replaceLessons = new Set(Object.keys(GERMAN_EXERCISES));
for (const course of courses) {
console.log(`📚 Kurs: ${course.title} (ID: ${course.id})`);
const lessons = await VocabCourseLesson.findAll({
where: { courseId: course.id },
order: [['lessonNumber', 'ASC']]
});
console.log(` ${lessons.length} Lektionen gefunden\n`);
for (const lesson of lessons) {
const exercises = getExercisesForLesson(lesson);
if (exercises.length === 0) {
console.log(` ⚠️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - keine Übungen definiert`);
continue;
}
const existingCount = await VocabGrammarExercise.count({ where: { lessonId: lesson.id } });
const replaceExisting = replaceLessons.has(lesson.title);
if (existingCount > 0 && !replaceExisting) {
console.log(` ⏭️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - bereits ${existingCount} Übung(en) vorhanden`);
continue;
}
if (replaceExisting && existingCount > 0) {
const deleted = await VocabGrammarExercise.destroy({ where: { lessonId: lesson.id } });
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deleted} bestehende Übung(en) entfernt`);
}
let exerciseNumber = 1;
for (const exerciseData of exercises) {
const exerciseTypeId = await resolveExerciseTypeId(exerciseData);
await VocabGrammarExercise.create({
lessonId: lesson.id,
exerciseTypeId,
exerciseNumber: exerciseNumber++,
title: exerciseData.title,
instruction: exerciseData.instruction,
questionData: JSON.stringify(exerciseData.questionData),
answerData: JSON.stringify(exerciseData.answerData),
explanation: exerciseData.explanation,
createdByUserId: course.ownerUserId || systemUser.id
});
totalExercisesAdded++;
}
console.log(` ✅ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${exercises.length} Übung(en) erstellt`);
totalLessonsProcessed++;
}
console.log('');
}
console.log('\n🎉 Zusammenfassung:');
console.log(` ${totalLessonsProcessed} Lektionen bearbeitet`);
console.log(` ${totalExercisesAdded} Übungen erstellt`);
}
createGermanForBisayaCourseContent()
.then(() => {
sequelize.close();
process.exit(0);
})
.catch((error) => {
console.error('❌ Fehler:', error);
sequelize.close();
process.exit(1);
});