Files
yourpart3/frontend/src/views/falukant/PoliticsView.vue
Torsten Schulz (local) 4510aa3d14 Implement politics overview feature in FalukantService and update UI
- Added a new method `getPoliticsOverview` in FalukantService to retrieve currently held offices, including office holders and term end dates.
- Enhanced the PoliticsView component to display the term end dates for current offices.
- Updated localization files to include a new message for applying to selected positions.
- Improved the handling of already applied positions in the open politics section, pre-selecting checkboxes accordingly.
2025-11-24 11:50:21 +01:00

421 lines
16 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.termEnds') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="pos in currentPositions" :key="pos.id">
<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.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: [],
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.loadCurrentPositions();
},
methods: {
onTabChange(tab) {
if (tab === 'current' && !this.currentPositions.length) {
this.loadCurrentPositions();
}
if (tab === 'openPolitics' && !this.openPolitics.length) {
this.loadOpenPolitics();
}
if (tab === 'elections' && !this.elections.length) {
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 submitApplications() {
try {
await apiClient.post(
'/api/falukant/politics/open',
{ electionIds: this.selectedApplications }
);
await this.loadOpenPolitics();
} 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;
}
.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>