Files
yourpart3/frontend/src/views/falukant/PoliticsView.vue

461 lines
17 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 />
<h2>{{ $t('falukant.politics.title') }}</h2>
<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 class="table-scroll">
<table class="politics-table">
<thead>
<tr>
<th>{{ $t('falukant.politics.current.office') }}</th>
<th>{{ $t('falukant.politics.current.region') }}</th>
<th>{{ $t('falukant.politics.current.holder') }}</th>
<th>{{ $t('falukant.politics.current.benefit') }}</th>
<th>{{ $t('falukant.politics.current.termEnds') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="pos in currentPositions" :key="pos.id" :class="{ 'own-position': isOwnPosition(pos) }">
<td>{{ $t(`falukant.politics.offices.${pos.officeType.name}`) }}</td>
<td>{{ pos.region.name }}</td>
<td>
<span v-if="pos.character">
{{ pos.character.definedFirstName.name }}
{{ pos.character.definedLastName.name }}
</span>
<span v-else></span>
</td>
<td>
<span v-if="pos.benefit && pos.benefit.length">
<span v-if="pos.benefit.includes('*')">{{ $t('falukant.politics.current.benefit_all') }}</span>
<span v-else>{{ pos.benefit.join(', ') }}</span>
</span>
<span v-else></span>
</td>
<td>
<span v-if="pos.termEnds">
{{ formatDate(pos.termEnds) }}
</span>
<span v-else></span>
</td>
</tr>
<tr v-if="!currentPositions.length">
<td colspan="4">{{ $t('falukant.politics.current.none') }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- OPEN Tab: hier zeigen wir 'openPolitics' -->
<div v-else-if="activeTab === 'openPolitics'" class="tab-pane">
<div v-if="loading.openPolitics" class="loading">{{ $t('loading') }}</div>
<div v-else class="table-scroll">
<table class="politics-table">
<thead>
<tr>
<th>{{ $t('falukant.politics.open.office') }}</th>
<th>{{ $t('falukant.politics.open.region') }}</th>
<th>{{ $t('falukant.politics.open.date') }}</th>
<th>{{ $t('falukant.politics.open.candidacy') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="e in openPolitics" :key="e.id">
<td>{{ $t(`falukant.politics.offices.${e.officeType.name}`) }}</td>
<td>{{ e.region.name }}</td>
<td>{{ formatDate(e.date) }}</td>
<!-- Checkbox ganz am Ende -->
<td>
<input
type="checkbox"
:id="`apply-${e.id}`"
v-model="selectedApplications"
:value="e.id"
:disabled="e.alreadyApplied"
/>
</td>
</tr>
<tr v-if="!openPolitics.length">
<td colspan="4">{{ $t('falukant.politics.open.none') }}</td>
</tr>
</tbody>
</table>
</div>
<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 class="table-scroll">
<table class="politics-table">
<thead>
<tr>
<th>{{ $t('falukant.politics.elections.office') }}</th>
<th>{{ $t('falukant.politics.elections.region') }}</th>
<th>{{ $t('falukant.politics.elections.date') }}</th>
<th>{{ $t('falukant.politics.elections.posts') }}</th>
<th>{{ $t('falukant.politics.elections.candidates') }}</th>
<th>{{ $t('falukant.politics.elections.action') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="e in elections" :key="e.id">
<td>{{ $t(`falukant.politics.offices.${e.officeType.name}`) }}</td>
<td>{{ e.region.name }}</td>
<td>{{ formatDate(e.date) }}</td>
<td>{{ e.postsToFill }}</td>
<td v-if="!e.voted">
<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>
</td>
<td v-else>
<ul 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>
</td>
<td>
<button v-if="!e.voted"
:disabled="!selectedCandidates[e.id] || !selectedCandidates[e.id].length"
@click="submitVote(e.id)">
{{ $t('falukant.politics.elections.vote') }}
</button>
</td>
</tr>
<tr v-if="!elections.length">
<td colspan="6">{{ $t('falukant.politics.elections.none') }}</td>
</tr>
</tbody>
</table>
</div>
<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';
export default {
name: 'PoliticsView',
components: { StatusBar, SimpleTabs, Multiselect },
data() {
return {
activeTab: 'current',
tabs: [
{ value: 'current', label: 'falukant.politics.tabs.current' },
{ value: 'openPolitics', label: 'falukant.politics.tabs.upcoming' },
{ value: 'elections', label: 'falukant.politics.tabs.elections' }
],
currentPositions: [],
openPolitics: [],
elections: [],
selectedCandidates: {},
selectedApplications: [],
ownCharacterId: null,
loading: {
current: false,
openPolitics: false,
elections: false
}
};
},
computed: {
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: {
onTabChange(tab) {
if (tab === 'current') {
this.loadCurrentPositions();
}
if (tab === 'openPolitics') {
this.loadOpenPolitics();
}
if (tab === 'elections') {
this.loadElections();
}
},
async loadCurrentPositions() {
this.loading.current = true;
try {
const { data } = await apiClient.get('/api/falukant/politics/overview');
this.currentPositions = data;
} catch (err) {
console.error('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 = data;
// Bereits beworbene Positionen vorselektieren, damit die Checkbox
// sichtbar markiert bleibt.
this.selectedApplications = data
.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();
} catch (err) {
console.error(`Error submitting vote for election ${electionId}`, err);
}
},
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();
} catch (err) {
console.error('Error submitting all votes', err);
}
},
formatDate(ts) {
return new Date(ts).toLocaleDateString(this.$i18n.locale, {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
},
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 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);
} catch (err) {
console.error('Error submitting applications', err);
}
}
}
};
</script>
<style scoped>
.politics-view {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
h2 {
margin: 0;
padding: 20px 0 0 0;
flex: 0 0 auto;
}
.simple-tabs {
flex: 0 0 auto;
}
.tab-content {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: hidden;
}
.tab-pane {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.table-scroll {
flex: 1;
overflow-y: auto;
border: 1px solid #ddd;
}
.politics-table {
border-collapse: collapse;
width: auto;
/* kein 100% */
}
.politics-table thead th {
position: sticky;
top: 0;
background: #FFF;
z-index: 1;
padding: 8px;
border: 1px solid #ddd;
text-align: left;
}
.politics-table tbody td {
padding: 8px;
border: 1px solid #ddd;
}
.politics-table tbody tr.own-position {
background-color: #e0e0e0;
font-weight: bold;
}
.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;
margin: 2em;
}
</style>