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:
Torsten Schulz (local)
2026-03-19 16:18:51 +01:00
parent 2c58ef37c4
commit 1774d7df88
35 changed files with 1097 additions and 1017 deletions

View File

@@ -1,7 +1,7 @@
<template>
<div class="contenthidden">
<div class="bank-view">
<StatusBar />
<div class="contentscroll">
<div class="bank-content">
<h2>{{ $t('falukant.bank.title') }}</h2>
<SimpleTabs v-model="activeTab" :tabs="tabs" />

View File

@@ -1,8 +1,7 @@
<template>
<div class="contenthidden">
<div class="falukant-branch-view">
<StatusBar ref="statusBar" />
<div class="contentscroll">
<div class="falukant-branch">
<div class="falukant-branch">
<section class="branch-hero surface-card">
<div>
<span class="branch-kicker">Niederlassung</span>
@@ -315,7 +314,6 @@
</div>
</div>
</div>
</div>
</div>
</div>
</template>
@@ -332,6 +330,7 @@ import RevenueSection from '@/components/falukant/RevenueSection.vue';
import BuyVehicleDialog from '@/dialogues/falukant/BuyVehicleDialog.vue';
import apiClient from '@/utils/axios.js';
import { mapState } from 'vuex';
import { showError, showSuccess, showApiError } from '@/utils/feedback.js';
export default {
name: "BranchView",
@@ -657,7 +656,7 @@ export default {
}
} catch (error) {
console.error('Error upgrading branch:', error);
alert(this.$t('falukant.branch.actions.upgradeAlert', { branchId: this.selectedBranch.id }));
showError(this, this.$t('falukant.branch.actions.upgradeAlert', { branchId: this.selectedBranch.id }));
}
},
@@ -956,7 +955,7 @@ export default {
async sendVehicles() {
if (!this.sendVehicleDialog.targetBranchId) {
alert(this.$t('falukant.branch.transport.selectTargetError'));
showError(this, this.$t('falukant.branch.transport.selectTargetError'));
return;
}
@@ -975,7 +974,7 @@ export default {
} else if (this.sendVehicleDialog.vehicleTypeId) {
payload.vehicleTypeId = this.sendVehicleDialog.vehicleTypeId;
} else {
alert(this.$t('falukant.branch.transport.noVehiclesSelected'));
showError(this, this.$t('falukant.branch.transport.noVehiclesSelected'));
return;
}
@@ -984,7 +983,7 @@ export default {
this.sendVehicleDialog.success = true;
} catch (error) {
console.error('Error sending vehicles:', error);
alert(this.$t('falukant.branch.transport.sendError'));
showApiError(this, error, this.$t('falukant.branch.transport.sendError'));
}
},
@@ -1043,12 +1042,12 @@ export default {
});
await this.loadVehicles();
this.closeRepairAllVehiclesDialog();
alert(this.$t('falukant.branch.transport.repairAllSuccess'));
showSuccess(this, this.$t('falukant.branch.transport.repairAllSuccess'));
this.$refs.statusBar?.fetchStatus();
} catch (error) {
console.error('Error repairing all vehicles:', error);
const errorMessage = error.response?.data?.message || this.$t('falukant.branch.transport.repairAllError');
alert(errorMessage);
showError(this, errorMessage);
}
},
@@ -1105,12 +1104,12 @@ export default {
await apiClient.post(`/api/falukant/vehicles/${this.repairVehicleDialog.vehicle.id}/repair`);
await this.loadVehicles();
this.closeRepairVehicleDialog();
alert(this.$t('falukant.branch.transport.repairSuccess'));
showSuccess(this, this.$t('falukant.branch.transport.repairSuccess'));
this.$refs.statusBar?.fetchStatus();
} catch (error) {
console.error('Error repairing vehicle:', error);
const errorMessage = error.response?.data?.message || this.$t('falukant.branch.transport.repairError');
alert(errorMessage);
showError(this, errorMessage);
}
},
},

View File

@@ -1,7 +1,7 @@
<template>
<div class="contenthidden">
<div class="church-view">
<StatusBar />
<div class="contentscroll">
<div class="church-content">
<h2>{{ $t('falukant.church.title') }}</h2>
<SimpleTabs v-model="activeTab" :tabs="tabs" />
@@ -450,4 +450,4 @@ th {
.reject-button:hover {
background-color: #c82333;
}
</style>
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="contenthidden">
<div class="education-view">
<StatusBar />
<div class="contentscroll">
<div class="education-content">
<h2>{{ $t('falukant.education.title') }}</h2>
<SimpleTabs v-model="activeTab" :tabs="tabs" />

View File

@@ -1,7 +1,7 @@
<template>
<div class="contenthidden">
<div class="family-view">
<StatusBar />
<div class="contentscroll family-layout">
<div class="family-layout">
<div class="family-content">
<section class="family-hero surface-card">
<div>
@@ -189,8 +189,7 @@
<p>{{ $t('falukant.family.lovers.none') }}</p>
</div>
</div>
</div>
</div>
</div>
<ChildDetailsDialog ref="childDetailsDialog" />
</div>
@@ -204,6 +203,7 @@ import ChildDetailsDialog from '@/dialogues/falukant/ChildDetailsDialog.vue'
import Character3D from '@/components/Character3D.vue'
import apiClient from '@/utils/axios.js'
import { confirmAction } from '@/utils/feedback.js'
import { mapState } from 'vuex'
const WOOING_PROGRESS_TARGET = 70
@@ -350,7 +350,10 @@ export default {
},
async cancelWooing() {
const confirmed = window.confirm(this.$t('falukant.family.spouse.wooing.cancelConfirm'));
const confirmed = await confirmAction(this, {
title: 'Werbung abbrechen',
message: this.$t('falukant.family.spouse.wooing.cancelConfirm')
});
if (!confirmed) return;
try {
await apiClient.post('/api/falukant/family/cancel-wooing');

View File

@@ -4,40 +4,33 @@
<h2>{{ $t('falukant.house.title') }}</h2>
<div class="existing-house">
<div :style="houseType ? houseStyle(houseType.position, 341) : {}" class="house"></div>
<div class="status-panel">
<div class="status-panel surface-card">
<h3>{{ $t('falukant.house.statusreport') }}</h3>
<table>
<thead>
<tr>
<th>{{ $t('falukant.house.element') }}</th>
<th>{{ $t('falukant.house.state') }}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="(value, key) in status" :key="key">
<td>{{ $t(`falukant.house.status.${key}`) }}</td>
<td>{{ conditionLabel(value) }}</td>
<td>
<button v-if="value < 100" @click="renovate(key)">
{{ $t('falukant.house.renovate') }} ({{ getRenovationCost(key, value) }})
</button>
</td>
</tr>
<tr>
<td>{{ $t('falukant.house.worth') }}</td>
<td>{{ getWorth() }} {{ currency }}</td>
<td>
<button @click="renovateAll" :disabled="allRenovated">
{{ $t('falukant.house.renovateAll') }} ({{ getAllRenovationCost() }})
</button>
<button @click="sellHouse">
{{ $t('falukant.house.sell') }}
</button>
</td>
</tr>
</tbody>
</table>
<div class="status-cards">
<article v-for="(value, key) in status" :key="key" class="status-card">
<div>
<span class="status-card__label">{{ $t(`falukant.house.status.${key}`) }}</span>
<strong>{{ conditionLabel(value) }}</strong>
</div>
<button v-if="value < 100" @click="renovate(key)">
{{ $t('falukant.house.renovate') }} ({{ getRenovationCost(key, value) }})
</button>
</article>
<article class="status-card status-card--summary">
<div>
<span class="status-card__label">{{ $t('falukant.house.worth') }}</span>
<strong>{{ getWorth() }} {{ currency }}</strong>
</div>
<div class="status-card__actions">
<button @click="renovateAll" :disabled="allRenovated">
{{ $t('falukant.house.renovateAll') }} ({{ getAllRenovationCost() }})
</button>
<button class="button-secondary" @click="sellHouse">
{{ $t('falukant.house.sell') }}
</button>
</div>
</article>
</div>
</div>
</div>
@@ -49,16 +42,15 @@
class="house-preview"></div>
<div class="house-info">
<h4>{{ $t(`falukant.house.type.${house.houseType.labelTr}`) }}</h4>
<table>
<tbody>
<tr v-for="(val, prop) in house" :key="prop"
v-if="['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition'].includes(prop)">
<td>{{ $t(`falukant.house.status.${prop}`) }}</td>
<td>{{ conditionLabel(val) }}</td>
</tr>
</tbody>
</table>
<div>
<div class="buyable-house-stats">
<div v-for="(val, prop) in house" :key="prop"
v-if="['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition'].includes(prop)"
class="buyable-house-stat">
<span>{{ $t(`falukant.house.status.${prop}`) }}</span>
<strong>{{ conditionLabel(val) }}</strong>
</div>
</div>
<div class="buyable-house-price">
{{ $t('falukant.house.price') }}: {{ buyCost(house) }}
</div>
<button @click="buyHouse(house.id)">
@@ -263,6 +255,7 @@ h2 {
.status-panel {
flex: 1;
padding: 18px;
}
.buyable-houses {
@@ -284,8 +277,12 @@ h2 {
.house-item {
display: flex;
flex-direction: column;
gap: 5px;
gap: 12px;
align-items: center;
padding: 18px;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: rgba(255, 255, 255, 0.68);
}
.house-preview {
@@ -301,20 +298,73 @@ h2 {
/* center sprite */
}
table {
.house-info {
width: 100%;
border-collapse: collapse;
display: flex;
flex-direction: column;
gap: 12px;
}
table th,
table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
.status-cards,
.buyable-house-stats {
display: grid;
gap: 12px;
}
.status-card,
.buyable-house-stat {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: rgba(255, 255, 255, 0.68);
}
.status-card__label,
.buyable-house-stat span {
display: block;
margin-bottom: 4px;
color: var(--color-text-secondary);
font-size: 0.88rem;
}
.status-card--summary {
align-items: flex-start;
flex-direction: column;
}
.status-card__actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.buyable-house-price {
font-weight: 700;
}
button {
padding: 6px 12px;
cursor: pointer;
}
@media (max-width: 960px) {
.existing-house {
flex-direction: column;
}
.house {
width: min(341px, 100%);
margin: 0 auto;
}
.status-card,
.buyable-house-stat {
flex-direction: column;
align-items: flex-start;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="contenthidden">
<div class="nobility-view">
<StatusBar />
<div class="contentscroll">
<div class="nobility-content">
<h2>{{ $t('falukant.nobility.title') }}</h2>
<SimpleTabs v-model="activeTab" :tabs="tabs" />
@@ -200,4 +200,4 @@
font-style: italic;
margin-top: 1rem;
}
</style>
</style>

View File

@@ -5,7 +5,7 @@
<div>
<span class="falukant-kicker">Falukant</span>
<h2>{{ $t('falukant.overview.title') }}</h2>
<p>Dein Stand in Wirtschaft, Familie und Besitz in einer verdichteten Uebersicht.</p>
<p>Dein Stand in Wirtschaft, Familie und Besitz in einer verdichteten Übersicht.</p>
</div>
</section>
@@ -27,7 +27,7 @@
<article class="summary-card surface-card">
<span class="summary-card__label">Niederlassungen</span>
<strong>{{ branchCount }}</strong>
<p>Direkter Zugriff auf deine wichtigsten Geschaeftsstandorte.</p>
<p>Direkter Zugriff auf deine wichtigsten Geschäftsstandorte.</p>
</article>
<article class="summary-card surface-card">
<span class="summary-card__label">Produktionen aktiv</span>
@@ -37,7 +37,7 @@
<article class="summary-card surface-card">
<span class="summary-card__label">Lagerpositionen</span>
<strong>{{ stockEntryCount }}</strong>
<p>Verdichteter Blick auf Warenbestand ueber alle Regionen.</p>
<p>Verdichteter Blick auf Warenbestand über alle Regionen.</p>
</article>
</section>
@@ -82,92 +82,76 @@
<!-- Normale Übersicht wenn Charakter vorhanden -->
<div v-if="falukantUser?.character" class="overviewcontainer">
<div>
<section class="overview-panel surface-card">
<h3>{{ $t('falukant.overview.metadata.title') }}</h3>
<table>
<tr>
<td>{{ $t('falukant.overview.metadata.name') }}</td>
<td>{{ falukantUser?.character?.definedFirstName?.name }} {{
falukantUser?.character?.definedLastName?.name }}</td>
</tr>
<tr>
<td>{{ $t('falukant.overview.metadata.nobleTitle') }}</td>
<td>{{ $t('falukant.titles.' + falukantUser?.character?.gender + '.' +
falukantUser?.character?.nobleTitle?.labelTr) }}</td>
</tr>
<tr>
<td>{{ $t('falukant.overview.metadata.money') }}</td>
<td>
<div class="detail-list">
<div class="detail-list__item">
<span>{{ $t('falukant.overview.metadata.name') }}</span>
<strong>{{ falukantUser?.character?.definedFirstName?.name }} {{ falukantUser?.character?.definedLastName?.name }}</strong>
</div>
<div class="detail-list__item">
<span>{{ $t('falukant.overview.metadata.nobleTitle') }}</span>
<strong>{{ $t('falukant.titles.' + falukantUser?.character?.gender + '.' + falukantUser?.character?.nobleTitle?.labelTr) }}</strong>
</div>
<div class="detail-list__item">
<span>{{ $t('falukant.overview.metadata.money') }}</span>
<strong>
{{ moneyValue != null
? moneyValue.toLocaleString(locale, { style: 'currency', currency: 'EUR' })
: '---' }}
</td>
</tr>
<tr>
<td>{{ $t('falukant.overview.metadata.age') }}</td>
<td>{{ falukantUser?.character?.age }}</td>
</tr>
<tr>
<td>{{ $t('falukant.overview.metadata.mainbranch') }}</td>
<td>{{ falukantUser?.mainBranchRegion?.name }}</td>
</tr>
</table>
</div>
<div>
</strong>
</div>
<div class="detail-list__item">
<span>{{ $t('falukant.overview.metadata.age') }}</span>
<strong>{{ falukantUser?.character?.age }}</strong>
</div>
<div class="detail-list__item">
<span>{{ $t('falukant.overview.metadata.mainbranch') }}</span>
<strong>{{ falukantUser?.mainBranchRegion?.name }}</strong>
</div>
</div>
</section>
<section class="overview-panel surface-card">
<h3>{{ $t('falukant.overview.productions.title') }}</h3>
<table v-if="productions.length > 0">
<thead>
<tr>
<th>{{ $t('falukant.branch.sale.region') }}</th>
<th>{{ $t('falukant.branch.production.product') }}</th>
<th>{{ $t('falukant.branch.production.quantity') }}</th>
<th>{{ $t('falukant.branch.production.ending') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(production, index) in productions" :key="index">
<td>{{ production.cityName }}</td>
<td>{{ $t(`falukant.product.${production.productName}`) }}</td>
<td>{{ production.quantity }}</td>
<td>{{ formatDate(production.endTimestamp) }}</td>
</tr>
</tbody>
</table>
<div v-if="productions.length > 0" class="overview-card-list">
<article v-for="(production, index) in productions" :key="index" class="overview-entry-card">
<strong>{{ $t(`falukant.product.${production.productName}`) }}</strong>
<div class="overview-entry-card__meta">
<span>{{ $t('falukant.branch.sale.region') }}: {{ production.cityName }}</span>
<span>{{ $t('falukant.branch.production.quantity') }}: {{ production.quantity }}</span>
<span>{{ $t('falukant.branch.production.ending') }}: {{ formatDate(production.endTimestamp) }}</span>
</div>
</article>
</div>
<p v-else>{{ $t('falukant.branch.production.noProductions') }}</p>
</div>
<div>
</section>
<section class="overview-panel surface-card">
<h3>{{ $t('falukant.overview.stock.title') }}</h3>
<table v-if="allStock.length > 0">
<thead>
<tr>
<th>{{ $t('falukant.branch.sale.region') }}</th>
<th>{{ $t('falukant.branch.sale.product') }}</th>
<th>{{ $t('falukant.branch.sale.quantity') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in allStock" :key="index">
<td>{{ item.regionName }}</td>
<td>{{ $t(`falukant.product.${item.productLabelTr}`) }}</td>
<td>{{ item.quantity }}</td>
</tr>
</tbody>
</table>
<div v-if="allStock.length > 0" class="overview-card-list">
<article v-for="(item, index) in allStock" :key="index" class="overview-entry-card">
<strong>{{ $t(`falukant.product.${item.productLabelTr}`) }}</strong>
<div class="overview-entry-card__meta">
<span>{{ $t('falukant.branch.sale.region') }}: {{ item.regionName }}</span>
<span>{{ $t('falukant.branch.sale.quantity') }}: {{ item.quantity }}</span>
</div>
</article>
</div>
<p v-else>{{ $t('falukant.branch.sale.noInventory') }}</p>
</div>
<div>
</section>
<section class="overview-panel surface-card">
<h3>{{ $t('falukant.overview.branches.title') }}</h3>
<table>
<tr v-for="branch in falukantUser?.branches" :key="branch.id">
<td>
<span @click="openBranch(branch.id)" class="link">{{ branch.region.name }}</span>
</td>
<td>
{{ $t(`falukant.overview.branches.level.${branch.branchType.labelTr}`) }}
</td>
</tr>
</table>
</div>
<div class="overview-card-list">
<article v-for="branch in falukantUser?.branches" :key="branch.id" class="overview-entry-card overview-entry-card--action">
<div>
<strong>{{ branch.region.name }}</strong>
<div class="overview-entry-card__meta">
<span>{{ $t(`falukant.overview.branches.level.${branch.branchType.labelTr}`) }}</span>
</div>
</div>
<button type="button" class="button-secondary" @click="openBranch(branch.id)">Öffnen</button>
</article>
</div>
</section>
</div>
</div>
</template>
@@ -287,7 +271,7 @@ export default {
return [
{
kicker: 'Routine',
title: 'Niederlassung oeffnen',
title: 'Niederlassung öffnen',
description: 'Die schnellste Route zu Produktion, Lager, Verkauf und Transport.',
cta: 'Zu den Betrieben',
route: 'BranchView',
@@ -303,8 +287,8 @@ export default {
{
kicker: 'Charakter',
title: 'Familie und Nachfolge',
description: 'Wichtige persoenliche Entscheidungen und Haushaltsstatus gesammelt.',
cta: 'Familie oeffnen',
description: 'Wichtige persönliche Entscheidungen und Haushaltsstatus gesammelt.',
cta: 'Familie öffnen',
route: 'FalukantFamily',
secondary: true,
},
@@ -499,7 +483,7 @@ export default {
await this.fetchAllStock();
await this.fetchProductions();
}
showSuccess(this, 'Erbe wurde uebernommen.');
showSuccess(this, 'Erbe wurde übernommen.');
} catch (error) {
console.error('Error selecting heir:', error);
showError(this, this.$t('falukant.overview.heirSelection.error'));
@@ -597,12 +581,54 @@ export default {
gap: 12px;
}
.overviewcontainer>div {
.overview-panel {
padding: 18px;
}
.detail-list,
.overview-card-list {
display: grid;
gap: 12px;
}
.detail-list__item,
.overview-entry-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
border: 1px solid var(--color-border);
padding: 16px;
border-radius: var(--radius-lg);
background: rgba(255, 253, 249, 0.82);
box-shadow: var(--shadow-soft);
background: rgba(255, 255, 255, 0.66);
}
.detail-list__item {
flex-direction: column;
align-items: flex-start;
}
.detail-list__item span,
.overview-entry-card__meta {
color: var(--color-text-secondary);
}
.overview-entry-card {
align-items: flex-start;
flex-direction: column;
}
.overview-entry-card__meta {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
margin-top: 6px;
font-size: 0.9rem;
}
.overview-entry-card--action {
flex-direction: row;
align-items: center;
}
.imagecontainer {

View File

@@ -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>