feat(bisaya-course): add new lesson and update course creation logic
All checks were successful
Deploy to production / deploy (push) Successful in 2m51s

- Introduced a new lesson titled "Zahlen & Preise" to enhance the numerical curriculum in Bisaya.
- Updated the course creation logic to check for existing "Familien"-Bisaya courses, ensuring idempotency in course creation.
- Enhanced the lesson didactics by mapping legacy titles to current lesson keys, improving data consistency.
- Adjusted the course generation script to limit Bisaya courses to German-speaking learners only, streamlining course offerings.
This commit is contained in:
Torsten Schulz (local)
2026-04-16 22:16:23 +02:00
parent 6dce418728
commit 68ac5ec281
6 changed files with 406 additions and 21 deletions

View File

@@ -0,0 +1,139 @@
#!/usr/bin/env node
/**
* Entfernt doppelte „Bisaya für Familien - Alltag & Stabilisierung“-Kurse und behält
* den Kurs mit der kleinsten ID (üblicherweise der erste / in der UI verlinkte).
*
* Standard: Nur Anzeige (Dry-Run). Löschen nur mit:
* VOCAB_DEDUPE_BISAYA_FAMILY_APPLY=1 node backend/scripts/dedupe-bisaya-family-courses.js
*/
import { sequelize } from '../utils/sequelize.js';
const CANONICAL_TITLE = 'Bisaya für Familien - Alltag & Stabilisierung';
async function deleteCourseCascade(courseId, transaction) {
const lessonRows = await sequelize.query(
`SELECT id FROM community.vocab_course_lesson WHERE course_id = :cid`,
{ replacements: { cid: courseId }, transaction, type: sequelize.QueryTypes.SELECT }
);
const lessonIds = lessonRows.map((r) => r.id);
if (lessonIds.length > 0) {
const exRows = await sequelize.query(
`SELECT id FROM community.vocab_grammar_exercise WHERE lesson_id IN (:lids)`,
{ replacements: { lids: lessonIds }, transaction, type: sequelize.QueryTypes.SELECT }
);
const exerciseIds = exRows.map((r) => r.id);
if (exerciseIds.length > 0) {
await sequelize.query(
`DELETE FROM community.vocab_grammar_exercise_progress WHERE exercise_id IN (:eids)`,
{ replacements: { eids: exerciseIds }, transaction }
);
await sequelize.query(`DELETE FROM community.vocab_grammar_exercise WHERE id IN (:eids)`, {
replacements: { eids: exerciseIds },
transaction
});
}
await sequelize.query(
`DELETE FROM community.vocab_course_progress WHERE lesson_id IN (:lids) OR course_id = :cid`,
{ replacements: { lids: lessonIds, cid: courseId }, transaction }
);
await sequelize.query(
`DELETE FROM community.vocab_course_lesson WHERE course_id = :cid`,
{ replacements: { cid: courseId }, transaction }
);
} else {
await sequelize.query(
`DELETE FROM community.vocab_course_progress WHERE course_id = :cid`,
{ replacements: { cid: courseId }, transaction }
);
}
await sequelize.query(
`DELETE FROM community.vocab_course_enrollment WHERE course_id = :cid`,
{ replacements: { cid: courseId }, transaction }
);
await sequelize.query(`DELETE FROM community.vocab_course WHERE id = :cid`, {
replacements: { cid: courseId },
transaction
});
}
async function main() {
const apply = process.env.VOCAB_DEDUPE_BISAYA_FAMILY_APPLY === '1';
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, owner_user_id AS "ownerUserId", created_at AS "createdAt"
FROM community.vocab_course
WHERE language_id = :languageId AND title = :title
ORDER BY id ASC`,
{
replacements: { languageId: bisayaLanguage.id, title: CANONICAL_TITLE },
type: sequelize.QueryTypes.SELECT
}
);
if (courses.length <= 1) {
console.log(
courses.length === 0
? `Kein Kurs mit Titel „${CANONICAL_TITLE}“ gefunden.`
: `Nur ein Kurs nichts zu bereinigen (ID ${courses[0].id}).`
);
await sequelize.close();
return;
}
const keep = courses[0];
const remove = courses.slice(1);
console.log(`Behalten: Kurs-ID ${keep.id} (älteste ID)\n`);
console.log(`${apply ? 'Löschen' : 'Würde löschen'} (${remove.length} Duplikate):`);
for (const c of remove) {
const stat = await sequelize.query(
`SELECT
(SELECT COUNT(*)::int FROM community.vocab_course_enrollment WHERE course_id = :cid) AS enroll,
(SELECT COUNT(*)::int FROM community.vocab_course_progress WHERE course_id = :cid) AS prog`,
{ replacements: { cid: c.id }, type: sequelize.QueryTypes.SELECT }
);
const row = stat[0];
console.log(
` ID ${c.id} (owner ${c.ownerUserId}, Enrollments ${row?.enroll ?? '?'}, Fortschrittszeilen ${row?.prog ?? '?'})`
);
}
if (!apply) {
console.log(
'\nDry-Run. Zum tatsächlichen Löschen: VOCAB_DEDUPE_BISAYA_FAMILY_APPLY=1 node backend/scripts/dedupe-bisaya-family-courses.js'
);
await sequelize.close();
return;
}
await sequelize.transaction(async (t) => {
for (const c of remove) {
await deleteCourseCascade(c.id, t);
console.log(`🗑️ Kurs ${c.id} entfernt.`);
}
});
console.log(`\n✅ Fertig. Verbleibender Kurs: ID ${keep.id}`);
await sequelize.close();
}
main().catch((e) => {
console.error('❌ Fehler:', e);
sequelize.close();
process.exit(1);
});