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:
@@ -29,6 +29,9 @@ import EroticVideo from '../models/community/erotic_video.js';
|
||||
import EroticContentReport from '../models/community/erotic_content_report.js';
|
||||
import TitleOfNobility from "../models/falukant/type/title_of_nobility.js";
|
||||
import ChildRelation from "../models/falukant/data/child_relation.js";
|
||||
import Relationship from "../models/falukant/data/relationship.js";
|
||||
import RelationshipType from "../models/falukant/type/relationship.js";
|
||||
import RelationshipState from "../models/falukant/data/relationship_state.js";
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import npcCreationJobService from './npcCreationJobService.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
@@ -668,7 +671,7 @@ class AdminService {
|
||||
required: true,
|
||||
attributes: ['money', 'certificate', 'id'],
|
||||
include: [{
|
||||
model: FalukantCharacter,
|
||||
model: FalukantCharacter.unscoped(),
|
||||
as: 'character',
|
||||
include: [{
|
||||
model: FalukantPredefineFirstname,
|
||||
@@ -945,10 +948,85 @@ class AdminService {
|
||||
await character.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ehepartner/Verlobte/Liebhaber eines Charakters (für Admin-Auswahl Vater).
|
||||
*/
|
||||
async adminGetPotentialFathersForCharacter(userId, motherCharacterId) {
|
||||
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const mid = Number(motherCharacterId);
|
||||
if (!Number.isFinite(mid)) {
|
||||
throw new Error('invalidCharacter');
|
||||
}
|
||||
const mother = await FalukantCharacter.findByPk(mid, { attributes: ['id'] });
|
||||
if (!mother) {
|
||||
throw new Error('notfound');
|
||||
}
|
||||
|
||||
const rels = await Relationship.findAll({
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ character1Id: mid },
|
||||
{ character2Id: mid }
|
||||
]
|
||||
},
|
||||
include: [
|
||||
{ model: RelationshipType, as: 'relationshipType', attributes: ['tr'] },
|
||||
{ model: RelationshipState, as: 'state', required: false },
|
||||
{
|
||||
model: FalukantCharacter,
|
||||
as: 'character1',
|
||||
attributes: ['id'],
|
||||
required: true,
|
||||
include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }]
|
||||
},
|
||||
{
|
||||
model: FalukantCharacter,
|
||||
as: 'character2',
|
||||
attributes: ['id'],
|
||||
required: true,
|
||||
include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const allowedTypes = new Set(['married', 'engaged', 'lover']);
|
||||
const typeOrder = { married: 0, engaged: 1, lover: 2 };
|
||||
const seen = new Set();
|
||||
const options = [];
|
||||
|
||||
for (const rel of rels) {
|
||||
const typeTr = rel.relationshipType?.tr;
|
||||
if (!typeTr || !allowedTypes.has(typeTr)) {
|
||||
continue;
|
||||
}
|
||||
if (typeTr === 'lover' && rel.state && rel.state.active === false) {
|
||||
continue;
|
||||
}
|
||||
const partnerId = rel.character1Id === mid ? rel.character2Id : rel.character1Id;
|
||||
if (partnerId === mid || seen.has(partnerId)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(partnerId);
|
||||
const partner = rel.character1Id === mid ? rel.character2 : rel.character1;
|
||||
const displayName = partner?.definedFirstName?.name?.trim() || `Charakter #${partnerId}`;
|
||||
options.push({
|
||||
characterId: partnerId,
|
||||
displayName,
|
||||
relationshipType: typeTr
|
||||
});
|
||||
}
|
||||
|
||||
options.sort((a, b) => (typeOrder[a.relationshipType] ?? 9) - (typeOrder[b.relationshipType] ?? 9));
|
||||
|
||||
return { options };
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin: Charakter als schwanger markieren (erwarteter Geburtstermin).
|
||||
* @param {number} fatherCharacterId - optional; Vater-Charakter-ID
|
||||
* @param {number} dueInDays - Tage bis zur „Geburt“ (Default 21)
|
||||
* @param {number} dueInDays - Tage bis zum Termin (0 = heute; Default 21)
|
||||
*/
|
||||
async adminForceFalukantPregnancy(userId, characterId, { fatherCharacterId = null, dueInDays = 21 } = {}) {
|
||||
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
||||
@@ -960,7 +1038,16 @@ class AdminService {
|
||||
const father = await FalukantCharacter.findByPk(Number(fatherCharacterId));
|
||||
if (!father) throw new Error('fatherNotFound');
|
||||
}
|
||||
const days = Math.max(1, Math.min(365, Number(dueInDays) || 21));
|
||||
let rawDays = dueInDays;
|
||||
if (rawDays === undefined || rawDays === null || rawDays === '') {
|
||||
rawDays = 21;
|
||||
} else {
|
||||
rawDays = Number(rawDays);
|
||||
if (!Number.isFinite(rawDays)) {
|
||||
rawDays = 21;
|
||||
}
|
||||
}
|
||||
const days = Math.max(0, Math.min(365, rawDays));
|
||||
const due = new Date();
|
||||
due.setDate(due.getDate() + days);
|
||||
await mother.update({
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user