Files
yourpart3/frontend/src/views/falukant/PoliticsView.vue
Torsten Schulz (local) 6897f0fadf feat(falukant): add political office catalog feature and localization updates
- 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.
2026-04-15 15:30:24 +02:00

938 lines
39 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="politics-view">
<StatusBar />
<SimpleTabs v-model="activeTab" :tabs="tabs" @change="onTabChange" />
<!-- TabInhalt -->
<div class="tab-content">
<!-- Aktuelle Positionen -->
<div v-if="activeTab === 'current'" class="tab-pane">
<div v-if="loading.current" class="loading">{{ $t('loading') }}</div>
<div v-else-if="currentPositions.length" class="politics-card-list">
<article v-for="pos in currentPositions" :key="pos.id" class="politics-card" :class="{ 'own-position': isOwnPosition(pos) }">
<div class="politics-card__header">
<strong>{{ $t(`falukant.politics.offices.${pos.officeType.name}`) }}</strong>
<span>{{ pos.region.name }}</span>
</div>
<div class="politics-card__meta">
<div class="politics-card__meta-row">
<span class="politics-card__meta-label">{{ $t('falukant.politics.current.holder') }}:</span>
<span class="politics-card__meta-value">
<template v-if="pos.character">
{{ pos.character.definedFirstName.name }} {{ pos.character.definedLastName.name }}
</template>
<template v-else></template>
</span>
</div>
<div class="politics-card__meta-row politics-card__meta-row--benefits">
<span class="politics-card__meta-label">{{ $t('falukant.politics.current.benefit') }}:</span>
<ul
v-if="politicsBenefitItems(pos).length"
class="politics-benefit-list"
>
<li
v-for="(b, i) in politicsBenefitItems(pos)"
:key="i"
>
{{ formatPoliticsBenefitItem(b) }}
</li>
</ul>
<span v-else class="politics-card__meta-value"></span>
</div>
<div class="politics-card__meta-row">
<span class="politics-card__meta-label">{{ $t('falukant.politics.current.termEnds') }}:</span>
<span class="politics-card__meta-value">
<template v-if="pos.termEnds">{{ formatDate(pos.termEnds) }}</template>
<template v-else></template>
</span>
</div>
</div>
</article>
</div>
<p v-else class="loading">{{ $t('falukant.politics.current.none') }}</p>
</div>
<div v-else-if="activeTab === 'powers'" class="tab-pane powers-tab">
<div v-if="loading.powers" class="loading">{{ $t('loading') }}</div>
<template v-else-if="myPowers">
<p v-if="!hasAnyPowers" class="politics-powers-empty">{{ $t('falukant.politics.powers.none') }}</p>
<template v-else>
<section v-if="myPowers.freeLoverSlots > 0" class="powers-section">
<h3>{{ $t('falukant.politics.powers.freeLoversTitle') }}</h3>
<p>{{ $t('falukant.politics.powers.freeLoversHint', { count: myPowers.freeLoverSlots }) }}</p>
</section>
<section v-if="myPowers.reputationPeriodic && myPowers.reputationPeriodic.length" class="powers-section">
<h3>{{ $t('falukant.politics.powers.reputationTitle') }}</h3>
<ul class="powers-list">
<li v-for="(r, i) in myPowers.reputationPeriodic" :key="i">
{{ $t('falukant.politics.powers.reputationLine', {
office: $t(`falukant.politics.offices.${r.officeTypeName}`),
days: r.daysUntilNext,
gain: r.gain
}) }}
</li>
</ul>
</section>
<section v-if="taxJurisdiction && taxJurisdiction.regions && taxJurisdiction.regions.length" class="powers-section">
<h3>{{ $t('falukant.politics.powers.taxTitle') }}</h3>
<p class="powers-hint">{{ $t('falukant.politics.powers.taxRange', { min: taxJurisdiction.taxMin, max: taxJurisdiction.taxMax }) }}</p>
<div v-for="reg in taxJurisdiction.regions" :key="reg.id" class="powers-tax-row">
<div>
<strong>{{ reg.name }}</strong>
<span class="powers-muted">({{ formatPoliticsRegionLevel(reg.regionType) }})</span>
</div>
<div class="powers-tax-edit">
<input
type="number"
:min="taxJurisdiction.taxMin"
:max="taxJurisdiction.taxMax"
step="0.1"
v-model.number="taxDraft[reg.id]"
/>
<button type="button" @click="saveRegionTax(reg.id)">{{ $t('falukant.politics.powers.taxSave') }}</button>
</div>
</div>
</section>
<section v-if="appointable.length" class="powers-section">
<h3>{{ $t('falukant.politics.powers.appointTitle') }}</h3>
<label class="powers-label">{{ $t('falukant.politics.powers.appointSlot') }}</label>
<select v-model="selectedAppointKey" class="powers-select">
<option value="">{{ $t('falukant.politics.powers.appointPick') }}</option>
<option v-for="a in appointable" :key="appointOptionKey(a)" :value="appointOptionKey(a)">
{{ $t(`falukant.politics.offices.${a.officeTypeName}`) }} {{ a.regionName }} ({{ a.seatsFree }})
</option>
</select>
<label class="powers-label">{{ $t('falukant.politics.powers.appointSearch') }}</label>
<input v-model="appointSearch" type="text" class="powers-input" @input="debouncedAppointSearch" />
<ul v-if="appointSearchResults.length" class="powers-search-results">
<li v-for="u in appointSearchResults" :key="u.characterId">
<button type="button" class="powers-linkish" @click="selectAppointTarget(u)">
{{ u.username }} {{ u.firstname }} {{ u.lastname }} ({{ u.town }})
</button>
</li>
</ul>
<p v-if="selectedAppointTarget" class="powers-selected-target">
{{ $t('falukant.politics.powers.appointSelected', { name: selectedAppointTarget.username }) }}
</p>
<button
type="button"
class="powers-submit"
:disabled="!canSubmitAppointment"
@click="submitAppointment"
>
{{ $t('falukant.politics.powers.appointSubmit') }}
</button>
</section>
</template>
</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>
<div v-if="loading.openPolitics" class="loading">{{ $t('loading') }}</div>
<div v-else-if="openPolitics.length" class="politics-card-list">
<article v-for="e in openPolitics" :key="e.id" class="politics-card">
<div class="politics-card__header">
<strong>{{ $t(`falukant.politics.offices.${e.officeType.name}`) }}</strong>
<span>{{ e.region.name }}</span>
</div>
<div class="politics-card__meta">
<span>{{ $t('falukant.politics.open.date') }}: {{ formatDate(e.date) }}</span>
</div>
<label class="politics-card__checkbox" :title="e.canApplyByAge === false ? $t('falukant.politics.open.minAgeHint') : null">
<input
type="checkbox"
:id="`apply-${e.id}`"
v-model="selectedApplications"
:value="e.id"
:disabled="e.alreadyApplied || e.canApplyByAge === false"
/>
<span>{{ $t('falukant.politics.bookmarkCandidate') }}</span>
</label>
</article>
</div>
<p v-else class="loading">{{ $t('falukant.politics.open.none') }}</p>
<div class="apply-button">
<button :disabled="!selectedApplications.length" @click="submitApplications">
{{ $t('falukant.politics.open.apply') }}
</button>
</div>
</div>
<!-- Wahlen -->
<div v-else-if="activeTab === 'elections'" class="tab-pane">
<div v-if="loading.elections" class="loading">{{ $t('loading') }}</div>
<div v-else-if="elections.length" class="politics-card-list">
<article v-for="e in elections" :key="e.id" class="politics-card politics-card--election">
<div class="politics-card__header">
<strong>{{ $t(`falukant.politics.offices.${e.officeType.name}`) }}</strong>
<span>{{ e.region.name }}</span>
</div>
<div class="politics-card__meta">
<span>{{ $t('falukant.politics.elections.date') }}: {{ formatDate(e.date) }}</span>
<span>{{ $t('falukant.politics.elections.posts') }}: {{ e.postsToFill }}</span>
</div>
<div v-if="!e.voted" class="politics-card__vote">
<Multiselect v-model="selectedCandidates[e.id]" :options="e.candidates" multiple
:max="e.postsToFill" :close-on-select="false" :clear-on-select="false"
track-by="id" label="name" :custom-label="candidateLabel" placeholder="">
<template #option="{ option }">
{{ $t(`falukant.titles.${option.gender}.${option.title}`) }}
{{ option.name }} ({{ option.age }})
</template>
<template #selected="{ option }">
{{ $t(`falukant.titles.${option.gender}.${option.title}`) }}
{{ option.name }}
</template>
</Multiselect>
<button
:disabled="!selectedCandidates[e.id] || !selectedCandidates[e.id].length"
@click="submitVote(e.id)">
{{ $t('falukant.politics.elections.vote') }}
</button>
</div>
<ul v-else class="voted-list">
<li v-for="cid in e.votedFor" :key="cid">
<span v-if="findCandidateById(e, cid)">
{{ formatCandidateTitle(findCandidateById(e, cid)) }}
{{ findCandidateById(e, cid).name }}
</span>
</li>
<li v-if="!e.votedFor || !e.votedFor.length"></li>
</ul>
</article>
</div>
<p v-else class="loading">{{ $t('falukant.politics.elections.none') }}</p>
<div class="all-vote-button" v-if="hasAnyUnvoted">
<button :disabled="!hasAnySelection" @click="submitAllVotes">
{{ $t('falukant.politics.elections.voteAll') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue';
import SimpleTabs from '@/components/SimpleTabs.vue';
import Multiselect from 'vue-multiselect';
import apiClient from '@/utils/axios.js';
import { showApiError, showSuccess } from '@/utils/feedback.js';
const debugLog = () => {};
export default {
name: 'PoliticsView',
components: { StatusBar, SimpleTabs, Multiselect },
data() {
return {
activeTab: 'current',
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: {},
selectedApplications: [],
ownCharacterId: null,
myPowers: null,
taxJurisdiction: null,
taxDraft: {},
appointable: [],
selectedAppointKey: '',
appointSearch: '',
appointSearchResults: [],
selectedAppointTarget: null,
_appointSearchTimer: null,
loading: {
current: false,
offices: false,
openPolitics: false,
elections: false,
powers: false
}
};
},
computed: {
hasAnyPowers() {
const p = this.myPowers;
if (!p) return false;
return (
(p.freeLoverSlots > 0) ||
(p.reputationPeriodic && p.reputationPeriodic.length) ||
(this.taxJurisdiction?.regions?.length > 0) ||
(this.appointable.length > 0)
);
},
canSubmitAppointment() {
return Boolean(this.selectedAppointKey && this.selectedAppointTarget?.characterId);
},
hasAnySelection() {
return Object.values(this.selectedCandidates)
.some(arr => Array.isArray(arr) && arr.length > 0);
},
hasAnyUnvoted() {
return this.elections.some(e => !e.voted);
}
},
mounted() {
this.loadOwnCharacterId();
this.loadCurrentPositions();
},
methods: {
politicsBenefitItems(pos) {
const raw = pos?.benefit;
if (!Array.isArray(raw) || !raw.length) {
return [];
}
if (raw.every((x) => typeof x === 'string') && raw.includes('*')) {
return [this.$t('falukant.politics.current.benefit_all')];
}
return raw;
},
formatPoliticsRegionLevel(key) {
const k = String(key || '');
const path = `falukant.politics.regionLevels.${k}`;
const t = this.$t(path);
return t !== path ? t : k;
},
formatPoliticsAppointOfficeLabels(officeTrs) {
if (!Array.isArray(officeTrs) || !officeTrs.length) {
return '';
}
return officeTrs
.map((tr) => {
const path = `falukant.politics.offices.${tr}`;
const t = this.$t(path);
return t !== path ? t : String(tr);
})
.join(', ');
},
formatPoliticsBenefitItem(b) {
if (b == null) {
return '';
}
if (typeof b === 'string') {
return b;
}
if (typeof b === 'object' && b.tr) {
if (b.tr === 'tax_exemption' && b.params?.all) {
return this.$t('falukant.politics.benefits.tax_exemption_all');
}
if (b.tr === 'tax_exemption' && Array.isArray(b.params?.regions)) {
const labels = b.params.regions.map((r) => this.formatPoliticsRegionLevel(r)).join(', ');
return this.$t('falukant.politics.benefits.tax_exemption', { regions: labels });
}
if (b.tr === 'daily_salary') {
return this.$t('falukant.politics.benefits.daily_salary', {
amount: this.formatPoliticsMoney(b.params?.amount)
});
}
if (b.tr === 'reputation_periodic') {
return this.$t('falukant.politics.benefits.reputation_periodic', {
days: b.params?.intervalDays ?? '—',
gain: b.params?.gain ?? '—'
});
}
if (b.tr === 'appoint_politicians' && Array.isArray(b.params?.officeTrs)) {
const labels = this.formatPoliticsAppointOfficeLabels(b.params.officeTrs);
return this.$t('falukant.politics.benefits.appoint_politicians', { offices: labels });
}
if (b.tr === 'set_regional_tax') {
const sk = String(b.params?.scope || 'local');
const scopePath = `falukant.politics.benefits.tax_scope.${sk}`;
const st = this.$t(scopePath);
const scopeLabel = st !== scopePath ? st : sk;
return this.$t('falukant.politics.benefits.set_regional_tax', { scope: scopeLabel });
}
if (b.tr === 'free_lover_slots') {
return this.$t('falukant.politics.benefits.free_lover_slots', { count: b.params?.count ?? 1 });
}
if (b.tr === 'guard_protection') {
return this.$t('falukant.politics.benefits.guard_protection');
}
if (b.tr === 'court_immunity') {
return this.$t('falukant.politics.benefits.court_immunity');
}
if (b.tr === 'generic_benefit') {
return this.$t('falukant.politics.benefits.generic', { code: b.params?.code || '' });
}
}
return String(b);
},
onTabChange(tab) {
if (tab === 'current') {
this.loadCurrentPositions();
}
if (tab === 'powers') {
this.loadPowers();
}
if (tab === 'offices') {
this.loadPoliticalOffices();
}
if (tab === 'openPolitics') {
this.loadOpenPolitics();
}
if (tab === 'elections') {
this.loadElections();
}
},
async loadPowers() {
this.loading.powers = true;
try {
const [mp, tj, ap] = await Promise.all([
apiClient.get('/api/falukant/politics/my-powers'),
apiClient.get('/api/falukant/politics/tax-jurisdiction'),
apiClient.get('/api/falukant/politics/appointable-offices')
]);
this.myPowers = mp.data;
this.taxJurisdiction = tj.data;
this.appointable = Array.isArray(ap.data) ? ap.data : [];
const draft = {};
(this.taxJurisdiction?.regions || []).forEach((r) => {
draft[r.id] = r.taxPercent;
});
this.taxDraft = draft;
} catch (err) {
console.error('[PoliticsView] loadPowers', err);
showApiError(this, err, this.$t('falukant.politics.powers.loadError'));
} finally {
this.loading.powers = false;
}
},
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}`;
},
parseAppointKey(key) {
const [officeTypeId, regionId] = String(key).split(':').map((x) => parseInt(x, 10));
if (Number.isNaN(officeTypeId) || Number.isNaN(regionId)) return null;
return { officeTypeId, regionId };
},
async saveRegionTax(regionId) {
const percent = this.taxDraft[regionId];
try {
await apiClient.put(`/api/falukant/politics/region/${regionId}/tax`, { percent });
showSuccess(this, this.$t('falukant.politics.powers.taxSaved'));
await this.loadPowers();
} catch (err) {
showApiError(this, err, this.$t('falukant.politics.powers.taxError'));
}
},
debouncedAppointSearch() {
if (this._appointSearchTimer) clearTimeout(this._appointSearchTimer);
this._appointSearchTimer = setTimeout(() => this.runAppointSearch(), 350);
},
async runAppointSearch() {
const q = (this.appointSearch || '').trim();
if (q.length < 2) {
this.appointSearchResults = [];
return;
}
try {
const { data } = await apiClient.get('/api/falukant/users/search', { params: { q } });
this.appointSearchResults = Array.isArray(data) ? data : [];
} catch (err) {
this.appointSearchResults = [];
}
},
selectAppointTarget(u) {
this.selectedAppointTarget = u;
},
async submitAppointment() {
const parsed = this.parseAppointKey(this.selectedAppointKey);
if (!parsed || !this.selectedAppointTarget?.characterId) return;
try {
await apiClient.post('/api/falukant/politics/appointments', {
targetCharacterId: this.selectedAppointTarget.characterId,
officeTypeId: parsed.officeTypeId,
regionId: parsed.regionId
});
showSuccess(this, this.$t('falukant.politics.powers.appointSuccess'));
this.selectedAppointKey = '';
this.selectedAppointTarget = null;
this.appointSearch = '';
this.appointSearchResults = [];
await this.loadPowers();
await this.loadCurrentPositions();
} catch (err) {
showApiError(this, err, this.$t('falukant.politics.powers.appointError'));
}
},
async loadCurrentPositions() {
this.loading.current = true;
try {
const { data } = await apiClient.get('/api/falukant/politics/overview');
debugLog('[PoliticsView] loadCurrentPositions - API response:', data);
debugLog('[PoliticsView] loadCurrentPositions - ownCharacterId at load time:', this.ownCharacterId);
this.currentPositions = data;
debugLog('[PoliticsView] loadCurrentPositions - Loaded', data.length, 'positions');
} catch (err) {
console.error('[PoliticsView] Error loading current positions', err);
} finally {
this.loading.current = false;
}
},
async loadOpenPolitics() {
this.loading.openPolitics = true;
try {
const { data } = await apiClient.get('/api/falukant/politics/open');
this.openPolitics = Array.isArray(data) ? data : [];
// Bereits beworbene Positionen vorselektieren, damit die Checkbox
// sichtbar markiert bleibt.
this.selectedApplications = this.openPolitics
.filter(e => e.alreadyApplied)
.map(e => e.id);
} catch (err) {
console.error('Error loading open politics', err);
} finally {
this.loading.openPolitics = false;
}
},
async loadElections() {
this.loading.elections = true;
try {
const { data } = await apiClient.get('/api/falukant/politics/elections');
this.elections = data;
data.forEach(e => {
this.selectedCandidates[e.id] = [];
});
} catch (err) {
console.error('Error loading elections', err);
} finally {
this.loading.elections = false;
}
},
candidateLabel(option) {
const title = this.$t(`falukant.titles.${option.gender}.${option.title}`);
return `${title} ${option.name} (${option.age})`;
},
findCandidateById(election, candidateId) {
return election.candidates.find(c => c.id === candidateId) || {};
},
formatCandidateTitle(candidate) {
if (!candidate) return '';
return this.$t(`falukant.titles.${candidate.gender}.${candidate.title}`);
},
async submitVote(electionId) {
const singlePayload = [
{
electionId: electionId,
candidateIds: this.selectedCandidates[electionId].map(c => c.id)
}
];
try {
await apiClient.post(
'/api/falukant/politics/elections',
{ votes: singlePayload }
);
await this.loadElections();
showSuccess(this, this.$t('falukant.politics.voteSuccess'));
} catch (err) {
console.error(`Error submitting vote for election ${electionId}`, err);
showApiError(this, err, this.$t('falukant.politics.voteError'));
}
},
async submitAllVotes() {
const payload = Object.entries(this.selectedCandidates)
.filter(([eid, arr]) => Array.isArray(arr) && arr.length > 0)
.map(([eid, arr]) => ({
electionId: parseInt(eid, 10),
candidateIds: arr.map(c => c.id)
}));
try {
await apiClient.post(
'/api/falukant/politics/elections',
{ votes: payload }
);
await this.loadElections();
showSuccess(this, this.$t('falukant.politics.voteAllSuccess'));
} catch (err) {
console.error('Error submitting all votes', err);
showApiError(this, err, this.$t('falukant.politics.voteAllError'));
}
},
formatDate(ts) {
return new Date(ts).toLocaleDateString(this.$i18n.locale, {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
},
/** Geldbetrag wie in Familie/Bank (exakter Wert, kein „ca.“). */
formatPoliticsMoney(value) {
const n = Number(value);
if (!Number.isFinite(n)) {
return '—';
}
return new Intl.NumberFormat(this.$i18n.locale, {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(n);
},
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('[PoliticsView] Error loading own character ID', err);
}
},
isOwnPosition(pos) {
if (!this.ownCharacterId || !pos.character) {
return false;
}
return pos.character.id === this.ownCharacterId;
},
async submitApplications() {
try {
const response = await apiClient.post(
'/api/falukant/politics/open',
{ electionIds: this.selectedApplications }
);
// Speichere die IDs der erfolgreich beworbenen Positionen
const appliedIds = response.data?.applied || [];
// Lade die Daten neu
await this.loadOpenPolitics();
// Stelle sicher, dass alle bereits beworbenen Positionen (inkl. der gerade beworbenen) vorselektiert bleiben
this.selectedApplications = this.openPolitics
.filter(e => e.alreadyApplied || appliedIds.includes(e.id))
.map(e => e.id);
showSuccess(this, this.$t('falukant.politics.applyBookmarkSuccess'));
} catch (err) {
console.error('Error submitting applications', err);
if (err?.response?.data?.error === 'too_young') {
showApiError(this, err, this.$t('falukant.politics.too_young'));
return;
}
showApiError(this, err, this.$t('falukant.politics.applyError'));
}
}
}
};
</script>
<style scoped>
.politics-view {
max-width: var(--content-max-width);
margin: 0 auto;
padding-bottom: 24px;
}
.simple-tabs {
margin-bottom: 16px;
}
.tab-content {
display: flex;
flex-direction: column;
}
.tab-pane {
display: flex;
flex-direction: column;
}
.politics-age-requirement {
margin: 0 0 10px 0;
font-size: 0.95em;
color: var(--color-text-secondary);
}
.politics-card-list {
display: grid;
gap: 12px;
}
.politics-card {
display: flex;
flex-direction: column;
gap: 12px;
padding: 18px;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: rgba(255, 255, 255, 0.72);
}
.politics-card.own-position {
border-color: rgba(120, 195, 138, 0.45);
background: rgba(236, 248, 238, 0.92);
}
.politics-card__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.politics-card__meta {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
color: var(--color-text-secondary);
}
.politics-card__meta-row {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
width: 100%;
}
.politics-card__meta-label {
font-weight: 600;
color: var(--color-text-muted, var(--color-text-secondary));
}
.politics-card__meta-value {
line-height: 1.45;
}
.politics-benefit-list {
margin: 0;
padding-left: 1.25rem;
line-height: 1.5;
list-style: disc;
}
.politics-benefit-list li {
margin: 0.2em 0;
padding-inline-start: 0.15em;
}
.politics-card__meta-row--benefits .politics-benefit-list {
width: 100%;
max-width: 100%;
}
.politics-card__checkbox {
display: inline-flex;
align-items: center;
gap: 10px;
width: fit-content;
}
.politics-card__vote {
display: grid;
gap: 12px;
}
.loading {
text-align: center;
font-style: italic;
margin: 20px 0;
}
.voted-list {
list-style: none;
margin: 0;
padding: 0;
}
.all-vote-button {
padding: 10px 0;
text-align: right;
}
.all-vote-button button {
padding: 6px 12px;
cursor: pointer;
}
@media (max-width: 900px) {
.politics-card__header {
flex-direction: column;
align-items: flex-start;
}
}
.powers-tab .powers-section {
margin-bottom: 1.5rem;
padding: 1rem 1.1rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: rgba(255, 255, 255, 0.75);
}
.powers-tab h3 {
margin: 0 0 0.5rem 0;
font-size: 1.05rem;
}
.powers-hint,
.politics-powers-empty {
color: var(--color-text-secondary);
font-size: 0.95rem;
}
.powers-list {
margin: 0;
padding-left: 1.2rem;
}
.powers-tax-row {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 10px 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.powers-tax-edit {
display: flex;
align-items: center;
gap: 8px;
}
.powers-tax-edit input {
width: 5.5rem;
padding: 4px 8px;
}
.powers-muted {
margin-left: 6px;
color: var(--color-text-secondary);
font-size: 0.9rem;
}
.powers-label {
display: block;
margin: 10px 0 4px;
font-size: 0.92rem;
}
.powers-input,
.powers-select {
width: 100%;
max-width: 28rem;
padding: 6px 10px;
}
.powers-search-results {
list-style: none;
margin: 8px 0 0;
padding: 0;
}
.powers-linkish {
background: none;
border: none;
padding: 4px 0;
cursor: pointer;
text-align: left;
color: var(--color-primary, #6b4a2a);
text-decoration: underline;
}
.powers-selected-target {
margin: 10px 0 0;
font-size: 0.95rem;
}
.powers-submit {
margin-top: 12px;
padding: 6px 14px;
cursor: pointer;
}
</style>