feat(falukant): add political office catalog feature and localization updates
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
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:
@@ -210,6 +210,7 @@ class FalukantController {
|
|||||||
}, { blockInDebtorsPrison: true });
|
}, { blockInDebtorsPrison: true });
|
||||||
|
|
||||||
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
|
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.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(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), { blockInDebtorsPrison: true });
|
this.vote = this._wrapWithUser((userId, req) => this.service.vote(userId, req.body.votes), { blockInDebtorsPrison: true });
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ router.post('/nobility', falukantController.advanceNobility);
|
|||||||
router.get('/health', falukantController.getHealth);
|
router.get('/health', falukantController.getHealth);
|
||||||
router.post('/health', falukantController.healthActivity);
|
router.post('/health', falukantController.healthActivity);
|
||||||
router.get('/politics/overview', falukantController.getPoliticsOverview);
|
router.get('/politics/overview', falukantController.getPoliticsOverview);
|
||||||
|
router.get('/politics/offices', falukantController.getPoliticalOfficeCatalog);
|
||||||
router.get('/politics/my-powers', falukantController.getPoliticalMyPowers);
|
router.get('/politics/my-powers', falukantController.getPoliticalMyPowers);
|
||||||
router.get('/politics/tax-jurisdiction', falukantController.getPoliticalTaxJurisdiction);
|
router.get('/politics/tax-jurisdiction', falukantController.getPoliticalTaxJurisdiction);
|
||||||
router.put('/politics/region/:regionId/tax', falukantController.setPoliticalRegionTax);
|
router.put('/politics/region/:regionId/tax', falukantController.setPoliticalRegionTax);
|
||||||
|
|||||||
@@ -6824,6 +6824,60 @@ class FalukantService extends BaseService {
|
|||||||
return result;
|
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) {
|
async applyForElections(hashedUserId, electionIds) {
|
||||||
// 1) Hole FalukantUser + Character
|
// 1) Hole FalukantUser + Character
|
||||||
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||||
|
|||||||
@@ -1452,6 +1452,7 @@
|
|||||||
"tabs": {
|
"tabs": {
|
||||||
"current": "Aktuelle Position",
|
"current": "Aktuelle Position",
|
||||||
"powers": "Amtsbefugnisse",
|
"powers": "Amtsbefugnisse",
|
||||||
|
"offices": "Alle Ämter",
|
||||||
"upcoming": "Anstehende Neuwahl-Positionen",
|
"upcoming": "Anstehende Neuwahl-Positionen",
|
||||||
"elections": "Wahlen"
|
"elections": "Wahlen"
|
||||||
},
|
},
|
||||||
@@ -1530,6 +1531,21 @@
|
|||||||
"minAgeHint": "Kandidatur erst ab 16 Jahren möglich.",
|
"minAgeHint": "Kandidatur erst ab 16 Jahren möglich.",
|
||||||
"ageRequirement": "Für alle politischen Ämter gilt: Kandidatur erst ab 16 Jahren."
|
"ageRequirement": "Für alle politischen Ämter gilt: Kandidatur erst ab 16 Jahren."
|
||||||
},
|
},
|
||||||
|
"officesTab": {
|
||||||
|
"loadError": "Ämterliste konnte nicht geladen werden.",
|
||||||
|
"none": "Keine Ämter gefunden.",
|
||||||
|
"ageRequirement": "Hier siehst du alle Ämter mit ihren Voraussetzungen. Bewerbungen sind erst ab 16 Jahren möglich.",
|
||||||
|
"seatsPerRegion": "Sitze pro Region",
|
||||||
|
"termLengthDays": "Amtsdauer (Tage)",
|
||||||
|
"prerequisites": "Voraussetzungen",
|
||||||
|
"noPrerequisites": "Keine (Einstiegsamt)",
|
||||||
|
"canApplyNow": "Bewerbung aktuell möglich",
|
||||||
|
"yes": "Ja",
|
||||||
|
"no": "Nein",
|
||||||
|
"blockedByAge": "Mindestalter nicht erreicht",
|
||||||
|
"blockedByTitle": "Standesvoraussetzung nicht erfüllt",
|
||||||
|
"blockedByPrerequisites": "Vorherige Ämter fehlen"
|
||||||
|
},
|
||||||
"too_young": "Dein Charakter ist noch zu jung. Eine Bewerbung ist erst ab 16 Jahren möglich.",
|
"too_young": "Dein Charakter ist noch zu jung. Eine Bewerbung ist erst ab 16 Jahren möglich.",
|
||||||
"upcoming": {
|
"upcoming": {
|
||||||
"office": "Amt",
|
"office": "Amt",
|
||||||
|
|||||||
@@ -658,6 +658,7 @@
|
|||||||
"tabs": {
|
"tabs": {
|
||||||
"current": "Current Position",
|
"current": "Current Position",
|
||||||
"powers": "Office powers",
|
"powers": "Office powers",
|
||||||
|
"offices": "All offices",
|
||||||
"upcoming": "Upcoming Positions",
|
"upcoming": "Upcoming Positions",
|
||||||
"elections": "Elections"
|
"elections": "Elections"
|
||||||
},
|
},
|
||||||
@@ -736,6 +737,21 @@
|
|||||||
"minAgeHint": "Candidacy is only possible from age 16.",
|
"minAgeHint": "Candidacy is only possible from age 16.",
|
||||||
"ageRequirement": "All political offices require candidates to be at least 16 years old."
|
"ageRequirement": "All political offices require candidates to be at least 16 years old."
|
||||||
},
|
},
|
||||||
|
"officesTab": {
|
||||||
|
"loadError": "Could not load office list.",
|
||||||
|
"none": "No offices found.",
|
||||||
|
"ageRequirement": "This tab lists all offices and their prerequisites. Applications are only possible from age 16.",
|
||||||
|
"seatsPerRegion": "Seats per region",
|
||||||
|
"termLengthDays": "Term length (days)",
|
||||||
|
"prerequisites": "Prerequisites",
|
||||||
|
"noPrerequisites": "None (entry office)",
|
||||||
|
"canApplyNow": "Can apply now",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No",
|
||||||
|
"blockedByAge": "Minimum age not met",
|
||||||
|
"blockedByTitle": "Title requirement not met",
|
||||||
|
"blockedByPrerequisites": "Required prior offices missing"
|
||||||
|
},
|
||||||
"too_young": "Your character is too young. Applications are only possible from age 16.",
|
"too_young": "Your character is too young. Applications are only possible from age 16.",
|
||||||
"upcoming": {
|
"upcoming": {
|
||||||
"office": "Office",
|
"office": "Office",
|
||||||
|
|||||||
@@ -128,6 +128,59 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- OPEN Tab: hier zeigen wir 'openPolitics' -->
|
||||||
|
<div v-else-if="activeTab === 'offices'" class="tab-pane">
|
||||||
|
<div v-if="loading.offices" class="loading">{{ $t('loading') }}</div>
|
||||||
|
<template v-else-if="politicalOffices.length">
|
||||||
|
<p class="politics-age-requirement">
|
||||||
|
{{ $t('falukant.politics.officesTab.ageRequirement') }}
|
||||||
|
</p>
|
||||||
|
<div class="politics-card-list">
|
||||||
|
<article v-for="office in politicalOffices" :key="office.officeTypeId" class="politics-card">
|
||||||
|
<div class="politics-card__header">
|
||||||
|
<strong>{{ $t(`falukant.politics.offices.${office.officeTypeName}`) }}</strong>
|
||||||
|
<span>{{ formatPoliticsRegionLevel(office.regionType) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="politics-card__meta">
|
||||||
|
<div class="politics-card__meta-row">
|
||||||
|
<span class="politics-card__meta-label">{{ $t('falukant.politics.officesTab.seatsPerRegion') }}:</span>
|
||||||
|
<span class="politics-card__meta-value">{{ office.seatsPerRegion }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="politics-card__meta-row">
|
||||||
|
<span class="politics-card__meta-label">{{ $t('falukant.politics.officesTab.termLengthDays') }}:</span>
|
||||||
|
<span class="politics-card__meta-value">{{ office.termLengthDays }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="politics-card__meta-row">
|
||||||
|
<span class="politics-card__meta-label">{{ $t('falukant.politics.officesTab.prerequisites') }}:</span>
|
||||||
|
<span v-if="!office.requiredOfficeNames || !office.requiredOfficeNames.length" class="politics-card__meta-value">
|
||||||
|
{{ $t('falukant.politics.officesTab.noPrerequisites') }}
|
||||||
|
</span>
|
||||||
|
<ul v-else class="politics-benefit-list">
|
||||||
|
<li v-for="req in office.requiredOfficeNames" :key="req">
|
||||||
|
{{ $t(`falukant.politics.offices.${req}`) }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="politics-card__meta-row">
|
||||||
|
<span class="politics-card__meta-label">{{ $t('falukant.politics.officesTab.canApplyNow') }}:</span>
|
||||||
|
<span class="politics-card__meta-value">
|
||||||
|
{{ office.canApplyNow
|
||||||
|
? $t('falukant.politics.officesTab.yes')
|
||||||
|
: $t('falukant.politics.officesTab.no') }}
|
||||||
|
<template v-if="!office.canApplyNow">
|
||||||
|
<span v-if="!office.canApplyByAge"> · {{ $t('falukant.politics.officesTab.blockedByAge') }}</span>
|
||||||
|
<span v-if="!office.titleEligible"> · {{ $t('falukant.politics.officesTab.blockedByTitle') }}</span>
|
||||||
|
<span v-if="!office.prerequisitesMet"> · {{ $t('falukant.politics.officesTab.blockedByPrerequisites') }}</span>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<p v-else class="loading">{{ $t('falukant.politics.officesTab.none') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- OPEN Tab: hier zeigen wir 'openPolitics' -->
|
<!-- OPEN Tab: hier zeigen wir 'openPolitics' -->
|
||||||
<div v-else-if="activeTab === 'openPolitics'" class="tab-pane">
|
<div v-else-if="activeTab === 'openPolitics'" class="tab-pane">
|
||||||
<p class="politics-age-requirement">{{ $t('falukant.politics.open.ageRequirement') }}</p>
|
<p class="politics-age-requirement">{{ $t('falukant.politics.open.ageRequirement') }}</p>
|
||||||
@@ -235,10 +288,12 @@ export default {
|
|||||||
tabs: [
|
tabs: [
|
||||||
{ value: 'current', label: 'falukant.politics.tabs.current' },
|
{ value: 'current', label: 'falukant.politics.tabs.current' },
|
||||||
{ value: 'powers', label: 'falukant.politics.tabs.powers' },
|
{ value: 'powers', label: 'falukant.politics.tabs.powers' },
|
||||||
|
{ value: 'offices', label: 'falukant.politics.tabs.offices' },
|
||||||
{ value: 'openPolitics', label: 'falukant.politics.tabs.upcoming' },
|
{ value: 'openPolitics', label: 'falukant.politics.tabs.upcoming' },
|
||||||
{ value: 'elections', label: 'falukant.politics.tabs.elections' }
|
{ value: 'elections', label: 'falukant.politics.tabs.elections' }
|
||||||
],
|
],
|
||||||
currentPositions: [],
|
currentPositions: [],
|
||||||
|
politicalOffices: [],
|
||||||
openPolitics: [],
|
openPolitics: [],
|
||||||
elections: [],
|
elections: [],
|
||||||
selectedCandidates: {},
|
selectedCandidates: {},
|
||||||
@@ -255,6 +310,7 @@ export default {
|
|||||||
_appointSearchTimer: null,
|
_appointSearchTimer: null,
|
||||||
loading: {
|
loading: {
|
||||||
current: false,
|
current: false,
|
||||||
|
offices: false,
|
||||||
openPolitics: false,
|
openPolitics: false,
|
||||||
elections: false,
|
elections: false,
|
||||||
powers: false
|
powers: false
|
||||||
@@ -379,6 +435,9 @@ export default {
|
|||||||
if (tab === 'powers') {
|
if (tab === 'powers') {
|
||||||
this.loadPowers();
|
this.loadPowers();
|
||||||
}
|
}
|
||||||
|
if (tab === 'offices') {
|
||||||
|
this.loadPoliticalOffices();
|
||||||
|
}
|
||||||
if (tab === 'openPolitics') {
|
if (tab === 'openPolitics') {
|
||||||
this.loadOpenPolitics();
|
this.loadOpenPolitics();
|
||||||
}
|
}
|
||||||
@@ -411,6 +470,20 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async loadPoliticalOffices() {
|
||||||
|
this.loading.offices = true;
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.get('/api/falukant/politics/offices');
|
||||||
|
this.politicalOffices = Array.isArray(data) ? data : [];
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[PoliticsView] loadPoliticalOffices', err);
|
||||||
|
this.politicalOffices = [];
|
||||||
|
showApiError(this, err, this.$t('falukant.politics.officesTab.loadError'));
|
||||||
|
} finally {
|
||||||
|
this.loading.offices = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
appointOptionKey(a) {
|
appointOptionKey(a) {
|
||||||
return `${a.officeTypeId}:${a.regionId}`;
|
return `${a.officeTypeId}:${a.regionId}`;
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user