Implement church career management features
- Added endpoints for church career functionalities including overview, available positions, application submission, and application decision-making. - Enhanced the FalukantController to handle church-related requests. - Updated associations and models to support church office types and requirements. - Integrated new routes in the falukantRouter for church career operations. - Implemented service methods for managing church applications and checking church career status. - Updated frontend components to display current positions, available positions, and manage applications with appropriate UI elements and loading states. - Localized new church-related strings in both English and German.
This commit is contained in:
@@ -148,6 +148,20 @@ class FalukantController {
|
|||||||
|
|
||||||
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
|
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
|
||||||
this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId));
|
this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId));
|
||||||
|
|
||||||
|
// Church career endpoints
|
||||||
|
this.getChurchOverview = this._wrapWithUser((userId) => this.service.getChurchOverview(userId));
|
||||||
|
this.getAvailableChurchPositions = this._wrapWithUser((userId) => this.service.getAvailableChurchPositions(userId));
|
||||||
|
this.applyForChurchPosition = this._wrapWithUser((userId, req) => {
|
||||||
|
const { officeTypeId, regionId } = req.body;
|
||||||
|
return this.service.applyForChurchPosition(userId, officeTypeId, regionId);
|
||||||
|
}, { successStatus: 201 });
|
||||||
|
this.getSupervisedApplications = this._wrapWithUser((userId) => this.service.getSupervisedApplications(userId));
|
||||||
|
this.decideOnChurchApplication = this._wrapWithUser((userId, req) => {
|
||||||
|
const { applicationId, decision } = req.body;
|
||||||
|
return this.service.decideOnChurchApplication(userId, applicationId, decision);
|
||||||
|
});
|
||||||
|
this.hasChurchCareer = this._wrapWithUser((userId) => this.service.hasChurchCareer(userId));
|
||||||
this.getElections = this._wrapWithUser((userId) => this.service.getElections(userId));
|
this.getElections = this._wrapWithUser((userId) => this.service.getElections(userId));
|
||||||
this.vote = this._wrapWithUser((userId, req) => this.service.vote(userId, req.body.votes));
|
this.vote = this._wrapWithUser((userId, req) => this.service.vote(userId, req.body.votes));
|
||||||
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds));
|
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds));
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ import PoliticalOfficeRequirement from './falukant/predefine/political_office_pr
|
|||||||
import PoliticalOfficePrerequisite from './falukant/predefine/political_office_prerequisite.js';
|
import PoliticalOfficePrerequisite from './falukant/predefine/political_office_prerequisite.js';
|
||||||
import PoliticalOfficeHistory from './falukant/log/political_office_history.js';
|
import PoliticalOfficeHistory from './falukant/log/political_office_history.js';
|
||||||
import ElectionHistory from './falukant/log/election_history.js';
|
import ElectionHistory from './falukant/log/election_history.js';
|
||||||
|
import ChurchOfficeType from './falukant/type/church_office_type.js';
|
||||||
|
import ChurchOfficeRequirement from './falukant/predefine/church_office_requirement.js';
|
||||||
|
import ChurchOffice from './falukant/data/church_office.js';
|
||||||
|
import ChurchApplication from './falukant/data/church_application.js';
|
||||||
import Underground from './falukant/data/underground.js';
|
import Underground from './falukant/data/underground.js';
|
||||||
import UndergroundType from './falukant/type/underground.js';
|
import UndergroundType from './falukant/type/underground.js';
|
||||||
import VehicleType from './falukant/type/vehicle.js';
|
import VehicleType from './falukant/type/vehicle.js';
|
||||||
@@ -866,6 +870,86 @@ export default function setupAssociations() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// — Church Offices —
|
||||||
|
|
||||||
|
// Requirements for church office
|
||||||
|
ChurchOfficeRequirement.belongsTo(ChurchOfficeType, {
|
||||||
|
foreignKey: 'officeTypeId',
|
||||||
|
as: 'officeType'
|
||||||
|
});
|
||||||
|
ChurchOfficeType.hasMany(ChurchOfficeRequirement, {
|
||||||
|
foreignKey: 'officeTypeId',
|
||||||
|
as: 'requirements'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prerequisite office type
|
||||||
|
ChurchOfficeRequirement.belongsTo(ChurchOfficeType, {
|
||||||
|
foreignKey: 'prerequisiteOfficeTypeId',
|
||||||
|
as: 'prerequisiteOfficeType'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Actual church office holdings
|
||||||
|
ChurchOffice.belongsTo(ChurchOfficeType, {
|
||||||
|
foreignKey: 'officeTypeId',
|
||||||
|
as: 'type'
|
||||||
|
});
|
||||||
|
ChurchOfficeType.hasMany(ChurchOffice, {
|
||||||
|
foreignKey: 'officeTypeId',
|
||||||
|
as: 'offices'
|
||||||
|
});
|
||||||
|
|
||||||
|
ChurchOffice.belongsTo(FalukantCharacter, {
|
||||||
|
foreignKey: 'characterId',
|
||||||
|
as: 'holder'
|
||||||
|
});
|
||||||
|
FalukantCharacter.hasOne(ChurchOffice, {
|
||||||
|
foreignKey: 'characterId',
|
||||||
|
as: 'heldChurchOffice'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Supervisor relationship
|
||||||
|
ChurchOffice.belongsTo(FalukantCharacter, {
|
||||||
|
foreignKey: 'supervisorId',
|
||||||
|
as: 'supervisor'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Applications for church office
|
||||||
|
ChurchApplication.belongsTo(ChurchOfficeType, {
|
||||||
|
foreignKey: 'officeTypeId',
|
||||||
|
as: 'officeType'
|
||||||
|
});
|
||||||
|
ChurchOfficeType.hasMany(ChurchApplication, {
|
||||||
|
foreignKey: 'officeTypeId',
|
||||||
|
as: 'applications'
|
||||||
|
});
|
||||||
|
|
||||||
|
ChurchApplication.belongsTo(FalukantCharacter, {
|
||||||
|
foreignKey: 'characterId',
|
||||||
|
as: 'applicant'
|
||||||
|
});
|
||||||
|
FalukantCharacter.hasMany(ChurchApplication, {
|
||||||
|
foreignKey: 'characterId',
|
||||||
|
as: 'churchApplications'
|
||||||
|
});
|
||||||
|
|
||||||
|
ChurchApplication.belongsTo(FalukantCharacter, {
|
||||||
|
foreignKey: 'supervisorId',
|
||||||
|
as: 'supervisor'
|
||||||
|
});
|
||||||
|
FalukantCharacter.hasMany(ChurchApplication, {
|
||||||
|
foreignKey: 'supervisorId',
|
||||||
|
as: 'supervisedApplications'
|
||||||
|
});
|
||||||
|
|
||||||
|
ChurchApplication.belongsTo(RegionData, {
|
||||||
|
foreignKey: 'regionId',
|
||||||
|
as: 'region'
|
||||||
|
});
|
||||||
|
RegionData.hasMany(ChurchApplication, {
|
||||||
|
foreignKey: 'regionId',
|
||||||
|
as: 'churchApplications'
|
||||||
|
});
|
||||||
|
|
||||||
Underground.belongsTo(UndergroundType, {
|
Underground.belongsTo(UndergroundType, {
|
||||||
foreignKey: 'undergroundTypeId',
|
foreignKey: 'undergroundTypeId',
|
||||||
as: 'undergroundType'
|
as: 'undergroundType'
|
||||||
|
|||||||
47
backend/models/falukant/data/church_application.js
Normal file
47
backend/models/falukant/data/church_application.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class ChurchApplication extends Model {}
|
||||||
|
|
||||||
|
ChurchApplication.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
officeTypeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
characterId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
regionId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
supervisorId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
comment: 'ID des Vorgesetzten, der über die Bewerbung entscheidet'
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: DataTypes.ENUM('pending', 'approved', 'rejected'),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'pending'
|
||||||
|
},
|
||||||
|
decisionDate: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'ChurchApplication',
|
||||||
|
tableName: 'church_application',
|
||||||
|
schema: 'falukant_data',
|
||||||
|
timestamps: true,
|
||||||
|
underscored: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ChurchApplication;
|
||||||
38
backend/models/falukant/data/church_office.js
Normal file
38
backend/models/falukant/data/church_office.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class ChurchOffice extends Model {}
|
||||||
|
|
||||||
|
ChurchOffice.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
officeTypeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
characterId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
regionId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
supervisorId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'ID des Vorgesetzten (höhere Position in der Hierarchie)'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'ChurchOffice',
|
||||||
|
tableName: 'church_office',
|
||||||
|
schema: 'falukant_data',
|
||||||
|
timestamps: true,
|
||||||
|
underscored: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ChurchOffice;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class ChurchOfficeRequirement extends Model {}
|
||||||
|
|
||||||
|
ChurchOfficeRequirement.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
officeTypeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
prerequisiteOfficeTypeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'Erforderliche niedrigere Position in der Hierarchie'
|
||||||
|
},
|
||||||
|
minTitleLevel: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'Mindest-Titel-Level (optional)'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'ChurchOfficeRequirement',
|
||||||
|
tableName: 'church_office_requirement',
|
||||||
|
schema: 'falukant_predefine',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ChurchOfficeRequirement;
|
||||||
38
backend/models/falukant/type/church_office_type.js
Normal file
38
backend/models/falukant/type/church_office_type.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class ChurchOfficeType extends Model {}
|
||||||
|
|
||||||
|
ChurchOfficeType.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
seatsPerRegion: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
regionType: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
hierarchyLevel: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
comment: 'Höhere Zahl = höhere Position in der Hierarchie'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'ChurchOfficeType',
|
||||||
|
tableName: 'church_office_type',
|
||||||
|
schema: 'falukant_type',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ChurchOfficeType;
|
||||||
@@ -113,6 +113,12 @@ import Vote from './falukant/data/vote.js';
|
|||||||
import ElectionResult from './falukant/data/election_result.js';
|
import ElectionResult from './falukant/data/election_result.js';
|
||||||
import PoliticalOfficeHistory from './falukant/log/political_office_history.js';
|
import PoliticalOfficeHistory from './falukant/log/political_office_history.js';
|
||||||
import ElectionHistory from './falukant/log/election_history.js';
|
import ElectionHistory from './falukant/log/election_history.js';
|
||||||
|
|
||||||
|
// — Kirchliche Ämter (Church) —
|
||||||
|
import ChurchOfficeType from './falukant/type/church_office_type.js';
|
||||||
|
import ChurchOfficeRequirement from './falukant/predefine/church_office_requirement.js';
|
||||||
|
import ChurchOffice from './falukant/data/church_office.js';
|
||||||
|
import ChurchApplication from './falukant/data/church_application.js';
|
||||||
import UndergroundType from './falukant/type/underground.js';
|
import UndergroundType from './falukant/type/underground.js';
|
||||||
import Underground from './falukant/data/underground.js';
|
import Underground from './falukant/data/underground.js';
|
||||||
import VehicleType from './falukant/type/vehicle.js';
|
import VehicleType from './falukant/type/vehicle.js';
|
||||||
@@ -242,6 +248,10 @@ const models = {
|
|||||||
ElectionResult,
|
ElectionResult,
|
||||||
PoliticalOfficeHistory,
|
PoliticalOfficeHistory,
|
||||||
ElectionHistory,
|
ElectionHistory,
|
||||||
|
ChurchOfficeType,
|
||||||
|
ChurchOfficeRequirement,
|
||||||
|
ChurchOffice,
|
||||||
|
ChurchApplication,
|
||||||
UndergroundType,
|
UndergroundType,
|
||||||
Underground,
|
Underground,
|
||||||
WeatherType,
|
WeatherType,
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ router.get('/reputation/actions', falukantController.getReputationActions);
|
|||||||
router.post('/reputation/actions', falukantController.executeReputationAction);
|
router.post('/reputation/actions', falukantController.executeReputationAction);
|
||||||
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.post('/church/positions/apply', falukantController.applyForChurchPosition);
|
||||||
|
router.get('/church/applications/supervised', falukantController.getSupervisedApplications);
|
||||||
|
router.post('/church/applications/decide', falukantController.decideOnChurchApplication);
|
||||||
|
router.get('/church/career/check', falukantController.hasChurchCareer);
|
||||||
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);
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ import PoliticalOfficeHistory from '../models/falukant/log/political_office_hist
|
|||||||
import UndergroundType from '../models/falukant/type/underground.js';
|
import UndergroundType from '../models/falukant/type/underground.js';
|
||||||
import Notification from '../models/falukant/log/notification.js';
|
import Notification from '../models/falukant/log/notification.js';
|
||||||
import PoliticalOffice from '../models/falukant/data/political_office.js';
|
import PoliticalOffice from '../models/falukant/data/political_office.js';
|
||||||
|
import ChurchOfficeType from '../models/falukant/type/church_office_type.js';
|
||||||
|
import ChurchOfficeRequirement from '../models/falukant/predefine/church_office_requirement.js';
|
||||||
|
import ChurchOffice from '../models/falukant/data/church_office.js';
|
||||||
|
import ChurchApplication from '../models/falukant/data/church_application.js';
|
||||||
import Underground from '../models/falukant/data/underground.js';
|
import Underground from '../models/falukant/data/underground.js';
|
||||||
import VehicleType from '../models/falukant/type/vehicle.js';
|
import VehicleType from '../models/falukant/type/vehicle.js';
|
||||||
import Vehicle from '../models/falukant/data/vehicle.js';
|
import Vehicle from '../models/falukant/data/vehicle.js';
|
||||||
@@ -1018,6 +1022,11 @@ class FalukantService extends BaseService {
|
|||||||
async createTransport(hashedUserId, { branchId, vehicleTypeId, vehicleIds, productId, quantity, targetBranchId }) {
|
async createTransport(hashedUserId, { branchId, vehicleTypeId, vehicleIds, productId, quantity, targetBranchId }) {
|
||||||
const user = await getFalukantUserOrFail(hashedUserId);
|
const user = await getFalukantUserOrFail(hashedUserId);
|
||||||
|
|
||||||
|
// Prüfe, ob User eine kirchliche Karriere hat (dann nur Direktoren können transportieren)
|
||||||
|
if (await this.hasChurchCareer(hashedUserId)) {
|
||||||
|
throw new Error('churchCareerNoDirectTransactions');
|
||||||
|
}
|
||||||
|
|
||||||
const sourceBranch = await Branch.findOne({
|
const sourceBranch = await Branch.findOne({
|
||||||
where: { id: branchId, falukantUserId: user.id },
|
where: { id: branchId, falukantUserId: user.id },
|
||||||
include: [{ model: RegionData, as: 'region' }],
|
include: [{ model: RegionData, as: 'region' }],
|
||||||
@@ -1604,6 +1613,12 @@ class FalukantService extends BaseService {
|
|||||||
|
|
||||||
async createProduction(hashedUserId, branchId, productId, quantity) {
|
async createProduction(hashedUserId, branchId, productId, quantity) {
|
||||||
const u = await getFalukantUserOrFail(hashedUserId);
|
const u = await getFalukantUserOrFail(hashedUserId);
|
||||||
|
|
||||||
|
// Prüfe, ob User eine kirchliche Karriere hat (dann nur Direktoren können produzieren)
|
||||||
|
if (await this.hasChurchCareer(hashedUserId)) {
|
||||||
|
throw new Error('churchCareerNoDirectTransactions');
|
||||||
|
}
|
||||||
|
|
||||||
const b = await getBranchOrFail(u.id, branchId);
|
const b = await getBranchOrFail(u.id, branchId);
|
||||||
const p = await ProductType.findOne({ where: { id: productId } });
|
const p = await ProductType.findOne({ where: { id: productId } });
|
||||||
const runningProductions = await Production.findAll({ where: { branchId: b.id } });
|
const runningProductions = await Production.findAll({ where: { branchId: b.id } });
|
||||||
@@ -1753,6 +1768,16 @@ class FalukantService extends BaseService {
|
|||||||
// Konsistenz wie sellAll: nur aus Stocks dieses Branches verkaufen und alles atomar ausführen
|
// Konsistenz wie sellAll: nur aus Stocks dieses Branches verkaufen und alles atomar ausführen
|
||||||
return await sequelize.transaction(async (t) => {
|
return await sequelize.transaction(async (t) => {
|
||||||
const user = await getFalukantUserOrFail(hashedUserId, { transaction: t });
|
const user = await getFalukantUserOrFail(hashedUserId, { transaction: t });
|
||||||
|
|
||||||
|
// Prüfe, ob User eine kirchliche Karriere hat (dann nur Direktoren können verkaufen)
|
||||||
|
const character = await FalukantCharacter.findOne({ where: { userId: user.id }, transaction: t });
|
||||||
|
if (character) {
|
||||||
|
const churchOffice = await ChurchOffice.findOne({ where: { characterId: character.id }, transaction: t });
|
||||||
|
if (churchOffice) {
|
||||||
|
throw new Error('churchCareerNoDirectTransactions');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const branch = await getBranchOrFail(user.id, branchId);
|
const branch = await getBranchOrFail(user.id, branchId);
|
||||||
|
|
||||||
const character = await FalukantCharacter.findOne({ where: { userId: user.id }, transaction: t });
|
const character = await FalukantCharacter.findOne({ where: { userId: user.id }, transaction: t });
|
||||||
@@ -1854,6 +1879,16 @@ class FalukantService extends BaseService {
|
|||||||
// Sonst kann es (wie beobachtet) zu "teilweise verkauft/gelöscht" kommen.
|
// Sonst kann es (wie beobachtet) zu "teilweise verkauft/gelöscht" kommen.
|
||||||
return await sequelize.transaction(async (t) => {
|
return await sequelize.transaction(async (t) => {
|
||||||
const falukantUser = await getFalukantUserOrFail(hashedUserId, { transaction: t });
|
const falukantUser = await getFalukantUserOrFail(hashedUserId, { transaction: t });
|
||||||
|
|
||||||
|
// Prüfe, ob User eine kirchliche Karriere hat (dann nur Direktoren können verkaufen)
|
||||||
|
const character = await FalukantCharacter.findOne({ where: { userId: falukantUser.id }, transaction: t });
|
||||||
|
if (character) {
|
||||||
|
const churchOffice = await ChurchOffice.findOne({ where: { characterId: character.id }, transaction: t });
|
||||||
|
if (churchOffice) {
|
||||||
|
throw new Error('churchCareerNoDirectTransactions');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const branch = await Branch.findOne({
|
const branch = await Branch.findOne({
|
||||||
where: { id: branchId, falukantUserId: falukantUser.id },
|
where: { id: branchId, falukantUserId: falukantUser.id },
|
||||||
include: [{ model: FalukantStock, as: 'stocks' }],
|
include: [{ model: FalukantStock, as: 'stocks' }],
|
||||||
@@ -2191,6 +2226,12 @@ class FalukantService extends BaseService {
|
|||||||
|
|
||||||
async buyStorage(hashedUserId, branchId, amount, stockTypeId) {
|
async buyStorage(hashedUserId, branchId, amount, stockTypeId) {
|
||||||
const user = await getFalukantUserOrFail(hashedUserId);
|
const user = await getFalukantUserOrFail(hashedUserId);
|
||||||
|
|
||||||
|
// Prüfe, ob User eine kirchliche Karriere hat (dann nur Direktoren können Lager kaufen)
|
||||||
|
if (await this.hasChurchCareer(hashedUserId)) {
|
||||||
|
throw new Error('churchCareerNoDirectTransactions');
|
||||||
|
}
|
||||||
|
|
||||||
const branch = await getBranchOrFail(user.id, branchId);
|
const branch = await getBranchOrFail(user.id, branchId);
|
||||||
const buyableStocks = await BuyableStock.findAll({
|
const buyableStocks = await BuyableStock.findAll({
|
||||||
where: { regionId: branch.regionId, stockTypeId },
|
where: { regionId: branch.regionId, stockTypeId },
|
||||||
@@ -2260,6 +2301,12 @@ class FalukantService extends BaseService {
|
|||||||
|
|
||||||
async sellStorage(hashedUserId, branchId, amount, stockTypeId) {
|
async sellStorage(hashedUserId, branchId, amount, stockTypeId) {
|
||||||
const user = await getFalukantUserOrFail(hashedUserId);
|
const user = await getFalukantUserOrFail(hashedUserId);
|
||||||
|
|
||||||
|
// Prüfe, ob User eine kirchliche Karriere hat (dann nur Direktoren können Lager verkaufen)
|
||||||
|
if (await this.hasChurchCareer(hashedUserId)) {
|
||||||
|
throw new Error('churchCareerNoDirectTransactions');
|
||||||
|
}
|
||||||
|
|
||||||
const branch = await getBranchOrFail(user.id, branchId);
|
const branch = await getBranchOrFail(user.id, branchId);
|
||||||
const stock = await FalukantStock.findOne({
|
const stock = await FalukantStock.findOne({
|
||||||
where: { branchId: branch.id, stockTypeId },
|
where: { branchId: branch.id, stockTypeId },
|
||||||
@@ -6239,4 +6286,558 @@ async function enrichNotificationsWithCharacterNames(notifications) {
|
|||||||
n.character_name = resolved;
|
n.character_name = resolved;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Church Career Methods ====================
|
||||||
|
|
||||||
|
async getChurchOverview(hashedUserId) {
|
||||||
|
// Liefert alle aktuell besetzten kirchlichen Ämter im eigenen Gebiet
|
||||||
|
const user = await getFalukantUserOrFail(hashedUserId);
|
||||||
|
const character = await FalukantCharacter.findOne({
|
||||||
|
where: { userId: user.id },
|
||||||
|
attributes: ['id', 'regionId']
|
||||||
|
});
|
||||||
|
if (!character) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const relevantRegionIds = await this.getRegionAndParentIds(character.regionId);
|
||||||
|
|
||||||
|
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'],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: FalukantPredefineFirstname,
|
||||||
|
as: 'definedFirstName',
|
||||||
|
attributes: ['name']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: FalukantPredefineLastname,
|
||||||
|
as: 'definedLastName',
|
||||||
|
attributes: ['name']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
order: [
|
||||||
|
[{ model: ChurchOfficeType, as: 'type' }, 'hierarchyLevel', 'ASC'],
|
||||||
|
[{ 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,
|
||||||
|
hierarchyLevel: o.type.hierarchyLevel
|
||||||
|
},
|
||||||
|
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) {
|
||||||
|
// Liefert verfügbare kirchliche Positionen, für die sich der User bewerben kann
|
||||||
|
const user = await getFalukantUserOrFail(hashedUserId);
|
||||||
|
const character = await FalukantCharacter.findOne({
|
||||||
|
where: { userId: user.id },
|
||||||
|
attributes: ['id', 'regionId'],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: TitleOfNobility,
|
||||||
|
as: 'nobleTitle',
|
||||||
|
attributes: ['labelTr', 'level']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
if (!character) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe, ob User bereits ein kirchliches Amt hat
|
||||||
|
const existingOffice = await ChurchOffice.findOne({
|
||||||
|
where: { characterId: character.id }
|
||||||
|
});
|
||||||
|
if (existingOffice) {
|
||||||
|
// User hat bereits ein Amt, kann sich nur auf höhere Positionen bewerben
|
||||||
|
const currentOffice = await ChurchOffice.findOne({
|
||||||
|
where: { characterId: character.id },
|
||||||
|
include: [{
|
||||||
|
model: ChurchOfficeType,
|
||||||
|
as: 'type',
|
||||||
|
attributes: ['hierarchyLevel']
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
const currentLevel = currentOffice?.type?.hierarchyLevel || 0;
|
||||||
|
|
||||||
|
// Finde höhere Positionen
|
||||||
|
const availableOffices = await ChurchOfficeType.findAll({
|
||||||
|
where: {
|
||||||
|
hierarchyLevel: {
|
||||||
|
[Op.gt]: currentLevel
|
||||||
|
}
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: ChurchOfficeRequirement,
|
||||||
|
as: 'requirements'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.filterAvailablePositions(availableOffices, character, existingOffice);
|
||||||
|
} else {
|
||||||
|
// User hat noch kein Amt, kann sich auf Einstiegspositionen bewerben
|
||||||
|
const entryLevelOffices = await ChurchOfficeType.findAll({
|
||||||
|
where: {
|
||||||
|
hierarchyLevel: 1 // Dorfgeistlicher ist Einstieg
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: ChurchOfficeRequirement,
|
||||||
|
as: 'requirements'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.filterAvailablePositions(entryLevelOffices, character, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async filterAvailablePositions(officeTypes, character, existingOffice) {
|
||||||
|
const available = [];
|
||||||
|
const relevantRegionIds = await this.getRegionAndParentIds(character.regionId);
|
||||||
|
|
||||||
|
for (const officeType of officeTypes) {
|
||||||
|
// Prüfe Voraussetzungen
|
||||||
|
const requirements = officeType.requirements || [];
|
||||||
|
let canApply = true;
|
||||||
|
|
||||||
|
// Prüfe, ob Voraussetzung erfüllt ist (niedrigere Position)
|
||||||
|
if (requirements.length > 0 && requirements[0].prerequisiteOfficeTypeId) {
|
||||||
|
if (!existingOffice) {
|
||||||
|
canApply = false; // Benötigt niedrigere Position, aber User hat keine
|
||||||
|
} else {
|
||||||
|
const currentOfficeType = await ChurchOfficeType.findByPk(existingOffice.officeTypeId);
|
||||||
|
if (currentOfficeType?.hierarchyLevel >= officeType.hierarchyLevel) {
|
||||||
|
canApply = false; // Aktuelle Position ist nicht niedrig genug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canApply) continue;
|
||||||
|
|
||||||
|
// Finde verfügbare Positionen in relevanten Regionen
|
||||||
|
const filledPositions = await ChurchOffice.count({
|
||||||
|
where: {
|
||||||
|
officeTypeId: officeType.id,
|
||||||
|
regionId: {
|
||||||
|
[Op.in]: relevantRegionIds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prüfe, ob noch Plätze frei sind
|
||||||
|
if (filledPositions < officeType.seatsPerRegion) {
|
||||||
|
// Finde Vorgesetzten für diese Position
|
||||||
|
const supervisor = await this.findSupervisorForPosition(officeType, character.regionId);
|
||||||
|
|
||||||
|
// Finde passende Region für diese Position
|
||||||
|
const region = await RegionData.findOne({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
[Op.in]: relevantRegionIds
|
||||||
|
}
|
||||||
|
},
|
||||||
|
include: [{
|
||||||
|
model: RegionType,
|
||||||
|
as: 'regionType',
|
||||||
|
attributes: ['labelTr']
|
||||||
|
}],
|
||||||
|
order: [['id', 'ASC']] // Nimm die erste passende Region
|
||||||
|
});
|
||||||
|
|
||||||
|
available.push({
|
||||||
|
id: officeType.id,
|
||||||
|
officeType: {
|
||||||
|
name: officeType.name,
|
||||||
|
hierarchyLevel: officeType.hierarchyLevel,
|
||||||
|
seatsPerRegion: officeType.seatsPerRegion,
|
||||||
|
regionType: officeType.regionType
|
||||||
|
},
|
||||||
|
region: region ? {
|
||||||
|
id: region.id,
|
||||||
|
name: region.name
|
||||||
|
} : null,
|
||||||
|
regionId: region?.id || character.regionId,
|
||||||
|
availableSeats: officeType.seatsPerRegion - filledPositions,
|
||||||
|
supervisor: supervisor ? {
|
||||||
|
id: supervisor.id,
|
||||||
|
name: `${supervisor.definedFirstName?.name || ''} ${supervisor.definedLastName?.name || ''}`.trim()
|
||||||
|
} : null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findSupervisorForPosition(officeType, regionId) {
|
||||||
|
// Finde den Vorgesetzten (höhere Position in der Hierarchie)
|
||||||
|
const supervisorOfficeType = await ChurchOfficeType.findOne({
|
||||||
|
where: {
|
||||||
|
hierarchyLevel: {
|
||||||
|
[Op.gt]: officeType.hierarchyLevel
|
||||||
|
}
|
||||||
|
},
|
||||||
|
order: [['hierarchyLevel', 'ASC']] // Nimm die nächsthöhere Position
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!supervisorOfficeType) {
|
||||||
|
return null; // Kein Vorgesetzter (z.B. Papst)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finde den Vorgesetzten in der Region oder übergeordneten Regionen
|
||||||
|
const relevantRegionIds = await this.getRegionAndParentIds(regionId);
|
||||||
|
const supervisorOffice = await ChurchOffice.findOne({
|
||||||
|
where: {
|
||||||
|
officeTypeId: supervisorOfficeType.id,
|
||||||
|
regionId: {
|
||||||
|
[Op.in]: relevantRegionIds
|
||||||
|
}
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: FalukantCharacter,
|
||||||
|
as: 'holder',
|
||||||
|
attributes: ['id'],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: FalukantPredefineFirstname,
|
||||||
|
as: 'definedFirstName',
|
||||||
|
attributes: ['name']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: FalukantPredefineLastname,
|
||||||
|
as: 'definedLastName',
|
||||||
|
attributes: ['name']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return supervisorOffice?.holder || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyForChurchPosition(hashedUserId, officeTypeId, regionId) {
|
||||||
|
// Bewerbung für eine kirchliche Position
|
||||||
|
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 bereits eine Bewerbung für diese Position existiert
|
||||||
|
const existingApplication = await ChurchApplication.findOne({
|
||||||
|
where: {
|
||||||
|
characterId: character.id,
|
||||||
|
officeTypeId: officeTypeId,
|
||||||
|
status: 'pending'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (existingApplication) {
|
||||||
|
throw new Error('Application already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finde Vorgesetzten
|
||||||
|
const officeType = await ChurchOfficeType.findByPk(officeTypeId);
|
||||||
|
if (!officeType) {
|
||||||
|
throw new Error('Office type not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const supervisor = await this.findSupervisorForPosition(officeType, regionId);
|
||||||
|
if (!supervisor) {
|
||||||
|
throw new Error('Supervisor not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erstelle Bewerbung
|
||||||
|
const application = await ChurchApplication.create({
|
||||||
|
officeTypeId: officeTypeId,
|
||||||
|
characterId: character.id,
|
||||||
|
regionId: regionId,
|
||||||
|
supervisorId: supervisor.id,
|
||||||
|
status: 'pending'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Benachrichtige Vorgesetzten
|
||||||
|
const supervisorUser = await FalukantUser.findOne({
|
||||||
|
where: { id: supervisor.userId },
|
||||||
|
attributes: ['id']
|
||||||
|
});
|
||||||
|
if (supervisorUser) {
|
||||||
|
await notifyUser(supervisorUser.id, {
|
||||||
|
tr: 'falukant.church.application.received',
|
||||||
|
characterId: character.id,
|
||||||
|
officeTypeId: officeTypeId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return application;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSupervisedApplications(hashedUserId) {
|
||||||
|
// Liefert alle Bewerbungen, über die der User als Vorgesetzter entscheiden kann
|
||||||
|
const user = await getFalukantUserOrFail(hashedUserId);
|
||||||
|
const character = await FalukantCharacter.findOne({
|
||||||
|
where: { userId: user.id },
|
||||||
|
attributes: ['id']
|
||||||
|
});
|
||||||
|
if (!character) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const applications = await ChurchApplication.findAll({
|
||||||
|
where: {
|
||||||
|
supervisorId: character.id,
|
||||||
|
status: 'pending'
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: ChurchOfficeType,
|
||||||
|
as: 'officeType',
|
||||||
|
attributes: ['name', 'hierarchyLevel']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
hierarchyLevel: a.officeType.hierarchyLevel
|
||||||
|
},
|
||||||
|
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 decideOnChurchApplication(hashedUserId, applicationId, decision) {
|
||||||
|
// Entscheidung über eine Bewerbung (approve/reject)
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!application) {
|
||||||
|
throw new Error('Application not found or already processed');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decision === 'approve') {
|
||||||
|
// Prüfe, ob noch Platz verfügbar ist
|
||||||
|
const filledPositions = await ChurchOffice.count({
|
||||||
|
where: {
|
||||||
|
officeTypeId: application.officeTypeId,
|
||||||
|
regionId: application.regionId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filledPositions >= application.officeType.seatsPerRegion) {
|
||||||
|
throw new Error('No available seats');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erstelle kirchliches Amt
|
||||||
|
await ChurchOffice.create({
|
||||||
|
officeTypeId: application.officeTypeId,
|
||||||
|
characterId: application.characterId,
|
||||||
|
regionId: application.regionId,
|
||||||
|
supervisorId: character.id
|
||||||
|
});
|
||||||
|
|
||||||
|
// Aktualisiere Bewerbung
|
||||||
|
application.status = 'approved';
|
||||||
|
application.decisionDate = new Date();
|
||||||
|
await application.save();
|
||||||
|
|
||||||
|
// Benachrichtige Bewerber
|
||||||
|
const applicantCharacter = await FalukantCharacter.findByPk(application.characterId);
|
||||||
|
if (applicantCharacter && applicantCharacter.userId) {
|
||||||
|
await notifyUser(applicantCharacter.userId, {
|
||||||
|
tr: 'falukant.church.application.approved',
|
||||||
|
officeTypeId: application.officeTypeId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wenn User bereits ein niedrigeres Amt hatte, entferne es
|
||||||
|
const lowerOffice = await ChurchOffice.findOne({
|
||||||
|
where: {
|
||||||
|
characterId: application.characterId,
|
||||||
|
officeTypeId: {
|
||||||
|
[Op.ne]: application.officeTypeId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
include: [{
|
||||||
|
model: ChurchOfficeType,
|
||||||
|
as: 'type',
|
||||||
|
attributes: ['hierarchyLevel']
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (lowerOffice && lowerOffice.type.hierarchyLevel < application.officeType.hierarchyLevel) {
|
||||||
|
await lowerOffice.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (decision === 'reject') {
|
||||||
|
application.status = 'rejected';
|
||||||
|
application.decisionDate = new Date();
|
||||||
|
await application.save();
|
||||||
|
|
||||||
|
// Benachrichtige Bewerber
|
||||||
|
const applicantCharacter = await FalukantCharacter.findByPk(application.characterId);
|
||||||
|
if (applicantCharacter && applicantCharacter.userId) {
|
||||||
|
await notifyUser(applicantCharacter.userId, {
|
||||||
|
tr: 'falukant.church.application.rejected',
|
||||||
|
officeTypeId: application.officeTypeId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid decision');
|
||||||
|
}
|
||||||
|
|
||||||
|
return application;
|
||||||
|
}
|
||||||
|
|
||||||
|
async hasChurchCareer(hashedUserId) {
|
||||||
|
// Prüft, ob der User eine kirchliche Karriere hat
|
||||||
|
const user = await getFalukantUserOrFail(hashedUserId);
|
||||||
|
const character = await FalukantCharacter.findOne({
|
||||||
|
where: { userId: user.id },
|
||||||
|
attributes: ['id']
|
||||||
|
});
|
||||||
|
if (!character) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const churchOffice = await ChurchOffice.findOne({
|
||||||
|
where: { characterId: character.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!churchOffice;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import ReputationActionType from "../../models/falukant/type/reputation_action.j
|
|||||||
import VehicleType from "../../models/falukant/type/vehicle.js";
|
import VehicleType from "../../models/falukant/type/vehicle.js";
|
||||||
import LearnRecipient from "../../models/falukant/type/learn_recipient.js";
|
import LearnRecipient from "../../models/falukant/type/learn_recipient.js";
|
||||||
import PoliticalOfficeType from "../../models/falukant/type/political_office_type.js";
|
import PoliticalOfficeType from "../../models/falukant/type/political_office_type.js";
|
||||||
|
import ChurchOfficeType from "../../models/falukant/type/church_office_type.js";
|
||||||
|
import ChurchOfficeRequirement from "../../models/falukant/predefine/church_office_requirement.js";
|
||||||
import PoliticalOfficeBenefitType from "../../models/falukant/type/political_office_benefit_type.js";
|
import PoliticalOfficeBenefitType from "../../models/falukant/type/political_office_benefit_type.js";
|
||||||
import PoliticalOfficePrerequisite from "../../models/falukant/predefine/political_office_prerequisite.js";
|
import PoliticalOfficePrerequisite from "../../models/falukant/predefine/political_office_prerequisite.js";
|
||||||
import UndergroundType from "../../models/falukant/type/underground.js";
|
import UndergroundType from "../../models/falukant/type/underground.js";
|
||||||
@@ -47,6 +49,8 @@ export const initializeFalukantTypes = async () => {
|
|||||||
await initializePoliticalOfficeBenefitTypes();
|
await initializePoliticalOfficeBenefitTypes();
|
||||||
await initializePoliticalOfficeTypes();
|
await initializePoliticalOfficeTypes();
|
||||||
await initializePoliticalOfficePrerequisites();
|
await initializePoliticalOfficePrerequisites();
|
||||||
|
await initializeChurchOfficeTypes();
|
||||||
|
await initializeChurchOfficePrerequisites();
|
||||||
await initializeUndergroundTypes();
|
await initializeUndergroundTypes();
|
||||||
await initializeVehicleTypes();
|
await initializeVehicleTypes();
|
||||||
await initializeFalukantWeatherTypes();
|
await initializeFalukantWeatherTypes();
|
||||||
@@ -1024,6 +1028,119 @@ export const initializePoliticalOfficePrerequisites = async () => {
|
|||||||
console.log(`[Falukant] OfficePrereq neu=${created} exist=${existing}${skipped?` skip=${skipped}`:''}`);
|
console.log(`[Falukant] OfficePrereq neu=${created} exist=${existing}${skipped?` skip=${skipped}`:''}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// — Church Offices —
|
||||||
|
|
||||||
|
const churchOffices = [
|
||||||
|
{ tr: "village-priest", seatsPerRegion: 1, regionType: "city", hierarchyLevel: 1 },
|
||||||
|
{ tr: "parish-priest", seatsPerRegion: 1, regionType: "city", hierarchyLevel: 2 },
|
||||||
|
{ tr: "dean", seatsPerRegion: 1, regionType: "county", hierarchyLevel: 3 },
|
||||||
|
{ tr: "archdeacon", seatsPerRegion: 1, regionType: "shire", hierarchyLevel: 4 },
|
||||||
|
{ tr: "bishop", seatsPerRegion: 1, regionType: "markgravate", hierarchyLevel: 5 },
|
||||||
|
{ tr: "archbishop", seatsPerRegion: 1, regionType: "duchy", hierarchyLevel: 6 },
|
||||||
|
{ tr: "cardinal", seatsPerRegion: 3, regionType: "country", hierarchyLevel: 7 },
|
||||||
|
{ tr: "pope", seatsPerRegion: 1, regionType: "country", hierarchyLevel: 8 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const churchOfficePrerequisites = [
|
||||||
|
{
|
||||||
|
officeTr: "village-priest",
|
||||||
|
prerequisite: {
|
||||||
|
prerequisiteOfficeTypeId: null // Einstiegsposition, keine Voraussetzung
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "parish-priest",
|
||||||
|
prerequisite: {
|
||||||
|
prerequisiteOfficeTypeId: "village-priest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "dean",
|
||||||
|
prerequisite: {
|
||||||
|
prerequisiteOfficeTypeId: "parish-priest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "archdeacon",
|
||||||
|
prerequisite: {
|
||||||
|
prerequisiteOfficeTypeId: "dean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "bishop",
|
||||||
|
prerequisite: {
|
||||||
|
prerequisiteOfficeTypeId: "archdeacon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "archbishop",
|
||||||
|
prerequisite: {
|
||||||
|
prerequisiteOfficeTypeId: "bishop"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "cardinal",
|
||||||
|
prerequisite: {
|
||||||
|
prerequisiteOfficeTypeId: "archbishop"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officeTr: "pope",
|
||||||
|
prerequisite: {
|
||||||
|
prerequisiteOfficeTypeId: "cardinal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const initializeChurchOfficeTypes = async () => {
|
||||||
|
for (const co of churchOffices) {
|
||||||
|
await ChurchOfficeType.findOrCreate({
|
||||||
|
where: { name: co.tr },
|
||||||
|
defaults: {
|
||||||
|
seatsPerRegion: co.seatsPerRegion,
|
||||||
|
regionType: co.regionType,
|
||||||
|
hierarchyLevel: co.hierarchyLevel
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(`[Falukant] ChurchOfficeTypes initialized`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initializeChurchOfficePrerequisites = async () => {
|
||||||
|
let created = 0;
|
||||||
|
let existing = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
for (const prereq of churchOfficePrerequisites) {
|
||||||
|
const office = await ChurchOfficeType.findOne({ where: { name: prereq.officeTr } });
|
||||||
|
if (!office) { skipped++; continue; }
|
||||||
|
|
||||||
|
let prerequisiteOfficeTypeId = null;
|
||||||
|
if (prereq.prerequisite.prerequisiteOfficeTypeId) {
|
||||||
|
const prerequisiteOffice = await ChurchOfficeType.findOne({
|
||||||
|
where: { name: prereq.prerequisite.prerequisiteOfficeTypeId }
|
||||||
|
});
|
||||||
|
if (prerequisiteOffice) {
|
||||||
|
prerequisiteOfficeTypeId = prerequisiteOffice.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [record, wasCreated] = await ChurchOfficeRequirement.findOrCreate({
|
||||||
|
where: { officeTypeId: office.id },
|
||||||
|
defaults: {
|
||||||
|
officeTypeId: office.id,
|
||||||
|
prerequisiteOfficeTypeId: prerequisiteOfficeTypeId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (wasCreated) created++; else existing++;
|
||||||
|
} catch (e) {
|
||||||
|
if (falukantDebug) console.error('[Falukant] ChurchOfficePrereq Fehler', { officeId: office?.id, error: e.message });
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`[Falukant] ChurchOfficePrereq neu=${created} exist=${existing}${skipped?` skip=${skipped}`:''}`);
|
||||||
|
};
|
||||||
|
|
||||||
export const initializeUndergroundTypes = async () => {
|
export const initializeUndergroundTypes = async () => {
|
||||||
for (const underground of undergroundTypes) {
|
for (const underground of undergroundTypes) {
|
||||||
await UndergroundType.findOrCreate({
|
await UndergroundType.findOrCreate({
|
||||||
|
|||||||
@@ -837,6 +837,58 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"church": {
|
"church": {
|
||||||
|
"title": "Kirche",
|
||||||
|
"tabs": {
|
||||||
|
"current": "Aktuelle Positionen",
|
||||||
|
"available": "Verfügbare Positionen",
|
||||||
|
"applications": "Bewerbungen"
|
||||||
|
},
|
||||||
|
"current": {
|
||||||
|
"office": "Amt",
|
||||||
|
"region": "Region",
|
||||||
|
"holder": "Inhaber",
|
||||||
|
"supervisor": "Vorgesetzter",
|
||||||
|
"none": "Keine aktuellen Positionen vorhanden."
|
||||||
|
},
|
||||||
|
"available": {
|
||||||
|
"office": "Amt",
|
||||||
|
"region": "Region",
|
||||||
|
"supervisor": "Vorgesetzter",
|
||||||
|
"seats": "Verfügbare Plätze",
|
||||||
|
"action": "Aktion",
|
||||||
|
"apply": "Bewerben",
|
||||||
|
"applySuccess": "Bewerbung erfolgreich eingereicht.",
|
||||||
|
"applyError": "Fehler beim Einreichen der Bewerbung.",
|
||||||
|
"none": "Keine verfügbaren Positionen."
|
||||||
|
},
|
||||||
|
"applications": {
|
||||||
|
"office": "Amt",
|
||||||
|
"region": "Region",
|
||||||
|
"applicant": "Bewerber",
|
||||||
|
"date": "Datum",
|
||||||
|
"action": "Aktion",
|
||||||
|
"approve": "Annehmen",
|
||||||
|
"reject": "Ablehnen",
|
||||||
|
"approveSuccess": "Bewerbung angenommen.",
|
||||||
|
"rejectSuccess": "Bewerbung abgelehnt.",
|
||||||
|
"decideError": "Fehler bei der Entscheidung.",
|
||||||
|
"none": "Keine Bewerbungen vorhanden."
|
||||||
|
},
|
||||||
|
"offices": {
|
||||||
|
"village-priest": "Dorfgeistlicher",
|
||||||
|
"parish-priest": "Pfarrer",
|
||||||
|
"dean": "Dekan",
|
||||||
|
"archdeacon": "Erzdiakon",
|
||||||
|
"bishop": "Bischof",
|
||||||
|
"archbishop": "Erzbischof",
|
||||||
|
"cardinal": "Kardinal",
|
||||||
|
"pope": "Papst"
|
||||||
|
},
|
||||||
|
"application": {
|
||||||
|
"received": "Neue Bewerbung erhalten",
|
||||||
|
"approved": "Bewerbung angenommen",
|
||||||
|
"rejected": "Bewerbung abgelehnt"
|
||||||
|
},
|
||||||
"title": "Kirche",
|
"title": "Kirche",
|
||||||
"baptism": {
|
"baptism": {
|
||||||
"title": "Taufen",
|
"title": "Taufen",
|
||||||
|
|||||||
@@ -376,6 +376,76 @@
|
|||||||
"assessor": "Assessor"
|
"assessor": "Assessor"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"church": {
|
||||||
|
"title": "Church",
|
||||||
|
"tabs": {
|
||||||
|
"current": "Current Positions",
|
||||||
|
"available": "Available Positions",
|
||||||
|
"applications": "Applications"
|
||||||
|
},
|
||||||
|
"current": {
|
||||||
|
"office": "Office",
|
||||||
|
"region": "Region",
|
||||||
|
"holder": "Holder",
|
||||||
|
"supervisor": "Supervisor",
|
||||||
|
"none": "No current positions available."
|
||||||
|
},
|
||||||
|
"available": {
|
||||||
|
"office": "Office",
|
||||||
|
"region": "Region",
|
||||||
|
"supervisor": "Supervisor",
|
||||||
|
"seats": "Available Seats",
|
||||||
|
"action": "Action",
|
||||||
|
"apply": "Apply",
|
||||||
|
"applySuccess": "Application submitted successfully.",
|
||||||
|
"applyError": "Error submitting application.",
|
||||||
|
"none": "No available positions."
|
||||||
|
},
|
||||||
|
"applications": {
|
||||||
|
"office": "Office",
|
||||||
|
"region": "Region",
|
||||||
|
"applicant": "Applicant",
|
||||||
|
"date": "Date",
|
||||||
|
"action": "Action",
|
||||||
|
"approve": "Approve",
|
||||||
|
"reject": "Reject",
|
||||||
|
"approveSuccess": "Application approved.",
|
||||||
|
"rejectSuccess": "Application rejected.",
|
||||||
|
"decideError": "Error making decision.",
|
||||||
|
"none": "No applications available."
|
||||||
|
},
|
||||||
|
"offices": {
|
||||||
|
"village-priest": "Village Priest",
|
||||||
|
"parish-priest": "Parish Priest",
|
||||||
|
"dean": "Dean",
|
||||||
|
"archdeacon": "Archdeacon",
|
||||||
|
"bishop": "Bishop",
|
||||||
|
"archbishop": "Archbishop",
|
||||||
|
"cardinal": "Cardinal",
|
||||||
|
"pope": "Pope"
|
||||||
|
},
|
||||||
|
"application": {
|
||||||
|
"received": "New application received",
|
||||||
|
"approved": "Application approved",
|
||||||
|
"rejected": "Application rejected"
|
||||||
|
},
|
||||||
|
"baptism": {
|
||||||
|
"title": "Baptism",
|
||||||
|
"table": {
|
||||||
|
"name": "First Name",
|
||||||
|
"gender": "Gender",
|
||||||
|
"age": "Age",
|
||||||
|
"baptise": "Baptize (50)",
|
||||||
|
"newName": "Suggest Name"
|
||||||
|
},
|
||||||
|
"gender": {
|
||||||
|
"male": "Boy",
|
||||||
|
"female": "Girl"
|
||||||
|
},
|
||||||
|
"success": "The child has been baptized.",
|
||||||
|
"error": "The child could not be baptized."
|
||||||
|
}
|
||||||
|
},
|
||||||
"family": {
|
"family": {
|
||||||
"children": {
|
"children": {
|
||||||
"title": "Children",
|
"title": "Children",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
||||||
|
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
|
<!-- Taufe -->
|
||||||
<div v-if="activeTab === 'baptism'">
|
<div v-if="activeTab === 'baptism'">
|
||||||
<h3>{{ $t('falukant.church.baptism.title') }}</h3>
|
<h3>{{ $t('falukant.church.baptism.title') }}</h3>
|
||||||
<table>
|
<table>
|
||||||
@@ -36,6 +37,124 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Aktuelle kirchliche Positionen -->
|
||||||
|
<div v-else-if="activeTab === 'current'" class="tab-pane">
|
||||||
|
<div v-if="loading.current" class="loading">{{ $t('loading') }}</div>
|
||||||
|
<div v-else class="table-scroll">
|
||||||
|
<table class="church-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ $t('falukant.church.current.office') }}</th>
|
||||||
|
<th>{{ $t('falukant.church.current.region') }}</th>
|
||||||
|
<th>{{ $t('falukant.church.current.holder') }}</th>
|
||||||
|
<th>{{ $t('falukant.church.current.supervisor') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="pos in currentPositions" :key="pos.id" :class="{ 'own-position': isOwnPosition(pos) }">
|
||||||
|
<td>{{ $t(`falukant.church.offices.${pos.officeType.name}`) }}</td>
|
||||||
|
<td>{{ pos.region.name }}</td>
|
||||||
|
<td>
|
||||||
|
<span v-if="pos.character">
|
||||||
|
{{ $t(`falukant.titles.${pos.character.gender}.${pos.character.title || 'noncivil'}`) }}
|
||||||
|
{{ pos.character.name }}
|
||||||
|
</span>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span v-if="pos.supervisor">
|
||||||
|
{{ pos.supervisor.name }}
|
||||||
|
</span>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!currentPositions.length">
|
||||||
|
<td colspan="4">{{ $t('falukant.church.current.none') }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Verfügbare Positionen -->
|
||||||
|
<div v-else-if="activeTab === 'available'" class="tab-pane">
|
||||||
|
<div v-if="loading.available" class="loading">{{ $t('loading') }}</div>
|
||||||
|
<div v-else class="table-scroll">
|
||||||
|
<table class="church-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ $t('falukant.church.available.office') }}</th>
|
||||||
|
<th>{{ $t('falukant.church.available.region') }}</th>
|
||||||
|
<th>{{ $t('falukant.church.available.supervisor') }}</th>
|
||||||
|
<th>{{ $t('falukant.church.available.seats') }}</th>
|
||||||
|
<th>{{ $t('falukant.church.available.action') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="pos in availablePositions" :key="pos.id">
|
||||||
|
<td>{{ $t(`falukant.church.offices.${pos.officeType.name}`) }}</td>
|
||||||
|
<td>{{ pos.region?.name || '—' }}</td>
|
||||||
|
<td>
|
||||||
|
<span v-if="pos.supervisor">
|
||||||
|
{{ pos.supervisor.name }}
|
||||||
|
</span>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ pos.availableSeats }}</td>
|
||||||
|
<td>
|
||||||
|
<button @click="applyForPosition(pos)" :disabled="pos.availableSeats === 0">
|
||||||
|
{{ $t('falukant.church.available.apply') }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!availablePositions.length">
|
||||||
|
<td colspan="5">{{ $t('falukant.church.available.none') }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bewerbungen (als Vorgesetzter) -->
|
||||||
|
<div v-else-if="activeTab === 'applications'" class="tab-pane">
|
||||||
|
<div v-if="loading.applications" class="loading">{{ $t('loading') }}</div>
|
||||||
|
<div v-else class="table-scroll">
|
||||||
|
<table class="church-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ $t('falukant.church.applications.office') }}</th>
|
||||||
|
<th>{{ $t('falukant.church.applications.region') }}</th>
|
||||||
|
<th>{{ $t('falukant.church.applications.applicant') }}</th>
|
||||||
|
<th>{{ $t('falukant.church.applications.date') }}</th>
|
||||||
|
<th>{{ $t('falukant.church.applications.action') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="app in supervisedApplications" :key="app.id">
|
||||||
|
<td>{{ $t(`falukant.church.offices.${app.officeType.name}`) }}</td>
|
||||||
|
<td>{{ app.region.name }}</td>
|
||||||
|
<td>
|
||||||
|
{{ $t(`falukant.titles.${app.applicant.gender}.${app.applicant.title || 'noncivil'}`) }}
|
||||||
|
{{ app.applicant.name }} ({{ app.applicant.age }})
|
||||||
|
</td>
|
||||||
|
<td>{{ formatDate(app.createdAt) }}</td>
|
||||||
|
<td>
|
||||||
|
<button @click="decideOnApplication(app.id, 'approve')" class="approve-button">
|
||||||
|
{{ $t('falukant.church.applications.approve') }}
|
||||||
|
</button>
|
||||||
|
<button @click="decideOnApplication(app.id, 'reject')" class="reject-button">
|
||||||
|
{{ $t('falukant.church.applications.reject') }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!supervisedApplications.length">
|
||||||
|
<td colspan="5">{{ $t('falukant.church.applications.none') }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,12 +180,36 @@ export default {
|
|||||||
activeTab: 'baptism',
|
activeTab: 'baptism',
|
||||||
tabs: [
|
tabs: [
|
||||||
{ value: 'baptism', label: 'falukant.church.baptism.title' },
|
{ value: 'baptism', label: 'falukant.church.baptism.title' },
|
||||||
|
{ value: 'current', label: 'falukant.church.tabs.current' },
|
||||||
|
{ value: 'available', label: 'falukant.church.tabs.available' },
|
||||||
|
{ value: 'applications', label: 'falukant.church.tabs.applications' },
|
||||||
],
|
],
|
||||||
baptismList: []
|
baptismList: [],
|
||||||
|
currentPositions: [],
|
||||||
|
availablePositions: [],
|
||||||
|
supervisedApplications: [],
|
||||||
|
ownCharacterId: null,
|
||||||
|
loading: {
|
||||||
|
current: false,
|
||||||
|
available: false,
|
||||||
|
applications: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.loadNotBaptisedChildren()
|
await this.loadNotBaptisedChildren();
|
||||||
|
await this.loadOwnCharacterId();
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
activeTab(newTab) {
|
||||||
|
if (newTab === 'current') {
|
||||||
|
this.loadCurrentPositions();
|
||||||
|
} else if (newTab === 'available') {
|
||||||
|
this.loadAvailablePositions();
|
||||||
|
} else if (newTab === 'applications') {
|
||||||
|
this.loadSupervisedApplications();
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async loadNotBaptisedChildren() {
|
async loadNotBaptisedChildren() {
|
||||||
@@ -99,6 +242,102 @@ export default {
|
|||||||
console.error(err)
|
console.error(err)
|
||||||
this.$root.$refs.errorDialog.open('tr:falukant.church.baptism.error')
|
this.$root.$refs.errorDialog.open('tr:falukant.church.baptism.error')
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async loadCurrentPositions() {
|
||||||
|
this.loading.current = true;
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.get('/api/falukant/church/overview');
|
||||||
|
this.currentPositions = data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading current positions', err);
|
||||||
|
} finally {
|
||||||
|
this.loading.current = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadAvailablePositions() {
|
||||||
|
this.loading.available = true;
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.get('/api/falukant/church/positions/available');
|
||||||
|
this.availablePositions = data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading available positions', err);
|
||||||
|
} finally {
|
||||||
|
this.loading.available = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadSupervisedApplications() {
|
||||||
|
this.loading.applications = true;
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.get('/api/falukant/church/applications/supervised');
|
||||||
|
this.supervisedApplications = data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading supervised applications', err);
|
||||||
|
} finally {
|
||||||
|
this.loading.applications = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadOwnCharacterId() {
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.get('/api/falukant/info');
|
||||||
|
if (data.character && data.character.id) {
|
||||||
|
this.ownCharacterId = data.character.id;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading own character ID', err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isOwnPosition(pos) {
|
||||||
|
if (!this.ownCharacterId || !pos.character) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return pos.character.id === this.ownCharacterId;
|
||||||
|
},
|
||||||
|
async applyForPosition(position) {
|
||||||
|
try {
|
||||||
|
const regionId = position.regionId || position.region?.id;
|
||||||
|
|
||||||
|
if (!regionId) {
|
||||||
|
throw new Error('Region not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await apiClient.post('/api/falukant/church/positions/apply', {
|
||||||
|
officeTypeId: position.id,
|
||||||
|
regionId: regionId
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$root.$refs.messageDialog?.open('tr:falukant.church.available.applySuccess');
|
||||||
|
await this.loadAvailablePositions();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error applying for position', err);
|
||||||
|
const errorMsg = err.response?.data?.message || 'falukant.church.available.applyError';
|
||||||
|
this.$root.$refs.errorDialog?.open(`tr:${errorMsg}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async decideOnApplication(applicationId, decision) {
|
||||||
|
try {
|
||||||
|
await apiClient.post('/api/falukant/church/applications/decide', {
|
||||||
|
applicationId: applicationId,
|
||||||
|
decision: decision
|
||||||
|
});
|
||||||
|
|
||||||
|
const msgKey = decision === 'approve'
|
||||||
|
? 'falukant.church.applications.approveSuccess'
|
||||||
|
: 'falukant.church.applications.rejectSuccess';
|
||||||
|
this.$root.$refs.messageDialog?.open(`tr:${msgKey}`);
|
||||||
|
await this.loadSupervisedApplications();
|
||||||
|
await this.loadCurrentPositions();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error deciding on application', err);
|
||||||
|
const errorMsg = err.response?.data?.message || 'falukant.church.applications.decideError';
|
||||||
|
this.$root.$refs.errorDialog?.open(`tr:${errorMsg}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatDate(date) {
|
||||||
|
return new Date(date).toLocaleDateString(this.$i18n.locale, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,4 +379,75 @@ input[type="text"] {
|
|||||||
th {
|
th {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tab-pane {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-scroll {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.church-table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.church-table thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: #FFF;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.church-table tbody td {
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.church-table tbody tr.own-position {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approve-button {
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin-right: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approve-button:hover {
|
||||||
|
background-color: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reject-button {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 4px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reject-button:hover {
|
||||||
|
background-color: #c82333;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user