Refactor feedback handling across components: Replace alert and confirm calls with centralized feedback functions for improved user experience. Update various components to utilize showError, showSuccess, and confirmAction for consistent messaging and confirmation dialogs. Enhance UI responsiveness and maintainability by streamlining feedback logic.
This commit is contained in:
@@ -2,7 +2,13 @@
|
||||
<div class="politics-view">
|
||||
<StatusBar />
|
||||
|
||||
<h2>{{ $t('falukant.politics.title') }}</h2>
|
||||
<section class="politics-hero surface-card">
|
||||
<div>
|
||||
<span class="politics-kicker">Falukant</span>
|
||||
<h2>{{ $t('falukant.politics.title') }}</h2>
|
||||
<p>Ämter, Kandidaturen und Wahlen als klare Aufgabenfläche statt als reine Verwaltungstabelle.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<SimpleTabs v-model="activeTab" :tabs="tabs" @change="onTabChange" />
|
||||
|
||||
@@ -11,86 +17,65 @@
|
||||
<!-- 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 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">
|
||||
<span>
|
||||
{{ $t('falukant.politics.current.holder') }}:
|
||||
<template v-if="pos.character">
|
||||
{{ pos.character.definedFirstName.name }} {{ pos.character.definedLastName.name }}
|
||||
</template>
|
||||
<template v-else>—</template>
|
||||
</span>
|
||||
<span>
|
||||
{{ $t('falukant.politics.current.benefit') }}:
|
||||
<template 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>
|
||||
</template>
|
||||
<template v-else>—</template>
|
||||
</span>
|
||||
<span>
|
||||
{{ $t('falukant.politics.current.termEnds') }}:
|
||||
<template v-if="pos.termEnds">{{ formatDate(pos.termEnds) }}</template>
|
||||
<template v-else>—</template>
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<p v-else class="loading">{{ $t('falukant.politics.current.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 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.candidacyWithAge') }}</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 :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"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!openPolitics.length">
|
||||
<td colspan="4">{{ $t('falukant.politics.open.none') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<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>Für diese Kandidatur vormerken</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">
|
||||
@@ -102,65 +87,47 @@
|
||||
<!-- 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 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">
|
||||
@@ -177,6 +144,9 @@ 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',
|
||||
@@ -232,10 +202,10 @@ export default {
|
||||
this.loading.current = true;
|
||||
try {
|
||||
const { data } = await apiClient.get('/api/falukant/politics/overview');
|
||||
console.log('[PoliticsView] loadCurrentPositions - API response:', data);
|
||||
console.log('[PoliticsView] loadCurrentPositions - ownCharacterId at load time:', this.ownCharacterId);
|
||||
debugLog('[PoliticsView] loadCurrentPositions - API response:', data);
|
||||
debugLog('[PoliticsView] loadCurrentPositions - ownCharacterId at load time:', this.ownCharacterId);
|
||||
this.currentPositions = data;
|
||||
console.log('[PoliticsView] loadCurrentPositions - Loaded', data.length, 'positions');
|
||||
debugLog('[PoliticsView] loadCurrentPositions - Loaded', data.length, 'positions');
|
||||
} catch (err) {
|
||||
console.error('[PoliticsView] Error loading current positions', err);
|
||||
} finally {
|
||||
@@ -304,8 +274,10 @@ export default {
|
||||
{ votes: singlePayload }
|
||||
);
|
||||
await this.loadElections();
|
||||
showSuccess(this, 'Stimme erfolgreich abgegeben.');
|
||||
} catch (err) {
|
||||
console.error(`Error submitting vote for election ${electionId}`, err);
|
||||
showApiError(this, err, 'Fehler beim Abgeben der Stimme');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -323,8 +295,10 @@ export default {
|
||||
{ votes: payload }
|
||||
);
|
||||
await this.loadElections();
|
||||
showSuccess(this, 'Alle Stimmen erfolgreich abgegeben.');
|
||||
} catch (err) {
|
||||
console.error('Error submitting all votes', err);
|
||||
showApiError(this, err, 'Fehler beim Abgeben der Stimmen');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -339,18 +313,8 @@ export default {
|
||||
async loadOwnCharacterId() {
|
||||
try {
|
||||
const { data } = await apiClient.get('/api/falukant/info');
|
||||
console.log('[PoliticsView] loadOwnCharacterId - API response:', data);
|
||||
console.log('[PoliticsView] loadOwnCharacterId - data.character:', data.character);
|
||||
console.log('[PoliticsView] loadOwnCharacterId - data.character?.id:', data.character?.id);
|
||||
if (data.character && data.character.id) {
|
||||
this.ownCharacterId = data.character.id;
|
||||
console.log('[PoliticsView] loadOwnCharacterId - Set ownCharacterId to:', this.ownCharacterId);
|
||||
} else {
|
||||
console.warn('[PoliticsView] loadOwnCharacterId - No character ID found in response', {
|
||||
hasCharacter: !!data.character,
|
||||
characterKeys: data.character ? Object.keys(data.character) : null,
|
||||
characterId: data.character?.id
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[PoliticsView] Error loading own character ID', err);
|
||||
@@ -358,20 +322,10 @@ export default {
|
||||
},
|
||||
|
||||
isOwnPosition(pos) {
|
||||
console.log('[PoliticsView] isOwnPosition - Checking position:', {
|
||||
posId: pos.id,
|
||||
posCharacter: pos.character,
|
||||
posCharacterId: pos.character?.id,
|
||||
ownCharacterId: this.ownCharacterId,
|
||||
match: pos.character?.id === this.ownCharacterId
|
||||
});
|
||||
if (!this.ownCharacterId || !pos.character) {
|
||||
console.log('[PoliticsView] isOwnPosition - Returning false (missing ownCharacterId or pos.character)');
|
||||
return false;
|
||||
}
|
||||
const isMatch = pos.character.id === this.ownCharacterId;
|
||||
console.log('[PoliticsView] isOwnPosition - Result:', isMatch);
|
||||
return isMatch;
|
||||
return pos.character.id === this.ownCharacterId;
|
||||
},
|
||||
|
||||
async submitApplications() {
|
||||
@@ -388,12 +342,14 @@ export default {
|
||||
this.selectedApplications = this.openPolitics
|
||||
.filter(e => e.alreadyApplied || appliedIds.includes(e.id))
|
||||
.map(e => e.id);
|
||||
showSuccess(this, 'Kandidatur erfolgreich vorgemerkt.');
|
||||
} catch (err) {
|
||||
console.error('Error submitting applications', err);
|
||||
const msg = err?.response?.data?.error === 'too_young'
|
||||
? this.$t('falukant.politics.too_young')
|
||||
: (err?.response?.data?.error || err?.message || this.$t('falukant.politics.applyError'));
|
||||
this.$root.$refs?.messageDialog?.open?.(msg, this.$t('falukant.politics.title'));
|
||||
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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,73 +358,99 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.politics-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
.politics-hero {
|
||||
padding: 24px 26px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.politics-kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(120, 195, 138, 0.14);
|
||||
color: #42634e;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.politics-hero p {
|
||||
margin: 0;
|
||||
padding: 20px 0 0 0;
|
||||
flex: 0 0 auto;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.simple-tabs {
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.politics-age-requirement {
|
||||
flex: 0 0 auto;
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 0.95em;
|
||||
color: #555;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.table-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
.politics-card-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.politics-table {
|
||||
border-collapse: collapse;
|
||||
width: auto;
|
||||
/* kein 100% */
|
||||
.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-table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #FFF;
|
||||
z-index: 1;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
text-align: left;
|
||||
.politics-card.own-position {
|
||||
border-color: rgba(120, 195, 138, 0.45);
|
||||
background: rgba(236, 248, 238, 0.92);
|
||||
}
|
||||
|
||||
.politics-table tbody td {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
.politics-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.politics-table tbody tr.own-position {
|
||||
background-color: #e0e0e0;
|
||||
font-weight: bold;
|
||||
.politics-card__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 16px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.politics-card__checkbox {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.politics-card__vote {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
@@ -491,6 +473,13 @@ h2 {
|
||||
.all-vote-button button {
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
margin: 2em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.politics-card__header,
|
||||
.politics-card__meta {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user