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

@@ -1452,6 +1452,7 @@
"tabs": {
"current": "Aktuelle Position",
"powers": "Amtsbefugnisse",
"offices": "Alle Ämter",
"upcoming": "Anstehende Neuwahl-Positionen",
"elections": "Wahlen"
},
@@ -1530,6 +1531,21 @@
"minAgeHint": "Kandidatur erst ab 16 Jahren möglich.",
"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.",
"upcoming": {
"office": "Amt",

View File

@@ -658,6 +658,7 @@
"tabs": {
"current": "Current Position",
"powers": "Office powers",
"offices": "All offices",
"upcoming": "Upcoming Positions",
"elections": "Elections"
},
@@ -736,6 +737,21 @@
"minAgeHint": "Candidacy is only possible from age 16.",
"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.",
"upcoming": {
"office": "Office",

View File

@@ -128,6 +128,59 @@
</template>
</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' -->
<div v-else-if="activeTab === 'openPolitics'" class="tab-pane">
<p class="politics-age-requirement">{{ $t('falukant.politics.open.ageRequirement') }}</p>
@@ -235,10 +288,12 @@ export default {
tabs: [
{ value: 'current', label: 'falukant.politics.tabs.current' },
{ value: 'powers', label: 'falukant.politics.tabs.powers' },
{ value: 'offices', label: 'falukant.politics.tabs.offices' },
{ value: 'openPolitics', label: 'falukant.politics.tabs.upcoming' },
{ value: 'elections', label: 'falukant.politics.tabs.elections' }
],
currentPositions: [],
politicalOffices: [],
openPolitics: [],
elections: [],
selectedCandidates: {},
@@ -255,6 +310,7 @@ export default {
_appointSearchTimer: null,
loading: {
current: false,
offices: false,
openPolitics: false,
elections: false,
powers: false
@@ -379,6 +435,9 @@ export default {
if (tab === 'powers') {
this.loadPowers();
}
if (tab === 'offices') {
this.loadPoliticalOffices();
}
if (tab === 'openPolitics') {
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) {
return `${a.officeTypeId}:${a.regionId}`;
},