Add church management features: Implement endpoints for church overview, available positions, supervised applications, and application processing in FalukantService and FalukantController. Update router to include new routes for these functionalities, enhancing church-related operations.

This commit is contained in:
Torsten Schulz (local)
2026-01-28 16:41:19 +01:00
parent 8550bd31d9
commit a8b76bc21a
5 changed files with 519 additions and 0 deletions

View File

@@ -140,6 +140,17 @@ class FalukantController {
const { characterId: childId, firstName } = req.body; const { characterId: childId, firstName } = req.body;
return this.service.baptise(userId, childId, firstName); return this.service.baptise(userId, childId, firstName);
}); });
this.getChurchOverview = this._wrapWithUser((userId) => this.service.getChurchOverview(userId));
this.getAvailableChurchPositions = this._wrapWithUser((userId) => this.service.getAvailableChurchPositions(userId));
this.getSupervisedApplications = this._wrapWithUser((userId) => this.service.getSupervisedApplications(userId));
this.applyForChurchPosition = this._wrapWithUser((userId, req) => {
const { officeTypeId, regionId } = req.body;
return this.service.applyForChurchPosition(userId, officeTypeId, regionId);
});
this.decideOnChurchApplication = this._wrapWithUser((userId, req) => {
const { applicationId, decision } = req.body;
return this.service.decideOnChurchApplication(userId, applicationId, decision);
});
this.getEducation = this._wrapWithUser((userId) => this.service.getEducation(userId)); this.getEducation = this._wrapWithUser((userId) => this.service.getEducation(userId));
this.sendToSchool = this._wrapWithUser((userId, req) => { this.sendToSchool = this._wrapWithUser((userId, req) => {

View File

@@ -56,6 +56,11 @@ router.post('/party', falukantController.createParty);
router.get('/party', falukantController.getParties); router.get('/party', falukantController.getParties);
router.get('/family/notbaptised', falukantController.getNotBaptisedChildren); router.get('/family/notbaptised', falukantController.getNotBaptisedChildren);
router.post('/church/baptise', falukantController.baptise); router.post('/church/baptise', falukantController.baptise);
router.get('/church/overview', falukantController.getChurchOverview);
router.get('/church/positions/available', falukantController.getAvailableChurchPositions);
router.get('/church/applications/supervised', falukantController.getSupervisedApplications);
router.post('/church/positions/apply', falukantController.applyForChurchPosition);
router.post('/church/applications/decide', falukantController.decideOnChurchApplication);
router.get('/education', falukantController.getEducation); router.get('/education', falukantController.getEducation);
router.post('/education', falukantController.sendToSchool); router.post('/education', falukantController.sendToSchool);
router.get('/bank/overview', falukantController.getBankOverview); router.get('/bank/overview', falukantController.getBankOverview);

View File

@@ -48,6 +48,9 @@ import Credit from '../models/falukant/data/credit.js';
import TitleRequirement from '../models/falukant/type/title_requirement.js'; import TitleRequirement from '../models/falukant/type/title_requirement.js';
import HealthActivity from '../models/falukant/log/health_activity.js'; import HealthActivity from '../models/falukant/log/health_activity.js';
import Election from '../models/falukant/data/election.js'; import Election from '../models/falukant/data/election.js';
import ChurchOffice from '../models/falukant/data/church_office.js';
import ChurchOfficeType from '../models/falukant/type/church_office_type.js';
import ChurchApplication from '../models/falukant/data/church_application.js';
import PoliticalOfficeType from '../models/falukant/type/political_office_type.js'; import PoliticalOfficeType from '../models/falukant/type/political_office_type.js';
import Candidate from '../models/falukant/data/candidate.js'; import Candidate from '../models/falukant/data/candidate.js';
import Vote from '../models/falukant/data/vote.js'; import Vote from '../models/falukant/data/vote.js';
@@ -4820,6 +4823,480 @@ class FalukantService extends BaseService {
all: mapped all: mapped
}; };
} }
async getChurchOverview(hashedUserId) {
const user = await getFalukantUserOrFail(hashedUserId);
const character = await FalukantCharacter.findOne({
where: { userId: user.id },
attributes: ['id', 'regionId']
});
if (!character) {
return [];
}
// Alle relevanten Regionen (Region + Eltern) laden
const relevantRegionIds = await this.getRegionAndParentIds(character.regionId);
// Aktuell besetzte Kirchenämter in diesen Regionen laden
const offices = await ChurchOffice.findAll({
where: {
regionId: {
[Op.in]: relevantRegionIds
}
},
include: [
{
model: ChurchOfficeType,
as: 'type',
attributes: ['name', 'hierarchyLevel']
},
{
model: RegionData,
as: 'region',
attributes: ['name']
},
{
model: FalukantCharacter,
as: 'holder',
attributes: ['id', 'gender'],
include: [
{
model: FalukantPredefineFirstname,
as: 'definedFirstName',
attributes: ['name']
},
{
model: FalukantPredefineLastname,
as: 'definedLastName',
attributes: ['name']
},
{
model: TitleOfNobility,
as: 'nobleTitle',
attributes: ['labelTr']
}
]
},
{
model: FalukantCharacter,
as: 'supervisor',
attributes: ['id', 'gender'],
include: [
{
model: FalukantPredefineFirstname,
as: 'definedFirstName',
attributes: ['name']
},
{
model: FalukantPredefineLastname,
as: 'definedLastName',
attributes: ['name']
}
],
required: false
}
],
order: [
[{ model: ChurchOfficeType, as: 'type' }, 'hierarchyLevel', 'DESC'],
[{ model: RegionData, as: 'region' }, 'name', 'ASC']
]
});
return offices.map(office => {
const o = office.get({ plain: true });
return {
id: o.id,
officeType: {
name: o.type?.name
},
region: {
name: o.region?.name
},
character: o.holder
? {
id: o.holder.id,
name: `${o.holder.definedFirstName?.name || ''} ${o.holder.definedLastName?.name || ''}`.trim(),
gender: o.holder.gender,
title: o.holder.nobleTitle?.labelTr
}
: null,
supervisor: o.supervisor
? {
id: o.supervisor.id,
name: `${o.supervisor.definedFirstName?.name || ''} ${o.supervisor.definedLastName?.name || ''}`.trim()
}
: null
};
});
}
async getAvailableChurchPositions(hashedUserId) {
const user = await getFalukantUserOrFail(hashedUserId);
const character = await FalukantCharacter.findOne({
where: { userId: user.id },
attributes: ['id', 'regionId']
});
if (!character) {
return [];
}
// Alle relevanten Regionen (Region + Eltern) laden
const relevantRegionIds = await this.getRegionAndParentIds(character.regionId);
// Alle Kirchenamt-Typen laden
const officeTypes = await ChurchOfficeType.findAll({
order: [['hierarchyLevel', 'ASC']]
});
const availablePositions = [];
for (const officeType of officeTypes) {
// Finde den RegionType für diesen officeType
const regionType = await RegionType.findOne({
where: { labelTr: officeType.regionType }
});
if (!regionType) continue;
// Finde alle Regionen dieses Typs in den relevanten Regionen
const regions = await RegionData.findAll({
where: {
id: { [Op.in]: relevantRegionIds },
regionTypeId: regionType.id
},
attributes: ['id', 'name']
});
for (const region of regions) {
// Zähle besetzte Positionen dieses Typs in dieser Region
const occupiedCount = await ChurchOffice.count({
where: {
officeTypeId: officeType.id,
regionId: region.id
}
});
const availableSeats = officeType.seatsPerRegion - occupiedCount;
if (availableSeats > 0) {
// Finde den Supervisor (höheres Amt in derselben Region oder Eltern-Region)
let supervisor = null;
const higherOfficeTypeIds = await ChurchOfficeType.findAll({
where: {
hierarchyLevel: { [Op.gt]: officeType.hierarchyLevel }
},
attributes: ['id']
}).then(types => types.map(t => t.id));
if (higherOfficeTypeIds.length > 0) {
const supervisorOffice = await ChurchOffice.findOne({
where: {
regionId: region.id,
officeTypeId: { [Op.in]: higherOfficeTypeIds }
},
include: [
{
model: ChurchOfficeType,
as: 'type',
attributes: ['hierarchyLevel']
},
{
model: FalukantCharacter,
as: 'holder',
attributes: ['id']
}
],
order: [
[{ model: ChurchOfficeType, as: 'type' }, 'hierarchyLevel', 'ASC']
],
limit: 1
});
if (supervisorOffice && supervisorOffice.holder) {
supervisor = {
id: supervisorOffice.holder.id,
name: 'Supervisor' // Wird später geladen falls nötig
};
}
}
if (supervisorOffice && supervisorOffice.holder) {
supervisor = {
id: supervisorOffice.holder.id,
name: 'Supervisor' // Wird später geladen falls nötig
};
}
availablePositions.push({
id: officeType.id, // Verwende officeTypeId als ID für die Frontend-Identifikation
officeType: {
name: officeType.name
},
region: {
name: region.name,
id: region.id
},
regionId: region.id,
availableSeats: availableSeats,
supervisor: supervisor
});
}
}
}
return availablePositions;
}
async getSupervisedApplications(hashedUserId) {
const user = await getFalukantUserOrFail(hashedUserId);
const character = await FalukantCharacter.findOne({
where: { userId: user.id },
attributes: ['id']
});
if (!character) {
return [];
}
// Finde alle Kirchenämter, die dieser Charakter hält
const heldOffices = await ChurchOffice.findAll({
where: { characterId: character.id },
include: [
{
model: ChurchOfficeType,
as: 'type',
attributes: ['id', 'hierarchyLevel']
}
]
});
if (heldOffices.length === 0) {
return [];
}
// Finde alle niedrigeren Ämter, die dieser Charakter superviden kann
const maxHierarchyLevel = Math.max(...heldOffices.map(o => o.type.hierarchyLevel));
const supervisedOfficeTypeIds = await ChurchOfficeType.findAll({
where: {
hierarchyLevel: { [Op.lt]: maxHierarchyLevel }
},
attributes: ['id']
}).then(types => types.map(t => t.id));
// Finde alle Bewerbungen für diese Ämter, bei denen dieser Charakter Supervisor ist
const applications = await ChurchApplication.findAll({
where: {
supervisorId: character.id,
status: 'pending',
officeTypeId: { [Op.in]: supervisedOfficeTypeIds }
},
include: [
{
model: ChurchOfficeType,
as: 'officeType',
attributes: ['name']
},
{
model: RegionData,
as: 'region',
attributes: ['name']
},
{
model: FalukantCharacter,
as: 'applicant',
attributes: ['id', 'gender', 'age'],
include: [
{
model: FalukantPredefineFirstname,
as: 'definedFirstName',
attributes: ['name']
},
{
model: FalukantPredefineLastname,
as: 'definedLastName',
attributes: ['name']
},
{
model: TitleOfNobility,
as: 'nobleTitle',
attributes: ['labelTr']
}
]
}
],
order: [['createdAt', 'DESC']]
});
return applications.map(app => {
const a = app.get({ plain: true });
return {
id: a.id,
officeType: {
name: a.officeType?.name
},
region: {
name: a.region?.name
},
applicant: {
id: a.applicant.id,
name: `${a.applicant.definedFirstName?.name || ''} ${a.applicant.definedLastName?.name || ''}`.trim(),
gender: a.applicant.gender,
age: a.applicant.age,
title: a.applicant.nobleTitle?.labelTr
},
createdAt: a.createdAt
};
});
}
async applyForChurchPosition(hashedUserId, officeTypeId, regionId) {
const user = await getFalukantUserOrFail(hashedUserId);
const character = await FalukantCharacter.findOne({
where: { userId: user.id },
attributes: ['id', 'regionId']
});
if (!character) {
throw new Error('Character not found');
}
// Prüfe ob Position verfügbar ist
const officeType = await ChurchOfficeType.findByPk(officeTypeId);
if (!officeType) {
throw new Error('Office type not found');
}
const occupiedCount = await ChurchOffice.count({
where: {
officeTypeId: officeTypeId,
regionId: regionId
}
});
if (occupiedCount >= officeType.seatsPerRegion) {
throw new Error('No available seats');
}
// Finde Supervisor
const higherOfficeTypeIds = await ChurchOfficeType.findAll({
where: {
hierarchyLevel: { [Op.gt]: officeType.hierarchyLevel }
},
attributes: ['id']
}).then(types => types.map(t => t.id));
if (higherOfficeTypeIds.length === 0) {
throw new Error('No supervisor office type found');
}
const supervisorOffice = await ChurchOffice.findOne({
where: {
regionId: regionId,
officeTypeId: { [Op.in]: higherOfficeTypeIds }
},
include: [
{
model: ChurchOfficeType,
as: 'type',
attributes: ['hierarchyLevel']
},
{
model: FalukantCharacter,
as: 'holder',
attributes: ['id']
}
],
order: [
[{ model: ChurchOfficeType, as: 'type' }, 'hierarchyLevel', 'ASC']
],
limit: 1
});
if (!supervisorOffice || !supervisorOffice.holder) {
throw new Error('No supervisor found');
}
// Prüfe ob bereits eine Bewerbung existiert
const existingApplication = await ChurchApplication.findOne({
where: {
characterId: character.id,
officeTypeId: officeTypeId,
regionId: regionId,
status: 'pending'
}
});
if (existingApplication) {
throw new Error('Application already exists');
}
// Erstelle Bewerbung
await ChurchApplication.create({
officeTypeId: officeTypeId,
characterId: character.id,
regionId: regionId,
supervisorId: supervisorOffice.holder.id,
status: 'pending'
});
return { success: true };
}
async decideOnChurchApplication(hashedUserId, applicationId, decision) {
const user = await getFalukantUserOrFail(hashedUserId);
const character = await FalukantCharacter.findOne({
where: { userId: user.id },
attributes: ['id']
});
if (!character) {
throw new Error('Character not found');
}
const application = await ChurchApplication.findOne({
where: {
id: applicationId,
supervisorId: character.id,
status: 'pending'
},
include: [
{
model: ChurchOfficeType,
as: 'officeType',
attributes: ['id', 'seatsPerRegion']
}
]
});
if (!application) {
throw new Error('Application not found or not authorized');
}
if (decision === 'approve') {
// Prüfe ob noch Platz verfügbar ist
const occupiedCount = await ChurchOffice.count({
where: {
officeTypeId: application.officeTypeId,
regionId: application.regionId
}
});
if (occupiedCount >= application.officeType.seatsPerRegion) {
throw new Error('No available seats');
}
// Erstelle Kirchenamt
await ChurchOffice.create({
officeTypeId: application.officeTypeId,
characterId: application.characterId,
regionId: application.regionId,
supervisorId: application.supervisorId
});
}
// Aktualisiere Bewerbung
application.status = decision === 'approve' ? 'approved' : 'rejected';
application.decisionDate = new Date();
await application.save();
return { success: true };
}
} }
export default new FalukantService(); export default new FalukantService();

View File

@@ -307,6 +307,9 @@
"current": "Laufende Produktionen", "current": "Laufende Produktionen",
"product": "Produkt", "product": "Produkt",
"remainingTime": "Verbleibende Zeit (Sekunden)", "remainingTime": "Verbleibende Zeit (Sekunden)",
"status": "Status",
"sleep": "Pausiert",
"active": "Aktiv",
"noProductions": "Keine laufenden Produktionen." "noProductions": "Keine laufenden Produktionen."
}, },
"columns": { "columns": {

View File

@@ -165,6 +165,29 @@
"income": "Income", "income": "Income",
"incomeUpdated": "Salary has been successfully updated." "incomeUpdated": "Salary has been successfully updated."
}, },
"production": {
"title": "Production",
"info": "Details about production in the branch.",
"selectProduct": "Select product",
"quantity": "Quantity",
"storageAvailable": "Free storage",
"cost": "Cost",
"duration": "Duration",
"revenue": "Revenue",
"start": "Start production",
"success": "Production started successfully!",
"error": "Error starting production.",
"minutes": "Minutes",
"ending": "Ending:",
"time": "Time",
"current": "Running productions",
"product": "Product",
"remainingTime": "Remaining time (seconds)",
"status": "Status",
"sleep": "Paused",
"active": "Active",
"noProductions": "No running productions."
},
"vehicles": { "vehicles": {
"cargo_cart": "Cargo cart", "cargo_cart": "Cargo cart",
"ox_cart": "Ox cart", "ox_cart": "Ox cart",