Implement church career information retrieval and update related components: Add a new method in FalukantService to fetch church career details for characters, including current and approved office levels. Enhance DashboardWidget, StatusBar, and ChurchView components to handle new church-related socket events and display relevant information. Update localization files for church-related terms and error messages in English, German, and Spanish.

This commit is contained in:
Torsten Schulz (local)
2026-03-23 11:05:48 +01:00
parent ce36315b58
commit 57ab85fe10
10 changed files with 749 additions and 24 deletions

View File

@@ -6965,6 +6965,66 @@ ORDER BY r.id`,
}
}
async getChurchCareerInfo(characterId) {
const [currentOffices, approvedApplications] = await Promise.all([
ChurchOffice.findAll({
where: { characterId },
include: [{
model: ChurchOfficeType,
as: 'type',
attributes: ['id', 'name', 'hierarchyLevel']
}]
}),
ChurchApplication.findAll({
where: {
characterId,
status: 'approved'
},
include: [{
model: ChurchOfficeType,
as: 'officeType',
attributes: ['id', 'name', 'hierarchyLevel']
}]
})
]);
const currentLevels = currentOffices.map((office) => Number(office.type?.hierarchyLevel ?? -1));
const approvedLevels = approvedApplications.map((application) => Number(application.officeType?.hierarchyLevel ?? -1));
const highestCurrentLevel = currentLevels.length ? Math.max(...currentLevels) : -1;
const highestEverLevel = [...currentLevels, ...approvedLevels].length
? Math.max(...currentLevels, ...approvedLevels)
: -1;
const currentHighest = currentOffices
.map((office) => office.type)
.filter(Boolean)
.sort((a, b) => Number(b.hierarchyLevel || 0) - Number(a.hierarchyLevel || 0))[0] || null;
const allAttained = [
...currentOffices.map((office) => office.type),
...approvedApplications.map((application) => application.officeType)
]
.filter(Boolean)
.sort((a, b) => Number(b.hierarchyLevel || 0) - Number(a.hierarchyLevel || 0));
const highestEver = allAttained[0] || null;
return {
highestCurrentLevel,
highestEverLevel,
highestCurrentOffice: currentHighest ? {
id: currentHighest.id,
name: currentHighest.name,
hierarchyLevel: currentHighest.hierarchyLevel
} : null,
highestEverOffice: highestEver ? {
id: highestEver.id,
name: highestEver.name,
hierarchyLevel: highestEver.hierarchyLevel
} : null
};
}
async getAvailableChurchPositions(hashedUserId) {
const user = await getFalukantUserOrFail(hashedUserId);
const character = await FalukantCharacter.findOne({
@@ -6975,6 +7035,8 @@ ORDER BY r.id`,
return [];
}
const churchCareer = await this.getChurchCareerInfo(character.id);
// Prüfe welche Kirchenämter der Charakter bereits innehat
const heldOffices = await ChurchOffice.findAll({
where: { characterId: character.id },
@@ -7029,7 +7091,8 @@ ORDER BY r.id`,
const availablePositions = [];
for (const officeType of officeTypes) {
// Prüfe Voraussetzungen: Hat der User bereits das erforderliche niedrigere Amt?
// Prüfe Voraussetzungen: Maßgeblich ist die höchste bisherige Kirchenlaufbahn,
// nicht nur das aktuell gehaltene Amt.
const requirement = officeType.requirements?.[0];
console.log(`[getAvailableChurchPositions] Checking ${officeType.name} (id=${officeType.id}, hierarchyLevel=${officeType.hierarchyLevel}):`, {
@@ -7043,9 +7106,12 @@ ORDER BY r.id`,
// Wenn eine Voraussetzung definiert ist
const prerequisiteId = requirement.prerequisiteOfficeTypeId;
if (prerequisiteId !== null && prerequisiteId !== undefined) {
// Prüfe ob der User das erforderliche Amt innehat
if (!heldOfficeTypeIds.includes(prerequisiteId)) {
console.log(`[getAvailableChurchPositions] Skipping ${officeType.name}: User doesn't have prerequisite office ${prerequisiteId}. Held offices:`, heldOfficeTypeIds);
const prerequisiteOffice = await ChurchOfficeType.findByPk(prerequisiteId, {
attributes: ['id', 'hierarchyLevel']
});
const requiredLevel = Number(prerequisiteOffice?.hierarchyLevel ?? (officeType.hierarchyLevel - 1));
if (churchCareer.highestEverLevel < requiredLevel) {
console.log(`[getAvailableChurchPositions] Skipping ${officeType.name}: User career too low for prerequisite level ${requiredLevel}. Highest ever:`, churchCareer.highestEverLevel);
continue; // Voraussetzung nicht erfüllt - User hat das erforderliche Amt nicht
}
}
@@ -7113,6 +7179,7 @@ ORDER BY r.id`,
if (availableSeats > 0) {
// Finde den Supervisor (höheres Amt in derselben Region oder Eltern-Region)
let supervisor = null;
let decisionMode = officeType.hierarchyLevel === 0 ? 'entry' : 'interim';
const higherOfficeTypeIds = await ChurchOfficeType.findAll({
where: {
hierarchyLevel: { [Op.gt]: officeType.hierarchyLevel }
@@ -7135,7 +7202,21 @@ ORDER BY r.id`,
{
model: FalukantCharacter,
as: 'holder',
attributes: ['id']
attributes: ['id', 'userId'],
include: [
{
model: FalukantPredefineFirstname,
as: 'definedFirstName',
attributes: ['name'],
required: false
},
{
model: FalukantPredefineLastname,
as: 'definedLastName',
attributes: ['name'],
required: false
}
]
}
],
order: [
@@ -7147,8 +7228,10 @@ ORDER BY r.id`,
if (supervisorOffice && supervisorOffice.holder) {
supervisor = {
id: supervisorOffice.holder.id,
name: 'Supervisor' // Wird später geladen falls nötig
name: `${supervisorOffice.holder.definedFirstName?.name || ''} ${supervisorOffice.holder.definedLastName?.name || ''}`.trim() || 'Supervisor',
controlledBy: supervisorOffice.holder.userId ? 'player' : 'npc'
};
decisionMode = supervisor.controlledBy;
}
}
@@ -7163,7 +7246,8 @@ ORDER BY r.id`,
},
regionId: region.id,
availableSeats: availableSeats,
supervisor: supervisor
supervisor: supervisor,
decisionMode
});
}
}
@@ -7280,13 +7364,31 @@ ORDER BY r.id`,
attributes: ['id', 'regionId']
});
if (!character) {
throw new Error('Character not found');
throw new Error('falukant.church.available.errors.characterNotFound');
}
const churchCareer = await this.getChurchCareerInfo(character.id);
// Prüfe ob Position verfügbar ist
const officeType = await ChurchOfficeType.findByPk(officeTypeId);
if (!officeType) {
throw new Error('Office type not found');
throw new Error('falukant.church.available.errors.officeTypeNotFound');
}
const requirement = await ChurchOfficeRequirement.findOne({
where: { officeTypeId },
attributes: ['prerequisiteOfficeTypeId']
});
if (requirement?.prerequisiteOfficeTypeId) {
const prerequisiteOffice = await ChurchOfficeType.findByPk(requirement.prerequisiteOfficeTypeId, {
attributes: ['hierarchyLevel']
});
const requiredLevel = Number(prerequisiteOffice?.hierarchyLevel ?? (officeType.hierarchyLevel - 1));
if (churchCareer.highestEverLevel < requiredLevel) {
throw new Error('falukant.church.available.errors.churchCareerTooLow');
}
} else if (officeType.hierarchyLevel > 0 && churchCareer.highestEverLevel < officeType.hierarchyLevel - 1) {
throw new Error('falukant.church.available.errors.churchCareerTooLow');
}
const occupiedCount = await ChurchOffice.count({
@@ -7297,7 +7399,7 @@ ORDER BY r.id`,
});
if (occupiedCount >= officeType.seatsPerRegion) {
throw new Error('No available seats');
throw new Error('falukant.church.available.errors.noAvailableSeats');
}
// Finde Supervisor (nur wenn es nicht die niedrigste Position ist)
@@ -7334,16 +7436,13 @@ ORDER BY r.id`,
limit: 1
});
if (!supervisorOffice) {
throw new Error('No supervisor position exists in this region. Higher church offices must be filled before you can apply.');
if (supervisorOffice?.holder) {
supervisorId = supervisorOffice.holder.id;
} else {
supervisorId = null;
}
if (!supervisorOffice.holder) {
const officeName = supervisorOffice.type?.name || 'higher';
throw new Error(`The ${officeName} position in this region is vacant. It must be filled before you can apply.`);
}
supervisorId = supervisorOffice.holder.id;
} else {
throw new Error('No supervisor office type found');
supervisorId = null;
}
}
// Für Einstiegspositionen (hierarchyLevel 0) ist kein Supervisor erforderlich
@@ -7359,7 +7458,7 @@ ORDER BY r.id`,
});
if (existingApplication) {
throw new Error('Application already exists');
throw new Error('falukant.church.available.errors.applicationAlreadyExists');
}
// Erstelle Bewerbung