#!/usr/bin/env node /** * Migriert ältere Bisaya-Kurse von einer kombinierten Lektion „Zahlen & Preise“ (nur #18) * zur viergeteilten Zahlenreihe (#18–#21). Erwartet vorher noch das alte Layout: * Lektion 19 = „Woche 2 - Wiederholung“ (liegt im neuen Kurs an Position 22). * * Ablauf je Kurs: * - Lektionen mit lesson_number >= 19 um +3 nach oben schieben (von oben nach unten) * - Lektion 18 auf „Zahlen 1–20“ setzen * - Neue Lektionen 19–21 („Zahlen: Zehner/Hunderter/Tausender“) einfügen * * Verwendung: * node backend/scripts/migrate-bisaya-zahlen-split.js * * Anschließend: * node backend/scripts/update-bisaya-didactics.js * node backend/scripts/create-bisaya-course-content.js */ import { sequelize } from '../utils/sequelize.js'; import VocabCourseLesson from '../models/community/vocab_course_lesson.js'; import { LESSON_DIDACTICS } from './update-bisaya-didactics.js'; import { getBisayaLessonPedagogy } from './bisaya-course-phase2-pedagogy.js'; const INSERTED_LESSONS = [ { lessonNumber: 19, title: 'Zahlen: Zehner', description: 'Runde Zehner von 20 bis 90', weekNumber: 2, dayNumber: 4, lessonType: 'vocab', culturalNotes: null, targetMinutes: 20, targetScorePercent: 85, requiresReview: true }, { lessonNumber: 20, title: 'Zahlen: Hunderter', description: 'Hunderter bis 900 (usa ka gatos … siyam ka gatos)', weekNumber: 2, dayNumber: 4, lessonType: 'vocab', culturalNotes: null, targetMinutes: 20, targetScorePercent: 85, requiresReview: true }, { lessonNumber: 21, title: 'Zahlen: Tausender', description: 'Tausender und große Beträge (libo)', weekNumber: 2, dayNumber: 4, lessonType: 'vocab', culturalNotes: null, targetMinutes: 18, targetScorePercent: 85, requiresReview: true } ]; function applyPedagogy(patch, lessonNumber) { const pedagogy = getBisayaLessonPedagogy(lessonNumber) || {}; patch.didacticMode = pedagogy.didacticMode || null; patch.phaseLabel = pedagogy.phaseLabel || null; patch.blockNumber = pedagogy.blockNumber ?? null; patch.difficultyWeight = pedagogy.difficultyWeight ?? null; patch.newUnitTarget = pedagogy.newUnitTarget ?? null; patch.reviewWeight = pedagogy.reviewWeight ?? null; patch.isIntensiveReview = Boolean(pedagogy.isIntensiveReview); } function applyDidactics(patch, title) { const d = LESSON_DIDACTICS[title]; if (!d) return; patch.learningGoals = d.learningGoals || []; patch.corePatterns = d.corePatterns || []; patch.grammarFocus = d.grammarFocus || []; patch.speakingPrompts = d.speakingPrompts || []; patch.practicalTasks = d.practicalTasks || []; } async function migrateCourse(courseId) { const maxRow = await sequelize.query( `SELECT MAX(lesson_number) AS max FROM community.vocab_course_lesson WHERE course_id = :courseId`, { replacements: { courseId }, type: sequelize.QueryTypes.SELECT } ); const maxNum = Number(maxRow[0]?.max || 0); if (maxNum < 19) { return { status: 'skip', reason: 'weniger als 19 Lektionen' }; } const l19 = await VocabCourseLesson.findOne({ where: { courseId, lessonNumber: 19 } }); if (!l19) { return { status: 'skip', reason: 'keine Lektion 19' }; } if (l19.title !== 'Woche 2 - Wiederholung') { return { status: 'skip', reason: 'Lektion 19 ist nicht „Woche 2 - Wiederholung“ (bereits migriert oder anderer Stand)' }; } const l18 = await VocabCourseLesson.findOne({ where: { courseId, lessonNumber: 18 } }); if (!l18) { return { status: 'skip', reason: 'keine Lektion 18' }; } if (l18.title !== 'Zahlen & Preise' && l18.title !== 'Zahlen 1–20') { return { status: 'skip', reason: `Lektion 18 unerwartet: „${l18.title}“` }; } await sequelize.transaction(async (t) => { for (let n = maxNum; n >= 19; n -= 1) { await VocabCourseLesson.update( { lessonNumber: n + 3 }, { where: { courseId, lessonNumber: n }, transaction: t } ); } const lesson18 = await VocabCourseLesson.findOne({ where: { courseId, lessonNumber: 18 }, transaction: t }); const patch18 = { title: 'Zahlen 1–20', description: 'Grundzahlen und Zahlen bis 20 (usa … baynte)', weekNumber: 2, dayNumber: 4, lessonType: 'vocab', culturalNotes: null, targetMinutes: 22, targetScorePercent: 85, requiresReview: true }; applyDidactics(patch18, 'Zahlen 1–20'); applyPedagogy(patch18, 18); await lesson18.update(patch18, { transaction: t }); for (const def of INSERTED_LESSONS) { const createPayload = { courseId, chapterId: null, lessonNumber: def.lessonNumber, title: def.title, description: def.description, weekNumber: def.weekNumber, dayNumber: def.dayNumber, lessonType: def.lessonType, culturalNotes: def.culturalNotes, targetMinutes: def.targetMinutes, targetScorePercent: def.targetScorePercent, requiresReview: def.requiresReview }; applyDidactics(createPayload, def.title); applyPedagogy(createPayload, def.lessonNumber); await VocabCourseLesson.create(createPayload, { transaction: t }); } }); return { status: 'ok' }; } async function main() { await sequelize.authenticate(); const [bisayaLanguage] = await sequelize.query( `SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`, { type: sequelize.QueryTypes.SELECT } ); if (!bisayaLanguage) { console.error('❌ Bisaya-Sprache nicht gefunden.'); process.exit(1); } const courses = await sequelize.query( `SELECT id, title FROM community.vocab_course WHERE language_id = :languageId ORDER BY id`, { replacements: { languageId: bisayaLanguage.id }, type: sequelize.QueryTypes.SELECT } ); console.log(`Gefunden: ${courses.length} Bisaya-Kurs(e)\n`); for (const course of courses) { const result = await migrateCourse(course.id); if (result.status === 'ok') { console.log(`✅ Kurs ${course.id}: „${course.title}“ – Zahlen-Split angewendet (Lektionen verschoben, 19–21 eingefügt).`); } else { console.log(`⏭️ Kurs ${course.id}: „${course.title}“ – ${result.reason}`); } } console.log('\nFertig. Danach: node backend/scripts/update-bisaya-didactics.js && node backend/scripts/create-bisaya-course-content.js'); await sequelize.close(); } main().catch((e) => { console.error('❌ Fehler:', e); sequelize.close(); process.exit(1); });