feat(falukant): add political office catalog feature and localization updates
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s

- Implemented the `getPoliticalOfficeCatalog` method in FalukantService to retrieve available political offices and their prerequisites based on user eligibility.
- Updated FalukantController and FalukantRouter to include a new endpoint for accessing the political office catalog.
- Enhanced PoliticsView component to display the list of political offices, including their details and application eligibility.
- Added localization entries for the new offices tab in both German and English, improving user experience and accessibility.
This commit is contained in:
Torsten Schulz (local)
2026-04-15 15:30:24 +02:00
parent 7b4c9a0b1c
commit 95ea6336b7
6 changed files with 161 additions and 0 deletions

View File

@@ -210,6 +210,7 @@ class FalukantController {
}, { blockInDebtorsPrison: true });
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
this.getPoliticalOfficeCatalog = this._wrapWithUser((userId) => this.service.getPoliticalOfficeCatalog(userId));
this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId));
this.getElections = this._wrapWithUser((userId) => this.service.getElections(userId));
this.vote = this._wrapWithUser((userId, req) => this.service.vote(userId, req.body.votes), { blockInDebtorsPrison: true });

View File

@@ -96,6 +96,7 @@ router.post('/nobility', falukantController.advanceNobility);
router.get('/health', falukantController.getHealth);
router.post('/health', falukantController.healthActivity);
router.get('/politics/overview', falukantController.getPoliticsOverview);
router.get('/politics/offices', falukantController.getPoliticalOfficeCatalog);
router.get('/politics/my-powers', falukantController.getPoliticalMyPowers);
router.get('/politics/tax-jurisdiction', falukantController.getPoliticalTaxJurisdiction);
router.put('/politics/region/:regionId/tax', falukantController.setPoliticalRegionTax);

View File

@@ -6824,6 +6824,60 @@ class FalukantService extends BaseService {
return result;
}
async getPoliticalOfficeCatalog(hashedUserId) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
const character = user.character;
const ageDays = character.birthdate ? calcAge(character.birthdate) : 0;
const canApplyByAge = ageDays >= FalukantService.MIN_AGE_POLITICS_DAYS;
const histories = await PoliticalOfficeHistory.findAll({
where: { characterId: character.id },
attributes: ['officeTypeId', 'startDate', 'endDate']
});
const heldOfficeTypeIds = new Set(histories.map((h) => Number(h.officeTypeId)));
const allTypes = await PoliticalOfficeType.findAll({
attributes: ['id', 'name', 'regionType', 'seatsPerRegion', 'termLength', 'hierarchyLevel'],
include: [{ model: PoliticalOfficePrerequisite, as: 'prerequisites', attributes: ['prerequisite'] }],
order: [['hierarchyLevel', 'ASC'], ['id', 'ASC']]
});
const nameToId = new Map(allTypes.map((t) => [t.name, t.id]));
const titleId = character.titleOfNobility ?? character.nobleTitle?.id;
const allowedOfficeNames = await getAllowedOfficeTypeNamesByTitle(titleId);
return allTypes.map((typeRow) => {
const type = typeRow.get({ plain: true });
const prereqRows = Array.isArray(type.prerequisites) ? type.prerequisites : [];
const requiredOfficeNames = [...new Set(
prereqRows.flatMap((pr) => Array.isArray(pr?.prerequisite?.jobs) ? pr.prerequisite.jobs : [])
)];
const matchedOfficeNames = requiredOfficeNames.filter((officeName) => {
const reqId = nameToId.get(officeName);
return reqId != null && heldOfficeTypeIds.has(Number(reqId));
});
const prerequisitesMet = requiredOfficeNames.length === 0 || matchedOfficeNames.length > 0;
const titleEligible = allowedOfficeNames.size === 0 || allowedOfficeNames.has(type.name);
const canApplyNow = canApplyByAge && titleEligible && prerequisitesMet;
return {
officeTypeId: type.id,
officeTypeName: type.name,
regionType: type.regionType,
seatsPerRegion: type.seatsPerRegion,
termLengthDays: type.termLength,
hierarchyLevel: type.hierarchyLevel,
requiredOfficeNames,
matchedOfficeNames,
prerequisitesMet,
titleEligible,
canApplyByAge,
canApplyNow
};
});
}
async applyForElections(hashedUserId, electionIds) {
// 1) Hole FalukantUser + Character
const user = await this.getFalukantUserByHashedId(hashedUserId);