feat(admin): add potential fathers retrieval for character management
All checks were successful
Deploy to production / deploy (push) Successful in 2m47s
All checks were successful
Deploy to production / deploy (push) Successful in 2m47s
- Implemented a new method in AdminService to fetch potential fathers for a given character based on existing relationships. - Updated AdminController to expose this functionality via a new API endpoint. - Enhanced adminRouter to include the route for retrieving potential fathers. - Modified frontend components to allow selection of potential fathers during pregnancy and birth management. - Updated internationalization files to include new translation keys related to father selection.
This commit is contained in:
@@ -13,6 +13,9 @@ import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
||||
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
|
||||
import VocabCourse from '../models/community/vocab_course.js';
|
||||
import User from '../models/community/user.js';
|
||||
import { BISAYA_PHASE3_DIDACTICS } from './bisaya-course-phase3-extension.js';
|
||||
import { BISAYA_PHASE4_DIDACTICS } from './bisaya-course-phase4-extension.js';
|
||||
import { BISAYA_PHASE5_DIDACTICS } from './bisaya-course-phase5-extension.js';
|
||||
|
||||
function withTypeName(exerciseTypeName, exercise) {
|
||||
return {
|
||||
@@ -21,6 +24,356 @@ function withTypeName(exerciseTypeName, exercise) {
|
||||
};
|
||||
}
|
||||
|
||||
const GENERATED_BISAYA_DIDACTICS = {
|
||||
...BISAYA_PHASE3_DIDACTICS,
|
||||
...BISAYA_PHASE4_DIDACTICS,
|
||||
...BISAYA_PHASE5_DIDACTICS
|
||||
};
|
||||
|
||||
const GENERIC_DISTRACTOR_PATTERNS = Array.from(new Set(
|
||||
Object.values(GENERATED_BISAYA_DIDACTICS)
|
||||
.flatMap((entry) => Array.isArray(entry?.corePatterns) ? entry.corePatterns : [])
|
||||
.map((pattern) => String(pattern || '').trim())
|
||||
.filter(Boolean)
|
||||
)).slice(0, 200);
|
||||
|
||||
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 = GENERATED_BISAYA_DIDACTICS[lesson.title] || {};
|
||||
const learningGoals = Array.isArray(lesson.learningGoals) ? lesson.learningGoals : (staticDidactics.learningGoals || []);
|
||||
const corePatterns = Array.isArray(lesson.corePatterns) ? lesson.corePatterns : (staticDidactics.corePatterns || []);
|
||||
const grammarFocus = Array.isArray(lesson.grammarFocus) ? lesson.grammarFocus : (staticDidactics.grammarFocus || []);
|
||||
const speakingPrompts = Array.isArray(lesson.speakingPrompts) ? lesson.speakingPrompts : (staticDidactics.speakingPrompts || []);
|
||||
const practicalTasks = Array.isArray(lesson.practicalTasks) ? lesson.practicalTasks : (staticDidactics.practicalTasks || []);
|
||||
|
||||
return {
|
||||
learningGoals,
|
||||
corePatterns: corePatterns.map((entry) => normalizeText(entry)).filter(Boolean),
|
||||
grammarFocus,
|
||||
speakingPrompts,
|
||||
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 Situation aus der Lektion "${lesson.title}".`;
|
||||
}
|
||||
|
||||
function getChoiceQuestion(lesson, didactics) {
|
||||
const scenarioPrompt = getScenarioPrompt(lesson, didactics);
|
||||
|
||||
switch (lesson.lessonType) {
|
||||
case 'conversation':
|
||||
return `${scenarioPrompt} Welche Bisaya-Formulierung passt am besten?`;
|
||||
case 'grammar':
|
||||
return `Welche Formulierung passt als kurze Alltagsstruktur zu "${lesson.title}"?`;
|
||||
case 'review':
|
||||
return `Welche Formulierung solltest du aus "${lesson.title}" sicher wiedererkennen?`;
|
||||
case 'culture':
|
||||
return `Welcher Ausdruck gehört am ehesten zum kulturellen Schwerpunkt "${lesson.title}"?`;
|
||||
case 'vocab':
|
||||
default:
|
||||
return `Welcher Ausdruck 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 seed = simpleHash(`${lesson.title}:${pattern}:${variant}`);
|
||||
const options = rotateArray([pattern, ...distractors], seed % 4);
|
||||
const correctAnswer = options.indexOf(pattern);
|
||||
|
||||
return {
|
||||
exerciseTypeId: 2,
|
||||
title: `${lesson.title}: Passende Formulierung wählen`,
|
||||
instruction: 'Wähle die natürlichste Formulierung für die Situation oder den Schwerpunkt der Lektion.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: getChoiceQuestion(lesson, didactics),
|
||||
options
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer
|
||||
},
|
||||
explanation: `"${pattern}" ist ein zentrales 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 >= 4);
|
||||
|
||||
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 Bisaya-Ausdruck.',
|
||||
questionData: {
|
||||
type: 'gap_fill',
|
||||
text: tokens.join(' '),
|
||||
gaps: 1
|
||||
},
|
||||
answerData: {
|
||||
type: 'gap_fill',
|
||||
answers: [gapTarget.token.replace(/[.,?!]/g, '')]
|
||||
},
|
||||
explanation: `Das Kernmuster lautet: "${pattern}".`
|
||||
};
|
||||
}
|
||||
|
||||
function buildContextGapExercise(lesson, didactics, pattern) {
|
||||
const gapExercise = buildGapExercise(lesson.title, pattern);
|
||||
if (!gapExercise) return null;
|
||||
|
||||
return {
|
||||
...gapExercise,
|
||||
title: `${lesson.title}: Kernmuster ergänzen`,
|
||||
instruction: `Vervollständige die Formulierung passend zur Situation: ${getScenarioPrompt(lesson, didactics)}`
|
||||
};
|
||||
}
|
||||
|
||||
function buildSentenceExercise(lessonTitle, pattern) {
|
||||
const tokens = normalizeText(pattern)
|
||||
.replace(/[?!]/g, '')
|
||||
.split(' ')
|
||||
.filter(Boolean);
|
||||
|
||||
if (tokens.length < 2) return null;
|
||||
|
||||
return {
|
||||
exerciseTypeId: 3,
|
||||
title: `${lessonTitle}: Satzmuster bauen`,
|
||||
instruction: 'Ordne die Wörter zu einem korrekten Bisaya-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 buildTaskSentenceExercise(lesson, didactics, pattern) {
|
||||
const sentenceExercise = buildSentenceExercise(lesson.title, pattern);
|
||||
if (!sentenceExercise) return null;
|
||||
|
||||
const practicalTask = Array.isArray(didactics.practicalTasks) ? didactics.practicalTasks[0] : null;
|
||||
|
||||
return {
|
||||
...sentenceExercise,
|
||||
title: `${lesson.title}: Satz aus dem Alltag bauen`,
|
||||
instruction: practicalTask?.text
|
||||
? `Baue die passende Formulierung für diese Aufgabe: ${practicalTask.text}`
|
||||
: 'Ordne die Wörter zu einem natürlichen Bisaya-Satz aus dem Alltag.'
|
||||
};
|
||||
}
|
||||
|
||||
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 >= 4)
|
||||
.slice(0, 4);
|
||||
|
||||
return {
|
||||
exerciseTypeId: 8,
|
||||
title: `${lessonTitle}: Laut sprechen`,
|
||||
instruction: 'Sprich das zentrale Muster oder den Dialog 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 hier ein flüssiger Abruf und die zentralen Schlüsselwörter.'
|
||||
};
|
||||
}
|
||||
|
||||
function buildReviewChoiceExercise(lesson, didactics, pattern, allPatterns) {
|
||||
const choiceExercise = buildChoiceExercise(lesson, didactics, pattern, allPatterns, 1);
|
||||
if (!choiceExercise) return null;
|
||||
|
||||
return {
|
||||
...choiceExercise,
|
||||
title: `${lesson.title}: Sicheren Abruf prüfen`,
|
||||
instruction: 'Wähle die Formulierung, die du aus dem aktiven Wiederholungsblock sicher können solltest.'
|
||||
};
|
||||
}
|
||||
|
||||
function buildCultureExercise(lesson, didactics, pattern, allPatterns) {
|
||||
const distractors = pickDistractors(pattern, allPatterns, 3);
|
||||
if (distractors.length < 3) return null;
|
||||
|
||||
const culturalNote = normalizeText(lesson.culturalNotes || '');
|
||||
const options = rotateArray([pattern, ...distractors], simpleHash(`${lesson.title}:culture`) % 4);
|
||||
|
||||
return {
|
||||
exerciseTypeId: 2,
|
||||
title: `${lesson.title}: Ausdruck kulturell einordnen`,
|
||||
instruction: 'Ordne den Ausdruck dem kulturellen Schwerpunkt der Lektion zu.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: culturalNote
|
||||
? `${culturalNote} Welcher Ausdruck passt dazu besonders gut?`
|
||||
: `Welcher Ausdruck gehört besonders gut zum Schwerpunkt "${lesson.title}"?`,
|
||||
options
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: options.indexOf(pattern)
|
||||
},
|
||||
explanation: `Der Ausdruck "${pattern}" gehört eng zum kulturellen Schwerpunkt dieser Lektion.`
|
||||
};
|
||||
}
|
||||
|
||||
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 >= 4)
|
||||
.slice(0, 4);
|
||||
|
||||
return withTypeName('situational_response', {
|
||||
title: `${lessonTitle}: Situativ reagieren`,
|
||||
instruction: 'Antworte kurz und passend auf die Situation.',
|
||||
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 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 lessonPool = Array.from(new Set([
|
||||
...corePatterns,
|
||||
...GENERIC_DISTRACTOR_PATTERNS
|
||||
]));
|
||||
let generated = [];
|
||||
|
||||
if (lesson.lessonType === 'conversation') {
|
||||
generated = [
|
||||
buildChoiceExercise(lesson, didactics, patternA, lessonPool, 0),
|
||||
buildContextGapExercise(lesson, didactics, patternA),
|
||||
buildTaskSentenceExercise(lesson, didactics, patternB),
|
||||
buildSituationalExercise(lesson.title, didactics, patternA),
|
||||
buildSpeakingExercise(lesson.title, didactics, patternB)
|
||||
];
|
||||
} else if (lesson.lessonType === 'grammar') {
|
||||
generated = [
|
||||
buildChoiceExercise(lesson, didactics, patternA, lessonPool, 0),
|
||||
buildChoiceExercise(lesson, didactics, patternB, lessonPool, 1),
|
||||
buildContextGapExercise(lesson, didactics, patternA),
|
||||
buildTaskSentenceExercise(lesson, didactics, patternB),
|
||||
buildSpeakingExercise(lesson.title, didactics, patternA)
|
||||
];
|
||||
} else if (lesson.lessonType === 'review' || lesson.didacticMode === 'intensive_review') {
|
||||
generated = [
|
||||
buildReviewChoiceExercise(lesson, didactics, patternA, lessonPool),
|
||||
buildReviewChoiceExercise(lesson, didactics, patternB, lessonPool),
|
||||
buildContextGapExercise(lesson, didactics, patternA),
|
||||
buildTaskSentenceExercise(lesson, didactics, patternB),
|
||||
buildSituationalExercise(lesson.title, didactics, patternA),
|
||||
buildSpeakingExercise(lesson.title, didactics, patternB)
|
||||
];
|
||||
} else if (lesson.lessonType === 'culture') {
|
||||
generated = [
|
||||
buildCultureExercise(lesson, didactics, patternA, lessonPool),
|
||||
buildContextGapExercise(lesson, didactics, patternA),
|
||||
buildSpeakingExercise(lesson.title, didactics, patternB)
|
||||
];
|
||||
} else {
|
||||
generated = [
|
||||
buildChoiceExercise(lesson, didactics, patternA, lessonPool, 0),
|
||||
buildChoiceExercise(lesson, didactics, patternB, lessonPool, 1),
|
||||
buildContextGapExercise(lesson, didactics, patternA),
|
||||
buildTaskSentenceExercise(lesson, didactics, patternB)
|
||||
];
|
||||
}
|
||||
|
||||
return generated.filter(Boolean);
|
||||
}
|
||||
|
||||
// Bisaya-spezifische Übungen basierend auf Lektionsthemen
|
||||
const BISAYA_EXERCISES = {
|
||||
// Lektion 1: Begrüßungen & Höflichkeit
|
||||
@@ -1489,7 +1842,8 @@ async function findOrCreateSystemUser() {
|
||||
return systemUser;
|
||||
}
|
||||
|
||||
function getExercisesForLesson(lessonTitle) {
|
||||
function getExercisesForLesson(lesson) {
|
||||
const lessonTitle = lesson.title;
|
||||
// Suche nach exaktem Titel
|
||||
if (BISAYA_EXERCISES[lessonTitle]) {
|
||||
return BISAYA_EXERCISES[lessonTitle];
|
||||
@@ -1501,9 +1855,8 @@ function getExercisesForLesson(lessonTitle) {
|
||||
return exercises;
|
||||
}
|
||||
}
|
||||
|
||||
// Keine Übungen für unbekannte Lektionen (statt Dummy-Übungen)
|
||||
return [];
|
||||
|
||||
return generateExercisesFromDidactics(lesson);
|
||||
}
|
||||
|
||||
async function createBisayaCourseContent() {
|
||||
@@ -1550,7 +1903,7 @@ async function createBisayaCourseContent() {
|
||||
console.log(` ${lessons.length} Lektionen gefunden\n`);
|
||||
|
||||
for (const lesson of lessons) {
|
||||
const exercises = getExercisesForLesson(lesson.title);
|
||||
const exercises = getExercisesForLesson(lesson);
|
||||
if (exercises.length === 0) {
|
||||
const existingCount = await VocabGrammarExercise.count({ where: { lessonId: lesson.id } });
|
||||
if (existingCount > 0) {
|
||||
|
||||
Reference in New Issue
Block a user