feat(admin): add potential fathers retrieval for character management
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:
Torsten Schulz (local)
2026-03-31 08:50:56 +02:00
parent ee11a989a0
commit 9a78bc7c4b
30 changed files with 3907 additions and 45 deletions

View File

@@ -188,6 +188,149 @@ export default class VocabService {
return [];
}
_normalizeOptionalInteger(value) {
if (value === undefined || value === null || value === '') {
return null;
}
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
_normalizeOptionalString(value) {
if (value === undefined || value === null) {
return null;
}
const trimmed = String(value).trim();
return trimmed || null;
}
_inferLessonPhaseLabel(plainLesson) {
if (plainLesson.phaseLabel) {
return plainLesson.phaseLabel;
}
const weekNumber = Number(plainLesson.weekNumber) || 0;
if (weekNumber > 0 && weekNumber <= 2) {
return 'quickstart';
}
if (weekNumber === 3) {
return 'daily_life';
}
if (weekNumber >= 4) {
return 'stabilization';
}
return 'quickstart';
}
_inferLessonDidacticMode(plainLesson) {
if (plainLesson.didacticMode) {
return plainLesson.didacticMode;
}
const lessonType = String(plainLesson.lessonType || '').toLowerCase();
const title = String(plainLesson.title || '').toLowerCase();
if (title.includes('abschluss') || title.includes('prüfung') || title.includes('test')) {
return 'checkpoint';
}
if (plainLesson.isIntensiveReview || lessonType === 'review' || lessonType === 'vocab_review' || title.includes('wiederholung')) {
return 'intensive_review';
}
if (lessonType === 'grammar') {
return 'pattern_drill';
}
if (lessonType === 'conversation' || lessonType === 'dialogue' || lessonType === 'phrases' || lessonType === 'survival') {
return 'guided_dialogue';
}
if (lessonType === 'culture') {
return 'real_life_scenario';
}
return 'core_input';
}
_inferLessonDifficultyWeight(plainLesson, didacticMode) {
if (plainLesson.difficultyWeight != null) {
return plainLesson.difficultyWeight;
}
switch (didacticMode) {
case 'pattern_drill':
return 3;
case 'guided_dialogue':
case 'real_life_scenario':
return 2;
case 'intensive_review':
case 'checkpoint':
return 2;
default:
return 1;
}
}
_inferLessonNewUnitTarget(plainLesson, didacticMode) {
if (plainLesson.newUnitTarget != null) {
return plainLesson.newUnitTarget;
}
switch (didacticMode) {
case 'core_input':
return 8;
case 'guided_dialogue':
return 5;
case 'pattern_drill':
return 4;
case 'real_life_scenario':
return 3;
case 'checkpoint':
return 2;
case 'intensive_review':
return 1;
default:
return 4;
}
}
_inferLessonReviewWeight(plainLesson, didacticMode) {
if (plainLesson.reviewWeight != null) {
return plainLesson.reviewWeight;
}
switch (didacticMode) {
case 'intensive_review':
return 90;
case 'checkpoint':
return 70;
case 'pattern_drill':
return 55;
case 'real_life_scenario':
return 45;
case 'guided_dialogue':
return 40;
default:
return 30;
}
}
_inferLessonBlockNumber(plainLesson) {
if (plainLesson.blockNumber != null) {
return plainLesson.blockNumber;
}
const weekNumber = Number(plainLesson.weekNumber) || 1;
return Math.max(1, Math.ceil(weekNumber / 2));
}
_buildLessonPedagogy(plainLesson) {
const didacticMode = this._inferLessonDidacticMode(plainLesson);
const phaseLabel = this._inferLessonPhaseLabel(plainLesson);
const isIntensiveReview = plainLesson.isIntensiveReview != null
? Boolean(plainLesson.isIntensiveReview)
: didacticMode === 'intensive_review';
return {
didacticMode,
phaseLabel,
blockNumber: this._inferLessonBlockNumber(plainLesson),
difficultyWeight: this._inferLessonDifficultyWeight(plainLesson, didacticMode),
newUnitTarget: this._inferLessonNewUnitTarget(plainLesson, didacticMode),
reviewWeight: this._inferLessonReviewWeight(plainLesson, didacticMode),
isIntensiveReview
};
}
_buildLessonDidactics(plainLesson) {
const grammarExercises = Array.isArray(plainLesson.grammarExercises) ? plainLesson.grammarExercises : [];
const grammarExplanations = [];
@@ -1020,6 +1163,11 @@ export default class VocabService {
return a.lessonNumber - b.lessonNumber;
});
courseData.lessons = courseData.lessons.map((lesson) => ({
...lesson,
pedagogy: this._buildLessonPedagogy(lesson)
}));
return courseData;
}
@@ -1129,6 +1277,7 @@ export default class VocabService {
}
plainLesson.didactics = this._buildLessonDidactics(plainLesson);
plainLesson.pedagogy = this._buildLessonPedagogy(plainLesson);
return plainLesson;
}
@@ -1301,7 +1450,7 @@ export default class VocabService {
return exercises.map(e => e.get({ plain: true }));
}
async addLessonToCourse(hashedUserId, courseId, { chapterId, lessonNumber, title, description, weekNumber, dayNumber, lessonType, audioUrl, culturalNotes, learningGoals, corePatterns, grammarFocus, speakingPrompts, practicalTasks, targetMinutes, targetScorePercent, requiresReview }) {
async addLessonToCourse(hashedUserId, courseId, { chapterId, lessonNumber, title, description, weekNumber, dayNumber, lessonType, didacticMode, phaseLabel, blockNumber, difficultyWeight, newUnitTarget, reviewWeight, isIntensiveReview, audioUrl, culturalNotes, learningGoals, corePatterns, grammarFocus, speakingPrompts, practicalTasks, targetMinutes, targetScorePercent, requiresReview }) {
const user = await this._getUserByHashedId(hashedUserId);
const course = await VocabCourse.findByPk(courseId);
@@ -1343,6 +1492,13 @@ export default class VocabService {
weekNumber: weekNumber ? Number(weekNumber) : null,
dayNumber: dayNumber ? Number(dayNumber) : null,
lessonType: lessonType || 'vocab',
didacticMode: this._normalizeOptionalString(didacticMode),
phaseLabel: this._normalizeOptionalString(phaseLabel),
blockNumber: this._normalizeOptionalInteger(blockNumber),
difficultyWeight: this._normalizeOptionalInteger(difficultyWeight),
newUnitTarget: this._normalizeOptionalInteger(newUnitTarget),
reviewWeight: this._normalizeOptionalInteger(reviewWeight),
isIntensiveReview: isIntensiveReview !== undefined ? Boolean(isIntensiveReview) : false,
audioUrl: audioUrl || null,
culturalNotes: culturalNotes || null,
learningGoals: this._normalizeStringList(learningGoals),
@@ -1358,7 +1514,7 @@ export default class VocabService {
return lesson.get({ plain: true });
}
async updateLesson(hashedUserId, lessonId, { title, description, lessonNumber, weekNumber, dayNumber, lessonType, audioUrl, culturalNotes, learningGoals, corePatterns, grammarFocus, speakingPrompts, practicalTasks, targetMinutes, targetScorePercent, requiresReview }) {
async updateLesson(hashedUserId, lessonId, { title, description, lessonNumber, weekNumber, dayNumber, lessonType, didacticMode, phaseLabel, blockNumber, difficultyWeight, newUnitTarget, reviewWeight, isIntensiveReview, audioUrl, culturalNotes, learningGoals, corePatterns, grammarFocus, speakingPrompts, practicalTasks, targetMinutes, targetScorePercent, requiresReview }) {
const user = await this._getUserByHashedId(hashedUserId);
const lesson = await VocabCourseLesson.findByPk(lessonId, {
include: [{ model: VocabCourse, as: 'course' }]
@@ -1383,6 +1539,13 @@ export default class VocabService {
if (weekNumber !== undefined) updates.weekNumber = weekNumber ? Number(weekNumber) : null;
if (dayNumber !== undefined) updates.dayNumber = dayNumber ? Number(dayNumber) : null;
if (lessonType !== undefined) updates.lessonType = lessonType;
if (didacticMode !== undefined) updates.didacticMode = this._normalizeOptionalString(didacticMode);
if (phaseLabel !== undefined) updates.phaseLabel = this._normalizeOptionalString(phaseLabel);
if (blockNumber !== undefined) updates.blockNumber = this._normalizeOptionalInteger(blockNumber);
if (difficultyWeight !== undefined) updates.difficultyWeight = this._normalizeOptionalInteger(difficultyWeight);
if (newUnitTarget !== undefined) updates.newUnitTarget = this._normalizeOptionalInteger(newUnitTarget);
if (reviewWeight !== undefined) updates.reviewWeight = this._normalizeOptionalInteger(reviewWeight);
if (isIntensiveReview !== undefined) updates.isIntensiveReview = Boolean(isIntensiveReview);
if (audioUrl !== undefined) updates.audioUrl = audioUrl;
if (culturalNotes !== undefined) updates.culturalNotes = culturalNotes;
if (learningGoals !== undefined) updates.learningGoals = this._normalizeStringList(learningGoals);