From a8b76bc21a4e7db220e01553d786cf7f3eda9580 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 28 Jan 2026 16:41:19 +0100 Subject: [PATCH] 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. --- backend/controllers/falukantController.js | 11 + backend/routers/falukantRouter.js | 5 + backend/services/falukantService.js | 477 +++++++++++++++++++++ frontend/src/i18n/locales/de/falukant.json | 3 + frontend/src/i18n/locales/en/falukant.json | 23 + 5 files changed, 519 insertions(+) diff --git a/backend/controllers/falukantController.js b/backend/controllers/falukantController.js index 2670649..1c36e67 100644 --- a/backend/controllers/falukantController.js +++ b/backend/controllers/falukantController.js @@ -140,6 +140,17 @@ class FalukantController { const { characterId: childId, firstName } = req.body; 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.sendToSchool = this._wrapWithUser((userId, req) => { diff --git a/backend/routers/falukantRouter.js b/backend/routers/falukantRouter.js index 56c6001..051a70e 100644 --- a/backend/routers/falukantRouter.js +++ b/backend/routers/falukantRouter.js @@ -56,6 +56,11 @@ router.post('/party', falukantController.createParty); router.get('/party', falukantController.getParties); router.get('/family/notbaptised', falukantController.getNotBaptisedChildren); 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.post('/education', falukantController.sendToSchool); router.get('/bank/overview', falukantController.getBankOverview); diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index 4d24cb8..5bb53e5 100644 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -48,6 +48,9 @@ import Credit from '../models/falukant/data/credit.js'; import TitleRequirement from '../models/falukant/type/title_requirement.js'; import HealthActivity from '../models/falukant/log/health_activity.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 Candidate from '../models/falukant/data/candidate.js'; import Vote from '../models/falukant/data/vote.js'; @@ -4820,6 +4823,480 @@ class FalukantService extends BaseService { 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(); diff --git a/frontend/src/i18n/locales/de/falukant.json b/frontend/src/i18n/locales/de/falukant.json index e036640..fbe28cd 100644 --- a/frontend/src/i18n/locales/de/falukant.json +++ b/frontend/src/i18n/locales/de/falukant.json @@ -307,6 +307,9 @@ "current": "Laufende Produktionen", "product": "Produkt", "remainingTime": "Verbleibende Zeit (Sekunden)", + "status": "Status", + "sleep": "Pausiert", + "active": "Aktiv", "noProductions": "Keine laufenden Produktionen." }, "columns": { diff --git a/frontend/src/i18n/locales/en/falukant.json b/frontend/src/i18n/locales/en/falukant.json index 248fee7..081acf1 100644 --- a/frontend/src/i18n/locales/en/falukant.json +++ b/frontend/src/i18n/locales/en/falukant.json @@ -165,6 +165,29 @@ "income": "Income", "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": { "cargo_cart": "Cargo cart", "ox_cart": "Ox cart",